Merge pull request from cypherstack/coin_control

Coin control
This commit is contained in:
julian-CStack 2023-03-08 16:18:38 -06:00 committed by GitHub
commit 6d55f3fffa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
84 changed files with 4328 additions and 1108 deletions
assets/svg/coin_control
lib
pubspec.yaml
test
cached_electrumx_test.mocks.dartelectrumx_test.mocks.dart
pages/send_view
screen_tests
widget_tests

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.8628 14.5871C17.6515 14.9523 17.2648 15.1564 16.8709 15.1564C16.6744 15.1564 16.4756 15.1061 16.2934 14.9998L15.395 14.4755L15.5876 15.2021C15.7091 15.6608 15.4352 16.132 14.976 16.2527C14.9021 16.2728 14.8281 16.2817 14.7553 16.2817C14.3746 16.2817 14.0262 16.0277 13.9242 15.6429L13.289 13.2467L11.1454 11.9946L11.1454 14.519L12.8777 16.2693C13.2121 16.6073 13.2087 17.1509 12.871 17.4846C12.703 17.6522 12.4846 17.7345 12.2661 17.7345C12.0444 17.7345 11.8225 17.6495 11.6545 17.4794L11.1454 16.9649V18.021C11.1454 18.6544 10.6319 19.1668 9.99845 19.1668C9.36502 19.1668 8.85155 18.6544 8.85155 18.021L8.85154 16.9647L8.34236 17.4792C8.00856 17.8172 7.46401 17.8194 7.12599 17.4859C6.78825 17.1525 6.78492 16.6087 7.11927 16.2706L8.85163 14.5204L8.82147 11.9946L6.67768 13.2454L6.04246 15.6416C5.94048 16.0265 5.59201 16.2804 5.21138 16.2804C5.13851 16.2804 5.06457 16.2715 4.99066 16.2513C4.53161 16.1305 4.25768 15.6594 4.37907 15.2007L4.57168 14.4742L3.67327 14.9984C3.52235 15.1063 3.32362 15.1564 3.12703 15.1564C2.7328 15.1564 2.34858 14.9527 2.13553 14.5878C1.8157 14.0417 2 13.3403 2.54731 13.0212L3.47687 12.4787L2.74891 12.2815C2.29022 12.1593 2.01987 11.6867 2.14412 11.2284C2.26837 10.77 2.73888 10.5015 3.19936 10.6232L5.55942 11.2642L7.72576 10.0002L5.55942 8.73616L3.19936 9.37712C3.12378 9.39726 3.04808 9.40733 2.9736 9.40733C2.59512 9.40733 2.24779 9.15557 2.1443 8.77282C2.01987 8.31364 2.29022 7.84456 2.74891 7.71924L3.47687 7.52201L2.54731 6.97803C2.00018 6.65913 1.81577 5.95752 2.13553 5.41146C2.45368 4.86432 3.15604 4.67956 3.70425 4.99967L4.60265 5.52389L4.41001 4.79736C4.28862 4.33939 4.56398 3.86816 4.99009 3.74749C5.48423 3.62324 5.92108 3.89967 6.07505 4.35729L6.71027 6.75352L8.82147 8.0057L8.82146 5.48128L7.12062 3.72959C6.78761 3.39157 6.78761 2.84766 7.12778 2.51429C7.46437 2.17949 8.00864 2.18415 8.34523 2.52109L8.85441 3.03564L8.82147 1.97933C8.82147 1.3459 9.33494 0.833496 9.96838 0.833496C10.6018 0.833496 11.1153 1.3459 11.1153 1.97933L11.1153 3.03564L11.6245 2.52109C11.9577 2.18429 12.5021 2.17981 12.8408 2.51438C13.1786 2.84782 13.1819 3.39166 12.8476 3.72968L11.1152 5.47994L11.1454 8.0057L13.2891 6.75495L13.9244 4.35872C14.0459 3.90111 14.5173 3.62467 14.9764 3.74893C15.4354 3.86978 15.7094 4.34082 15.588 4.79951L15.3954 5.52604L16.2938 5.00182C16.8409 4.68292 17.5438 4.86755 17.8625 5.41361C18.1823 5.95967 17.998 6.66113 17.4507 6.98018L16.5211 7.52266L17.2491 7.71988C17.7078 7.8441 17.9785 8.31643 17.8539 8.77405C17.7502 9.15683 17.4031 9.40855 17.0246 9.40855C16.9501 9.40855 16.8745 9.39848 16.7988 9.37834L14.4074 8.73616L12.2733 10.0002L14.4382 11.2634L16.7997 10.6236C17.2612 10.5015 17.7303 10.77 17.8556 11.2284C17.98 11.686 17.7095 12.1583 17.2509 12.2825L16.5229 12.4797L17.4525 13.0222C17.9989 13.341 18.1815 14.0428 17.8628 14.5871Z" fill="#96B0D6"/>
</svg>

After

(image error) Size: 2.9 KiB

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.0002 14.3397C20.0002 14.1755 19.9859 14.0085 19.9562 13.8404L18.9131 7.22787C18.4814 4.76975 16.3127 3.3335 9.97206 3.3335C3.71894 3.3335 1.52237 4.75412 1.08706 7.22725L0.0439375 13.8397C0.0142906 14.008 0 14.1747 0 14.3391C0 15.9607 1.39313 17.3332 3.17188 17.3332C4.72469 17.3332 6.1025 16.3938 6.59375 15.0007L6.82812 14.3332H13.1719L13.4062 15.0007C13.8975 16.3938 15.2753 17.3332 16.8281 17.3332C18.6064 17.3054 20.0002 15.9616 20.0002 14.3397ZM7.72206 10.0835L6.72331 10.0832L6.72206 11.0835C6.72206 11.496 6.38519 11.8335 5.97269 11.8335C5.56019 11.8335 5.22206 11.496 5.22206 11.0835L5.22321 10.0832L4.22206 10.0835C3.80956 10.0835 3.47269 9.746 3.47269 9.3335C3.47269 8.921 3.80925 8.5835 4.22206 8.5835L5.22331 8.58315L5.22206 7.5835C5.22206 7.171 5.56019 6.8335 5.97269 6.8335C6.38519 6.8335 6.72206 7.171 6.72206 7.5835L6.72321 8.58318L7.72206 8.5835C8.13456 8.5835 8.47269 8.921 8.47269 9.3335C8.47206 9.746 8.16269 10.0835 7.72206 10.0835ZM13.5002 12.0554C12.8099 12.0554 12.2502 11.4954 12.2502 10.8054C12.2502 10.1154 12.8099 9.55537 13.5002 9.55537C14.1905 9.55537 14.7502 10.1154 14.7502 10.8054C14.7502 11.5241 14.1908 12.0554 13.5002 12.0554ZM15.5002 9.05537C14.8099 9.05537 14.2502 8.49537 14.2502 7.80537C14.2502 7.11537 14.8099 6.55537 15.5002 6.55537C16.1905 6.55537 16.7502 7.11537 16.7502 7.80537C16.7502 8.52412 16.1908 9.05537 15.5002 9.05537Z" fill="#8E9192"/>
</svg>

After

(image error) Size: 1.5 KiB

View file

@ -0,0 +1,3 @@
<svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.7179 3.5972C16.0821 3.92551 16.0821 4.45639 15.7179 4.75327L6.28929 13.9739C5.98571 14.3301 5.44286 14.3301 5.10714 13.9739L0.251036 9.22385C-0.0836786 8.92698 -0.0836786 8.3961 0.251036 8.06779C0.585714 7.74297 1.12857 7.74297 1.46321 8.06779L5.71429 12.2275L14.5357 3.5972C14.8714 3.27099 15.4143 3.27099 15.7179 3.5972Z" fill="white"/>
</svg>

After

(image error) Size: 455 B

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.27148 8.01188C4.27148 9.27588 6.83529 10.3035 10.0007 10.3035C13.166 10.3035 15.7298 9.27588 15.7298 8.01188C15.7298 6.74788 13.166 5.72021 10.0007 5.72021C6.83529 5.72021 4.27148 6.74788 4.27148 8.01188ZM19.1673 8.29834C19.1673 5.45166 15.0638 3.14209 10.0007 3.14209C4.9375 3.14209 0.833984 5.45166 0.833984 8.29834C0.833984 11.145 4.9375 13.4546 10.0007 13.4546C15.0638 13.4546 19.1673 11.145 19.1673 8.29834ZM3.93454 9.73779C3.50163 9.32601 3.09342 8.74593 3.09342 8.01188C3.09342 7.27783 3.50163 6.69775 3.93454 6.28597C4.36458 5.88135 4.92676 5.56624 5.49251 5.32633C6.72786 4.84652 8.31055 4.57438 9.96842 4.57438C11.6908 4.57438 13.2734 4.84652 14.4766 5.32633C15.0745 5.56624 15.6367 5.88135 16.0664 6.28597C16.4997 6.69775 16.8757 7.27783 16.8757 8.01188C16.8757 8.74593 16.4997 9.32601 16.0664 9.73779C15.6367 10.1424 15.0745 10.4575 14.4766 10.6652C13.2734 11.1772 11.6908 11.4494 10.0007 11.4494C8.31055 11.4494 6.72786 11.1772 5.49251 10.6652C4.92676 10.4575 4.36458 10.1424 3.93454 9.73779ZM19.1673 11.2386C18.6947 11.8008 18.1038 12.2878 17.4486 12.6709V14.9805C18.5299 14.2285 19.1673 13.3047 19.1673 12.277V11.2386ZM16.3027 15.6071V13.3226C15.2858 13.7917 14.1221 14.1462 12.8652 14.361V16.6634C14.1615 16.4521 15.3324 16.0977 16.3027 15.6071ZM9.90337 14.5687C9.3533 14.5689 8.81281 14.5691 8.2819 14.515V16.8138C8.8022 16.8608 9.34137 16.8606 9.89058 16.8604C9.92723 16.8604 9.96392 16.8604 10.0007 16.8604C10.0374 16.8604 10.0741 16.8604 10.1107 16.8604C10.6599 16.8606 11.1991 16.8608 11.7194 16.8138V14.515C11.1885 14.5691 10.648 14.5689 10.0979 14.5687C10.0655 14.5687 10.0331 14.5687 10.0007 14.5687C9.96819 14.5687 9.93576 14.5687 9.90337 14.5687ZM7.13607 16.6634V14.361C5.87923 14.1462 4.71549 13.7917 3.69857 13.3226V15.6071C4.66895 16.0977 5.83984 16.4521 7.13607 16.6634ZM0.833984 12.277C0.833984 13.3047 1.471 14.2285 2.55273 14.9805V12.6709C1.89818 12.2878 1.307 11.8008 0.833984 11.2386V12.277Z" fill="#F7931A"/>
</svg>

After

(image error) Size: 2.1 KiB

View file

@ -1,6 +1,5 @@
import 'package:isar/isar.dart';
import 'package:stackwallet/exceptions/main_db/main_db_exception.dart';
import 'package:stackwallet/exceptions/sw_exception.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/utilities/stack_file_system.dart';
import 'package:tuple/tuple.dart';
@ -161,6 +160,43 @@ class MainDB {
await isar.utxos.putAll(utxos);
});
Future<void> updateUTXOs(String walletId, List<UTXO> utxos) async {
await isar.writeTxn(() async {
final set = utxos.toSet();
for (final utxo in utxos) {
// check if utxo exists in db and update accordingly
final storedUtxo = await isar.utxos
.where()
.txidWalletIdVoutEqualTo(utxo.txid, utxo.walletId, utxo.vout)
.findFirst();
if (storedUtxo != null) {
// update
set.remove(utxo);
set.add(
storedUtxo.copyWith(
value: utxo.value,
address: utxo.address,
blockTime: utxo.blockTime,
blockHeight: utxo.blockHeight,
blockHash: utxo.blockHash,
),
);
}
}
await isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await isar.utxos.putAll(set.toList());
});
}
Stream<UTXO?> watchUTXO({
required Id id,
bool fireImmediately = false,
}) {
return isar.utxos.watchObject(id, fireImmediately: fireImmediately);
}
// transaction notes
QueryBuilder<TransactionNote, TransactionNote, QAfterWhereClause>
getTransactionNotes(String walletId) =>
@ -236,10 +272,6 @@ class MainDB {
Future<int> updateAddressLabel(AddressLabel addressLabel) async {
try {
return await isar.writeTxn(() async {
final deleted = await isar.addresses.delete(addressLabel.id);
if (!deleted) {
throw SWException("Failed to delete $addressLabel before updating");
}
return await isar.addressLabels.put(addressLabel);
});
} catch (e) {

View file

@ -1,6 +1,6 @@
import 'package:stackwallet/exceptions/sw_exception.dart';
enum ExchangeExceptionType { generic, serializeResponseError }
enum ExchangeExceptionType { generic, serializeResponseError, orderNotFound }
class ExchangeException extends SWException {
ExchangeExceptionType type;

View file

@ -19,7 +19,7 @@ class Balance {
required this.pendingSpendable,
});
Decimal getTotal({bool includeBlocked = false}) => Format.satoshisToAmount(
Decimal getTotal({bool includeBlocked = true}) => Format.satoshisToAmount(
includeBlocked ? total : total - blockedTotal,
coin: coin,
);
@ -39,12 +39,7 @@ class Balance {
coin: coin,
);
String toJsonIgnoreCoin() => jsonEncode({
"total": total,
"spendable": spendable,
"blockedTotal": blockedTotal,
"pendingSpendable": pendingSpendable,
});
String toJsonIgnoreCoin() => jsonEncode(toMap()..remove("coin"));
factory Balance.fromJson(String json, Coin coin) {
final decoded = jsonDecode(json);
@ -56,4 +51,17 @@ class Balance {
pendingSpendable: decoded["pendingSpendable"] as int,
);
}
Map<String, dynamic> toMap() => {
"coin": coin,
"total": total,
"spendable": spendable,
"blockedTotal": blockedTotal,
"pendingSpendable": pendingSpendable,
};
@override
String toString() {
return toMap().toString();
}
}

View file

@ -4,7 +4,7 @@ import 'package:isar/isar.dart';
part 'utxo.g.dart';
@Collection(accessor: "utxos")
@Collection(accessor: "utxos", inheritance: false)
class UTXO {
UTXO({
required this.walletId,
@ -18,6 +18,8 @@ class UTXO {
required this.blockHash,
required this.blockHeight,
required this.blockTime,
this.address,
this.used,
this.otherData,
});
@ -26,7 +28,10 @@ class UTXO {
@Index()
late final String walletId;
@Index(unique: true, replace: true, composite: [CompositeIndex("walletId")])
@Index(unique: true, replace: true, composite: [
CompositeIndex("walletId"),
CompositeIndex("vout"),
])
late final String txid;
late final int vout;
@ -48,6 +53,10 @@ class UTXO {
late final int? blockTime;
late final String? address;
late final bool? used;
late final String? otherData;
int getConfirmations(int currentChainHeight) {
@ -61,6 +70,40 @@ class UTXO {
return confirmations >= minimumConfirms;
}
UTXO copyWith({
Id? id,
String? walletId,
String? txid,
int? vout,
int? value,
String? name,
bool? isBlocked,
String? blockedReason,
bool? isCoinbase,
String? blockHash,
int? blockHeight,
int? blockTime,
String? address,
bool? used,
String? otherData,
}) =>
UTXO(
walletId: walletId ?? this.walletId,
txid: txid ?? this.txid,
vout: vout ?? this.vout,
value: value ?? this.value,
name: name ?? this.name,
isBlocked: isBlocked ?? this.isBlocked,
blockedReason: blockedReason ?? this.blockedReason,
isCoinbase: isCoinbase ?? this.isCoinbase,
blockHash: blockHash ?? this.blockHash,
blockHeight: blockHeight ?? this.blockHeight,
blockTime: blockTime ?? this.blockTime,
address: address ?? this.address,
used: used ?? this.used,
otherData: otherData ?? this.otherData,
)..id = id ?? this.id;
@override
String toString() => "{ "
"id: $id, "
@ -75,5 +118,20 @@ class UTXO {
"blockHash: $blockHash, "
"blockHeight: $blockHeight, "
"blockTime: $blockTime, "
"address: $address, "
"used: $used, "
"otherData: $otherData, "
"}";
@override
bool operator ==(Object other) {
return other is UTXO &&
other.walletId == walletId &&
other.txid == txid &&
other.vout == vout;
}
@override
@ignore
int get hashCode => Object.hashAll([walletId, txid, vout]);
}

View file

@ -17,63 +17,73 @@ const UTXOSchema = CollectionSchema(
name: r'UTXO',
id: 5934032492047519621,
properties: {
r'blockHash': PropertySchema(
r'address': PropertySchema(
id: 0,
name: r'address',
type: IsarType.string,
),
r'blockHash': PropertySchema(
id: 1,
name: r'blockHash',
type: IsarType.string,
),
r'blockHeight': PropertySchema(
id: 1,
id: 2,
name: r'blockHeight',
type: IsarType.long,
),
r'blockTime': PropertySchema(
id: 2,
id: 3,
name: r'blockTime',
type: IsarType.long,
),
r'blockedReason': PropertySchema(
id: 3,
id: 4,
name: r'blockedReason',
type: IsarType.string,
),
r'isBlocked': PropertySchema(
id: 4,
id: 5,
name: r'isBlocked',
type: IsarType.bool,
),
r'isCoinbase': PropertySchema(
id: 5,
id: 6,
name: r'isCoinbase',
type: IsarType.bool,
),
r'name': PropertySchema(
id: 6,
id: 7,
name: r'name',
type: IsarType.string,
),
r'otherData': PropertySchema(
id: 7,
id: 8,
name: r'otherData',
type: IsarType.string,
),
r'txid': PropertySchema(
id: 8,
id: 9,
name: r'txid',
type: IsarType.string,
),
r'used': PropertySchema(
id: 10,
name: r'used',
type: IsarType.bool,
),
r'value': PropertySchema(
id: 9,
id: 11,
name: r'value',
type: IsarType.long,
),
r'vout': PropertySchema(
id: 10,
id: 12,
name: r'vout',
type: IsarType.long,
),
r'walletId': PropertySchema(
id: 11,
id: 13,
name: r'walletId',
type: IsarType.string,
)
@ -97,9 +107,9 @@ const UTXOSchema = CollectionSchema(
)
],
),
r'txid_walletId': IndexSchema(
id: -2771771174176035985,
name: r'txid_walletId',
r'txid_walletId_vout': IndexSchema(
id: -2984264099359759359,
name: r'txid_walletId_vout',
unique: true,
replace: true,
properties: [
@ -112,6 +122,11 @@ const UTXOSchema = CollectionSchema(
name: r'walletId',
type: IndexType.hash,
caseSensitive: true,
),
IndexPropertySchema(
name: r'vout',
type: IndexType.value,
caseSensitive: false,
)
],
),
@ -143,6 +158,12 @@ int _uTXOEstimateSize(
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
{
final value = object.address;
if (value != null) {
bytesCount += 3 + value.length * 3;
}
}
{
final value = object.blockHash;
if (value != null) {
@ -173,18 +194,20 @@ void _uTXOSerialize(
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeString(offsets[0], object.blockHash);
writer.writeLong(offsets[1], object.blockHeight);
writer.writeLong(offsets[2], object.blockTime);
writer.writeString(offsets[3], object.blockedReason);
writer.writeBool(offsets[4], object.isBlocked);
writer.writeBool(offsets[5], object.isCoinbase);
writer.writeString(offsets[6], object.name);
writer.writeString(offsets[7], object.otherData);
writer.writeString(offsets[8], object.txid);
writer.writeLong(offsets[9], object.value);
writer.writeLong(offsets[10], object.vout);
writer.writeString(offsets[11], object.walletId);
writer.writeString(offsets[0], object.address);
writer.writeString(offsets[1], object.blockHash);
writer.writeLong(offsets[2], object.blockHeight);
writer.writeLong(offsets[3], object.blockTime);
writer.writeString(offsets[4], object.blockedReason);
writer.writeBool(offsets[5], object.isBlocked);
writer.writeBool(offsets[6], object.isCoinbase);
writer.writeString(offsets[7], object.name);
writer.writeString(offsets[8], object.otherData);
writer.writeString(offsets[9], object.txid);
writer.writeBool(offsets[10], object.used);
writer.writeLong(offsets[11], object.value);
writer.writeLong(offsets[12], object.vout);
writer.writeString(offsets[13], object.walletId);
}
UTXO _uTXODeserialize(
@ -194,18 +217,20 @@ UTXO _uTXODeserialize(
Map<Type, List<int>> allOffsets,
) {
final object = UTXO(
blockHash: reader.readStringOrNull(offsets[0]),
blockHeight: reader.readLongOrNull(offsets[1]),
blockTime: reader.readLongOrNull(offsets[2]),
blockedReason: reader.readStringOrNull(offsets[3]),
isBlocked: reader.readBool(offsets[4]),
isCoinbase: reader.readBool(offsets[5]),
name: reader.readString(offsets[6]),
otherData: reader.readStringOrNull(offsets[7]),
txid: reader.readString(offsets[8]),
value: reader.readLong(offsets[9]),
vout: reader.readLong(offsets[10]),
walletId: reader.readString(offsets[11]),
address: reader.readStringOrNull(offsets[0]),
blockHash: reader.readStringOrNull(offsets[1]),
blockHeight: reader.readLongOrNull(offsets[2]),
blockTime: reader.readLongOrNull(offsets[3]),
blockedReason: reader.readStringOrNull(offsets[4]),
isBlocked: reader.readBool(offsets[5]),
isCoinbase: reader.readBool(offsets[6]),
name: reader.readString(offsets[7]),
otherData: reader.readStringOrNull(offsets[8]),
txid: reader.readString(offsets[9]),
used: reader.readBoolOrNull(offsets[10]),
value: reader.readLong(offsets[11]),
vout: reader.readLong(offsets[12]),
walletId: reader.readString(offsets[13]),
);
object.id = id;
return object;
@ -221,26 +246,30 @@ P _uTXODeserializeProp<P>(
case 0:
return (reader.readStringOrNull(offset)) as P;
case 1:
return (reader.readLongOrNull(offset)) as P;
return (reader.readStringOrNull(offset)) as P;
case 2:
return (reader.readLongOrNull(offset)) as P;
case 3:
return (reader.readStringOrNull(offset)) as P;
return (reader.readLongOrNull(offset)) as P;
case 4:
return (reader.readBool(offset)) as P;
return (reader.readStringOrNull(offset)) as P;
case 5:
return (reader.readBool(offset)) as P;
case 6:
return (reader.readString(offset)) as P;
return (reader.readBool(offset)) as P;
case 7:
return (reader.readStringOrNull(offset)) as P;
case 8:
return (reader.readString(offset)) as P;
case 8:
return (reader.readStringOrNull(offset)) as P;
case 9:
return (reader.readLong(offset)) as P;
return (reader.readString(offset)) as P;
case 10:
return (reader.readLong(offset)) as P;
return (reader.readBoolOrNull(offset)) as P;
case 11:
return (reader.readLong(offset)) as P;
case 12:
return (reader.readLong(offset)) as P;
case 13:
return (reader.readString(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
@ -260,89 +289,91 @@ void _uTXOAttach(IsarCollection<dynamic> col, Id id, UTXO object) {
}
extension UTXOByIndex on IsarCollection<UTXO> {
Future<UTXO?> getByTxidWalletId(String txid, String walletId) {
return getByIndex(r'txid_walletId', [txid, walletId]);
Future<UTXO?> getByTxidWalletIdVout(String txid, String walletId, int vout) {
return getByIndex(r'txid_walletId_vout', [txid, walletId, vout]);
}
UTXO? getByTxidWalletIdSync(String txid, String walletId) {
return getByIndexSync(r'txid_walletId', [txid, walletId]);
UTXO? getByTxidWalletIdVoutSync(String txid, String walletId, int vout) {
return getByIndexSync(r'txid_walletId_vout', [txid, walletId, vout]);
}
Future<bool> deleteByTxidWalletId(String txid, String walletId) {
return deleteByIndex(r'txid_walletId', [txid, walletId]);
Future<bool> deleteByTxidWalletIdVout(
String txid, String walletId, int vout) {
return deleteByIndex(r'txid_walletId_vout', [txid, walletId, vout]);
}
bool deleteByTxidWalletIdSync(String txid, String walletId) {
return deleteByIndexSync(r'txid_walletId', [txid, walletId]);
bool deleteByTxidWalletIdVoutSync(String txid, String walletId, int vout) {
return deleteByIndexSync(r'txid_walletId_vout', [txid, walletId, vout]);
}
Future<List<UTXO?>> getAllByTxidWalletId(
List<String> txidValues, List<String> walletIdValues) {
Future<List<UTXO?>> getAllByTxidWalletIdVout(List<String> txidValues,
List<String> walletIdValues, List<int> voutValues) {
final len = txidValues.length;
assert(walletIdValues.length == len,
assert(walletIdValues.length == len && voutValues.length == len,
'All index values must have the same length');
final values = <List<dynamic>>[];
for (var i = 0; i < len; i++) {
values.add([txidValues[i], walletIdValues[i]]);
values.add([txidValues[i], walletIdValues[i], voutValues[i]]);
}
return getAllByIndex(r'txid_walletId', values);
return getAllByIndex(r'txid_walletId_vout', values);
}
List<UTXO?> getAllByTxidWalletIdSync(
List<String> txidValues, List<String> walletIdValues) {
List<UTXO?> getAllByTxidWalletIdVoutSync(List<String> txidValues,
List<String> walletIdValues, List<int> voutValues) {
final len = txidValues.length;
assert(walletIdValues.length == len,
assert(walletIdValues.length == len && voutValues.length == len,
'All index values must have the same length');
final values = <List<dynamic>>[];
for (var i = 0; i < len; i++) {
values.add([txidValues[i], walletIdValues[i]]);
values.add([txidValues[i], walletIdValues[i], voutValues[i]]);
}
return getAllByIndexSync(r'txid_walletId', values);
return getAllByIndexSync(r'txid_walletId_vout', values);
}
Future<int> deleteAllByTxidWalletId(
List<String> txidValues, List<String> walletIdValues) {
Future<int> deleteAllByTxidWalletIdVout(List<String> txidValues,
List<String> walletIdValues, List<int> voutValues) {
final len = txidValues.length;
assert(walletIdValues.length == len,
assert(walletIdValues.length == len && voutValues.length == len,
'All index values must have the same length');
final values = <List<dynamic>>[];
for (var i = 0; i < len; i++) {
values.add([txidValues[i], walletIdValues[i]]);
values.add([txidValues[i], walletIdValues[i], voutValues[i]]);
}
return deleteAllByIndex(r'txid_walletId', values);
return deleteAllByIndex(r'txid_walletId_vout', values);
}
int deleteAllByTxidWalletIdSync(
List<String> txidValues, List<String> walletIdValues) {
int deleteAllByTxidWalletIdVoutSync(List<String> txidValues,
List<String> walletIdValues, List<int> voutValues) {
final len = txidValues.length;
assert(walletIdValues.length == len,
assert(walletIdValues.length == len && voutValues.length == len,
'All index values must have the same length');
final values = <List<dynamic>>[];
for (var i = 0; i < len; i++) {
values.add([txidValues[i], walletIdValues[i]]);
values.add([txidValues[i], walletIdValues[i], voutValues[i]]);
}
return deleteAllByIndexSync(r'txid_walletId', values);
return deleteAllByIndexSync(r'txid_walletId_vout', values);
}
Future<Id> putByTxidWalletId(UTXO object) {
return putByIndex(r'txid_walletId', object);
Future<Id> putByTxidWalletIdVout(UTXO object) {
return putByIndex(r'txid_walletId_vout', object);
}
Id putByTxidWalletIdSync(UTXO object, {bool saveLinks = true}) {
return putByIndexSync(r'txid_walletId', object, saveLinks: saveLinks);
Id putByTxidWalletIdVoutSync(UTXO object, {bool saveLinks = true}) {
return putByIndexSync(r'txid_walletId_vout', object, saveLinks: saveLinks);
}
Future<List<Id>> putAllByTxidWalletId(List<UTXO> objects) {
return putAllByIndex(r'txid_walletId', objects);
Future<List<Id>> putAllByTxidWalletIdVout(List<UTXO> objects) {
return putAllByIndex(r'txid_walletId_vout', objects);
}
List<Id> putAllByTxidWalletIdSync(List<UTXO> objects,
List<Id> putAllByTxidWalletIdVoutSync(List<UTXO> objects,
{bool saveLinks = true}) {
return putAllByIndexSync(r'txid_walletId', objects, saveLinks: saveLinks);
return putAllByIndexSync(r'txid_walletId_vout', objects,
saveLinks: saveLinks);
}
}
@ -472,29 +503,29 @@ extension UTXOQueryWhere on QueryBuilder<UTXO, UTXO, QWhereClause> {
});
}
QueryBuilder<UTXO, UTXO, QAfterWhereClause> txidEqualToAnyWalletId(
QueryBuilder<UTXO, UTXO, QAfterWhereClause> txidEqualToAnyWalletIdVout(
String txid) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IndexWhereClause.equalTo(
indexName: r'txid_walletId',
indexName: r'txid_walletId_vout',
value: [txid],
));
});
}
QueryBuilder<UTXO, UTXO, QAfterWhereClause> txidNotEqualToAnyWalletId(
QueryBuilder<UTXO, UTXO, QAfterWhereClause> txidNotEqualToAnyWalletIdVout(
String txid) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(IndexWhereClause.between(
indexName: r'txid_walletId',
indexName: r'txid_walletId_vout',
lower: [],
upper: [txid],
includeUpper: false,
))
.addWhereClause(IndexWhereClause.between(
indexName: r'txid_walletId',
indexName: r'txid_walletId_vout',
lower: [txid],
includeLower: false,
upper: [],
@ -502,13 +533,13 @@ extension UTXOQueryWhere on QueryBuilder<UTXO, UTXO, QWhereClause> {
} else {
return query
.addWhereClause(IndexWhereClause.between(
indexName: r'txid_walletId',
indexName: r'txid_walletId_vout',
lower: [txid],
includeLower: false,
upper: [],
))
.addWhereClause(IndexWhereClause.between(
indexName: r'txid_walletId',
indexName: r'txid_walletId_vout',
lower: [],
upper: [txid],
includeUpper: false,
@ -517,29 +548,29 @@ extension UTXOQueryWhere on QueryBuilder<UTXO, UTXO, QWhereClause> {
});
}
QueryBuilder<UTXO, UTXO, QAfterWhereClause> txidWalletIdEqualTo(
QueryBuilder<UTXO, UTXO, QAfterWhereClause> txidWalletIdEqualToAnyVout(
String txid, String walletId) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IndexWhereClause.equalTo(
indexName: r'txid_walletId',
indexName: r'txid_walletId_vout',
value: [txid, walletId],
));
});
}
QueryBuilder<UTXO, UTXO, QAfterWhereClause> txidEqualToWalletIdNotEqualTo(
String txid, String walletId) {
QueryBuilder<UTXO, UTXO, QAfterWhereClause>
txidEqualToWalletIdNotEqualToAnyVout(String txid, String walletId) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(IndexWhereClause.between(
indexName: r'txid_walletId',
indexName: r'txid_walletId_vout',
lower: [txid],
upper: [txid, walletId],
includeUpper: false,
))
.addWhereClause(IndexWhereClause.between(
indexName: r'txid_walletId',
indexName: r'txid_walletId_vout',
lower: [txid, walletId],
includeLower: false,
upper: [txid],
@ -547,13 +578,13 @@ extension UTXOQueryWhere on QueryBuilder<UTXO, UTXO, QWhereClause> {
} else {
return query
.addWhereClause(IndexWhereClause.between(
indexName: r'txid_walletId',
indexName: r'txid_walletId_vout',
lower: [txid, walletId],
includeLower: false,
upper: [txid],
))
.addWhereClause(IndexWhereClause.between(
indexName: r'txid_walletId',
indexName: r'txid_walletId_vout',
lower: [txid],
upper: [txid, walletId],
includeUpper: false,
@ -562,6 +593,103 @@ extension UTXOQueryWhere on QueryBuilder<UTXO, UTXO, QWhereClause> {
});
}
QueryBuilder<UTXO, UTXO, QAfterWhereClause> txidWalletIdVoutEqualTo(
String txid, String walletId, int vout) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IndexWhereClause.equalTo(
indexName: r'txid_walletId_vout',
value: [txid, walletId, vout],
));
});
}
QueryBuilder<UTXO, UTXO, QAfterWhereClause> txidWalletIdEqualToVoutNotEqualTo(
String txid, String walletId, int vout) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(IndexWhereClause.between(
indexName: r'txid_walletId_vout',
lower: [txid, walletId],
upper: [txid, walletId, vout],
includeUpper: false,
))
.addWhereClause(IndexWhereClause.between(
indexName: r'txid_walletId_vout',
lower: [txid, walletId, vout],
includeLower: false,
upper: [txid, walletId],
));
} else {
return query
.addWhereClause(IndexWhereClause.between(
indexName: r'txid_walletId_vout',
lower: [txid, walletId, vout],
includeLower: false,
upper: [txid, walletId],
))
.addWhereClause(IndexWhereClause.between(
indexName: r'txid_walletId_vout',
lower: [txid, walletId],
upper: [txid, walletId, vout],
includeUpper: false,
));
}
});
}
QueryBuilder<UTXO, UTXO, QAfterWhereClause>
txidWalletIdEqualToVoutGreaterThan(
String txid,
String walletId,
int vout, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IndexWhereClause.between(
indexName: r'txid_walletId_vout',
lower: [txid, walletId, vout],
includeLower: include,
upper: [txid, walletId],
));
});
}
QueryBuilder<UTXO, UTXO, QAfterWhereClause> txidWalletIdEqualToVoutLessThan(
String txid,
String walletId,
int vout, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IndexWhereClause.between(
indexName: r'txid_walletId_vout',
lower: [txid, walletId],
upper: [txid, walletId, vout],
includeUpper: include,
));
});
}
QueryBuilder<UTXO, UTXO, QAfterWhereClause> txidWalletIdEqualToVoutBetween(
String txid,
String walletId,
int lowerVout,
int upperVout, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IndexWhereClause.between(
indexName: r'txid_walletId_vout',
lower: [txid, walletId, lowerVout],
includeLower: includeLower,
upper: [txid, walletId, upperVout],
includeUpper: includeUpper,
));
});
}
QueryBuilder<UTXO, UTXO, QAfterWhereClause> isBlockedEqualTo(bool isBlocked) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IndexWhereClause.equalTo(
@ -608,6 +736,150 @@ extension UTXOQueryWhere on QueryBuilder<UTXO, UTXO, QWhereClause> {
}
extension UTXOQueryFilter on QueryBuilder<UTXO, UTXO, QFilterCondition> {
QueryBuilder<UTXO, UTXO, QAfterFilterCondition> addressIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'address',
));
});
}
QueryBuilder<UTXO, UTXO, QAfterFilterCondition> addressIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'address',
));
});
}
QueryBuilder<UTXO, UTXO, QAfterFilterCondition> addressEqualTo(
String? value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'address',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<UTXO, UTXO, QAfterFilterCondition> addressGreaterThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'address',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<UTXO, UTXO, QAfterFilterCondition> addressLessThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'address',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<UTXO, UTXO, QAfterFilterCondition> addressBetween(
String? lower,
String? upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'address',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<UTXO, UTXO, QAfterFilterCondition> addressStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'address',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<UTXO, UTXO, QAfterFilterCondition> addressEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'address',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<UTXO, UTXO, QAfterFilterCondition> addressContains(String value,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'address',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<UTXO, UTXO, QAfterFilterCondition> addressMatches(String pattern,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'address',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<UTXO, UTXO, QAfterFilterCondition> addressIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'address',
value: '',
));
});
}
QueryBuilder<UTXO, UTXO, QAfterFilterCondition> addressIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'address',
value: '',
));
});
}
QueryBuilder<UTXO, UTXO, QAfterFilterCondition> blockHashIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
@ -1510,6 +1782,31 @@ extension UTXOQueryFilter on QueryBuilder<UTXO, UTXO, QFilterCondition> {
});
}
QueryBuilder<UTXO, UTXO, QAfterFilterCondition> usedIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'used',
));
});
}
QueryBuilder<UTXO, UTXO, QAfterFilterCondition> usedIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'used',
));
});
}
QueryBuilder<UTXO, UTXO, QAfterFilterCondition> usedEqualTo(bool? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'used',
value: value,
));
});
}
QueryBuilder<UTXO, UTXO, QAfterFilterCondition> valueEqualTo(int value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
@ -1749,6 +2046,18 @@ extension UTXOQueryObject on QueryBuilder<UTXO, UTXO, QFilterCondition> {}
extension UTXOQueryLinks on QueryBuilder<UTXO, UTXO, QFilterCondition> {}
extension UTXOQuerySortBy on QueryBuilder<UTXO, UTXO, QSortBy> {
QueryBuilder<UTXO, UTXO, QAfterSortBy> sortByAddress() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'address', Sort.asc);
});
}
QueryBuilder<UTXO, UTXO, QAfterSortBy> sortByAddressDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'address', Sort.desc);
});
}
QueryBuilder<UTXO, UTXO, QAfterSortBy> sortByBlockHash() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'blockHash', Sort.asc);
@ -1857,6 +2166,18 @@ extension UTXOQuerySortBy on QueryBuilder<UTXO, UTXO, QSortBy> {
});
}
QueryBuilder<UTXO, UTXO, QAfterSortBy> sortByUsed() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'used', Sort.asc);
});
}
QueryBuilder<UTXO, UTXO, QAfterSortBy> sortByUsedDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'used', Sort.desc);
});
}
QueryBuilder<UTXO, UTXO, QAfterSortBy> sortByValue() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'value', Sort.asc);
@ -1895,6 +2216,18 @@ extension UTXOQuerySortBy on QueryBuilder<UTXO, UTXO, QSortBy> {
}
extension UTXOQuerySortThenBy on QueryBuilder<UTXO, UTXO, QSortThenBy> {
QueryBuilder<UTXO, UTXO, QAfterSortBy> thenByAddress() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'address', Sort.asc);
});
}
QueryBuilder<UTXO, UTXO, QAfterSortBy> thenByAddressDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'address', Sort.desc);
});
}
QueryBuilder<UTXO, UTXO, QAfterSortBy> thenByBlockHash() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'blockHash', Sort.asc);
@ -2015,6 +2348,18 @@ extension UTXOQuerySortThenBy on QueryBuilder<UTXO, UTXO, QSortThenBy> {
});
}
QueryBuilder<UTXO, UTXO, QAfterSortBy> thenByUsed() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'used', Sort.asc);
});
}
QueryBuilder<UTXO, UTXO, QAfterSortBy> thenByUsedDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'used', Sort.desc);
});
}
QueryBuilder<UTXO, UTXO, QAfterSortBy> thenByValue() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'value', Sort.asc);
@ -2053,6 +2398,13 @@ extension UTXOQuerySortThenBy on QueryBuilder<UTXO, UTXO, QSortThenBy> {
}
extension UTXOQueryWhereDistinct on QueryBuilder<UTXO, UTXO, QDistinct> {
QueryBuilder<UTXO, UTXO, QDistinct> distinctByAddress(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'address', caseSensitive: caseSensitive);
});
}
QueryBuilder<UTXO, UTXO, QDistinct> distinctByBlockHash(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
@ -2113,6 +2465,12 @@ extension UTXOQueryWhereDistinct on QueryBuilder<UTXO, UTXO, QDistinct> {
});
}
QueryBuilder<UTXO, UTXO, QDistinct> distinctByUsed() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'used');
});
}
QueryBuilder<UTXO, UTXO, QDistinct> distinctByValue() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'value');
@ -2140,6 +2498,12 @@ extension UTXOQueryProperty on QueryBuilder<UTXO, UTXO, QQueryProperty> {
});
}
QueryBuilder<UTXO, String?, QQueryOperations> addressProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'address');
});
}
QueryBuilder<UTXO, String?, QQueryOperations> blockHashProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'blockHash');
@ -2194,6 +2558,12 @@ extension UTXOQueryProperty on QueryBuilder<UTXO, UTXO, QQueryProperty> {
});
}
QueryBuilder<UTXO, bool?, QQueryOperations> usedProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'used');
});
}
QueryBuilder<UTXO, int, QQueryOperations> valueProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'value');

View file

@ -0,0 +1,439 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/db/main_db.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/pages/coin_control/utxo_card.dart';
import 'package:stackwallet/pages/coin_control/utxo_details_view.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/mixins/coin_control_interface.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/toggle.dart';
import 'package:tuple/tuple.dart';
enum CoinControlViewType {
manage,
use;
}
class CoinControlView extends ConsumerStatefulWidget {
const CoinControlView({
Key? key,
required this.walletId,
required this.type,
this.requestedTotal,
this.selectedUTXOs,
}) : super(key: key);
static const routeName = "/coinControl";
final String walletId;
final CoinControlViewType type;
final int? requestedTotal;
final Set<UTXO>? selectedUTXOs;
@override
ConsumerState<CoinControlView> createState() => _CoinControlViewState();
}
class _CoinControlViewState extends ConsumerState<CoinControlView> {
bool _showBlocked = false;
final Set<UTXO> _selectedAvailable = {};
final Set<UTXO> _selectedBlocked = {};
Future<void> _refreshBalance() async {
final coinControlInterface = ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as CoinControlInterface;
await coinControlInterface.refreshBalance(notify: true);
}
@override
void initState() {
if (widget.selectedUTXOs != null) {
_selectedAvailable.addAll(widget.selectedUTXOs!);
}
super.initState();
}
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
final coin = ref.watch(
walletsChangeNotifierProvider.select(
(value) => value
.getManager(
widget.walletId,
)
.coin,
),
);
final currentChainHeight = ref.watch(
walletsChangeNotifierProvider.select(
(value) => value
.getManager(
widget.walletId,
)
.currentHeight,
),
);
final ids = MainDB.instance
.getUTXOs(widget.walletId)
.filter()
.isBlockedEqualTo(_showBlocked)
.and()
.group(
(q) => q.usedIsNull().or().usedEqualTo(false),
)
.idProperty()
.findAllSync();
return WillPopScope(
onWillPop: () async {
unawaited(_refreshBalance());
Navigator.of(context).pop(
widget.type == CoinControlViewType.use ? _selectedAvailable : null);
return false;
},
child: Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: widget.type == CoinControlViewType.use &&
_selectedAvailable.isNotEmpty
? AppBarIconButton(
icon: XIcon(
width: 24,
height: 24,
color: Theme.of(context)
.extension<StackColors>()!
.topNavIconPrimary,
),
onPressed: () {
setState(() {
_selectedAvailable.clear();
});
},
)
: AppBarBackButton(
onPressed: () {
unawaited(_refreshBalance());
Navigator.of(context).pop(
widget.type == CoinControlViewType.use
? _selectedAvailable
: null);
},
),
title: Text(
"Coin control",
style: STextStyles.navBarTitle(context),
),
titleSpacing: 0,
),
body: SafeArea(
child: Column(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
child: Column(
children: [
const SizedBox(
height: 10,
),
RoundedWhiteContainer(
child: Text(
"This option allows you to control, freeze, and utilize "
"outputs at your discretion. Tap the output circle to "
"select.",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
),
const SizedBox(
height: 10,
),
SizedBox(
height: 48,
child: Toggle(
key: UniqueKey(),
onColor: Theme.of(context)
.extension<StackColors>()!
.popupBG,
onText: "Available outputs",
offColor: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
offText: "Frozen outputs",
isOn: _showBlocked,
onValueChanged: (value) {
setState(() {
_showBlocked = value;
});
},
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
),
const SizedBox(
height: 10,
),
Expanded(
child: ListView.separated(
itemCount: ids.length,
separatorBuilder: (context, _) => const SizedBox(
height: 10,
),
itemBuilder: (context, index) {
final utxo = MainDB.instance.isar.utxos
.where()
.idEqualTo(ids[index])
.findFirstSync()!;
final isSelected = _showBlocked
? _selectedBlocked.contains(utxo)
: _selectedAvailable.contains(utxo);
return UtxoCard(
key: Key(
"${utxo.walletId}_${utxo.id}_$isSelected"),
walletId: widget.walletId,
utxo: utxo,
canSelect: widget.type ==
CoinControlViewType.manage ||
(widget.type == CoinControlViewType.use &&
!_showBlocked &&
utxo.isConfirmed(
currentChainHeight,
coin.requiredConfirmations,
)),
initialSelectedState: isSelected,
onSelectedChanged: (value) {
if (value) {
_showBlocked
? _selectedBlocked.add(utxo)
: _selectedAvailable.add(utxo);
} else {
_showBlocked
? _selectedBlocked.remove(utxo)
: _selectedAvailable.remove(utxo);
}
setState(() {});
},
onPressed: () async {
final result =
await Navigator.of(context).pushNamed(
UtxoDetailsView.routeName,
arguments: Tuple2(
utxo.id,
widget.walletId,
),
);
if (mounted && result == "refresh") {
setState(() {});
}
},
);
},
),
),
],
),
),
),
if (((_showBlocked && _selectedBlocked.isNotEmpty) ||
(!_showBlocked && _selectedAvailable.isNotEmpty)) &&
widget.type == CoinControlViewType.manage)
Container(
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.backgroundAppBar,
boxShadow: [
Theme.of(context)
.extension<StackColors>()!
.standardBoxShadow,
],
),
child: Padding(
padding: const EdgeInsets.all(16),
child: SecondaryButton(
label: _showBlocked ? "Unfreeze" : "Freeze",
onPressed: () async {
if (_showBlocked) {
await MainDB.instance.putUTXOs(_selectedBlocked
.map(
(e) => e.copyWith(
isBlocked: false,
),
)
.toList());
_selectedBlocked.clear();
} else {
await MainDB.instance.putUTXOs(_selectedAvailable
.map(
(e) => e.copyWith(
isBlocked: true,
),
)
.toList());
_selectedAvailable.clear();
}
setState(() {});
},
),
),
),
if (!_showBlocked && widget.type == CoinControlViewType.use)
Container(
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.backgroundAppBar,
boxShadow: [
Theme.of(context)
.extension<StackColors>()!
.standardBoxShadow,
],
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
"Selected amount",
style: STextStyles.w600_14(context),
),
Builder(
builder: (context) {
int selectedSum =
_selectedAvailable.isEmpty
? 0
: _selectedAvailable
.map((e) => e.value)
.reduce(
(value, element) =>
value += element,
);
return Text(
"${Format.satoshisToAmount(
selectedSum,
coin: coin,
).toStringAsFixed(
coin.decimals,
)} ${coin.ticker}",
style: widget.requestedTotal == null
? STextStyles.w600_14(context)
: STextStyles.w600_14(context).copyWith(
color: selectedSum >=
widget
.requestedTotal!
? Theme.of(context)
.extension<
StackColors>()!
.accentColorGreen
: Theme.of(context)
.extension<
StackColors>()!
.accentColorRed),
);
},
),
],
),
),
if (widget.requestedTotal != null)
Container(
width: double.infinity,
height: 1.5,
color: Theme.of(context)
.extension<StackColors>()!
.backgroundAppBar,
),
if (widget.requestedTotal != null)
Padding(
padding: const EdgeInsets.all(12),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
"Amount to send",
style: STextStyles.w600_14(context),
),
Text(
"${Format.satoshisToAmount(
widget.requestedTotal!,
coin: coin,
).toStringAsFixed(
coin.decimals,
)} ${coin.ticker}",
style: STextStyles.w600_14(context),
),
],
),
),
],
),
),
const SizedBox(
height: 12,
),
PrimaryButton(
label: "Use coins",
enabled: _selectedAvailable.isNotEmpty,
onPressed: () async {
Navigator.of(context).pop(
_selectedAvailable,
);
},
),
],
),
),
),
],
),
),
),
),
);
}
}

View file

@ -0,0 +1,161 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/db/main_db.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/icon_widgets/utxo_status_icon.dart';
import 'package:stackwallet/widgets/rounded_container.dart';
class UtxoCard extends ConsumerStatefulWidget {
const UtxoCard({
Key? key,
required this.utxo,
required this.walletId,
required this.onSelectedChanged,
required this.initialSelectedState,
required this.canSelect,
this.onPressed,
}) : super(key: key);
final String walletId;
final UTXO utxo;
final void Function(bool) onSelectedChanged;
final bool initialSelectedState;
final VoidCallback? onPressed;
final bool canSelect;
@override
ConsumerState<UtxoCard> createState() => _UtxoCardState();
}
class _UtxoCardState extends ConsumerState<UtxoCard> {
late final UTXO utxo;
late bool _selected;
@override
void initState() {
_selected = widget.initialSelectedState;
utxo = widget.utxo;
super.initState();
}
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
final coin = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(widget.walletId).coin));
final currentChainHeight = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(widget.walletId).currentHeight));
String? label;
if (utxo.address != null) {
label = MainDB.instance.isar.addressLabels
.where()
.addressStringWalletIdEqualTo(utxo.address!, widget.walletId)
.findFirstSync()
?.value;
if (label != null && label.isEmpty) {
label = null;
}
}
return ConditionalParent(
condition: widget.onPressed != null,
builder: (child) => MaterialButton(
padding: const EdgeInsets.all(0),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
color: Theme.of(context).extension<StackColors>()!.popupBG,
elevation: 0,
disabledElevation: 0,
hoverElevation: 0,
focusElevation: 0,
highlightElevation: 0,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(Constants.size.circularBorderRadius),
),
onPressed: widget.onPressed,
child: child,
),
child: RoundedContainer(
color: widget.onPressed == null
? Theme.of(context).extension<StackColors>()!.popupBG
: Colors.transparent,
child: Row(
children: [
ConditionalParent(
condition: widget.canSelect,
builder: (child) => GestureDetector(
onTap: () {
_selected = !_selected;
widget.onSelectedChanged(_selected);
setState(() {});
},
child: child,
),
child: UTXOStatusIcon(
blocked: utxo.isBlocked,
status: utxo.isConfirmed(
currentChainHeight,
coin.requiredConfirmations,
)
? UTXOStatusIconStatus.confirmed
: UTXOStatusIconStatus.unconfirmed,
background: Theme.of(context).extension<StackColors>()!.popupBG,
selected: _selected,
width: 32,
height: 32,
),
),
const SizedBox(
width: 10,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
"${Format.satoshisToAmount(
utxo.value,
coin: coin,
).toStringAsFixed(coin.decimals)} ${coin.ticker}",
style: STextStyles.w600_14(context),
),
const SizedBox(
height: 2,
),
Row(
children: [
Flexible(
child: Text(
label ?? utxo.address ?? utxo.txid,
style: STextStyles.w500_12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
),
],
),
],
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,417 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/db/main_db.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_edit_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class UtxoDetailsView extends ConsumerStatefulWidget {
const UtxoDetailsView({
Key? key,
required this.utxoId,
required this.walletId,
}) : super(key: key);
static const routeName = "/utxoDetails";
final Id utxoId;
final String walletId;
@override
ConsumerState<UtxoDetailsView> createState() => _UtxoDetailsViewState();
}
class _UtxoDetailsViewState extends ConsumerState<UtxoDetailsView> {
static const double _spacing = 12;
late Stream<UTXO?> streamUTXO;
UTXO? utxo;
Stream<AddressLabel?>? streamLabel;
AddressLabel? label;
bool _popWithRefresh = false;
Future<void> _toggleFreeze() async {
_popWithRefresh = true;
await MainDB.instance.putUTXO(utxo!.copyWith(isBlocked: !utxo!.isBlocked));
}
@override
void initState() {
utxo = MainDB.instance.isar.utxos
.where()
.idEqualTo(widget.utxoId)
.findFirstSync()!;
streamUTXO = MainDB.instance.watchUTXO(id: widget.utxoId);
if (utxo?.address != null) {
label = MainDB.instance.getAddressLabelSync(
widget.walletId,
utxo!.address!,
);
if (label != null) {
streamLabel = MainDB.instance.watchAddressLabel(id: label!.id);
}
}
super.initState();
}
@override
Widget build(BuildContext context) {
final coin = ref.watch(
walletsChangeNotifierProvider.select(
(value) => value.getManager(widget.walletId).coin,
),
);
final currentHeight = ref.watch(
walletsChangeNotifierProvider.select(
(value) => value.getManager(widget.walletId).currentHeight,
),
);
final confirmed = utxo!.isConfirmed(
currentHeight,
coin.requiredConfirmations,
);
return ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop(_popWithRefresh ? "refresh" : null);
},
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: child,
),
),
),
);
},
),
),
),
),
child: StreamBuilder<UTXO?>(
stream: streamUTXO,
builder: (context, snapshot) {
if (snapshot.hasData) {
utxo = snapshot.data!;
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(
height: 10,
),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${Format.satoshisToAmount(
utxo!.value,
coin: coin,
).toStringAsFixed(
coin.decimals,
)} ${coin.ticker}",
style: STextStyles.pageTitleH2(context),
),
Text(
utxo!.isBlocked
? "Frozen"
: confirmed
? "Available"
: "Unconfirmed",
style: STextStyles.w500_14(context).copyWith(
color: utxo!.isBlocked
? const Color(0xFF7FA2D4) // todo theme
: confirmed
? Theme.of(context)
.extension<StackColors>()!
.accentColorGreen
: Theme.of(context)
.extension<StackColors>()!
.accentColorYellow,
),
),
],
),
),
if (label != null && label!.value.isNotEmpty)
const SizedBox(
height: _spacing,
),
if (label != null && label!.value.isNotEmpty)
RoundedWhiteContainer(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Address label",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
SimpleCopyButton(
data: label!.value,
),
],
),
const SizedBox(
height: 4,
),
Text(
label!.value,
style: STextStyles.w500_14(context),
),
],
),
),
const SizedBox(
height: _spacing,
),
RoundedWhiteContainer(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Address",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
SimpleCopyButton(
data: utxo!.address!,
),
],
),
const SizedBox(
height: 4,
),
Text(
utxo!.address!,
style: STextStyles.w500_14(context),
),
],
),
),
const SizedBox(
height: _spacing,
),
RoundedWhiteContainer(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Transaction ID",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
SimpleCopyButton(
data: utxo!.txid,
),
],
),
const SizedBox(
height: 4,
),
Text(
utxo!.txid,
style: STextStyles.w500_14(context),
),
],
),
),
const SizedBox(
height: _spacing,
),
RoundedWhiteContainer(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Confirmations",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
const SizedBox(
height: 4,
),
Text(
"${utxo!.getConfirmations(currentHeight)}",
style: STextStyles.w500_14(context),
),
],
),
),
const SizedBox(
height: _spacing,
),
RoundedWhiteContainer(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Note",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
SimpleEditButton(
editValue: utxo!.name,
editLabel: "note",
onValueChanged: (newName) {
MainDB.instance.putUTXO(
utxo!.copyWith(
name: newName,
),
);
},
),
],
),
const SizedBox(
height: 4,
),
Text(
utxo!.name,
style: STextStyles.w500_14(context),
),
],
),
),
const SizedBox(
height: _spacing,
),
if (utxo!.isBlocked)
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
RoundedWhiteContainer(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Freeze reason",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
SimpleEditButton(
editValue: utxo!.blockedReason ?? "",
editLabel: "freeze reason",
onValueChanged: (newReason) {
MainDB.instance.putUTXO(
utxo!.copyWith(
blockedReason: newReason,
),
);
},
),
],
),
const SizedBox(
height: 4,
),
Text(
utxo!.blockedReason ?? "",
style: STextStyles.w500_14(context),
),
],
),
),
const SizedBox(
height: _spacing,
),
],
),
const Spacer(),
SecondaryButton(
label: utxo!.isBlocked ? "Unfreeze" : "Freeze",
onPressed: _toggleFreeze,
),
const SizedBox(
height: 16,
),
],
);
},
),
);
}
}

View file

@ -377,12 +377,46 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
padding: isDesktop
? const EdgeInsets.all(16)
: const EdgeInsets.all(12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Status",
style: STextStyles.itemSubtitle(context),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Status",
style: STextStyles.itemSubtitle(context),
),
if (trade.exchangeName ==
MajesticBankExchange.exchangeName &&
trade.status == "Completed")
Row(
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
onTap: () {
showDialog<void>(
context: context,
builder: (context) => const StackOkDialog(
title: "Trade Info",
message:
"Majestic Bank does not store order data indefinitely",
),
);
},
child: SvgPicture.asset(
Assets.svg.circleInfo,
height: 20,
width: 20,
color: Theme.of(context)
.extension<StackColors>()!
.infoItemIcons,
),
),
],
)
],
),
const SizedBox(
height: 4,
@ -395,8 +429,6 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
.colorForStatus(trade.status),
),
),
// ),
// ),
],
),
),
@ -636,11 +668,15 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
text: address,
),
);
unawaited(showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
context: context,
));
if (mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
context: context,
),
);
}
},
child: Row(
children: [
@ -1091,11 +1127,15 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
onTap: () async {
final data = ClipboardData(text: trade.tradeId);
await clipboard.setData(data);
unawaited(showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
context: context,
));
if (mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
context: context,
),
);
}
},
child: SvgPicture.asset(
Assets.svg.copy,

View file

@ -0,0 +1,204 @@
import 'package:flutter/material.dart';
import 'package:flutter_native_splash/cli_commands.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class SingleFieldEditView extends StatefulWidget {
const SingleFieldEditView({
Key? key,
required this.initialValue,
required this.label,
}) : super(key: key);
static const String routeName = "/singleFieldEdit";
final String initialValue;
final String label;
@override
State<SingleFieldEditView> createState() => _SingleFieldEditViewState();
}
class _SingleFieldEditViewState extends State<SingleFieldEditView> {
late final TextEditingController _textController;
final _textFocusNode = FocusNode();
late final bool isDesktop;
@override
void initState() {
isDesktop = Util.isDesktop;
_textController = TextEditingController()..text = widget.initialValue;
super.initState();
}
@override
void dispose() {
_textController.dispose();
_textFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: !isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
"Edit ${widget.label}",
style: STextStyles.navBarTitle(context),
),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: child,
),
),
);
},
),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (!isDesktop)
const SizedBox(
height: 10,
),
if (isDesktop)
Padding(
padding: const EdgeInsets.only(
left: 32,
bottom: 12,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Edit ${widget.label}",
style: STextStyles.desktopH3(context),
),
const DesktopDialogCloseButton(),
],
),
),
Padding(
padding: isDesktop
? const EdgeInsets.symmetric(
horizontal: 32,
)
: const EdgeInsets.all(0),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: _textController,
style: isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveText,
height: 1.8,
)
: STextStyles.field(context),
focusNode: _textFocusNode,
decoration: standardInputDecoration(
widget.label.capitalize(),
_textFocusNode,
context,
desktopMed: isDesktop,
).copyWith(
contentPadding: isDesktop
? const EdgeInsets.only(
left: 16,
top: 11,
bottom: 12,
right: 5,
)
: null,
suffixIcon: _textController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
_textController.text = "";
});
},
),
],
),
),
)
: null,
),
),
),
),
// if (!isDesktop)
const Spacer(),
ConditionalParent(
condition: isDesktop,
builder: (child) => Padding(
padding: const EdgeInsets.all(32),
child: child,
),
child: PrimaryButton(
label: "Save",
onPressed: () {
if (mounted) {
Navigator.of(context).pop(_textController.text);
}
},
),
)
],
),
);
}
}

View file

@ -21,13 +21,13 @@ import 'package:stackwallet/widgets/rounded_white_container.dart';
class AddressCard extends StatefulWidget {
const AddressCard({
Key? key,
required this.address,
required this.addressId,
required this.walletId,
required this.coin,
this.clipboard = const ClipboardWrapper(),
}) : super(key: key);
final Address address;
final int addressId;
final String walletId;
final Coin coin;
final ClipboardInterface clipboard;
@ -38,18 +38,23 @@ class AddressCard extends StatefulWidget {
class _AddressCardState extends State<AddressCard> {
late Stream<AddressLabel?> stream;
late final Address address;
AddressLabel? label;
@override
void initState() {
label = MainDB.instance
.getAddressLabelSync(widget.walletId, widget.address.value);
address = MainDB.instance.isar.addresses
.where()
.idEqualTo(widget.addressId)
.findFirstSync()!;
label = MainDB.instance.getAddressLabelSync(widget.walletId, address.value);
Id? id = label?.id;
if (id == null) {
label = AddressLabel(
walletId: widget.walletId,
addressString: widget.address.value,
addressString: address.value,
value: "",
);
id = MainDB.instance.putAddressLabelSync(label!);
@ -84,7 +89,7 @@ class _AddressCardState extends State<AddressCard> {
onTap: () {
Navigator.of(context).pushNamed(
EditAddressLabelView.routeName,
arguments: label!,
arguments: label!.id,
);
},
),
@ -97,7 +102,7 @@ class _AddressCardState extends State<AddressCard> {
children: [
Expanded(
child: SelectableText(
widget.address.value,
address.value,
style: STextStyles.itemSubtitle12(context),
),
)
@ -119,16 +124,18 @@ class _AddressCardState extends State<AddressCard> {
onPressed: () async {
await widget.clipboard.setData(
ClipboardData(
text: widget.address.value,
),
);
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
context: context,
text: address.value,
),
);
if (mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
context: context,
),
);
}
},
),
),
@ -147,7 +154,7 @@ class _AddressCardState extends State<AddressCard> {
showDialog<void>(
context: context,
builder: (context) => AddressQrPopup(
addressString: widget.address.value,
addressString: address.value,
coin: widget.coin,
clipboard: widget.clipboard,
),

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/db/main_db.dart';
import 'package:stackwallet/models/isar/models/address_label.dart';
import 'package:stackwallet/utilities/constants.dart';
@ -18,12 +19,12 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart';
class EditAddressLabelView extends ConsumerStatefulWidget {
const EditAddressLabelView({
Key? key,
required this.addressLabel,
required this.addressLabelId,
}) : super(key: key);
static const String routeName = "/editAddressLabel";
final AddressLabel addressLabel;
final int addressLabelId;
@override
ConsumerState<EditAddressLabelView> createState() =>
@ -36,11 +37,17 @@ class _EditAddressLabelViewState extends ConsumerState<EditAddressLabelView> {
late final bool isDesktop;
late AddressLabel addressLabel;
@override
void initState() {
isDesktop = Util.isDesktop;
_labelFieldController = TextEditingController();
_labelFieldController.text = widget.addressLabel.value;
addressLabel = MainDB.instance.isar.addressLabels
.where()
.idEqualTo(widget.addressLabelId)
.findFirstSync()!;
_labelFieldController.text = addressLabel.value;
super.initState();
}
@ -195,7 +202,7 @@ class _EditAddressLabelViewState extends ConsumerState<EditAddressLabelView> {
label: "Save",
onPressed: () async {
await MainDB.instance.updateAddressLabel(
widget.addressLabel.copyWith(
addressLabel.copyWith(
label: _labelFieldController.text,
),
);
@ -209,7 +216,7 @@ class _EditAddressLabelViewState extends ConsumerState<EditAddressLabelView> {
TextButton(
onPressed: () async {
await MainDB.instance.updateAddressLabel(
widget.addressLabel.copyWith(
addressLabel.copyWith(
label: _labelFieldController.text,
),
);

View file

@ -1,96 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/db/main_db.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/pages/receive_view/addresses/address_card.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
class ReceivingAddressesView extends ConsumerWidget {
const ReceivingAddressesView({
Key? key,
required this.walletId,
required this.isDesktop,
this.clipboard = const ClipboardWrapper(),
}) : super(key: key);
static const String routeName = "/receivingAddressesView";
final String walletId;
final bool isDesktop;
final ClipboardInterface clipboard;
@override
Widget build(BuildContext context, WidgetRef ref) {
final coin = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId).coin));
return ConditionalParent(
condition: !isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
backgroundColor:
Theme.of(context).extension<StackColors>()!.backgroundAppBar,
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Receiving addresses",
style: STextStyles.navBarTitle(context),
),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
child: FutureBuilder(
future: MainDB.instance
.getAddresses(walletId)
.filter()
.subTypeEqualTo(AddressSubType.receiving)
.and()
.not()
.typeEqualTo(AddressType.nonWallet)
.sortByDerivationIndex()
.findAll(),
builder: (context, AsyncSnapshot<List<Address>> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.data != null) {
// listview
return ListView.separated(
itemCount: snapshot.data!.length,
separatorBuilder: (_, __) => Container(
height: 10,
),
itemBuilder: (_, index) => AddressCard(
walletId: walletId,
address: snapshot.data![index],
coin: coin,
),
);
} else {
return const Center(
child: LoadingIndicator(
height: 200,
width: 200,
),
);
}
},
),
);
}
}

View file

@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/db/main_db.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/pages/receive_view/addresses/address_card.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
import 'package:stackwallet/widgets/toggle.dart';
class WalletAddressesView extends ConsumerStatefulWidget {
const WalletAddressesView({
Key? key,
required this.walletId,
}) : super(key: key);
static const String routeName = "/walletAddressesView";
final String walletId;
@override
ConsumerState<WalletAddressesView> createState() =>
_WalletAddressesViewState();
}
class _WalletAddressesViewState extends ConsumerState<WalletAddressesView> {
final bool isDesktop = Util.isDesktop;
bool _showChange = false;
@override
Widget build(BuildContext context) {
final coin = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(widget.walletId).coin));
return ConditionalParent(
condition: !isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
backgroundColor:
Theme.of(context).extension<StackColors>()!.backgroundAppBar,
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
titleSpacing: 0,
title: Text(
"Wallet addresses",
style: STextStyles.navBarTitle(context),
),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
child: Column(
children: [
SizedBox(
height: isDesktop ? 56 : 48,
width: isDesktop ? 490 : null,
child: Toggle(
key: UniqueKey(),
onColor: Theme.of(context).extension<StackColors>()!.popupBG,
onText: "Receiving",
offColor: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
offText: "Change",
isOn: _showChange,
onValueChanged: (value) {
setState(() {
_showChange = value;
});
},
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
),
SizedBox(
height: isDesktop ? 20 : 16,
),
Expanded(
child: FutureBuilder(
future: MainDB.instance
.getAddresses(widget.walletId)
.filter()
.group(
(q) => _showChange
? q.subTypeEqualTo(AddressSubType.change)
: q
.subTypeEqualTo(AddressSubType.receiving)
.or()
.subTypeEqualTo(AddressSubType.paynymReceive),
)
.and()
.not()
.typeEqualTo(AddressType.nonWallet)
.sortByDerivationIndex()
.idProperty()
.findAll(),
builder: (context, AsyncSnapshot<List<int>> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.data != null) {
// listview
return ListView.separated(
itemCount: snapshot.data!.length,
separatorBuilder: (_, __) => Container(
height: 10,
),
itemBuilder: (_, index) => AddressCard(
walletId: widget.walletId,
addressId: snapshot.data![index],
coin: coin,
),
);
} else {
return const Center(
child: LoadingIndicator(
height: 200,
width: 200,
),
);
}
},
),
),
],
),
);
}
}

View file

@ -6,7 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/receive_view/addresses/receiving_addresses_view.dart';
import 'package:stackwallet/pages/receive_view/addresses/wallet_addresses_view.dart';
import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/route_generator.dart';
@ -21,7 +21,6 @@ import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/custom_loading_overlay.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:tuple/tuple.dart';
class ReceiveView extends ConsumerStatefulWidget {
const ReceiveView({
@ -182,8 +181,8 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed(
ReceivingAddressesView.routeName,
arguments: Tuple2(walletId, false),
WalletAddressesView.routeName,
arguments: walletId,
);
},
child: RoundedWhiteContainer(

View file

@ -7,9 +7,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/models/paynym/paynym_account_lite.dart';
import 'package:stackwallet/models/send_view_auto_fill_data.dart';
import 'package:stackwallet/pages/address_book_views/address_book_view.dart';
import 'package:stackwallet/pages/coin_control/coin_control_view.dart';
import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart';
import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart';
import 'package:stackwallet/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart';
@ -43,9 +45,11 @@ import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
import 'package:tuple/tuple.dart';
class SendView extends ConsumerStatefulWidget {
const SendView({
@ -104,6 +108,8 @@ class _SendViewState extends ConsumerState<SendView> {
Decimal? _cachedBalance;
Set<UTXO> selectedUTXOs = {};
void _cryptoAmountChanged() async {
if (!_cryptoAmountChangeLock) {
final String cryptoAmount = cryptoAmountController.text;
@ -313,51 +319,59 @@ class _SendViewState extends ConsumerState<SendView> {
Format.decimalAmountToSatoshis(manager.balance.getSpendable(), coin);
}
// confirm send all
if (amount == availableBalance) {
final bool? shouldSendAll = await showDialog<bool>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return StackDialog(
title: "Confirm send all",
message:
"You are about to send your entire balance. Would you like to continue?",
leftButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(context),
child: Text(
"Cancel",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
onPressed: () {
Navigator.of(context).pop(false);
},
),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context),
child: Text(
"Yes",
style: STextStyles.button(context),
),
onPressed: () {
Navigator.of(context).pop(true);
},
),
);
},
);
final coinControlEnabled =
ref.read(prefsChangeNotifierProvider).enableCoinControl;
if (shouldSendAll == null || shouldSendAll == false) {
// cancel preview
return;
if (!(manager.hasCoinControlSupport && coinControlEnabled) ||
(manager.hasCoinControlSupport &&
coinControlEnabled &&
selectedUTXOs.isEmpty)) {
// confirm send all
if (amount == availableBalance) {
final bool? shouldSendAll = await showDialog<bool>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return StackDialog(
title: "Confirm send all",
message:
"You are about to send your entire balance. Would you like to continue?",
leftButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(context),
child: Text(
"Cancel",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
onPressed: () {
Navigator.of(context).pop(false);
},
),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context),
child: Text(
"Yes",
style: STextStyles.button(context),
),
onPressed: () {
Navigator.of(context).pop(true);
},
),
);
},
);
if (shouldSendAll == null || shouldSendAll == false) {
// cancel preview
return;
}
}
}
@ -393,7 +407,14 @@ class _SendViewState extends ConsumerState<SendView> {
txData = await wallet.preparePaymentCodeSend(
paymentCode: paymentCode,
satoshiAmount: amount,
args: {"feeRate": feeRate},
args: {
"feeRate": feeRate,
"UTXOs": (manager.hasCoinControlSupport &&
coinControlEnabled &&
selectedUTXOs.isNotEmpty)
? selectedUTXOs
: null,
},
);
} else if ((coin == Coin.firo || coin == Coin.firoTestNet) &&
ref.read(publicPrivateBalanceStateProvider.state).state !=
@ -407,7 +428,14 @@ class _SendViewState extends ConsumerState<SendView> {
txData = await manager.prepareSend(
address: _address!,
satoshiAmount: amount,
args: {"feeRate": ref.read(feeRateTypeStateProvider)},
args: {
"feeRate": ref.read(feeRateTypeStateProvider),
"UTXOs": (manager.hasCoinControlSupport &&
coinControlEnabled &&
selectedUTXOs.isNotEmpty)
? selectedUTXOs
: null,
},
);
}
@ -565,6 +593,17 @@ class _SendViewState extends ConsumerState<SendView> {
final String locale = ref.watch(
localeServiceChangeNotifierProvider.select((value) => value.locale));
final showCoinControl = ref.watch(
walletsChangeNotifierProvider.select(
(value) => value.getManager(walletId).hasCoinControlSupport,
),
) &&
ref.watch(
prefsChangeNotifierProvider.select(
(value) => value.enableCoinControl,
),
);
if (coin == Coin.firo || coin == Coin.firoTestNet) {
ref.listen(publicPrivateBalanceStateProvider, (previous, next) {
if (_amountToSend == null) {
@ -1506,6 +1545,56 @@ class _SendViewState extends ConsumerState<SendView> {
),
),
),
if (showCoinControl)
const SizedBox(
height: 8,
),
if (showCoinControl)
RoundedWhiteContainer(
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
"Coin control",
style:
STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
CustomTextButton(
text: selectedUTXOs.isEmpty
? "Select coins"
: "Selected coins (${selectedUTXOs.length})",
onTap: () async {
final result =
await Navigator.of(context).pushNamed(
CoinControlView.routeName,
arguments: Tuple4(
walletId,
CoinControlViewType.use,
_amountToSend != null
? Format.decimalAmountToSatoshis(
_amountToSend!,
coin,
)
: null,
selectedUTXOs,
),
);
if (result is Set<UTXO>) {
setState(() {
selectedUTXOs = result;
});
}
},
),
],
),
),
const SizedBox(
height: 12,
),

View file

@ -121,6 +121,53 @@ class AdvancedSettingsView extends StatelessWidget {
const SizedBox(
height: 8,
),
RoundedWhiteContainer(
child: Consumer(
builder: (_, ref, __) {
return RawMaterialButton(
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: null,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Enable coin control",
style: STextStyles.titleBold12(context),
textAlign: TextAlign.left,
),
SizedBox(
height: 20,
width: 40,
child: DraggableSwitchButton(
isOn: ref.watch(
prefsChangeNotifierProvider.select(
(value) => value.enableCoinControl),
),
onValueChanged: (newValue) {
ref
.read(prefsChangeNotifierProvider)
.enableCoinControl = newValue;
},
),
),
],
),
),
);
},
),
),
const SizedBox(
height: 8,
),
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: Consumer(

View file

@ -3,9 +3,11 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/pages/coin_control/coin_control_view.dart';
import 'package:stackwallet/pages/paynym/paynym_claim_view.dart';
import 'package:stackwallet/pages/paynym/paynym_home_view.dart';
import 'package:stackwallet/providers/global/paynym_api_provider.dart';
import 'package:stackwallet/providers/global/prefs_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart';
import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
@ -16,6 +18,7 @@ import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
import 'package:tuple/tuple.dart';
class WalletNavigationBar extends ConsumerStatefulWidget {
const WalletNavigationBar({
@ -50,6 +53,23 @@ class _WalletNavigationBarState extends ConsumerState<WalletNavigationBar> {
@override
Widget build(BuildContext context) {
final showMore = ref.watch(
walletsChangeNotifierProvider.select(
(value) => value.getManager(widget.walletId).hasPaynymSupport,
),
) ||
(ref.watch(
walletsChangeNotifierProvider.select(
(value) =>
value.getManager(widget.walletId).hasCoinControlSupport,
),
) &&
ref.watch(
prefsChangeNotifierProvider.select(
(value) => value.enableCoinControl,
),
));
return Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
@ -97,62 +117,36 @@ class _WalletNavigationBarState extends ConsumerState<WalletNavigationBar> {
// const SizedBox(
// height: 8,
// ),
AnimatedOpacity(
opacity: scale,
duration: duration,
child: Consumer(builder: (context, ref, __) {
return GestureDetector(
onTap: () async {
setState(() {
scale = 0;
});
unawaited(
showDialog(
context: context,
builder: (context) => const LoadingIndicator(
width: 100,
),
),
);
final manager = ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId);
final paynymInterface =
manager.wallet as PaynymWalletInterface;
final code = await paynymInterface.getPaymentCode(
DerivePathTypeExt.primaryFor(manager.coin));
final account = await ref
.read(paynymAPIProvider)
.nym(code.toString());
Logging.instance.log(
"my nym account: $account",
level: LogLevel.Info,
);
if (ref.watch(
walletsChangeNotifierProvider.select(
(value) => value
.getManager(widget.walletId)
.hasCoinControlSupport,
),
) &&
ref.watch(
prefsChangeNotifierProvider.select(
(value) => value.enableCoinControl,
),
))
AnimatedOpacity(
opacity: scale,
duration: duration,
child: GestureDetector(
onTap: () {
if (mounted) {
Navigator.of(context).pop();
// hide more context menu
setState(() {
scale = 0;
});
// check if account exists and for matching code to see if claimed
if (account.value != null &&
account.value!.codes.first.claimed) {
ref.read(myPaynymAccountStateProvider.state).state =
account.value!;
await Navigator.of(context).pushNamed(
PaynymHomeView.routeName,
arguments: widget.walletId,
);
} else {
await Navigator.of(context).pushNamed(
PaynymClaimView.routeName,
arguments: widget.walletId,
);
}
Navigator.of(context).pushNamed(
CoinControlView.routeName,
arguments: Tuple2(
widget.walletId,
CoinControlViewType.manage,
),
);
}
},
child: Container(
@ -174,14 +168,14 @@ class _WalletNavigationBarState extends ConsumerState<WalletNavigationBar> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Paynym",
"Coin control",
style: STextStyles.buttonSmall(context),
),
const SizedBox(
width: 16,
),
SvgPicture.asset(
Assets.svg.robotHead,
Assets.svg.coinControl.gamePad,
height: 20,
width: 20,
color: Theme.of(context)
@ -191,9 +185,129 @@ class _WalletNavigationBarState extends ConsumerState<WalletNavigationBar> {
],
),
),
);
}),
),
),
),
if (ref.watch(
walletsChangeNotifierProvider.select(
(value) => value
.getManager(widget.walletId)
.hasCoinControlSupport,
),
) &&
ref.watch(
prefsChangeNotifierProvider.select(
(value) => value.enableCoinControl,
),
) &&
ref.watch(
walletsChangeNotifierProvider.select(
(value) =>
value.getManager(widget.walletId).hasPaynymSupport,
),
))
const SizedBox(
height: 8,
),
if (ref.watch(walletsChangeNotifierProvider.select((value) =>
value.getManager(widget.walletId).hasPaynymSupport)))
AnimatedOpacity(
opacity: scale,
duration: duration,
child: Consumer(builder: (context, ref, __) {
return GestureDetector(
onTap: () async {
setState(() {
scale = 0;
});
unawaited(
showDialog(
context: context,
builder: (context) => const LoadingIndicator(
width: 100,
),
),
);
final manager = ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId);
final paynymInterface =
manager.wallet as PaynymWalletInterface;
final code = await paynymInterface.getPaymentCode(
DerivePathTypeExt.primaryFor(manager.coin));
final account = await ref
.read(paynymAPIProvider)
.nym(code.toString());
Logging.instance.log(
"my nym account: $account",
level: LogLevel.Info,
);
if (mounted) {
Navigator.of(context).pop();
// check if account exists and for matching code to see if claimed
if (account.value != null &&
account.value!.codes.first.claimed) {
ref.read(myPaynymAccountStateProvider.state).state =
account.value!;
await Navigator.of(context).pushNamed(
PaynymHomeView.routeName,
arguments: widget.walletId,
);
} else {
await Navigator.of(context).pushNamed(
PaynymClaimView.routeName,
arguments: widget.walletId,
);
}
}
},
child: Container(
padding: const EdgeInsets.all(16),
width: 146,
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.popupBG,
boxShadow: [
Theme.of(context)
.extension<StackColors>()!
.standardBoxShadow
],
borderRadius: BorderRadius.circular(
widget.height / 2.0,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Paynym",
style: STextStyles.buttonSmall(context),
),
const SizedBox(
width: 16,
),
SvgPicture.asset(
Assets.svg.robotHead,
height: 20,
width: 20,
color: Theme.of(context)
.extension<StackColors>()!
.bottomNavIconIcon,
),
],
),
),
);
}),
),
const SizedBox(
height: 8,
),
@ -410,8 +524,7 @@ class _WalletNavigationBarState extends ConsumerState<WalletNavigationBar> {
),
),
),
if (ref.watch(walletsChangeNotifierProvider.select((value) =>
value.getManager(widget.walletId).hasPaynymSupport)))
if (showMore)
RawMaterialButton(
constraints: const BoxConstraints(
minWidth: 66,

View file

@ -48,8 +48,10 @@ class WalletSummary extends StatelessWidget {
builder: (_, ref, __) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).extension<StackColors>()!.colorForCoin(ref
.watch(managerProvider.select((value) => value.coin))),
color: Theme.of(context)
.extension<StackColors>()!
.colorForCoin(ref.watch(
managerProvider.select((value) => value.coin))),
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
@ -112,7 +114,6 @@ class WalletSummary extends StatelessWidget {
padding: const EdgeInsets.all(16.0),
child: WalletSummaryInfo(
walletId: walletId,
managerProvider: managerProvider,
initialSyncStatus: initialSyncStatus,
),
),

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -7,35 +9,32 @@ import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/services/event_bus/events/global/balance_refreshed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/animated_text.dart';
class WalletSummaryInfo extends StatefulWidget {
class WalletSummaryInfo extends ConsumerStatefulWidget {
const WalletSummaryInfo({
Key? key,
required this.walletId,
required this.managerProvider,
required this.initialSyncStatus,
}) : super(key: key);
final String walletId;
final ChangeNotifierProvider<Manager> managerProvider;
final WalletSyncStatus initialSyncStatus;
@override
State<WalletSummaryInfo> createState() => _WalletSummaryInfoState();
ConsumerState<WalletSummaryInfo> createState() => _WalletSummaryInfoState();
}
class _WalletSummaryInfoState extends State<WalletSummaryInfo> {
late final String walletId;
late final ChangeNotifierProvider<Manager> managerProvider;
class _WalletSummaryInfoState extends ConsumerState<WalletSummaryInfo> {
late StreamSubscription<BalanceRefreshedEvent> _balanceUpdated;
void showSheet() {
showModalBottomSheet<dynamic>(
@ -46,251 +45,154 @@ class _WalletSummaryInfoState extends State<WalletSummaryInfo> {
top: Radius.circular(20),
),
),
builder: (_) => WalletBalanceToggleSheet(walletId: walletId),
builder: (_) => WalletBalanceToggleSheet(walletId: widget.walletId),
);
}
Decimal? _balanceTotalCached;
Decimal? _balanceCached;
@override
void initState() {
walletId = widget.walletId;
managerProvider = widget.managerProvider;
_balanceUpdated =
GlobalEventBus.instance.on<BalanceRefreshedEvent>().listen(
(event) async {
if (event.walletId == widget.walletId) {
setState(() {});
}
},
);
super.initState();
}
@override
void dispose() {
_balanceUpdated.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
final externalCalls = ref.watch(
prefsChangeNotifierProvider.select((value) => value.externalCalls));
final coin = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(widget.walletId).coin));
final balance = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(widget.walletId).balance));
final locale = ref.watch(
localeServiceChangeNotifierProvider.select((value) => value.locale));
final baseCurrency = ref
.watch(prefsChangeNotifierProvider.select((value) => value.currency));
final priceTuple = ref.watch(priceAnd24hChangeNotifierProvider
.select((value) => value.getPrice(coin)));
final _showAvailable =
ref.watch(walletBalanceToggleStateProvider.state).state ==
WalletBalanceToggleState.available;
final Decimal totalBalance;
final Decimal availableBalance;
if (coin == Coin.firo || coin == Coin.firoTestNet) {
final firoWallet = ref.watch(walletsChangeNotifierProvider.select(
(value) => value.getManager(widget.walletId).wallet)) as FiroWallet;
totalBalance = firoWallet.balance.getSpendable();
availableBalance = firoWallet.balancePrivate.getSpendable();
} else {
totalBalance = balance.getTotal();
availableBalance = balance.getSpendable();
}
final balanceToShow = _showAvailable ? availableBalance : totalBalance;
return Row(
children: [
Expanded(
child: Consumer(
builder: (_, ref, __) {
final Coin coin =
ref.watch(managerProvider.select((value) => value.coin));
final externalCalls = ref.watch(prefsChangeNotifierProvider
.select((value) => value.externalCalls));
Future<Decimal>? totalBalanceFuture;
Future<Decimal>? availableBalanceFuture;
if (coin == Coin.firo || coin == Coin.firoTestNet) {
final firoWallet =
ref.watch(managerProvider.select((value) => value.wallet))
as FiroWallet;
totalBalanceFuture =
Future(() => firoWallet.balance.getSpendable());
availableBalanceFuture =
Future(() => firoWallet.balancePrivate.getSpendable());
} else {
final manager = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId)));
totalBalanceFuture = Future(() => manager.balance.getTotal());
availableBalanceFuture =
Future(() => manager.balance.getSpendable());
}
final locale = ref.watch(localeServiceChangeNotifierProvider
.select((value) => value.locale));
final baseCurrency = ref.watch(prefsChangeNotifierProvider
.select((value) => value.currency));
final priceTuple = ref.watch(priceAnd24hChangeNotifierProvider
.select((value) => value.getPrice(coin)));
final _showAvailable =
ref.watch(walletBalanceToggleStateProvider.state).state ==
WalletBalanceToggleState.available;
return FutureBuilder(
future: _showAvailable
? availableBalanceFuture
: totalBalanceFuture,
builder: (fbContext, AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData &&
snapshot.data != null) {
if (_showAvailable) {
_balanceCached = snapshot.data!;
} else {
_balanceTotalCached = snapshot.data!;
}
}
Decimal? balanceToShow =
_showAvailable ? _balanceCached : _balanceTotalCached;
if (balanceToShow != null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: showSheet,
child: Row(
children: [
if (coin == Coin.firo || coin == Coin.firoTestNet)
Text(
"${_showAvailable ? "Private" : "Public"} Balance",
style:
STextStyles.subtitle500(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
if (coin != Coin.firo && coin != Coin.firoTestNet)
Text(
"${_showAvailable ? "Available" : "Full"} Balance",
style:
STextStyles.subtitle500(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
const SizedBox(
width: 4,
),
SvgPicture.asset(
Assets.svg.chevronDown,
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
width: 8,
height: 4,
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: showSheet,
child: Row(
children: [
if (coin == Coin.firo || coin == Coin.firoTestNet)
Text(
"${_showAvailable ? "Private" : "Public"} Balance",
style: STextStyles.subtitle500(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
const Spacer(),
FittedBox(
fit: BoxFit.scaleDown,
child: SelectableText(
"${Format.localizedStringAsFixed(
value: balanceToShow,
locale: locale,
decimalPlaces: 8,
)} ${coin.ticker}",
style: STextStyles.pageTitleH1(context).copyWith(
fontSize: 24,
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
),
if (coin != Coin.firo && coin != Coin.firoTestNet)
Text(
"${_showAvailable ? "Available" : "Full"} Balance",
style: STextStyles.subtitle500(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
if (externalCalls)
Text(
"${Format.localizedStringAsFixed(
value: priceTuple.item1 * balanceToShow,
locale: locale,
decimalPlaces: 2,
)} $baseCurrency",
style: STextStyles.subtitle500(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
],
);
} else {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: showSheet,
child: Row(
children: [
if (coin == Coin.firo || coin == Coin.firoTestNet)
Text(
"${_showAvailable ? "Private" : "Public"} Balance",
style:
STextStyles.subtitle500(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
if (coin != Coin.firo && coin != Coin.firoTestNet)
Text(
"${_showAvailable ? "Available" : "Full"} Balance",
style:
STextStyles.subtitle500(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
const SizedBox(
width: 4,
),
SvgPicture.asset(
Assets.svg.chevronDown,
width: 8,
height: 4,
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
],
),
),
const Spacer(),
AnimatedText(
stringsToLoopThrough: const [
"Loading balance",
"Loading balance.",
"Loading balance..",
"Loading balance..."
],
style: STextStyles.pageTitleH1(context).copyWith(
fontSize: 24,
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
AnimatedText(
stringsToLoopThrough: const [
"Loading balance",
"Loading balance.",
"Loading balance..",
"Loading balance..."
],
style: STextStyles.subtitle500(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
],
);
}
},
);
},
),
const SizedBox(
width: 4,
),
SvgPicture.asset(
Assets.svg.chevronDown,
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
width: 8,
height: 4,
),
],
),
),
const Spacer(),
FittedBox(
fit: BoxFit.scaleDown,
child: SelectableText(
"${Format.localizedStringAsFixed(
value: balanceToShow,
locale: locale,
decimalPlaces: 8,
)} ${coin.ticker}",
style: STextStyles.pageTitleH1(context).copyWith(
fontSize: 24,
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
),
if (externalCalls)
Text(
"${Format.localizedStringAsFixed(
value: priceTuple.item1 * balanceToShow,
locale: locale,
decimalPlaces: 2,
)} $baseCurrency",
style: STextStyles.subtitle500(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
],
),
),
Column(
children: [
Consumer(
builder: (_, ref, __) {
return SvgPicture.asset(
Assets.svg.iconFor(
coin: ref.watch(
managerProvider.select((value) => value.coin),
),
),
width: 24,
height: 24,
);
},
SvgPicture.asset(
Assets.svg.iconFor(
coin: coin,
),
width: 24,
height: 24,
),
const Spacer(),
WalletRefreshButton(
walletId: walletId,
walletId: widget.walletId,
initialSyncStatus: widget.initialSyncStatus,
),
],

View file

@ -814,6 +814,8 @@ class _DesktopTransactionCardRowState
} else {
return "Sending";
}
} else if (type == TransactionType.sentToSelf) {
return "Sent to self";
} else {
return type.name;
}

View file

@ -127,6 +127,8 @@ class _TransactionDetailsViewState
} else {
return "Sending";
}
} else if (type == TransactionType.sentToSelf) {
return "Sent to self";
} else {
return type.name;
}

View file

@ -2,6 +2,7 @@ import 'package:decimal/decimal.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/models/buy/response_objects/quote.dart';
import 'package:stackwallet/models/contact_address_entry.dart';
import 'package:stackwallet/models/exchange/incomplete_exchange.dart';
@ -27,6 +28,8 @@ import 'package:stackwallet/pages/address_book_views/subviews/edit_contact_name_
import 'package:stackwallet/pages/buy_view/buy_in_wallet_view.dart';
import 'package:stackwallet/pages/buy_view/buy_quote_preview.dart';
import 'package:stackwallet/pages/buy_view/buy_view.dart';
import 'package:stackwallet/pages/coin_control/coin_control_view.dart';
import 'package:stackwallet/pages/coin_control/utxo_details_view.dart';
import 'package:stackwallet/pages/exchange_view/choose_from_stack_view.dart';
import 'package:stackwallet/pages/exchange_view/edit_trade_note_view.dart';
import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_1_view.dart';
@ -36,6 +39,7 @@ import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_4_view.
import 'package:stackwallet/pages/exchange_view/send_from_view.dart';
import 'package:stackwallet/pages/exchange_view/trade_details_view.dart';
import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart';
import 'package:stackwallet/pages/generic/single_field_edit_view.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/intro_view.dart';
import 'package:stackwallet/pages/manage_favorites_view/manage_favorites_view.dart';
@ -45,7 +49,7 @@ import 'package:stackwallet/pages/paynym/paynym_claim_view.dart';
import 'package:stackwallet/pages/paynym/paynym_home_view.dart';
import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart';
import 'package:stackwallet/pages/receive_view/addresses/edit_address_label_view.dart';
import 'package:stackwallet/pages/receive_view/addresses/receiving_addresses_view.dart';
import 'package:stackwallet/pages/receive_view/addresses/wallet_addresses_view.dart';
import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart';
import 'package:stackwallet/pages/receive_view/receive_view.dart';
import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart';
@ -130,8 +134,6 @@ import 'package:stackwallet/utilities/enums/add_wallet_type_enum.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:tuple/tuple.dart';
import 'models/isar/models/blockchain_data/transaction.dart';
class RouteGenerator {
static const bool useMaterialPageRoute = true;
@ -200,6 +202,65 @@ class RouteGenerator {
builder: (_) => const AddWalletView(),
settings: RouteSettings(name: settings.name));
case SingleFieldEditView.routeName:
if (args is Tuple2<String, String>) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => SingleFieldEditView(
initialValue: args.item1,
label: args.item2,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case CoinControlView.routeName:
if (args is Tuple2<String, CoinControlViewType>) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => CoinControlView(
walletId: args.item1,
type: args.item2,
),
settings: RouteSettings(
name: settings.name,
),
);
} else if (args
is Tuple4<String, CoinControlViewType, int?, Set<UTXO>?>) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => CoinControlView(
walletId: args.item1,
type: args.item2,
requestedTotal: args.item3,
selectedUTXOs: args.item4,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case UtxoDetailsView.routeName:
if (args is Tuple2<Id, String>) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => UtxoDetailsView(
walletId: args.item2,
utxoId: args.item1,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case PaynymClaimView.routeName:
if (args is String) {
return getRoute(
@ -476,11 +537,11 @@ class RouteGenerator {
return _routeError("${settings.name} invalid args: ${args.toString()}");
case EditAddressLabelView.routeName:
if (args is AddressLabel) {
if (args is int) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => EditAddressLabelView(
addressLabel: args,
addressLabelId: args,
),
settings: RouteSettings(
name: settings.name,
@ -836,13 +897,12 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case ReceivingAddressesView.routeName:
if (args is Tuple2<String, bool>) {
case WalletAddressesView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => ReceivingAddressesView(
walletId: args.item1,
isDesktop: args.item2,
builder: (_) => WalletAddressesView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,

View file

@ -24,6 +24,7 @@ import 'package:stackwallet/services/event_bus/events/global/refresh_percent_cha
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/coin_control_interface.dart';
import 'package:stackwallet/services/mixins/electrum_x_parsing.dart';
import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
import 'package:stackwallet/services/mixins/wallet_cache.dart';
@ -94,7 +95,12 @@ String constructDerivePath({
}
class BitcoinWallet extends CoinServiceAPI
with WalletCache, WalletDB, ElectrumXParsing, PaynymWalletInterface {
with
WalletCache,
WalletDB,
ElectrumXParsing,
PaynymWalletInterface,
CoinControlInterface {
BitcoinWallet({
required String walletId,
required String walletName,
@ -114,6 +120,17 @@ class BitcoinWallet extends CoinServiceAPI
_secureStore = secureStore;
initCache(walletId, coin);
initWalletDB(mockableOverride: mockableOverride);
initCoinControlInterface(
walletId: walletId,
walletName: walletName,
coin: coin,
db: db,
getChainHeight: () => chainHeight,
refreshedBalanceCallback: (balance) async {
_balance = balance;
await updateCachedBalance(_balance!);
},
);
initPaynymWalletInterface(
walletId: walletId,
walletName: walletName,
@ -1106,6 +1123,7 @@ class BitcoinWallet extends CoinServiceAPI
try {
final feeRateType = args?["feeRate"];
final feeRateAmount = args?["feeRateAmount"];
final utxos = args?["UTXOs"] as Set<isar_models.UTXO>?;
if (feeRateType is FeeRateType || feeRateAmount is int) {
late final int rate;
if (feeRateType is FeeRateType) {
@ -1133,8 +1151,14 @@ class BitcoinWallet extends CoinServiceAPI
isSendAll = true;
}
final txData =
await coinSelection(satoshiAmount, rate, address, isSendAll);
final txData = await coinSelection(
satoshiAmountToSend: satoshiAmount,
selectedTxFeeRate: rate,
recipientAddress: address,
isSendAll: isSendAll,
utxos: utxos?.toList(),
coinControl: utxos is List<isar_models.UTXO>,
);
Logging.instance.log("prepare send: $txData", level: LogLevel.Info);
try {
@ -1197,6 +1221,11 @@ class BitcoinWallet extends CoinServiceAPI
final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex);
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
final utxos = txData["usedUTXOs"] as List<isar_models.UTXO>;
// mark utxos as used
await db.putUTXOs(utxos.map((e) => e.copyWith(used: true)).toList());
return txHash;
} catch (e, s) {
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
@ -1790,18 +1819,14 @@ class BitcoinWallet extends CoinServiceAPI
}
}
final currentChainHeight = await chainHeight;
final List<isar_models.UTXO> outputArray = [];
int satoshiBalanceTotal = 0;
int satoshiBalancePending = 0;
int satoshiBalanceSpendable = 0;
int satoshiBalanceBlocked = 0;
for (int i = 0; i < fetchedUtxoList.length; i++) {
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
final jsonUTXO = fetchedUtxoList[i][j];
final txn = await cachedElectrumXClient.getTransaction(
txHash: fetchedUtxoList[i][j]["tx_hash"] as String,
txHash: jsonUTXO["tx_hash"] as String,
verbose: true,
coin: coin,
);
@ -1809,7 +1834,7 @@ class BitcoinWallet extends CoinServiceAPI
// fetch stored tx to see if paynym notification tx and block utxo
final storedTx = await db.getTransaction(
walletId,
fetchedUtxoList[i][j]["tx_hash"] as String,
jsonUTXO["tx_hash"] as String,
);
bool shouldBlock = false;
@ -1826,32 +1851,35 @@ class BitcoinWallet extends CoinServiceAPI
blockReason = "Incoming paynym notification transaction.";
}
final vout = jsonUTXO["tx_pos"] as int;
final outputs = txn["vout"] as List;
String? utxoOwnerAddress;
// get UTXO owner address
for (final output in outputs) {
if (output["n"] == vout) {
utxoOwnerAddress =
output["scriptPubKey"]?["addresses"]?[0] as String? ??
output["scriptPubKey"]?["address"] as String?;
}
}
final utxo = isar_models.UTXO(
walletId: walletId,
txid: txn["txid"] as String,
vout: fetchedUtxoList[i][j]["tx_pos"] as int,
value: fetchedUtxoList[i][j]["value"] as int,
vout: vout,
value: jsonUTXO["value"] as int,
name: "",
isBlocked: shouldBlock,
blockedReason: blockReason,
isCoinbase: txn["is_coinbase"] as bool? ?? false,
blockHash: txn["blockhash"] as String?,
blockHeight: fetchedUtxoList[i][j]["height"] as int?,
blockHeight: jsonUTXO["height"] as int?,
blockTime: txn["blocktime"] as int?,
address: utxoOwnerAddress,
);
satoshiBalanceTotal += utxo.value;
if (utxo.isBlocked) {
satoshiBalanceBlocked += utxo.value;
} else {
if (utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) {
satoshiBalanceSpendable += utxo.value;
} else {
satoshiBalancePending += utxo.value;
}
}
outputArray.add(utxo);
}
}
@ -1859,27 +1887,20 @@ class BitcoinWallet extends CoinServiceAPI
Logging.instance
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});
await db.updateUTXOs(walletId, outputArray);
// finally update balance
_balance = Balance(
coin: coin,
total: satoshiBalanceTotal,
spendable: satoshiBalanceSpendable,
blockedTotal: satoshiBalanceBlocked,
pendingSpendable: satoshiBalancePending,
);
await updateCachedBalance(_balance!);
await _updateBalance();
} catch (e, s) {
Logging.instance
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
}
}
Future<void> _updateBalance() async {
await refreshBalance();
}
@override
Balance get balance => _balance ??= getCachedBalance();
Balance? _balance;
@ -2252,11 +2273,12 @@ class BitcoinWallet extends CoinServiceAPI
/// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return
/// a map containing the tx hex along with other important information. If not, then it will return
/// an integer (1 or 2)
dynamic coinSelection(
int satoshiAmountToSend,
int selectedTxFeeRate,
String _recipientAddress,
bool isSendAll, {
dynamic coinSelection({
required int satoshiAmountToSend,
required int selectedTxFeeRate,
required String recipientAddress,
required bool coinControl,
required bool isSendAll,
int additionalOutputs = 0,
List<isar_models.UTXO>? utxos,
}) async {
@ -2268,18 +2290,26 @@ class BitcoinWallet extends CoinServiceAPI
int spendableSatoshiValue = 0;
// Build list of spendable outputs and totaling their satoshi amount
for (var i = 0; i < availableOutputs.length; i++) {
if (availableOutputs[i].isBlocked == false &&
availableOutputs[i]
.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) ==
true) {
spendableOutputs.add(availableOutputs[i]);
spendableSatoshiValue += availableOutputs[i].value;
for (final utxo in availableOutputs) {
if (utxo.isBlocked == false &&
utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) &&
utxo.used != true) {
spendableOutputs.add(utxo);
spendableSatoshiValue += utxo.value;
}
}
// sort spendable by age (oldest first)
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
if (coinControl) {
if (spendableOutputs.length < availableOutputs.length) {
throw ArgumentError("Attempted to use an unavailable utxo");
}
}
// don't care about sorting if using all utxos
if (!coinControl) {
// sort spendable by age (oldest first)
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
}
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
level: LogLevel.Info);
@ -2310,19 +2340,26 @@ class BitcoinWallet extends CoinServiceAPI
int inputsBeingConsumed = 0;
List<isar_models.UTXO> utxoObjectsToUse = [];
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
if (!coinControl) {
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend &&
i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs &&
inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
}
} else {
satoshisBeingUsed = spendableSatoshiValue;
utxoObjectsToUse = spendableOutputs;
}
Logging.instance
@ -2333,7 +2370,7 @@ class BitcoinWallet extends CoinServiceAPI
.log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info);
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
List<String> recipientsArray = [_recipientAddress];
List<String> recipientsArray = [recipientAddress];
List<int> recipientsAmtArray = [satoshiAmountToSend];
// gather required signing data
@ -2346,7 +2383,7 @@ class BitcoinWallet extends CoinServiceAPI
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
int feeForOneOutput = estimateTxFee(
@ -2373,6 +2410,7 @@ class BitcoinWallet extends CoinServiceAPI
"recipientAmt": amount,
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2382,7 +2420,7 @@ class BitcoinWallet extends CoinServiceAPI
vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
} catch (e) {
@ -2396,7 +2434,7 @@ class BitcoinWallet extends CoinServiceAPI
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
recipientAddress,
await _getCurrentAddressForChain(
1, DerivePathTypeExt.primaryFor(coin)),
],
@ -2505,6 +2543,7 @@ class BitcoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": feeBeingPaid,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2532,6 +2571,7 @@ class BitcoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2561,6 +2601,7 @@ class BitcoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2590,6 +2631,7 @@ class BitcoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2601,9 +2643,15 @@ class BitcoinWallet extends CoinServiceAPI
level: LogLevel.Warning);
// try adding more outputs
if (spendableOutputs.length > inputsBeingConsumed) {
return coinSelection(satoshiAmountToSend, selectedTxFeeRate,
_recipientAddress, isSendAll,
additionalOutputs: additionalOutputs + 1, utxos: utxos);
return coinSelection(
satoshiAmountToSend: satoshiAmountToSend,
selectedTxFeeRate: selectedTxFeeRate,
recipientAddress: recipientAddress,
isSendAll: isSendAll,
additionalOutputs: additionalOutputs + 1,
utxos: utxos,
coinControl: coinControl,
);
}
return 2;
}

View file

@ -24,6 +24,7 @@ import 'package:stackwallet/services/event_bus/events/global/refresh_percent_cha
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/coin_control_interface.dart';
import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart';
import 'package:stackwallet/services/node_service.dart';
@ -98,7 +99,8 @@ String constructDerivePath({
return "m/$purpose'/$coinType'/$account'/$chain/$index";
}
class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
class BitcoinCashWallet extends CoinServiceAPI
with WalletCache, WalletDB, CoinControlInterface {
BitcoinCashWallet({
required String walletId,
required String walletName,
@ -118,6 +120,17 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
_secureStore = secureStore;
initCache(walletId, coin);
initWalletDB(mockableOverride: mockableOverride);
initCoinControlInterface(
walletId: walletId,
walletName: walletName,
coin: coin,
db: db,
getChainHeight: () => chainHeight,
refreshedBalanceCallback: (balance) async {
_balance = balance;
await updateCachedBalance(_balance!);
},
);
}
static const integrationTestFlag =
@ -1041,6 +1054,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
try {
final feeRateType = args?["feeRate"];
final feeRateAmount = args?["feeRateAmount"];
final utxos = args?["UTXOs"] as Set<isar_models.UTXO>?;
if (feeRateType is FeeRateType || feeRateAmount is int) {
late final int rate;
if (feeRateType is FeeRateType) {
@ -1067,9 +1081,17 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
isSendAll = true;
}
final result =
await coinSelection(satoshiAmount, rate, address, isSendAll);
Logging.instance.log("SEND RESULT: $result", level: LogLevel.Info);
final result = await coinSelection(
satoshiAmountToSend: satoshiAmount,
selectedTxFeeRate: rate,
recipientAddress: address,
isSendAll: isSendAll,
utxos: utxos?.toList(),
coinControl: utxos is List<isar_models.UTXO>,
);
Logging.instance
.log("PREPARE SEND RESULT: $result", level: LogLevel.Info);
if (result is int) {
switch (result) {
case 1:
@ -1115,6 +1137,12 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
final txHash = await _electrumXClient.broadcastTransaction(
rawTx: txData["hex"] as String);
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
final utxos = txData["usedUTXOs"] as List<isar_models.UTXO>;
// mark utxos as used
await db.putUTXOs(utxos.map((e) => e.copyWith(used: true)).toList());
return txHash;
} catch (e, s) {
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
@ -1719,49 +1747,47 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
}
}
final currentChainHeight = await chainHeight;
final List<isar_models.UTXO> outputArray = [];
int satoshiBalanceTotal = 0;
int satoshiBalancePending = 0;
int satoshiBalanceSpendable = 0;
int satoshiBalanceBlocked = 0;
for (int i = 0; i < fetchedUtxoList.length; i++) {
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
final jsonUTXO = fetchedUtxoList[i][j];
final txn = await cachedElectrumXClient.getTransaction(
txHash: fetchedUtxoList[i][j]["tx_hash"] as String,
txHash: jsonUTXO["tx_hash"] as String,
verbose: true,
coin: coin,
);
// todo check here if we should mark as blocked
final vout = jsonUTXO["tx_pos"] as int;
final outputs = txn["vout"] as List;
String? utxoOwnerAddress;
// get UTXO owner address
for (final output in outputs) {
if (output["n"] == vout) {
utxoOwnerAddress =
output["scriptPubKey"]?["addresses"]?[0] as String? ??
output["scriptPubKey"]?["address"] as String?;
}
}
final utxo = isar_models.UTXO(
walletId: walletId,
txid: txn["txid"] as String,
vout: fetchedUtxoList[i][j]["tx_pos"] as int,
value: fetchedUtxoList[i][j]["value"] as int,
vout: vout,
value: jsonUTXO["value"] as int,
name: "",
isBlocked: false,
blockedReason: null,
isCoinbase: txn["is_coinbase"] as bool? ?? false,
blockHash: txn["blockhash"] as String?,
blockHeight: fetchedUtxoList[i][j]["height"] as int?,
blockHeight: jsonUTXO["height"] as int?,
blockTime: txn["blocktime"] as int?,
address: utxoOwnerAddress,
);
satoshiBalanceTotal += utxo.value;
if (utxo.isBlocked) {
satoshiBalanceBlocked += utxo.value;
} else {
if (utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) {
satoshiBalanceSpendable += utxo.value;
} else {
satoshiBalancePending += utxo.value;
}
}
outputArray.add(utxo);
}
}
@ -1769,27 +1795,20 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
Logging.instance
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});
await db.updateUTXOs(walletId, outputArray);
// finally update balance
_balance = Balance(
coin: coin,
total: satoshiBalanceTotal,
spendable: satoshiBalanceSpendable,
blockedTotal: satoshiBalanceBlocked,
pendingSpendable: satoshiBalancePending,
);
await updateCachedBalance(_balance!);
await _updateBalance();
} catch (e, s) {
Logging.instance
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
}
}
Future<void> _updateBalance() async {
await refreshBalance();
}
@override
Balance get balance => _balance ??= getCachedBalance();
Balance? _balance;
@ -2322,11 +2341,12 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
/// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return
/// a map containing the tx hex along with other important information. If not, then it will return
/// an integer (1 or 2)
dynamic coinSelection(
int satoshiAmountToSend,
int selectedTxFeeRate,
String _recipientAddress,
bool isSendAll, {
dynamic coinSelection({
required int satoshiAmountToSend,
required int selectedTxFeeRate,
required String recipientAddress,
required bool coinControl,
required bool isSendAll,
int additionalOutputs = 0,
List<isar_models.UTXO>? utxos,
}) async {
@ -2338,18 +2358,26 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
int spendableSatoshiValue = 0;
// Build list of spendable outputs and totaling their satoshi amount
for (var i = 0; i < availableOutputs.length; i++) {
if (availableOutputs[i].isBlocked == false &&
availableOutputs[i]
.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) ==
true) {
spendableOutputs.add(availableOutputs[i]);
spendableSatoshiValue += availableOutputs[i].value;
for (final utxo in availableOutputs) {
if (utxo.isBlocked == false &&
utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) &&
utxo.used != true) {
spendableOutputs.add(utxo);
spendableSatoshiValue += utxo.value;
}
}
// sort spendable by age (oldest first)
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
if (coinControl) {
if (spendableOutputs.length < availableOutputs.length) {
throw ArgumentError("Attempted to use an unavailable utxo");
}
}
// don't care about sorting if using all utxos
if (!coinControl) {
// sort spendable by age (oldest first)
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
}
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
level: LogLevel.Info);
@ -2378,19 +2406,26 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
int inputsBeingConsumed = 0;
List<isar_models.UTXO> utxoObjectsToUse = [];
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
if (!coinControl) {
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend &&
i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs &&
inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
}
} else {
satoshisBeingUsed = spendableSatoshiValue;
utxoObjectsToUse = spendableOutputs;
}
Logging.instance
@ -2403,7 +2438,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
.log('satoshiAmountToSend $satoshiAmountToSend', level: LogLevel.Info);
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
List<String> recipientsArray = [_recipientAddress];
List<String> recipientsArray = [recipientAddress];
List<int> recipientsAmtArray = [satoshiAmountToSend];
// gather required signing data
@ -2416,7 +2451,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
int feeForOneOutput = estimateTxFee(
@ -2440,6 +2475,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
"recipientAmt": amount,
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2447,14 +2483,14 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
final int vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
recipientAddress,
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
],
satoshiAmounts: [
@ -2572,6 +2608,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
"recipientAmt": recipientsAmtArray[0],
"fee": feeBeingPaid,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2599,6 +2636,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2628,6 +2666,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2657,6 +2696,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
"recipientAmt": recipientsAmtArray[0],
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2668,9 +2708,15 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
level: LogLevel.Warning);
// try adding more outputs
if (spendableOutputs.length > inputsBeingConsumed) {
return coinSelection(satoshiAmountToSend, selectedTxFeeRate,
_recipientAddress, isSendAll,
additionalOutputs: additionalOutputs + 1, utxos: utxos);
return coinSelection(
satoshiAmountToSend: satoshiAmountToSend,
selectedTxFeeRate: selectedTxFeeRate,
recipientAddress: recipientAddress,
isSendAll: isSendAll,
additionalOutputs: additionalOutputs + 1,
utxos: utxos,
coinControl: coinControl,
);
}
return 2;
}

View file

@ -24,6 +24,7 @@ import 'package:stackwallet/services/event_bus/events/global/refresh_percent_cha
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/coin_control_interface.dart';
import 'package:stackwallet/services/mixins/electrum_x_parsing.dart';
import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart';
@ -85,7 +86,7 @@ String constructDerivePath({
}
class DogecoinWallet extends CoinServiceAPI
with WalletCache, WalletDB, ElectrumXParsing {
with WalletCache, WalletDB, ElectrumXParsing, CoinControlInterface {
DogecoinWallet({
required String walletId,
required String walletName,
@ -105,6 +106,17 @@ class DogecoinWallet extends CoinServiceAPI
_secureStore = secureStore;
initCache(walletId, coin);
initWalletDB(mockableOverride: mockableOverride);
initCoinControlInterface(
walletId: walletId,
walletName: walletName,
coin: coin,
db: db,
getChainHeight: () => chainHeight,
refreshedBalanceCallback: (balance) async {
_balance = balance;
await updateCachedBalance(_balance!);
},
);
// paynym stuff
// initPaynymWalletInterface(
@ -907,6 +919,7 @@ class DogecoinWallet extends CoinServiceAPI
try {
final feeRateType = args?["feeRate"];
final feeRateAmount = args?["feeRateAmount"];
final utxos = args?["UTXOs"] as Set<isar_models.UTXO>?;
if (feeRateType is FeeRateType || feeRateAmount is int) {
late final int rate;
if (feeRateType is FeeRateType) {
@ -933,9 +946,17 @@ class DogecoinWallet extends CoinServiceAPI
isSendAll = true;
}
final result =
await coinSelection(satoshiAmount, rate, address, isSendAll);
Logging.instance.log("SEND RESULT: $result", level: LogLevel.Info);
final result = await coinSelection(
satoshiAmountToSend: satoshiAmount,
selectedTxFeeRate: rate,
recipientAddress: address,
isSendAll: isSendAll,
utxos: utxos?.toList(),
coinControl: utxos is List<isar_models.UTXO>,
);
Logging.instance
.log("PREPARE SEND RESULT: $result", level: LogLevel.Info);
if (result is int) {
switch (result) {
case 1:
@ -981,6 +1002,12 @@ class DogecoinWallet extends CoinServiceAPI
final txHash = await _electrumXClient.broadcastTransaction(
rawTx: txData["hex"] as String);
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
final utxos = txData["usedUTXOs"] as List<isar_models.UTXO>;
// mark utxos as used
await db.putUTXOs(utxos.map((e) => e.copyWith(used: true)).toList());
return txHash;
} catch (e, s) {
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
@ -1537,18 +1564,14 @@ class DogecoinWallet extends CoinServiceAPI
}
}
final currentChainHeight = await chainHeight;
final List<isar_models.UTXO> outputArray = [];
int satoshiBalanceTotal = 0;
int satoshiBalancePending = 0;
int satoshiBalanceSpendable = 0;
int satoshiBalanceBlocked = 0;
for (int i = 0; i < fetchedUtxoList.length; i++) {
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
final jsonUTXO = fetchedUtxoList[i][j];
final txn = await cachedElectrumXClient.getTransaction(
txHash: fetchedUtxoList[i][j]["tx_hash"] as String,
txHash: jsonUTXO["tx_hash"] as String,
verbose: true,
coin: coin,
);
@ -1556,7 +1579,7 @@ class DogecoinWallet extends CoinServiceAPI
// fetch stored tx to see if paynym notification tx and block utxo
final storedTx = await db.getTransaction(
walletId,
fetchedUtxoList[i][j]["tx_hash"] as String,
jsonUTXO["tx_hash"] as String,
);
bool shouldBlock = false;
@ -1573,32 +1596,35 @@ class DogecoinWallet extends CoinServiceAPI
blockReason = "Incoming paynym notification transaction.";
}
final vout = jsonUTXO["tx_pos"] as int;
final outputs = txn["vout"] as List;
String? utxoOwnerAddress;
// get UTXO owner address
for (final output in outputs) {
if (output["n"] == vout) {
utxoOwnerAddress =
output["scriptPubKey"]?["addresses"]?[0] as String? ??
output["scriptPubKey"]?["address"] as String?;
}
}
final utxo = isar_models.UTXO(
walletId: walletId,
txid: txn["txid"] as String,
vout: fetchedUtxoList[i][j]["tx_pos"] as int,
value: fetchedUtxoList[i][j]["value"] as int,
vout: vout,
value: jsonUTXO["value"] as int,
name: "",
isBlocked: shouldBlock,
blockedReason: blockReason,
isCoinbase: txn["is_coinbase"] as bool? ?? false,
blockHash: txn["blockhash"] as String?,
blockHeight: fetchedUtxoList[i][j]["height"] as int?,
blockHeight: jsonUTXO["height"] as int?,
blockTime: txn["blocktime"] as int?,
address: utxoOwnerAddress,
);
satoshiBalanceTotal += utxo.value;
if (utxo.isBlocked) {
satoshiBalanceBlocked += utxo.value;
} else {
if (utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) {
satoshiBalanceSpendable += utxo.value;
} else {
satoshiBalancePending += utxo.value;
}
}
outputArray.add(utxo);
}
}
@ -1606,27 +1632,20 @@ class DogecoinWallet extends CoinServiceAPI
Logging.instance
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});
await db.updateUTXOs(walletId, outputArray);
// finally update balance
_balance = Balance(
coin: coin,
total: satoshiBalanceTotal,
spendable: satoshiBalanceSpendable,
blockedTotal: satoshiBalanceBlocked,
pendingSpendable: satoshiBalancePending,
);
await updateCachedBalance(_balance!);
await _updateBalance();
} catch (e, s) {
Logging.instance
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
}
}
Future<void> _updateBalance() async {
await refreshBalance();
}
@override
Balance get balance => _balance ??= getCachedBalance();
Balance? _balance;
@ -2022,11 +2041,12 @@ class DogecoinWallet extends CoinServiceAPI
/// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return
/// a map containing the tx hex along with other important information. If not, then it will return
/// an integer (1 or 2)
dynamic coinSelection(
int satoshiAmountToSend,
int selectedTxFeeRate,
String _recipientAddress,
bool isSendAll, {
dynamic coinSelection({
required int satoshiAmountToSend,
required int selectedTxFeeRate,
required String recipientAddress,
required bool coinControl,
required bool isSendAll,
int additionalOutputs = 0,
List<isar_models.UTXO>? utxos,
}) async {
@ -2038,18 +2058,26 @@ class DogecoinWallet extends CoinServiceAPI
int spendableSatoshiValue = 0;
// Build list of spendable outputs and totaling their satoshi amount
for (var i = 0; i < availableOutputs.length; i++) {
if (availableOutputs[i].isBlocked == false &&
availableOutputs[i]
.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) ==
true) {
spendableOutputs.add(availableOutputs[i]);
spendableSatoshiValue += availableOutputs[i].value;
for (final utxo in availableOutputs) {
if (utxo.isBlocked == false &&
utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) &&
utxo.used != true) {
spendableOutputs.add(utxo);
spendableSatoshiValue += utxo.value;
}
}
// sort spendable by age (oldest first)
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
if (coinControl) {
if (spendableOutputs.length < availableOutputs.length) {
throw ArgumentError("Attempted to use an unavailable utxo");
}
}
// don't care about sorting if using all utxos
if (!coinControl) {
// sort spendable by age (oldest first)
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
}
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
level: LogLevel.Info);
@ -2078,19 +2106,26 @@ class DogecoinWallet extends CoinServiceAPI
int inputsBeingConsumed = 0;
List<isar_models.UTXO> utxoObjectsToUse = [];
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
if (!coinControl) {
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend &&
i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs &&
inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
}
} else {
satoshisBeingUsed = spendableSatoshiValue;
utxoObjectsToUse = spendableOutputs;
}
Logging.instance
@ -2103,7 +2138,7 @@ class DogecoinWallet extends CoinServiceAPI
.log('satoshiAmountToSend $satoshiAmountToSend', level: LogLevel.Info);
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
List<String> recipientsArray = [_recipientAddress];
List<String> recipientsArray = [recipientAddress];
List<int> recipientsAmtArray = [satoshiAmountToSend];
// gather required signing data
@ -2116,7 +2151,7 @@ class DogecoinWallet extends CoinServiceAPI
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
int feeForOneOutput = estimateTxFee(
@ -2140,6 +2175,7 @@ class DogecoinWallet extends CoinServiceAPI
"recipientAmt": amount,
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2147,14 +2183,14 @@ class DogecoinWallet extends CoinServiceAPI
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
final int vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
recipientAddress,
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
],
satoshiAmounts: [
@ -2272,6 +2308,7 @@ class DogecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": feeBeingPaid,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2299,6 +2336,7 @@ class DogecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2328,6 +2366,7 @@ class DogecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2357,6 +2396,7 @@ class DogecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2368,9 +2408,15 @@ class DogecoinWallet extends CoinServiceAPI
level: LogLevel.Warning);
// try adding more outputs
if (spendableOutputs.length > inputsBeingConsumed) {
return coinSelection(satoshiAmountToSend, selectedTxFeeRate,
_recipientAddress, isSendAll,
additionalOutputs: additionalOutputs + 1, utxos: utxos);
return coinSelection(
satoshiAmountToSend: satoshiAmountToSend,
selectedTxFeeRate: selectedTxFeeRate,
recipientAddress: recipientAddress,
isSendAll: isSendAll,
additionalOutputs: additionalOutputs + 1,
utxos: utxos,
coinControl: coinControl,
);
}
return 2;
}

View file

@ -23,6 +23,7 @@ import 'package:stackwallet/services/event_bus/events/global/refresh_percent_cha
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/coin_control_interface.dart';
import 'package:stackwallet/services/mixins/electrum_x_parsing.dart';
import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart';
@ -90,7 +91,7 @@ String constructDerivePath({
}
class LitecoinWallet extends CoinServiceAPI
with WalletCache, WalletDB, ElectrumXParsing {
with WalletCache, WalletDB, ElectrumXParsing, CoinControlInterface {
LitecoinWallet({
required String walletId,
required String walletName,
@ -110,6 +111,17 @@ class LitecoinWallet extends CoinServiceAPI
_secureStore = secureStore;
initCache(walletId, coin);
initWalletDB(mockableOverride: mockableOverride);
initCoinControlInterface(
walletId: walletId,
walletName: walletName,
coin: coin,
db: db,
getChainHeight: () => chainHeight,
refreshedBalanceCallback: (balance) async {
_balance = balance;
await updateCachedBalance(_balance!);
},
);
}
static const integrationTestFlag =
@ -1018,6 +1030,7 @@ class LitecoinWallet extends CoinServiceAPI
try {
final feeRateType = args?["feeRate"];
final feeRateAmount = args?["feeRateAmount"];
final utxos = args?["UTXOs"] as Set<isar_models.UTXO>?;
if (feeRateType is FeeRateType || feeRateAmount is int) {
late final int rate;
if (feeRateType is FeeRateType) {
@ -1045,8 +1058,14 @@ class LitecoinWallet extends CoinServiceAPI
isSendAll = true;
}
final txData =
await coinSelection(satoshiAmount, rate, address, isSendAll);
final txData = await coinSelection(
satoshiAmountToSend: satoshiAmount,
selectedTxFeeRate: rate,
recipientAddress: address,
isSendAll: isSendAll,
utxos: utxos?.toList(),
coinControl: utxos is List<isar_models.UTXO>,
);
Logging.instance.log("prepare send: $txData", level: LogLevel.Info);
try {
@ -1109,6 +1128,11 @@ class LitecoinWallet extends CoinServiceAPI
final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex);
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
final utxos = txData["usedUTXOs"] as List<isar_models.UTXO>;
// mark utxos as used
await db.putUTXOs(utxos.map((e) => e.copyWith(used: true)).toList());
return txHash;
} catch (e, s) {
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
@ -1724,49 +1748,47 @@ class LitecoinWallet extends CoinServiceAPI
}
}
final currentChainHeight = await chainHeight;
final List<isar_models.UTXO> outputArray = [];
int satoshiBalanceTotal = 0;
int satoshiBalancePending = 0;
int satoshiBalanceSpendable = 0;
int satoshiBalanceBlocked = 0;
for (int i = 0; i < fetchedUtxoList.length; i++) {
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
final jsonUTXO = fetchedUtxoList[i][j];
final txn = await cachedElectrumXClient.getTransaction(
txHash: fetchedUtxoList[i][j]["tx_hash"] as String,
txHash: jsonUTXO["tx_hash"] as String,
verbose: true,
coin: coin,
);
// todo check here if we should mark as blocked
final vout = jsonUTXO["tx_pos"] as int;
final outputs = txn["vout"] as List;
String? utxoOwnerAddress;
// get UTXO owner address
for (final output in outputs) {
if (output["n"] == vout) {
utxoOwnerAddress =
output["scriptPubKey"]?["addresses"]?[0] as String? ??
output["scriptPubKey"]?["address"] as String?;
}
}
final utxo = isar_models.UTXO(
walletId: walletId,
txid: txn["txid"] as String,
vout: fetchedUtxoList[i][j]["tx_pos"] as int,
value: fetchedUtxoList[i][j]["value"] as int,
vout: vout,
value: jsonUTXO["value"] as int,
name: "",
isBlocked: false,
blockedReason: null,
isCoinbase: txn["is_coinbase"] as bool? ?? false,
blockHash: txn["blockhash"] as String?,
blockHeight: fetchedUtxoList[i][j]["height"] as int?,
blockHeight: jsonUTXO["height"] as int?,
blockTime: txn["blocktime"] as int?,
address: utxoOwnerAddress,
);
satoshiBalanceTotal += utxo.value;
if (utxo.isBlocked) {
satoshiBalanceBlocked += utxo.value;
} else {
if (utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) {
satoshiBalanceSpendable += utxo.value;
} else {
satoshiBalancePending += utxo.value;
}
}
outputArray.add(utxo);
}
}
@ -1774,27 +1796,20 @@ class LitecoinWallet extends CoinServiceAPI
Logging.instance
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});
await db.updateUTXOs(walletId, outputArray);
// finally update balance
_balance = Balance(
coin: coin,
total: satoshiBalanceTotal,
spendable: satoshiBalanceSpendable,
blockedTotal: satoshiBalanceBlocked,
pendingSpendable: satoshiBalancePending,
);
await updateCachedBalance(_balance!);
await _updateBalance();
} catch (e, s) {
Logging.instance
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
}
}
Future<void> _updateBalance() async {
await refreshBalance();
}
@override
Balance get balance => _balance ??= getCachedBalance();
Balance? _balance;
@ -2214,11 +2229,12 @@ class LitecoinWallet extends CoinServiceAPI
/// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return
/// a map containing the tx hex along with other important information. If not, then it will return
/// an integer (1 or 2)
dynamic coinSelection(
int satoshiAmountToSend,
int selectedTxFeeRate,
String _recipientAddress,
bool isSendAll, {
dynamic coinSelection({
required int satoshiAmountToSend,
required int selectedTxFeeRate,
required String recipientAddress,
required bool coinControl,
required bool isSendAll,
int additionalOutputs = 0,
List<isar_models.UTXO>? utxos,
}) async {
@ -2230,18 +2246,26 @@ class LitecoinWallet extends CoinServiceAPI
int spendableSatoshiValue = 0;
// Build list of spendable outputs and totaling their satoshi amount
for (var i = 0; i < availableOutputs.length; i++) {
if (availableOutputs[i].isBlocked == false &&
availableOutputs[i]
.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) ==
true) {
spendableOutputs.add(availableOutputs[i]);
spendableSatoshiValue += availableOutputs[i].value;
for (final utxo in availableOutputs) {
if (utxo.isBlocked == false &&
utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) &&
utxo.used != true) {
spendableOutputs.add(utxo);
spendableSatoshiValue += utxo.value;
}
}
// sort spendable by age (oldest first)
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
if (coinControl) {
if (spendableOutputs.length < availableOutputs.length) {
throw ArgumentError("Attempted to use an unavailable utxo");
}
}
// don't care about sorting if using all utxos
if (!coinControl) {
// sort spendable by age (oldest first)
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
}
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
level: LogLevel.Info);
@ -2270,19 +2294,26 @@ class LitecoinWallet extends CoinServiceAPI
int inputsBeingConsumed = 0;
List<isar_models.UTXO> utxoObjectsToUse = [];
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
if (!coinControl) {
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend &&
i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs &&
inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
}
} else {
satoshisBeingUsed = spendableSatoshiValue;
utxoObjectsToUse = spendableOutputs;
}
Logging.instance
@ -2293,7 +2324,7 @@ class LitecoinWallet extends CoinServiceAPI
.log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info);
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
List<String> recipientsArray = [_recipientAddress];
List<String> recipientsArray = [recipientAddress];
List<int> recipientsAmtArray = [satoshiAmountToSend];
// gather required signing data
@ -2306,7 +2337,7 @@ class LitecoinWallet extends CoinServiceAPI
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
int feeForOneOutput = estimateTxFee(
@ -2333,6 +2364,7 @@ class LitecoinWallet extends CoinServiceAPI
"recipientAmt": amount,
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2340,14 +2372,14 @@ class LitecoinWallet extends CoinServiceAPI
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
final int vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
recipientAddress,
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
],
satoshiAmounts: [
@ -2451,6 +2483,7 @@ class LitecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": feeBeingPaid,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2478,6 +2511,7 @@ class LitecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2507,6 +2541,7 @@ class LitecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2536,6 +2571,7 @@ class LitecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2547,9 +2583,15 @@ class LitecoinWallet extends CoinServiceAPI
level: LogLevel.Warning);
// try adding more outputs
if (spendableOutputs.length > inputsBeingConsumed) {
return coinSelection(satoshiAmountToSend, selectedTxFeeRate,
_recipientAddress, isSendAll,
additionalOutputs: additionalOutputs + 1, utxos: utxos);
return coinSelection(
satoshiAmountToSend: satoshiAmountToSend,
selectedTxFeeRate: selectedTxFeeRate,
recipientAddress: recipientAddress,
isSendAll: isSendAll,
additionalOutputs: additionalOutputs + 1,
utxos: utxos,
coinControl: coinControl,
);
}
return 2;
}

View file

@ -10,6 +10,7 @@ import 'package:stackwallet/services/coins/coin_service.dart';
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/coin_control_interface.dart';
import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
@ -229,6 +230,8 @@ class Manager with ChangeNotifier {
bool get hasPaynymSupport => _currentWallet is PaynymWalletInterface;
bool get hasCoinControlSupport => _currentWallet is CoinControlInterface;
int get rescanOnOpenVersion =>
DB.instance.get<dynamic>(
boxName: DB.boxNameDBInfo,

View file

@ -23,6 +23,7 @@ import 'package:stackwallet/services/event_bus/events/global/refresh_percent_cha
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/coin_control_interface.dart';
import 'package:stackwallet/services/mixins/electrum_x_parsing.dart';
import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart';
@ -87,7 +88,7 @@ String constructDerivePath({
}
class NamecoinWallet extends CoinServiceAPI
with WalletCache, WalletDB, ElectrumXParsing {
with WalletCache, WalletDB, ElectrumXParsing, CoinControlInterface {
NamecoinWallet({
required String walletId,
required String walletName,
@ -107,6 +108,17 @@ class NamecoinWallet extends CoinServiceAPI
_secureStore = secureStore;
initCache(walletId, coin);
initWalletDB(mockableOverride: mockableOverride);
initCoinControlInterface(
walletId: walletId,
walletName: walletName,
coin: coin,
db: db,
getChainHeight: () => chainHeight,
refreshedBalanceCallback: (balance) async {
_balance = balance;
await updateCachedBalance(_balance!);
},
);
}
static const integrationTestFlag =
@ -1009,6 +1021,7 @@ class NamecoinWallet extends CoinServiceAPI
try {
final feeRateType = args?["feeRate"];
final feeRateAmount = args?["feeRateAmount"];
final utxos = args?["UTXOs"] as Set<isar_models.UTXO>?;
if (feeRateType is FeeRateType || feeRateAmount is int) {
late final int rate;
if (feeRateType is FeeRateType) {
@ -1036,8 +1049,14 @@ class NamecoinWallet extends CoinServiceAPI
isSendAll = true;
}
final txData =
await coinSelection(satoshiAmount, rate, address, isSendAll);
final txData = await coinSelection(
satoshiAmountToSend: satoshiAmount,
selectedTxFeeRate: rate,
recipientAddress: address,
isSendAll: isSendAll,
utxos: utxos?.toList(),
coinControl: utxos is List<isar_models.UTXO>,
);
Logging.instance.log("prepare send: $txData", level: LogLevel.Info);
try {
@ -1100,6 +1119,11 @@ class NamecoinWallet extends CoinServiceAPI
final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex);
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
final utxos = txData["usedUTXOs"] as List<isar_models.UTXO>;
// mark utxos as used
await db.putUTXOs(utxos.map((e) => e.copyWith(used: true)).toList());
return txHash;
} catch (e, s) {
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
@ -1709,49 +1733,47 @@ class NamecoinWallet extends CoinServiceAPI
}
}
final currentChainHeight = await chainHeight;
final List<isar_models.UTXO> outputArray = [];
int satoshiBalanceTotal = 0;
int satoshiBalancePending = 0;
int satoshiBalanceSpendable = 0;
int satoshiBalanceBlocked = 0;
for (int i = 0; i < fetchedUtxoList.length; i++) {
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
final jsonUTXO = fetchedUtxoList[i][j];
final txn = await cachedElectrumXClient.getTransaction(
txHash: fetchedUtxoList[i][j]["tx_hash"] as String,
txHash: jsonUTXO["tx_hash"] as String,
verbose: true,
coin: coin,
);
// todo check here if we should mark as blocked
final vout = jsonUTXO["tx_pos"] as int;
final outputs = txn["vout"] as List;
String? utxoOwnerAddress;
// get UTXO owner address
for (final output in outputs) {
if (output["n"] == vout) {
utxoOwnerAddress =
output["scriptPubKey"]?["addresses"]?[0] as String? ??
output["scriptPubKey"]?["address"] as String?;
}
}
final utxo = isar_models.UTXO(
walletId: walletId,
txid: txn["txid"] as String,
vout: fetchedUtxoList[i][j]["tx_pos"] as int,
value: fetchedUtxoList[i][j]["value"] as int,
vout: vout,
value: jsonUTXO["value"] as int,
name: "",
isBlocked: false,
blockedReason: null,
isCoinbase: txn["is_coinbase"] as bool? ?? false,
blockHash: txn["blockhash"] as String?,
blockHeight: fetchedUtxoList[i][j]["height"] as int?,
blockHeight: jsonUTXO["height"] as int?,
blockTime: txn["blocktime"] as int?,
address: utxoOwnerAddress,
);
satoshiBalanceTotal += utxo.value;
if (utxo.isBlocked) {
satoshiBalanceBlocked += utxo.value;
} else {
if (utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) {
satoshiBalanceSpendable += utxo.value;
} else {
satoshiBalancePending += utxo.value;
}
}
outputArray.add(utxo);
}
}
@ -1759,27 +1781,20 @@ class NamecoinWallet extends CoinServiceAPI
Logging.instance
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});
await db.updateUTXOs(walletId, outputArray);
// finally update balance
_balance = Balance(
coin: coin,
total: satoshiBalanceTotal,
spendable: satoshiBalanceSpendable,
blockedTotal: satoshiBalanceBlocked,
pendingSpendable: satoshiBalancePending,
);
await updateCachedBalance(_balance!);
await _updateBalance();
} catch (e, s) {
Logging.instance
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
}
}
Future<void> _updateBalance() async {
await refreshBalance();
}
@override
Balance get balance => _balance ??= getCachedBalance();
Balance? _balance;
@ -2207,11 +2222,12 @@ class NamecoinWallet extends CoinServiceAPI
/// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return
/// a map containing the tx hex along with other important information. If not, then it will return
/// an integer (1 or 2)
dynamic coinSelection(
int satoshiAmountToSend,
int selectedTxFeeRate,
String _recipientAddress,
bool isSendAll, {
dynamic coinSelection({
required int satoshiAmountToSend,
required int selectedTxFeeRate,
required String recipientAddress,
required bool coinControl,
required bool isSendAll,
int additionalOutputs = 0,
List<isar_models.UTXO>? utxos,
}) async {
@ -2223,18 +2239,26 @@ class NamecoinWallet extends CoinServiceAPI
int spendableSatoshiValue = 0;
// Build list of spendable outputs and totaling their satoshi amount
for (var i = 0; i < availableOutputs.length; i++) {
if (availableOutputs[i].isBlocked == false &&
availableOutputs[i]
.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) ==
true) {
spendableOutputs.add(availableOutputs[i]);
spendableSatoshiValue += availableOutputs[i].value;
for (final utxo in availableOutputs) {
if (utxo.isBlocked == false &&
utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) &&
utxo.used != true) {
spendableOutputs.add(utxo);
spendableSatoshiValue += utxo.value;
}
}
// sort spendable by age (oldest first)
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
if (coinControl) {
if (spendableOutputs.length < availableOutputs.length) {
throw ArgumentError("Attempted to use an unavailable utxo");
}
}
// don't care about sorting if using all utxos
if (!coinControl) {
// sort spendable by age (oldest first)
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
}
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
level: LogLevel.Info);
@ -2263,19 +2287,26 @@ class NamecoinWallet extends CoinServiceAPI
int inputsBeingConsumed = 0;
List<isar_models.UTXO> utxoObjectsToUse = [];
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
if (!coinControl) {
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend &&
i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs &&
inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
}
} else {
satoshisBeingUsed = spendableSatoshiValue;
utxoObjectsToUse = spendableOutputs;
}
Logging.instance
@ -2286,7 +2317,7 @@ class NamecoinWallet extends CoinServiceAPI
.log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info);
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
List<String> recipientsArray = [_recipientAddress];
List<String> recipientsArray = [recipientAddress];
List<int> recipientsAmtArray = [satoshiAmountToSend];
// gather required signing data
@ -2299,7 +2330,7 @@ class NamecoinWallet extends CoinServiceAPI
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
int feeForOneOutput = estimateTxFee(
@ -2326,6 +2357,7 @@ class NamecoinWallet extends CoinServiceAPI
"recipientAmt": amount,
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2333,14 +2365,14 @@ class NamecoinWallet extends CoinServiceAPI
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
final int vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
recipientAddress,
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
],
satoshiAmounts: [
@ -2444,6 +2476,7 @@ class NamecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": feeBeingPaid,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2471,6 +2504,7 @@ class NamecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2500,6 +2534,7 @@ class NamecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2529,6 +2564,7 @@ class NamecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2540,9 +2576,15 @@ class NamecoinWallet extends CoinServiceAPI
level: LogLevel.Warning);
// try adding more outputs
if (spendableOutputs.length > inputsBeingConsumed) {
return coinSelection(satoshiAmountToSend, selectedTxFeeRate,
_recipientAddress, isSendAll,
additionalOutputs: additionalOutputs + 1, utxos: utxos);
return coinSelection(
satoshiAmountToSend: satoshiAmountToSend,
selectedTxFeeRate: selectedTxFeeRate,
recipientAddress: recipientAddress,
isSendAll: isSendAll,
additionalOutputs: additionalOutputs + 1,
utxos: utxos,
coinControl: coinControl,
);
}
return 2;
}

View file

@ -23,6 +23,7 @@ import 'package:stackwallet/services/event_bus/events/global/refresh_percent_cha
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/coin_control_interface.dart';
import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart';
import 'package:stackwallet/services/node_service.dart';
@ -81,7 +82,8 @@ String constructDerivePath({
return "m/$purpose'/$coinType'/$account'/$chain/$index";
}
class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
class ParticlWallet extends CoinServiceAPI
with WalletCache, WalletDB, CoinControlInterface {
ParticlWallet({
required String walletId,
required String walletName,
@ -101,6 +103,17 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
_secureStore = secureStore;
initCache(walletId, coin);
initWalletDB(mockableOverride: mockableOverride);
initCoinControlInterface(
walletId: walletId,
walletName: walletName,
coin: coin,
db: db,
getChainHeight: () => chainHeight,
refreshedBalanceCallback: (balance) async {
_balance = balance;
await updateCachedBalance(_balance!);
},
);
}
static const integrationTestFlag =
@ -935,6 +948,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
try {
final feeRateType = args?["feeRate"];
final feeRateAmount = args?["feeRateAmount"];
final utxos = args?["UTXOs"] as Set<isar_models.UTXO>?;
if (feeRateType is FeeRateType || feeRateAmount is int) {
late final int rate;
if (feeRateType is FeeRateType) {
@ -962,8 +976,14 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
isSendAll = true;
}
final txData =
await coinSelection(satoshiAmount, rate, address, isSendAll);
final txData = await coinSelection(
satoshiAmountToSend: satoshiAmount,
selectedTxFeeRate: rate,
recipientAddress: address,
isSendAll: isSendAll,
utxos: utxos?.toList(),
coinControl: utxos is List<isar_models.UTXO>,
);
Logging.instance.log("prepare send: $txData", level: LogLevel.Info);
try {
@ -1026,6 +1046,11 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex);
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
final utxos = txData["usedUTXOs"] as List<isar_models.UTXO>;
// mark utxos as used
await db.putUTXOs(utxos.map((e) => e.copyWith(used: true)).toList());
return txHash;
} catch (e, s) {
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
@ -1595,49 +1620,47 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
}
}
final currentChainHeight = await chainHeight;
final List<isar_models.UTXO> outputArray = [];
int satoshiBalanceTotal = 0;
int satoshiBalancePending = 0;
int satoshiBalanceSpendable = 0;
int satoshiBalanceBlocked = 0;
for (int i = 0; i < fetchedUtxoList.length; i++) {
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
final jsonUTXO = fetchedUtxoList[i][j];
final txn = await cachedElectrumXClient.getTransaction(
txHash: fetchedUtxoList[i][j]["tx_hash"] as String,
txHash: jsonUTXO["tx_hash"] as String,
verbose: true,
coin: coin,
);
// todo check here if we should mark as blocked
final vout = jsonUTXO["tx_pos"] as int;
final outputs = txn["vout"] as List;
String? utxoOwnerAddress;
// get UTXO owner address
for (final output in outputs) {
if (output["n"] == vout) {
utxoOwnerAddress =
output["scriptPubKey"]?["addresses"]?[0] as String? ??
output["scriptPubKey"]?["address"] as String?;
}
}
final utxo = isar_models.UTXO(
walletId: walletId,
txid: txn["txid"] as String,
vout: fetchedUtxoList[i][j]["tx_pos"] as int,
value: fetchedUtxoList[i][j]["value"] as int,
vout: vout,
value: jsonUTXO["value"] as int,
name: "",
isBlocked: false,
blockedReason: null,
isCoinbase: txn["is_coinbase"] as bool? ?? false,
blockHash: txn["blockhash"] as String?,
blockHeight: fetchedUtxoList[i][j]["height"] as int?,
blockHeight: jsonUTXO["height"] as int?,
blockTime: txn["blocktime"] as int?,
address: utxoOwnerAddress,
);
satoshiBalanceTotal += utxo.value;
if (utxo.isBlocked) {
satoshiBalanceBlocked += utxo.value;
} else {
if (utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) {
satoshiBalanceSpendable += utxo.value;
} else {
satoshiBalancePending += utxo.value;
}
}
outputArray.add(utxo);
}
}
@ -1645,27 +1668,20 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
Logging.instance
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});
await db.updateUTXOs(walletId, outputArray);
// finally update balance
_balance = Balance(
coin: coin,
total: satoshiBalanceTotal,
spendable: satoshiBalanceSpendable,
blockedTotal: satoshiBalanceBlocked,
pendingSpendable: satoshiBalancePending,
);
await updateCachedBalance(_balance!);
await _updateBalance();
} catch (e, s) {
Logging.instance
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
}
}
Future<void> _updateBalance() async {
await refreshBalance();
}
@override
Balance get balance => _balance ??= getCachedBalance();
Balance? _balance;
@ -2367,11 +2383,12 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
/// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return
/// a map containing the tx hex along with other important information. If not, then it will return
/// an integer (1 or 2)
dynamic coinSelection(
int satoshiAmountToSend,
int selectedTxFeeRate,
String _recipientAddress,
bool isSendAll, {
dynamic coinSelection({
required int satoshiAmountToSend,
required int selectedTxFeeRate,
required String recipientAddress,
required bool coinControl,
required bool isSendAll,
int additionalOutputs = 0,
List<isar_models.UTXO>? utxos,
}) async {
@ -2381,19 +2398,28 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
final currentChainHeight = await chainHeight;
final List<isar_models.UTXO> spendableOutputs = [];
int spendableSatoshiValue = 0;
// Build list of spendable outputs and totaling their satoshi amount
for (var i = 0; i < availableOutputs.length; i++) {
if (availableOutputs[i].isBlocked == false &&
availableOutputs[i]
.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) ==
true) {
spendableOutputs.add(availableOutputs[i]);
spendableSatoshiValue += availableOutputs[i].value;
for (final utxo in availableOutputs) {
if (utxo.isBlocked == false &&
utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) &&
utxo.used != true) {
spendableOutputs.add(utxo);
spendableSatoshiValue += utxo.value;
}
}
// sort spendable by age (oldest first)
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
if (coinControl) {
if (spendableOutputs.length < availableOutputs.length) {
throw ArgumentError("Attempted to use an unavailable utxo");
}
}
// don't care about sorting if using all utxos
if (!coinControl) {
// sort spendable by age (oldest first)
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
}
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
level: LogLevel.Info);
@ -2422,19 +2448,26 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
int inputsBeingConsumed = 0;
List<isar_models.UTXO> utxoObjectsToUse = [];
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
if (!coinControl) {
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend &&
i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs &&
inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
}
} else {
satoshisBeingUsed = spendableSatoshiValue;
utxoObjectsToUse = spendableOutputs;
}
Logging.instance
@ -2445,7 +2478,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
.log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info);
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
List<String> recipientsArray = [_recipientAddress];
List<String> recipientsArray = [recipientAddress];
List<int> recipientsAmtArray = [satoshiAmountToSend];
// gather required signing data
@ -2458,7 +2491,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
int feeForOneOutput = estimateTxFee(
@ -2485,6 +2518,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
"recipientAmt": amount,
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2492,14 +2526,14 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
final int vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
recipientAddress,
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
],
satoshiAmounts: [
@ -2603,6 +2637,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
"recipientAmt": recipientsAmtArray[0],
"fee": feeBeingPaid,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2630,6 +2665,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2659,6 +2695,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2688,6 +2725,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
"recipientAmt": recipientsAmtArray[0],
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2699,9 +2737,15 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
level: LogLevel.Warning);
// try adding more outputs
if (spendableOutputs.length > inputsBeingConsumed) {
return coinSelection(satoshiAmountToSend, selectedTxFeeRate,
_recipientAddress, isSendAll,
additionalOutputs: additionalOutputs + 1, utxos: utxos);
return coinSelection(
satoshiAmountToSend: satoshiAmountToSend,
selectedTxFeeRate: selectedTxFeeRate,
recipientAddress: recipientAddress,
isSendAll: isSendAll,
additionalOutputs: additionalOutputs + 1,
utxos: utxos,
coinControl: coinControl,
);
}
return 2;
}

View file

@ -0,0 +1,12 @@
import 'package:stackwallet/utilities/logger.dart';
class BalanceRefreshedEvent {
final String walletId;
BalanceRefreshedEvent(this.walletId) {
Logging.instance.log(
"BalanceRefreshedEvent fired on $walletId",
level: LogLevel.Info,
);
}
}

View file

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:decimal/decimal.dart';
import 'package:http/http.dart' as http;
import 'package:stackwallet/exceptions/exchange/exchange_exception.dart';
import 'package:stackwallet/exceptions/exchange/majestic_bank/mb_exception.dart';
import 'package:stackwallet/exceptions/exchange/pair_unavailable_exception.dart';
import 'package:stackwallet/models/exchange/majestic_bank/mb_limit.dart';
import 'package:stackwallet/models/exchange/majestic_bank/mb_order.dart';
@ -335,6 +336,15 @@ class MajesticBankAPI {
final jsonObject = await _makeGetRequest(uri);
final json = Map<String, dynamic>.from(jsonObject as Map);
if (json.length == 2) {
return ExchangeResponse(
exception: MBException(
json["status"] as String,
ExchangeExceptionType.orderNotFound,
),
);
}
final status = MBOrderStatus(
orderId: json["trx"] as String,
status: json["status"] as String,

View file

@ -282,6 +282,33 @@ class MajesticBankExchange extends Exchange {
return ExchangeResponse(value: updatedTrade);
} else {
if (response.exception?.type == ExchangeExceptionType.orderNotFound) {
final updatedTrade = Trade(
uuid: trade.uuid,
tradeId: trade.tradeId,
rateType: trade.rateType,
direction: trade.direction,
timestamp: trade.timestamp,
updatedAt: DateTime.now(),
payInCurrency: trade.payInCurrency,
payInAmount: trade.payInAmount,
payInAddress: trade.payInAddress,
payInNetwork: trade.payInNetwork,
payInExtraId: trade.payInExtraId,
payInTxid: trade.payInTxid,
payOutCurrency: trade.payOutCurrency,
payOutAmount: trade.payOutAmount,
payOutAddress: trade.payOutAddress,
payOutNetwork: trade.payOutNetwork,
payOutExtraId: trade.payOutExtraId,
payOutTxid: trade.payOutTxid,
refundAddress: trade.refundAddress,
refundExtraId: trade.refundExtraId,
status: "Completed",
exchangeName: exchangeName,
);
return ExchangeResponse(value: updatedTrade);
}
return ExchangeResponse(exception: response.exception);
}
}

View file

@ -0,0 +1,78 @@
import 'dart:async';
import 'package:isar/isar.dart';
import 'package:stackwallet/db/main_db.dart';
import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/services/event_bus/events/global/balance_refreshed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
mixin CoinControlInterface {
late final String _walletId;
late final String _walletName;
late final Coin _coin;
late final MainDB _db;
late final Future<int> Function() _getChainHeight;
late final Future<void> Function(Balance) _refreshedBalanceCallback;
void initCoinControlInterface({
required String walletId,
required String walletName,
required Coin coin,
required MainDB db,
required Future<int> Function() getChainHeight,
required Future<void> Function(Balance) refreshedBalanceCallback,
}) {
_walletId = walletId;
_walletName = walletName;
_coin = coin;
_db = db;
_getChainHeight = getChainHeight;
_refreshedBalanceCallback = refreshedBalanceCallback;
}
Future<void> refreshBalance({bool notify = false}) async {
final utxos = await _db.getUTXOs(_walletId).findAll();
final currentChainHeight = await _getChainHeight();
int satoshiBalanceTotal = 0;
int satoshiBalancePending = 0;
int satoshiBalanceSpendable = 0;
int satoshiBalanceBlocked = 0;
for (final utxo in utxos) {
satoshiBalanceTotal += utxo.value;
if (utxo.isBlocked) {
satoshiBalanceBlocked += utxo.value;
} else {
if (utxo.isConfirmed(
currentChainHeight,
_coin.requiredConfirmations,
)) {
satoshiBalanceSpendable += utxo.value;
} else {
satoshiBalancePending += utxo.value;
}
}
}
final balance = Balance(
coin: _coin,
total: satoshiBalanceTotal,
spendable: satoshiBalanceSpendable,
blockedTotal: satoshiBalanceBlocked,
pendingSpendable: satoshiBalancePending,
);
await _refreshedBalanceCallback(balance);
if (notify) {
GlobalEventBus.instance.fire(
BalanceRefreshedEvent(
_walletId,
),
);
}
}
}

View file

@ -223,6 +223,9 @@ class NotificationsService extends ChangeNotifier {
case "expired":
case "Finished":
case "finished":
case "Completed":
case "completed":
case "Not found":
shouldWatchForUpdates = false;
break;
default:

View file

@ -48,8 +48,20 @@ class _BUY {
}
}
class _COIN_CONTROL {
const _COIN_CONTROL();
String get blocked => "assets/svg/coin_control/frozen.svg";
String get unBlocked => "assets/svg/coin_control/unfrozen.svg";
String get gamePad => "assets/svg/coin_control/gamepad.svg";
String get selected => "assets/svg/coin_control/selected.svg";
}
class _SVG {
const _SVG();
final coinControl = const _COIN_CONTROL();
String? background(BuildContext context) {
switch (Theme.of(context).extension<StackColors>()!.themeType) {
case ThemeType.light:

View file

@ -14,6 +14,7 @@ import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'
import 'package:stackwallet/services/coins/particl/particl_wallet.dart'
as particl;
import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart' as wow;
import 'package:stackwallet/utilities/constants.dart';
enum Coin {
bitcoin,
@ -232,6 +233,8 @@ extension CoinExt on Coin {
return nmc.MINIMUM_CONFIRMATIONS;
}
}
int get decimals => Constants.decimalPlacesForCoin(this);
}
Coin coinFromPrettyName(String name) {

View file

@ -40,6 +40,7 @@ class Prefs extends ChangeNotifier {
_familiarity = await _getHasFamiliarity();
_userId = await _getUserId();
_signupEpoch = await _getSignupEpoch();
_enableCoinControl = await _getEnableCoinControl();
_initialized = true;
}
@ -645,4 +646,27 @@ class Prefs extends ChangeNotifier {
boxName: DB.boxNamePrefs, key: "signupEpoch", value: _signupEpoch);
// notifyListeners();
}
// show testnet coins
bool _enableCoinControl = false;
bool get enableCoinControl => _enableCoinControl;
set enableCoinControl(bool enableCoinControl) {
if (_enableCoinControl != enableCoinControl) {
DB.instance.put<dynamic>(
boxName: DB.boxNamePrefs,
key: "enableCoinControl",
value: enableCoinControl);
_enableCoinControl = enableCoinControl;
notifyListeners();
}
}
Future<bool> _getEnableCoinControl() async {
return await DB.instance.get<dynamic>(
boxName: DB.boxNamePrefs, key: "enableCoinControl") as bool? ??
false;
}
}

View file

@ -0,0 +1,53 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
class SimpleCopyButton extends StatelessWidget {
const SimpleCopyButton({
Key? key,
required this.data,
}) : super(key: key);
final String data;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () async {
await Clipboard.setData(ClipboardData(text: data));
if (context.mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
context: context,
),
);
}
},
child: Row(
children: [
SvgPicture.asset(
Assets.svg.copy,
width: 10,
height: 10,
color: Theme.of(context).extension<StackColors>()!.infoItemIcons,
),
const SizedBox(
width: 4,
),
Text(
"Copy",
style: STextStyles.link2(context),
),
],
),
);
}
}

View file

@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/pages/generic/single_field_edit_view.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:tuple/tuple.dart';
class SimpleEditButton extends StatelessWidget {
const SimpleEditButton({
Key? key,
required this.editValue,
required this.editLabel,
required this.onValueChanged,
}) : super(key: key);
final String editValue;
final String editLabel;
final void Function(String) onValueChanged;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () async {
final result = await Navigator.of(context).pushNamed(
SingleFieldEditView.routeName,
arguments: Tuple2(
editValue,
editLabel,
),
);
if (result is String && result != editValue) {
onValueChanged(result);
}
},
child: Row(
children: [
SvgPicture.asset(
Assets.svg.pencil,
width: 10,
height: 10,
color: Theme.of(context).extension<StackColors>()!.infoItemIcons,
),
const SizedBox(
width: 4,
),
Text(
"Edit",
style: STextStyles.link2(context),
),
],
),
);
}
}

View file

@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/rounded_container.dart';
import '../../utilities/theme/stack_colors.dart';
enum UTXOStatusIconStatus {
confirmed,
unconfirmed;
}
class UTXOStatusIcon extends StatelessWidget {
const UTXOStatusIcon({
Key? key,
required this.width,
required this.height,
required this.blocked,
required this.selected,
required this.status,
required this.background,
}) : super(key: key);
final double width;
final double height;
final bool blocked;
final bool selected;
final UTXOStatusIconStatus status;
final Color background;
final _availableColor = const Color(0xFFF7931A);
final _blockedColor = const Color(0xFF96B0D6);
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: status == UTXOStatusIconStatus.unconfirmed,
builder: (child) => Stack(
children: [
child,
Positioned(
right: 0,
bottom: 0,
child: Stack(
children: [
RoundedContainer(
radiusMultiplier: 100,
color: background,
width: width / 2.8,
height: height / 2.8,
),
Positioned(
right: width / 2.8 - width / 3,
left: width / 2.8 - width / 3,
top: height / 2.8 - height / 3,
child: SvgPicture.asset(
Assets.svg.pending,
width: width / 3,
height: height / 3,
),
),
],
),
),
],
),
child: Stack(
alignment: Alignment.center,
children: [
RoundedContainer(
radiusMultiplier: 100,
color: selected
? Theme.of(context).extension<StackColors>()!.infoItemIcons
: blocked
? _blockedColor.withOpacity(0.3)
: _availableColor.withOpacity(0.2),
width: width,
height: height,
),
SvgPicture.asset(
selected
? Assets.svg.coinControl.selected
: blocked
? Assets.svg.coinControl.blocked
: Assets.svg.coinControl.unBlocked,
width: 20,
height: 20,
color: selected
? Colors.white
: blocked
? _blockedColor
: _availableColor,
),
],
),
);
}
}

View file

@ -82,6 +82,8 @@ class _TransactionCardState extends ConsumerState<TransactionCard> {
} else {
return "Sending";
}
} else if (type == TransactionType.sentToSelf) {
return "Sent to self";
} else {
return type.name;
}

View file

@ -324,6 +324,9 @@ flutter:
# coin icons
- assets/svg/coin_icons/
# coin control icons
- assets/svg/coin_control/
# lottie animations
- assets/lottie/test.json
- assets/lottie/test2.json

View file

@ -652,6 +652,19 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs {
returnValueForMissingStub: null,
);
@override
bool get enableCoinControl => (super.noSuchMethod(
Invocation.getter(#enableCoinControl),
returnValue: false,
) as bool);
@override
set enableCoinControl(bool? enableCoinControl) => super.noSuchMethod(
Invocation.setter(
#enableCoinControl,
enableCoinControl,
),
returnValueForMissingStub: null,
);
@override
bool get hasListeners => (super.noSuchMethod(
Invocation.getter(#hasListeners),
returnValue: false,

View file

@ -373,6 +373,19 @@ class MockPrefs extends _i1.Mock implements _i4.Prefs {
returnValueForMissingStub: null,
);
@override
bool get enableCoinControl => (super.noSuchMethod(
Invocation.getter(#enableCoinControl),
returnValue: false,
) as bool);
@override
set enableCoinControl(bool? enableCoinControl) => super.noSuchMethod(
Invocation.setter(
#enableCoinControl,
enableCoinControl,
),
returnValueForMissingStub: null,
);
@override
bool get hasListeners => (super.noSuchMethod(
Invocation.getter(#hasListeners),
returnValue: false,

View file

@ -52,6 +52,7 @@ void main() {
when(mockLocaleService.locale).thenAnswer((_) => "en_US");
when(mockPrefs.currency).thenAnswer((_) => "USD");
when(mockPrefs.enableCoinControl).thenAnswer((_) => false);
when(wallet.validateAddress("send to address"))
.thenAnswer((realInvocation) => true);
@ -114,6 +115,7 @@ void main() {
when(mockLocaleService.locale).thenAnswer((_) => "en_US");
when(mockPrefs.currency).thenAnswer((_) => "USD");
when(mockPrefs.enableCoinControl).thenAnswer((_) => false);
when(wallet.validateAddress("send to address"))
.thenAnswer((realInvocation) => false);

View file

@ -1415,23 +1415,24 @@ class MockBitcoinWallet extends _i1.Mock implements _i26.BitcoinWallet {
returnValue: 0,
) as int);
@override
dynamic coinSelection(
int? satoshiAmountToSend,
int? selectedTxFeeRate,
String? _recipientAddress,
bool? isSendAll, {
dynamic coinSelection({
required int? satoshiAmountToSend,
required int? selectedTxFeeRate,
required String? recipientAddress,
required bool? coinControl,
required bool? isSendAll,
int? additionalOutputs = 0,
List<_i16.UTXO>? utxos,
}) =>
super.noSuchMethod(Invocation.method(
#coinSelection,
[
satoshiAmountToSend,
selectedTxFeeRate,
_recipientAddress,
isSendAll,
],
[],
{
#satoshiAmountToSend: satoshiAmountToSend,
#selectedTxFeeRate: selectedTxFeeRate,
#recipientAddress: recipientAddress,
#coinControl: coinControl,
#isSendAll: isSendAll,
#additionalOutputs: additionalOutputs,
#utxos: utxos,
},
@ -1988,6 +1989,22 @@ class MockBitcoinWallet extends _i1.Mock implements _i26.BitcoinWallet {
returnValue: _i22.Future<_i18.PaymentCode?>.value(),
) as _i22.Future<_i18.PaymentCode?>);
@override
_i22.Future<_i18.PaymentCode?> unBlindedPaymentCodeFromTransactionBad({
required _i16.Transaction? transaction,
required _i16.Address? myNotificationAddress,
}) =>
(super.noSuchMethod(
Invocation.method(
#unBlindedPaymentCodeFromTransactionBad,
[],
{
#transaction: transaction,
#myNotificationAddress: myNotificationAddress,
},
),
returnValue: _i22.Future<_i18.PaymentCode?>.value(),
) as _i22.Future<_i18.PaymentCode?>);
@override
_i22.Future<List<_i18.PaymentCode>>
getAllPaymentCodesFromNotificationTransactions() => (super.noSuchMethod(
Invocation.method(
@ -2152,6 +2169,41 @@ class MockBitcoinWallet extends _i1.Mock implements _i26.BitcoinWallet {
),
returnValue: _i22.Future<String>.value(''),
) as _i22.Future<String>);
@override
void initCoinControlInterface({
required String? walletId,
required String? walletName,
required _i21.Coin? coin,
required _i13.MainDB? db,
required _i22.Future<int> Function()? getChainHeight,
required _i22.Future<void> Function(_i12.Balance)? refreshedBalanceCallback,
}) =>
super.noSuchMethod(
Invocation.method(
#initCoinControlInterface,
[],
{
#walletId: walletId,
#walletName: walletName,
#coin: coin,
#db: db,
#getChainHeight: getChainHeight,
#refreshedBalanceCallback: refreshedBalanceCallback,
},
),
returnValueForMissingStub: null,
);
@override
_i22.Future<void> refreshBalance({bool? notify = false}) =>
(super.noSuchMethod(
Invocation.method(
#refreshBalance,
[],
{#notify: notify},
),
returnValue: _i22.Future<void>.value(),
returnValueForMissingStub: _i22.Future<void>.value(),
) as _i22.Future<void>);
}
/// A class which mocks [LocaleService].
@ -2483,6 +2535,19 @@ class MockPrefs extends _i1.Mock implements _i23.Prefs {
returnValueForMissingStub: null,
);
@override
bool get enableCoinControl => (super.noSuchMethod(
Invocation.getter(#enableCoinControl),
returnValue: false,
) as bool);
@override
set enableCoinControl(bool? enableCoinControl) => super.noSuchMethod(
Invocation.setter(
#enableCoinControl,
enableCoinControl,
),
returnValueForMissingStub: null,
);
@override
bool get hasListeners => (super.noSuchMethod(
Invocation.getter(#hasListeners),
returnValue: false,
@ -2712,6 +2777,11 @@ class MockManager extends _i1.Mock implements _i6.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -373,6 +373,11 @@ class MockManager extends _i1.Mock implements _i11.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -334,6 +334,11 @@ class MockManager extends _i1.Mock implements _i9.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -332,6 +332,11 @@ class MockManager extends _i1.Mock implements _i9.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -322,6 +322,19 @@ class MockPrefs extends _i1.Mock implements _i3.Prefs {
returnValueForMissingStub: null,
);
@override
bool get enableCoinControl => (super.noSuchMethod(
Invocation.getter(#enableCoinControl),
returnValue: false,
) as bool);
@override
set enableCoinControl(bool? enableCoinControl) => super.noSuchMethod(
Invocation.setter(
#enableCoinControl,
enableCoinControl,
),
returnValueForMissingStub: null,
);
@override
bool get hasListeners => (super.noSuchMethod(
Invocation.getter(#hasListeners),
returnValue: false,

View file

@ -641,6 +641,11 @@ class MockManager extends _i1.Mock implements _i12.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -428,6 +428,11 @@ class MockManager extends _i1.Mock implements _i9.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -428,6 +428,11 @@ class MockManager extends _i1.Mock implements _i9.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -428,6 +428,11 @@ class MockManager extends _i1.Mock implements _i9.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -203,6 +203,11 @@ class MockManager extends _i1.Mock implements _i5.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -426,6 +426,11 @@ class MockManager extends _i1.Mock implements _i9.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -641,6 +641,11 @@ class MockManager extends _i1.Mock implements _i12.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -482,6 +482,11 @@ class MockManager extends _i1.Mock implements _i12.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -203,6 +203,11 @@ class MockManager extends _i1.Mock implements _i5.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -203,6 +203,11 @@ class MockManager extends _i1.Mock implements _i5.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -418,6 +418,11 @@ class MockManager extends _i1.Mock implements _i11.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -418,6 +418,11 @@ class MockManager extends _i1.Mock implements _i11.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -203,6 +203,11 @@ class MockManager extends _i1.Mock implements _i5.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -203,6 +203,11 @@ class MockManager extends _i1.Mock implements _i5.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -426,6 +426,11 @@ class MockManager extends _i1.Mock implements _i9.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -683,6 +683,11 @@ class MockManager extends _i1.Mock implements _i15.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -426,6 +426,11 @@ class MockManager extends _i1.Mock implements _i9.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -205,6 +205,11 @@ class MockManager extends _i1.Mock implements _i5.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -204,6 +204,11 @@ class MockManager extends _i1.Mock implements _i5.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -203,6 +203,11 @@ class MockManager extends _i1.Mock implements _i5.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -245,6 +245,11 @@ class MockManager extends _i1.Mock implements _i8.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -205,6 +205,11 @@ class MockManager extends _i1.Mock implements _i5.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -1207,23 +1207,24 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet {
returnValue: 0,
) as int);
@override
dynamic coinSelection(
int? satoshiAmountToSend,
int? selectedTxFeeRate,
String? _recipientAddress,
bool? isSendAll, {
dynamic coinSelection({
required int? satoshiAmountToSend,
required int? selectedTxFeeRate,
required String? recipientAddress,
required bool? coinControl,
required bool? isSendAll,
int? additionalOutputs = 0,
List<_i15.UTXO>? utxos,
}) =>
super.noSuchMethod(Invocation.method(
#coinSelection,
[
satoshiAmountToSend,
selectedTxFeeRate,
_recipientAddress,
isSendAll,
],
[],
{
#satoshiAmountToSend: satoshiAmountToSend,
#selectedTxFeeRate: selectedTxFeeRate,
#recipientAddress: recipientAddress,
#coinControl: coinControl,
#isSendAll: isSendAll,
#additionalOutputs: additionalOutputs,
#utxos: utxos,
},
@ -1780,6 +1781,22 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet {
returnValue: _i22.Future<_i17.PaymentCode?>.value(),
) as _i22.Future<_i17.PaymentCode?>);
@override
_i22.Future<_i17.PaymentCode?> unBlindedPaymentCodeFromTransactionBad({
required _i15.Transaction? transaction,
required _i15.Address? myNotificationAddress,
}) =>
(super.noSuchMethod(
Invocation.method(
#unBlindedPaymentCodeFromTransactionBad,
[],
{
#transaction: transaction,
#myNotificationAddress: myNotificationAddress,
},
),
returnValue: _i22.Future<_i17.PaymentCode?>.value(),
) as _i22.Future<_i17.PaymentCode?>);
@override
_i22.Future<List<_i17.PaymentCode>>
getAllPaymentCodesFromNotificationTransactions() => (super.noSuchMethod(
Invocation.method(
@ -1944,6 +1961,41 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet {
),
returnValue: _i22.Future<String>.value(''),
) as _i22.Future<String>);
@override
void initCoinControlInterface({
required String? walletId,
required String? walletName,
required _i21.Coin? coin,
required _i12.MainDB? db,
required _i22.Future<int> Function()? getChainHeight,
required _i22.Future<void> Function(_i11.Balance)? refreshedBalanceCallback,
}) =>
super.noSuchMethod(
Invocation.method(
#initCoinControlInterface,
[],
{
#walletId: walletId,
#walletName: walletName,
#coin: coin,
#db: db,
#getChainHeight: getChainHeight,
#refreshedBalanceCallback: refreshedBalanceCallback,
},
),
returnValueForMissingStub: null,
);
@override
_i22.Future<void> refreshBalance({bool? notify = false}) =>
(super.noSuchMethod(
Invocation.method(
#refreshBalance,
[],
{#notify: notify},
),
returnValue: _i22.Future<void>.value(),
returnValueForMissingStub: _i22.Future<void>.value(),
) as _i22.Future<void>);
}
/// A class which mocks [LocaleService].
@ -2355,6 +2407,11 @@ class MockManager extends _i1.Mock implements _i6.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -561,6 +561,19 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs {
returnValueForMissingStub: null,
);
@override
bool get enableCoinControl => (super.noSuchMethod(
Invocation.getter(#enableCoinControl),
returnValue: false,
) as bool);
@override
set enableCoinControl(bool? enableCoinControl) => super.noSuchMethod(
Invocation.setter(
#enableCoinControl,
enableCoinControl,
),
returnValueForMissingStub: null,
);
@override
bool get hasListeners => (super.noSuchMethod(
Invocation.getter(#hasListeners),
returnValue: false,

View file

@ -1194,23 +1194,24 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet {
returnValue: 0,
) as int);
@override
dynamic coinSelection(
int? satoshiAmountToSend,
int? selectedTxFeeRate,
String? _recipientAddress,
bool? isSendAll, {
dynamic coinSelection({
required int? satoshiAmountToSend,
required int? selectedTxFeeRate,
required String? recipientAddress,
required bool? coinControl,
required bool? isSendAll,
int? additionalOutputs = 0,
List<_i15.UTXO>? utxos,
}) =>
super.noSuchMethod(Invocation.method(
#coinSelection,
[
satoshiAmountToSend,
selectedTxFeeRate,
_recipientAddress,
isSendAll,
],
[],
{
#satoshiAmountToSend: satoshiAmountToSend,
#selectedTxFeeRate: selectedTxFeeRate,
#recipientAddress: recipientAddress,
#coinControl: coinControl,
#isSendAll: isSendAll,
#additionalOutputs: additionalOutputs,
#utxos: utxos,
},
@ -1767,6 +1768,22 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet {
returnValue: _i21.Future<_i17.PaymentCode?>.value(),
) as _i21.Future<_i17.PaymentCode?>);
@override
_i21.Future<_i17.PaymentCode?> unBlindedPaymentCodeFromTransactionBad({
required _i15.Transaction? transaction,
required _i15.Address? myNotificationAddress,
}) =>
(super.noSuchMethod(
Invocation.method(
#unBlindedPaymentCodeFromTransactionBad,
[],
{
#transaction: transaction,
#myNotificationAddress: myNotificationAddress,
},
),
returnValue: _i21.Future<_i17.PaymentCode?>.value(),
) as _i21.Future<_i17.PaymentCode?>);
@override
_i21.Future<List<_i17.PaymentCode>>
getAllPaymentCodesFromNotificationTransactions() => (super.noSuchMethod(
Invocation.method(
@ -1931,6 +1948,41 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet {
),
returnValue: _i21.Future<String>.value(''),
) as _i21.Future<String>);
@override
void initCoinControlInterface({
required String? walletId,
required String? walletName,
required _i20.Coin? coin,
required _i12.MainDB? db,
required _i21.Future<int> Function()? getChainHeight,
required _i21.Future<void> Function(_i11.Balance)? refreshedBalanceCallback,
}) =>
super.noSuchMethod(
Invocation.method(
#initCoinControlInterface,
[],
{
#walletId: walletId,
#walletName: walletName,
#coin: coin,
#db: db,
#getChainHeight: getChainHeight,
#refreshedBalanceCallback: refreshedBalanceCallback,
},
),
returnValueForMissingStub: null,
);
@override
_i21.Future<void> refreshBalance({bool? notify = false}) =>
(super.noSuchMethod(
Invocation.method(
#refreshBalance,
[],
{#notify: notify},
),
returnValue: _i21.Future<void>.value(),
returnValueForMissingStub: _i21.Future<void>.value(),
) as _i21.Future<void>);
}
/// A class which mocks [Manager].
@ -2080,6 +2132,11 @@ class MockManager extends _i1.Mock implements _i6.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -549,6 +549,11 @@ class MockManager extends _i1.Mock implements _i6.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,
@ -2322,6 +2327,19 @@ class MockPrefs extends _i1.Mock implements _i19.Prefs {
returnValueForMissingStub: null,
);
@override
bool get enableCoinControl => (super.noSuchMethod(
Invocation.getter(#enableCoinControl),
returnValue: false,
) as bool);
@override
set enableCoinControl(bool? enableCoinControl) => super.noSuchMethod(
Invocation.setter(
#enableCoinControl,
enableCoinControl,
),
returnValueForMissingStub: null,
);
@override
bool get hasListeners => (super.noSuchMethod(
Invocation.getter(#hasListeners),
returnValue: false,

View file

@ -957,23 +957,24 @@ class MockBitcoinWallet extends _i1.Mock implements _i23.BitcoinWallet {
returnValue: 0,
) as int);
@override
dynamic coinSelection(
int? satoshiAmountToSend,
int? selectedTxFeeRate,
String? _recipientAddress,
bool? isSendAll, {
dynamic coinSelection({
required int? satoshiAmountToSend,
required int? selectedTxFeeRate,
required String? recipientAddress,
required bool? coinControl,
required bool? isSendAll,
int? additionalOutputs = 0,
List<_i15.UTXO>? utxos,
}) =>
super.noSuchMethod(Invocation.method(
#coinSelection,
[
satoshiAmountToSend,
selectedTxFeeRate,
_recipientAddress,
isSendAll,
],
[],
{
#satoshiAmountToSend: satoshiAmountToSend,
#selectedTxFeeRate: selectedTxFeeRate,
#recipientAddress: recipientAddress,
#coinControl: coinControl,
#isSendAll: isSendAll,
#additionalOutputs: additionalOutputs,
#utxos: utxos,
},
@ -1530,6 +1531,22 @@ class MockBitcoinWallet extends _i1.Mock implements _i23.BitcoinWallet {
returnValue: _i20.Future<_i17.PaymentCode?>.value(),
) as _i20.Future<_i17.PaymentCode?>);
@override
_i20.Future<_i17.PaymentCode?> unBlindedPaymentCodeFromTransactionBad({
required _i15.Transaction? transaction,
required _i15.Address? myNotificationAddress,
}) =>
(super.noSuchMethod(
Invocation.method(
#unBlindedPaymentCodeFromTransactionBad,
[],
{
#transaction: transaction,
#myNotificationAddress: myNotificationAddress,
},
),
returnValue: _i20.Future<_i17.PaymentCode?>.value(),
) as _i20.Future<_i17.PaymentCode?>);
@override
_i20.Future<List<_i17.PaymentCode>>
getAllPaymentCodesFromNotificationTransactions() => (super.noSuchMethod(
Invocation.method(
@ -1694,6 +1711,41 @@ class MockBitcoinWallet extends _i1.Mock implements _i23.BitcoinWallet {
),
returnValue: _i20.Future<String>.value(''),
) as _i20.Future<String>);
@override
void initCoinControlInterface({
required String? walletId,
required String? walletName,
required _i19.Coin? coin,
required _i12.MainDB? db,
required _i20.Future<int> Function()? getChainHeight,
required _i20.Future<void> Function(_i11.Balance)? refreshedBalanceCallback,
}) =>
super.noSuchMethod(
Invocation.method(
#initCoinControlInterface,
[],
{
#walletId: walletId,
#walletName: walletName,
#coin: coin,
#db: db,
#getChainHeight: getChainHeight,
#refreshedBalanceCallback: refreshedBalanceCallback,
},
),
returnValueForMissingStub: null,
);
@override
_i20.Future<void> refreshBalance({bool? notify = false}) =>
(super.noSuchMethod(
Invocation.method(
#refreshBalance,
[],
{#notify: notify},
),
returnValue: _i20.Future<void>.value(),
returnValueForMissingStub: _i20.Future<void>.value(),
) as _i20.Future<void>);
}
/// A class which mocks [LocaleService].

View file

@ -1206,23 +1206,24 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet {
returnValue: 0,
) as int);
@override
dynamic coinSelection(
int? satoshiAmountToSend,
int? selectedTxFeeRate,
String? _recipientAddress,
bool? isSendAll, {
dynamic coinSelection({
required int? satoshiAmountToSend,
required int? selectedTxFeeRate,
required String? recipientAddress,
required bool? coinControl,
required bool? isSendAll,
int? additionalOutputs = 0,
List<_i15.UTXO>? utxos,
}) =>
super.noSuchMethod(Invocation.method(
#coinSelection,
[
satoshiAmountToSend,
selectedTxFeeRate,
_recipientAddress,
isSendAll,
],
[],
{
#satoshiAmountToSend: satoshiAmountToSend,
#selectedTxFeeRate: selectedTxFeeRate,
#recipientAddress: recipientAddress,
#coinControl: coinControl,
#isSendAll: isSendAll,
#additionalOutputs: additionalOutputs,
#utxos: utxos,
},
@ -1779,6 +1780,22 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet {
returnValue: _i22.Future<_i17.PaymentCode?>.value(),
) as _i22.Future<_i17.PaymentCode?>);
@override
_i22.Future<_i17.PaymentCode?> unBlindedPaymentCodeFromTransactionBad({
required _i15.Transaction? transaction,
required _i15.Address? myNotificationAddress,
}) =>
(super.noSuchMethod(
Invocation.method(
#unBlindedPaymentCodeFromTransactionBad,
[],
{
#transaction: transaction,
#myNotificationAddress: myNotificationAddress,
},
),
returnValue: _i22.Future<_i17.PaymentCode?>.value(),
) as _i22.Future<_i17.PaymentCode?>);
@override
_i22.Future<List<_i17.PaymentCode>>
getAllPaymentCodesFromNotificationTransactions() => (super.noSuchMethod(
Invocation.method(
@ -1943,6 +1960,41 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet {
),
returnValue: _i22.Future<String>.value(''),
) as _i22.Future<String>);
@override
void initCoinControlInterface({
required String? walletId,
required String? walletName,
required _i21.Coin? coin,
required _i12.MainDB? db,
required _i22.Future<int> Function()? getChainHeight,
required _i22.Future<void> Function(_i11.Balance)? refreshedBalanceCallback,
}) =>
super.noSuchMethod(
Invocation.method(
#initCoinControlInterface,
[],
{
#walletId: walletId,
#walletName: walletName,
#coin: coin,
#db: db,
#getChainHeight: getChainHeight,
#refreshedBalanceCallback: refreshedBalanceCallback,
},
),
returnValueForMissingStub: null,
);
@override
_i22.Future<void> refreshBalance({bool? notify = false}) =>
(super.noSuchMethod(
Invocation.method(
#refreshBalance,
[],
{#notify: notify},
),
returnValue: _i22.Future<void>.value(),
returnValueForMissingStub: _i22.Future<void>.value(),
) as _i22.Future<void>);
}
/// A class which mocks [NodeService].
@ -2292,6 +2344,11 @@ class MockManager extends _i1.Mock implements _i6.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,

View file

@ -1206,23 +1206,24 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet {
returnValue: 0,
) as int);
@override
dynamic coinSelection(
int? satoshiAmountToSend,
int? selectedTxFeeRate,
String? _recipientAddress,
bool? isSendAll, {
dynamic coinSelection({
required int? satoshiAmountToSend,
required int? selectedTxFeeRate,
required String? recipientAddress,
required bool? coinControl,
required bool? isSendAll,
int? additionalOutputs = 0,
List<_i15.UTXO>? utxos,
}) =>
super.noSuchMethod(Invocation.method(
#coinSelection,
[
satoshiAmountToSend,
selectedTxFeeRate,
_recipientAddress,
isSendAll,
],
[],
{
#satoshiAmountToSend: satoshiAmountToSend,
#selectedTxFeeRate: selectedTxFeeRate,
#recipientAddress: recipientAddress,
#coinControl: coinControl,
#isSendAll: isSendAll,
#additionalOutputs: additionalOutputs,
#utxos: utxos,
},
@ -1779,6 +1780,22 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet {
returnValue: _i22.Future<_i17.PaymentCode?>.value(),
) as _i22.Future<_i17.PaymentCode?>);
@override
_i22.Future<_i17.PaymentCode?> unBlindedPaymentCodeFromTransactionBad({
required _i15.Transaction? transaction,
required _i15.Address? myNotificationAddress,
}) =>
(super.noSuchMethod(
Invocation.method(
#unBlindedPaymentCodeFromTransactionBad,
[],
{
#transaction: transaction,
#myNotificationAddress: myNotificationAddress,
},
),
returnValue: _i22.Future<_i17.PaymentCode?>.value(),
) as _i22.Future<_i17.PaymentCode?>);
@override
_i22.Future<List<_i17.PaymentCode>>
getAllPaymentCodesFromNotificationTransactions() => (super.noSuchMethod(
Invocation.method(
@ -1943,6 +1960,41 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet {
),
returnValue: _i22.Future<String>.value(''),
) as _i22.Future<String>);
@override
void initCoinControlInterface({
required String? walletId,
required String? walletName,
required _i21.Coin? coin,
required _i12.MainDB? db,
required _i22.Future<int> Function()? getChainHeight,
required _i22.Future<void> Function(_i11.Balance)? refreshedBalanceCallback,
}) =>
super.noSuchMethod(
Invocation.method(
#initCoinControlInterface,
[],
{
#walletId: walletId,
#walletName: walletName,
#coin: coin,
#db: db,
#getChainHeight: getChainHeight,
#refreshedBalanceCallback: refreshedBalanceCallback,
},
),
returnValueForMissingStub: null,
);
@override
_i22.Future<void> refreshBalance({bool? notify = false}) =>
(super.noSuchMethod(
Invocation.method(
#refreshBalance,
[],
{#notify: notify},
),
returnValue: _i22.Future<void>.value(),
returnValueForMissingStub: _i22.Future<void>.value(),
) as _i22.Future<void>);
}
/// A class which mocks [NodeService].
@ -2292,6 +2344,11 @@ class MockManager extends _i1.Mock implements _i6.Manager {
returnValue: false,
) as bool);
@override
bool get hasCoinControlSupport => (super.noSuchMethod(
Invocation.getter(#hasCoinControlSupport),
returnValue: false,
) as bool);
@override
int get rescanOnOpenVersion => (super.noSuchMethod(
Invocation.getter(#rescanOnOpenVersion),
returnValue: 0,