From faf78831533958fe3a6cc6fd48700d8859310cca Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 19 Nov 2025 13:24:45 -0600 Subject: [PATCH 01/11] Amount and Balance QoL constructors --- lib/models/balance.dart | 31 ++++++++-------- lib/utilities/amount/amount.dart | 62 ++++++++++---------------------- 2 files changed, 34 insertions(+), 59 deletions(-) diff --git a/lib/models/balance.dart b/lib/models/balance.dart index 968041254..4fc728ade 100644 --- a/lib/models/balance.dart +++ b/lib/models/balance.dart @@ -19,19 +19,18 @@ class Balance { final Amount blockedTotal; final Amount pendingSpendable; - Balance({ + const Balance({ required this.total, required this.spendable, required this.blockedTotal, required this.pendingSpendable, }); - factory Balance.zeroFor({required CryptoCurrency currency}) { - final amount = Amount( - rawValue: BigInt.zero, - fractionDigits: currency.fractionDigits, - ); + factory Balance.zeroFor({required CryptoCurrency currency}) => + .zeroWith(fractionDigits: currency.fractionDigits); + factory Balance.zeroWith({required int fractionDigits}) { + final amount = Amount.zeroWith(fractionDigits: fractionDigits); return Balance( total: amount, spendable: amount, @@ -41,11 +40,11 @@ class Balance { } String toJsonIgnoreCoin() => jsonEncode({ - "total": total.toJsonString(), - "spendable": spendable.toJsonString(), - "blockedTotal": blockedTotal.toJsonString(), - "pendingSpendable": pendingSpendable.toJsonString(), - }); + "total": total.toJsonString(), + "spendable": spendable.toJsonString(), + "blockedTotal": blockedTotal.toJsonString(), + "pendingSpendable": pendingSpendable.toJsonString(), + }); // need to fall back to parsing from int due to cached balances being previously // stored as int values instead of Amounts @@ -82,11 +81,11 @@ class Balance { } Map toMap() => { - "total": total, - "spendable": spendable, - "blockedTotal": blockedTotal, - "pendingSpendable": pendingSpendable, - }; + "total": total, + "spendable": spendable, + "blockedTotal": blockedTotal, + "pendingSpendable": pendingSpendable, + }; @override String toString() { diff --git a/lib/utilities/amount/amount.dart b/lib/utilities/amount/amount.dart index a1a68576f..b31d87d7a 100644 --- a/lib/utilities/amount/amount.dart +++ b/lib/utilities/amount/amount.dart @@ -15,31 +15,24 @@ import 'package:decimal/decimal.dart'; import '../util.dart'; class Amount { - Amount({ - required BigInt rawValue, - required this.fractionDigits, - }) : assert(fractionDigits >= 0), - _value = rawValue; + const Amount({required BigInt rawValue, required this.fractionDigits}) + : assert(fractionDigits >= 0), + _value = rawValue; /// special zero case with [fractionDigits] set to 0 - static Amount get zero => Amount( - rawValue: BigInt.zero, - fractionDigits: 0, - ); + static Amount get zero => .zeroWith(fractionDigits: 0); - Amount.zeroWith({required this.fractionDigits}) - : assert(fractionDigits >= 0), - _value = BigInt.zero; + Amount.zeroWith({required int fractionDigits}) + : this(rawValue: BigInt.from(0), fractionDigits: fractionDigits); /// truncate decimal value to [fractionDigits] places - Amount.fromDecimal(Decimal amount, {required this.fractionDigits}) - : assert(fractionDigits >= 0), - _value = amount.shift(fractionDigits).toBigInt(); - - static Amount? tryParseFiatString( - String value, { - required String locale, - }) { + Amount.fromDecimal(Decimal amount, {required int fractionDigits}) + : this( + rawValue: amount.shift(fractionDigits).toBigInt(), + fractionDigits: fractionDigits, + ); + + static Amount? tryParseFiatString(String value, {required String locale}) { final parts = value.split(" "); if (parts.first.isEmpty) { @@ -98,9 +91,7 @@ class Amount { return jsonEncode(toMap()); } - String fiatString({ - required String locale, - }) { + String fiatString({required String locale}) { final wholeNumber = decimal.truncate(); // get number symbols for decimal place and group separator @@ -172,10 +163,7 @@ class Amount { "fractionDigits do not match: this=$this, other=$other", ); } - return Amount( - rawValue: raw + other.raw, - fractionDigits: fractionDigits, - ); + return Amount(rawValue: raw + other.raw, fractionDigits: fractionDigits); } Amount operator -(Amount other) { @@ -184,10 +172,7 @@ class Amount { "fractionDigits do not match: this=$this, other=$other", ); } - return Amount( - rawValue: raw - other.raw, - fractionDigits: fractionDigits, - ); + return Amount(rawValue: raw - other.raw, fractionDigits: fractionDigits); } Amount operator *(Amount other) { @@ -196,10 +181,7 @@ class Amount { "fractionDigits do not match: this=$this, other=$other", ); } - return Amount( - rawValue: raw * other.raw, - fractionDigits: fractionDigits, - ); + return Amount(rawValue: raw * other.raw, fractionDigits: fractionDigits); } // =========================================================================== @@ -226,18 +208,12 @@ class Amount { extension DecimalAmountExt on Decimal { Amount toAmount({required int fractionDigits}) { - return Amount.fromDecimal( - this, - fractionDigits: fractionDigits, - ); + return Amount.fromDecimal(this, fractionDigits: fractionDigits); } } extension IntAmountExtension on int { Amount toAmountAsRaw({required int fractionDigits}) { - return Amount( - rawValue: BigInt.from(this), - fractionDigits: fractionDigits, - ); + return Amount(rawValue: BigInt.from(this), fractionDigits: fractionDigits); } } From 80539b3dd0cda3937e1ab9dd9e96726a3244f6e2 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 21 Nov 2025 15:59:10 -0600 Subject: [PATCH 02/11] mweb things --- .../mweb_interface.dart | 70 +++++++++++++++---- 1 file changed, 57 insertions(+), 13 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart index 35a00595a..e183be63b 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart @@ -185,8 +185,45 @@ mixin MwebInterface Logging.instance.i("info.restoreHeight: ${info.restoreHeight}"); Logging.instance.i( - "info.otherData[WalletInfoKeys.mwebScanHeight]: ${info.otherData[WalletInfoKeys.mwebScanHeight]}", + "info.otherData[WalletInfoKeys.mwebScanHeight]:" + " ${info.otherData[WalletInfoKeys.mwebScanHeight]}", ); + + // ========================================================================= + final List utxos = []; + final stream = await client.utxos( + UtxosRequest( + fromHeight: info.restoreHeight, + scanSecret: await _scanSecret, + ), + ); + try { + await for (final utxo in stream.timeout(const Duration(seconds: 2))) { + final newUtxo = MwebUtxosCompanion( + outputId: Value(utxo.outputId), + address: Value(utxo.address), + value: Value(utxo.value.toInt()), + height: Value(utxo.height), + blockTime: Value(utxo.blockTime), + blocked: const Value(false), + used: const Value(false), + ); + utxos.add(newUtxo); + } + } catch (_) {} + + try { + await stream.cancel(); + } catch (_) {} + final db = Drift.get(walletId); + await db.transaction(() async { + await db.delete(db.mwebUtxos).go(); + for (final utxo in utxos) { + await db.into(db.mwebUtxos).insert(utxo); + } + }); + // ========================================================================= + final fromHeight = info.otherData[WalletInfoKeys.mwebScanHeight] as int? ?? info.restoreHeight; @@ -196,7 +233,6 @@ mixin MwebInterface scanSecret: await _scanSecret, ); - final db = Drift.get(walletId); _mwebUtxoSubscription = (await client.utxos(request)).listen((utxo) async { Logging.instance.t( "Found UTXO in stream: Utxo(" @@ -322,7 +358,8 @@ mixin MwebInterface Future
generateNextMwebAddress({bool isChange = false}) async { if (!info.isMwebEnabled) { throw Exception( - "Tried calling generateNextMwebAddress with mweb disabled for $walletId ${info.name}", + "Tried calling generateNextMwebAddress with mweb " + "disabled for $walletId ${info.name}", ); } @@ -382,7 +419,8 @@ mixin MwebInterface SpentRequest(outputId: [input.outpoint!.txid]), ); if (response.outputId.contains(input.outpoint!.txid)) { - // dummy to show tx as confirmed. Need a better way to handle this as its kind of stupid, resulting in terrible UX + // dummy to show tx as confirmed. Need a better way to handle + // this as its kind of stupid, resulting in terrible UX final dummyHeight = await chainHeight; TransactionV2? transaction = await mainDB.isar.transactionV2s @@ -480,7 +518,8 @@ mixin MwebInterface Future _confirmSendMweb({required TxData txData}) async { if (!info.isMwebEnabled) { throw Exception( - "Tried calling _confirmSendMweb with mweb disabled for $walletId ${info.name}", + "Tried calling _confirmSendMweb with mweb disabled for" + " $walletId ${info.name}", ); } @@ -533,7 +572,11 @@ mixin MwebInterface final db = Drift.get(walletId); await db.transaction(() async { for (final used in usedMwebUtxos) { - await db.update(db.mwebUtxos).replace(used); + await db + .update(db.mwebUtxos) + .replace( + used.copyWith(used: true), + ); // used should already be set to true here but... } }); } @@ -578,7 +621,8 @@ mixin MwebInterface Future anonymizeAllMweb() async { if (!info.isMwebEnabled) { Logging.instance.e( - "Tried calling anonymizeAllMweb with mweb disabled for $walletId ${info.name}", + "Tried calling anonymizeAllMweb with mweb disabled for" + " $walletId ${info.name}", ); return; } @@ -909,6 +953,9 @@ mixin MwebInterface final preOutputSum = outputs.fold(BigInt.zero, (p, e) => p + e.amount.raw); final fee = sumOfUtxosValue - preOutputSum; + final feeRate = + txData.satsPerVByte ?? (txData.feeRateAmount!.toInt() / 1000).ceil(); + final client = await _client; final resp = await client.create( @@ -916,7 +963,7 @@ mixin MwebInterface rawTx: txData.raw!.toUint8ListFromHex, scanSecret: await _scanSecret, spendSecret: await _spendSecret, - feeRatePerKb: Int64(txData.feeRateAmount!.toInt()), + feeRatePerKb: Int64(feeRate * 1000), dryRun: true, ), ); @@ -948,14 +995,11 @@ mixin MwebInterface BigInt feeIncrease = posOutputSum - expectedPegin; if (expectedPegin > BigInt.zero) { - feeIncrease += - BigInt.from((txData.feeRateAmount! / BigInt.from(1000)).ceil()) * - BigInt.from(41); + feeIncrease += BigInt.from(feeRate * 41); } - // bandaid: add one to account for a rounding error that happens sometimes return Amount( - rawValue: fee + feeIncrease + BigInt.one, + rawValue: fee + feeIncrease, fractionDigits: cryptoCurrency.fractionDigits, ); } From 31ca85926d2868731f482458ff4aba7d0f4c4935 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 20 Nov 2025 09:14:42 -0600 Subject: [PATCH 03/11] duo splash image --- asset_sources/icon/stack_duo/splash.png | Bin 0 -> 12284 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 asset_sources/icon/stack_duo/splash.png diff --git a/asset_sources/icon/stack_duo/splash.png b/asset_sources/icon/stack_duo/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..3078ee9e27480f786820d7ab3a5722c85e4ed735 GIT binary patch literal 12284 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 Z*qJWTyshl;9l%i` Date: Wed, 19 Nov 2025 15:25:19 -0600 Subject: [PATCH 04/11] only run flutter_native_splash for mobile builds --- scripts/app_config/shared/asset_generators.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/app_config/shared/asset_generators.sh b/scripts/app_config/shared/asset_generators.sh index 1aaef46d8..c062f95fa 100755 --- a/scripts/app_config/shared/asset_generators.sh +++ b/scripts/app_config/shared/asset_generators.sh @@ -21,6 +21,9 @@ if [[ "${APP_BUILD_PLATFORM}" = 'windows' ]]; then else flutter pub get dart run flutter_launcher_icons -f "${YAML_FILE}" - dart run flutter_native_splash:create + + if [[ "${APP_BUILD_PLATFORM}" = 'ios' || "${APP_BUILD_PLATFORM}" = 'android' ]]; then + dart run flutter_native_splash:create + fi fi popd \ No newline at end of file From acd9de2f1f7631a47e6ed887b72c97612ff74a84 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 24 Nov 2025 09:47:31 -0600 Subject: [PATCH 05/11] Increase spark mint tx size limit up to 250kb --- .../wallet/wallet_mixin_interfaces/spark_interface.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 5e1560f0a..138b58d4a 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -39,7 +39,8 @@ import 'electrumx_interface.dart'; const kDefaultSparkIndex = 1; // TODO dart style constants. Maybe move to spark lib? -const MAX_STANDARD_TX_WEIGHT = 400000; +// https://github.com/firoorg/firo/pull/1457/files#diff-1fc0f6b5081e8ed5dfa8bf230744ad08cc6f4c1147e98552f1f424b0492fe9bdR28 +const MAX_NEW_TX_WEIGHT = 1000000; //https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L16 const SPARK_OUT_LIMIT_PER_TX = 16; @@ -1734,7 +1735,7 @@ mixin SparkInterface final dummyTx = dummyTxb.build(); final nBytes = dummyTx.virtualSize(); - if (dummyTx.weight() > MAX_STANDARD_TX_WEIGHT) { + if (dummyTx.weight() > MAX_NEW_TX_WEIGHT) { throw Exception("Transaction too large"); } From 0aea1b2f767274e7b8dbf05a792c2b69f0579da2 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 24 Nov 2025 13:54:52 -0600 Subject: [PATCH 06/11] update mwebd --- .../wallet/wallet_mixin_interfaces/electrumx_interface.dart | 1 + pubspec.lock | 4 ++-- scripts/app_config/templates/pubspec.template.yaml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index c5dcd279b..98fd505ff 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -1942,6 +1942,7 @@ mixin ElectrumXInterface final data = await (this as MwebInterface).processMwebTransaction( mwebData, ); + Logging.instance.d("prepare MWEB send: $data"); return data.copyWith(fee: fee); } diff --git a/pubspec.lock b/pubspec.lock index b98546188..64550758b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1061,10 +1061,10 @@ packages: dependency: "direct main" description: name: flutter_mwebd - sha256: faaec843d9749c5d3cd02e9c7afbd7754449f93a4316fec43b2c26f372e0eb55 + sha256: "14f2a331b2621b78ddf62081ca8a466f6a2b4352a66950fffd68615c14e63edf" url: "https://pub.dev" source: hosted - version: "0.0.1-pre.10" + version: "0.0.1-pre.11" flutter_native_splash: dependency: "direct main" description: diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml index 0723e2d5b..030c902c5 100644 --- a/scripts/app_config/templates/pubspec.template.yaml +++ b/scripts/app_config/templates/pubspec.template.yaml @@ -76,7 +76,7 @@ dependencies: # %%END_ENABLE_SAL%% # %%ENABLE_MWEBD%% -# flutter_mwebd: ^0.0.1-pre.10 +# flutter_mwebd: 0.0.1-pre.11 # %%END_ENABLE_MWEBD%% monero_rpc: ^2.0.0 From f8c949036cb40c42f870eae9fc7333738267ea6f Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Nov 2025 07:55:37 -0600 Subject: [PATCH 07/11] spark transparent out limit increase --- .../wallet/wallet_mixin_interfaces/spark_interface.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 138b58d4a..3b4e819c0 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -499,13 +499,15 @@ mixin SparkInterface // See SPARK_VALUE_SPEND_LIMIT_PER_TRANSACTION at https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L17 // and COIN https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/bitcoin/amount.h#L17 // Note that as MAX_MONEY is greater than this limit, we can ignore it. See https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/bitcoin/amount.h#L31 + // NOTE: This was updated to 5x what is was before (previously 10k) if (transparentSumOut > Amount.fromDecimal( - Decimal.parse("10000"), + Decimal.parse("50000"), fractionDigits: cryptoCurrency.fractionDigits, )) { throw Exception( - "Spend to transparent address limit exceeded (10,000 Firo per transaction).", + "Spend to transparent address limit exceeded " + "(50,000 Firo per transaction).", ); } From 4907ba1ec49c9e75fb623e3da1edee72210ed952 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Nov 2025 10:00:45 -0600 Subject: [PATCH 08/11] spark view only wallet electrumx cache clear option fix --- .../wallet_settings_view.dart | 516 +++++++++--------- .../wallet_settings_wallet_settings_view.dart | 16 +- .../sub_widgets/wallet_navigation_bar.dart | 9 - .../sub_widgets/desktop_wallet_features.dart | 3 +- .../more_features/more_features_dialog.dart | 9 +- 5 files changed, 275 insertions(+), 278 deletions(-) delete mode 100644 lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart index 57808de9f..58ffaad71 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart @@ -352,6 +352,11 @@ class _WalletSettingsViewState extends ConsumerState { canBackup = false; } + final shouldShowClearSparkCache = + wallet is SparkInterface && + (!wallet.isViewOnly || + (wallet.isViewOnly && wallet.viewOnlyType == .spark)); + return Background( child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, @@ -363,258 +368,251 @@ class _WalletSettingsViewState extends ConsumerState { ), title: Text("Settings", style: STextStyles.navBarTitle(context)), ), - body: SafeArea( - child: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only(left: 12, top: 12, right: 12), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(4), - child: Column( - children: [ - SettingsListButton( - iconAssetName: Assets.svg.addressBook, - iconSize: 16, - title: "Address book", - onPressed: () { - Navigator.of(context).pushNamed( - AddressBookView.routeName, - arguments: coin, - ); - }, - ), - if (coin is FrostCurrency) - const SizedBox(height: 8), - if (coin is FrostCurrency) - SettingsListButton( - iconAssetName: Assets.svg.addressBook2, - iconSize: 16, - title: "FROST Multisig settings", - onPressed: () { - Navigator.of(context).pushNamed( - FrostMSWalletOptionsView.routeName, - arguments: walletId, - ); - }, - ), - const SizedBox(height: 8), - SettingsListButton( - iconAssetName: Assets.svg.node, - iconSize: 16, - title: "Network", - onPressed: () { - Navigator.of(context).pushNamed( - WalletNetworkSettingsView.routeName, - arguments: Tuple3( - walletId, - _currentSyncStatus, - widget.initialNodeStatus, - ), - ); - }, - ), - if (canBackup) const SizedBox(height: 8), - if (canBackup) - Consumer( - builder: (_, ref, __) { - return SettingsListButton( - iconAssetName: Assets.svg.lock, - iconSize: 16, - title: "Wallet backup", - onPressed: _walletBackupPressed, - ); - }, - ), - const SizedBox(height: 8), - SettingsListButton( - iconAssetName: Assets.svg.downloadFolder, - title: "Wallet settings", - iconSize: 16, - onPressed: () { - Navigator.of(context).pushNamed( - WalletSettingsWalletSettingsView - .routeName, - arguments: walletId, - ); - }, - ), - const SizedBox(height: 8), - SettingsListButton( - iconAssetName: Assets.svg.arrowRotate, - title: "Syncing preferences", - onPressed: () { - Navigator.of(context).pushNamed( - SyncingPreferencesView.routeName, - ); - }, - ), - if (xPubEnabled) const SizedBox(height: 8), - if (xPubEnabled) - Consumer( - builder: (_, ref, __) { - return SettingsListButton( - iconAssetName: Assets.svg.eye, - title: "Wallet xPub", - onPressed: _walletXPubPressed, - ); - }, - ), - if (sparkViewKeyEnabled) - const SizedBox(height: 8), - if (sparkViewKeyEnabled) - Consumer( - builder: (_, ref, __) { - return SettingsListButton( - iconAssetName: Assets.svg.eye, - title: "Spark view key", - onPressed: _walletSparkViewKeyPressed, - ); - }, - ), - if (coin is Firo) const SizedBox(height: 8), - if (coin is Firo) - Consumer( - builder: (_, ref, __) { - return SettingsListButton( - iconAssetName: Assets.svg.eye, - title: "Clear electrumx cache", - onPressed: () async { - String? result; - await showDialog( - useSafeArea: false, - barrierDismissible: true, - context: context, - builder: (_) => StackOkDialog( - title: - "Are you sure you want to clear " - "${coin.prettyName} electrumx cache?", - onOkPressed: (value) { - result = value; - }, - leftButton: SecondaryButton( - label: "Cancel", - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ), - ); - - if (result == "OK" && - context.mounted) { - await showLoading( - whileFuture: Future.wait([ - Future.delayed( - const Duration( - milliseconds: 1500, - ), - ), - DB.instance - .clearSharedTransactionCache( - currency: coin, - ), - if (coin is Firo) - FiroCacheCoordinator.clearSharedCache( - coin.network, - ), - ]), - context: context, - message: "Clearing cache...", - ); - } - }, - ); - }, - ), - if (coin is NanoCurrency) - const SizedBox(height: 8), - if (coin is NanoCurrency) - Consumer( - builder: (_, ref, __) { - return SettingsListButton( - iconAssetName: Assets.svg.eye, - title: "Change representative", - onPressed: () { - Navigator.of(context).pushNamed( - ChangeRepresentativeView - .routeName, - arguments: widget.walletId, - ); - }, - ); - }, - ), - // const SizedBox( - // height: 8, - // ), - // SettingsListButton( - // iconAssetName: Assets.svg.ellipsis, - // title: "Debug Info", - // onPressed: () { - // Navigator.of(context) - // .pushNamed(DebugView.routeName); - // }, - // ), - ], - ), + body: _WalletSettingsViewBody( + children: [ + SettingsListButton( + iconAssetName: Assets.svg.addressBook, + iconSize: 16, + title: "Address book", + onPressed: () { + Navigator.of( + context, + ).pushNamed(AddressBookView.routeName, arguments: coin); + }, + ), + if (coin is FrostCurrency) const SizedBox(height: 8), + if (coin is FrostCurrency) + SettingsListButton( + iconAssetName: Assets.svg.addressBook2, + iconSize: 16, + title: "FROST Multisig settings", + onPressed: () { + Navigator.of(context).pushNamed( + FrostMSWalletOptionsView.routeName, + arguments: walletId, + ); + }, + ), + const SizedBox(height: 8), + SettingsListButton( + iconAssetName: Assets.svg.node, + iconSize: 16, + title: "Network", + onPressed: () { + Navigator.of(context).pushNamed( + WalletNetworkSettingsView.routeName, + arguments: Tuple3( + walletId, + _currentSyncStatus, + widget.initialNodeStatus, + ), + ); + }, + ), + if (canBackup) const SizedBox(height: 8), + if (canBackup) + Consumer( + builder: (_, ref, __) { + return SettingsListButton( + iconAssetName: Assets.svg.lock, + iconSize: 16, + title: "Wallet backup", + onPressed: _walletBackupPressed, + ); + }, + ), + const SizedBox(height: 8), + SettingsListButton( + iconAssetName: Assets.svg.downloadFolder, + title: "Wallet settings", + iconSize: 16, + onPressed: () { + Navigator.of(context).pushNamed( + WalletSettingsWalletSettingsView.routeName, + arguments: walletId, + ); + }, + ), + const SizedBox(height: 8), + SettingsListButton( + iconAssetName: Assets.svg.arrowRotate, + title: "Syncing preferences", + onPressed: () { + Navigator.of( + context, + ).pushNamed(SyncingPreferencesView.routeName); + }, + ), + if (xPubEnabled) const SizedBox(height: 8), + if (xPubEnabled) + Consumer( + builder: (_, ref, __) { + return SettingsListButton( + iconAssetName: Assets.svg.eye, + title: "Wallet xPub", + onPressed: _walletXPubPressed, + ); + }, + ), + if (sparkViewKeyEnabled) const SizedBox(height: 8), + if (sparkViewKeyEnabled) + Consumer( + builder: (_, ref, __) { + return SettingsListButton( + iconAssetName: Assets.svg.eye, + title: "Spark view key", + onPressed: _walletSparkViewKeyPressed, + ); + }, + ), + if (shouldShowClearSparkCache) const SizedBox(height: 8), + if (shouldShowClearSparkCache) + Consumer( + builder: (_, ref, __) { + return SettingsListButton( + iconAssetName: Assets.svg.eye, + title: "Clear electrumx cache", + onPressed: () async { + String? result; + await showDialog( + useSafeArea: false, + barrierDismissible: true, + context: context, + builder: (_) => StackOkDialog( + title: + "Are you sure you want to clear " + "${coin.prettyName} electrumx cache?", + onOkPressed: (value) { + result = value; + }, + leftButton: SecondaryButton( + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ); + + if (result == "OK" && context.mounted) { + await showLoading( + whileFuture: Future.wait([ + Future.delayed(const Duration(milliseconds: 1500)), + DB.instance.clearSharedTransactionCache( + currency: coin, ), - const SizedBox(height: 12), - const Spacer(), - Consumer( - builder: (_, ref, __) { - return TextButton( - onPressed: () { - // TODO: [prio=med] needs more thought if this is still required - // ref - // .read(pWallets) - // .getWallet(walletId) - // .isActiveWallet = false; - ref - .read( - transactionFilterProvider.state, - ) - .state = - null; - - Navigator.of(context).popUntil( - ModalRoute.withName(HomeView.routeName), - ); - }, - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - "Log out", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - ), + if (coin is Firo) + FiroCacheCoordinator.clearSharedCache( + coin.network, + ), + ]), + context: context, + message: "Clearing cache...", + ); + } + }, + ); + }, + ), + if (coin is NanoCurrency) const SizedBox(height: 8), + if (coin is NanoCurrency) + Consumer( + builder: (_, ref, __) { + return SettingsListButton( + iconAssetName: Assets.svg.eye, + title: "Change representative", + onPressed: () { + Navigator.of(context).pushNamed( + ChangeRepresentativeView.routeName, + arguments: widget.walletId, + ); + }, + ); + }, + ), + // const SizedBox( + // height: 8, + // ), + // SettingsListButton( + // iconAssetName: Assets.svg.ellipsis, + // title: "Debug Info", + // onPressed: () { + // Navigator.of(context) + // .pushNamed(DebugView.routeName); + // }, + // ), + ], + ), + ), + ); + } +} + +class _WalletSettingsViewBody extends StatelessWidget { + const _WalletSettingsViewBody({super.key, required this.children}); + + final List children; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only(left: 12, top: 12, right: 12), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(4), + child: Column(children: children), + ), + + const SizedBox(height: 12), + const Spacer(), + Consumer( + builder: (_, ref, __) { + return TextButton( + onPressed: () { + ref + .read(transactionFilterProvider.state) + .state = + null; + + Navigator.of(context).popUntil( + ModalRoute.withName(HomeView.routeName), ); }, - ), - ], + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Log out", + style: STextStyles.button(context).copyWith( + color: Theme.of( + context, + ).extension()!.accentColorDark, + ), + ), + ); + }, ), - ), + ], ), ), ), - ); - }, - ), - ), + ), + ), + ); + }, ), ); } @@ -683,7 +681,7 @@ class _EpiBoxInfoFormState extends ConsumerState { hostController.text, int.parse(portController.text), ); - if (mounted) { + if (context.mounted) { await showFloatingFlushBar( context: context, message: "Epicbox info saved!", @@ -692,11 +690,13 @@ class _EpiBoxInfoFormState extends ConsumerState { } unawaited(wallet.refresh()); } catch (e) { - await showFloatingFlushBar( - context: context, - message: "Failed to save epicbox info: $e", - type: FlushBarType.warning, - ); + if (context.mounted) { + await showFloatingFlushBar( + context: context, + message: "Failed to save epicbox info: $e", + type: FlushBarType.warning, + ); + } } }, child: Text( @@ -778,7 +778,7 @@ class _MwcmqsInfoFormState extends ConsumerState { hostController.text, int.parse(portController.text), ); - if (mounted) { + if (context.mounted) { await showFloatingFlushBar( context: context, message: "Mwcmqs info saved!", @@ -787,11 +787,13 @@ class _MwcmqsInfoFormState extends ConsumerState { } unawaited(wallet.refresh()); } catch (e) { - await showFloatingFlushBar( - context: context, - message: "Failed to save mwcmqs info: $e", - type: FlushBarType.warning, - ); + if (context.mounted) { + await showFloatingFlushBar( + context: context, + message: "Failed to save mwcmqs info: $e", + type: FlushBarType.warning, + ); + } } }, child: Text( diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart index 04907952e..a5464cdb5 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart @@ -134,7 +134,9 @@ class _WalletSettingsWalletSettingsViewState return StackDialog( title: "Warning!", message: - "Reusing addresses reduces your privacy and security. Are you sure you want to reuse addresses by default?", + "Reusing addresses reduces your privacy and " + "security. Are you sure you want to reuse " + "addresses by default?", leftButton: TextButton( style: Theme.of(context) .extension()! @@ -187,8 +189,9 @@ class _WalletSettingsWalletSettingsViewState return StackDialog( title: "Notice", message: - "Activating MWEB requires synchronizing on-chain MWEB related data. " - "This currently requires about 800 MB of storage.", + "Activating MWEB requires synchronizing on-chain MWEB " + "related data. This currently requires about " + "800 MB of storage.", leftButton: SecondaryButton( onPressed: () { Navigator.of(context).pop(false); @@ -292,7 +295,6 @@ class _WalletSettingsWalletSettingsViewState RoundedWhiteContainer( padding: const EdgeInsets.all(0), child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -362,7 +364,6 @@ class _WalletSettingsWalletSettingsViewState RoundedWhiteContainer( padding: const EdgeInsets.all(0), child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( @@ -412,7 +413,6 @@ class _WalletSettingsWalletSettingsViewState RoundedWhiteContainer( padding: const EdgeInsets.all(0), child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( @@ -628,7 +628,6 @@ class _WalletSettingsWalletSettingsViewState RoundedWhiteContainer( padding: const EdgeInsets.all(0), child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -642,7 +641,8 @@ class _WalletSettingsWalletSettingsViewState context: context, builder: (_) => StackDialog( title: - "Do you want to delete ${ref.read(pWalletName(widget.walletId))}?", + "Do you want to delete " + "${ref.read(pWalletName(widget.walletId))}?", leftButton: TextButton( style: Theme.of(context) .extension()! diff --git a/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart b/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart deleted file mode 100644 index a022be588..000000000 --- a/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart +++ /dev/null @@ -1,9 +0,0 @@ -/* - * This file is part of Stack Wallet. - * - * Copyright (c) 2023 Cypher Stack - * All Rights Reserved. - * The code is distributed under GPLv3 license, see LICENSE file for details. - * Generated by Cypher Stack on 2023-05-26 - * - */ diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart index 63366f22f..e052df551 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart @@ -580,7 +580,8 @@ class _DesktopWalletFeaturesState extends ConsumerState { final showMwebOption = wallet is MwebInterface && !wallet.isViewOnly; final extraOptions = [ - if (wallet is SparkInterface && !isViewOnly) + if (wallet is SparkInterface && + (!isViewOnly || (isViewOnly && wallet.viewOnlyType == .spark))) (WalletFeature.clearSparkCache, Assets.svg.key, () => ()), if (wallet is RbfInterface) (WalletFeature.rbf, Assets.svg.key, () => ()), diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart index 74be0581c..2a635e5c0 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart @@ -134,7 +134,9 @@ class _MoreFeaturesDialogState extends ConsumerState { mainAxisSize: MainAxisSize.min, children: [ Text( - "Reusing addresses reduces your privacy and security. Are you sure you want to reuse addresses by default?", + "Reusing addresses reduces your privacy and " + "security. Are you sure you want to reuse " + "addresses by default?", style: STextStyles.desktopTextSmall(context), ), const SizedBox(height: 43), @@ -238,8 +240,9 @@ class _MoreFeaturesDialogState extends ConsumerState { mainAxisSize: MainAxisSize.min, children: [ Text( - "Activating MWEB requires synchronizing on-chain MWEB related data. " - "This currently requires about 800 MB of storage.", + "Activating MWEB requires synchronizing on-chain " + "MWEB related data. This currently requires about " + "800 MB of storage.", style: STextStyles.desktopTextSmall(context), ), const SizedBox(height: 43), From ada6142f07365a60e9466b7df7909e8dc334322d Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Nov 2025 12:06:36 -0600 Subject: [PATCH 09/11] spark min fee error info logging --- .../wallet/wallet_mixin_interfaces/spark_interface.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 3b4e819c0..2fa46378d 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -1994,6 +1994,9 @@ mixin SparkInterface ); if (nFeeRet.toInt() < data.vSize!) { + Logging.instance.w( + "Spark mint transaction failed: $nFeeRet is less than ${data.vSize}", + ); throw Exception("fee is less than vSize"); } From 7d8ef17936362a52f888d141895c5c7afbbe6cc9 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Nov 2025 12:06:50 -0600 Subject: [PATCH 10/11] update fee estimate --- lib/wallets/wallet/impl/bitcoin_wallet.dart | 25 +-- .../wallet/impl/bitcoincash_wallet.dart | 47 ++-- lib/wallets/wallet/impl/dash_wallet.dart | 52 ++--- lib/wallets/wallet/impl/dogecoin_wallet.dart | 52 ++--- lib/wallets/wallet/impl/ecash_wallet.dart | 48 ++-- lib/wallets/wallet/impl/fact0rn_wallet.dart | 52 ++--- lib/wallets/wallet/impl/firo_wallet.dart | 2 +- lib/wallets/wallet/impl/litecoin_wallet.dart | 76 +++---- lib/wallets/wallet/impl/namecoin_wallet.dart | 212 +++++++++--------- lib/wallets/wallet/impl/particl_wallet.dart | 111 +++++---- lib/wallets/wallet/impl/peercoin_wallet.dart | 52 ++--- 11 files changed, 342 insertions(+), 387 deletions(-) diff --git a/lib/wallets/wallet/impl/bitcoin_wallet.dart b/lib/wallets/wallet/impl/bitcoin_wallet.dart index 6361ec135..dc1cf5df4 100644 --- a/lib/wallets/wallet/impl/bitcoin_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_wallet.dart @@ -37,18 +37,17 @@ class BitcoinWallet extends Bip39HDWallet @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = - await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); + final allAddresses = await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); return allAddresses; } @@ -71,7 +70,7 @@ class BitcoinWallet extends Bip39HDWallet @override int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { - return vSize * (feeRatePerKB.toInt() / 1000).ceil(); + return (feeRatePerKB * BigInt.from(vSize) ~/ BigInt.from(1000)).toInt(); } // diff --git a/lib/wallets/wallet/impl/bitcoincash_wallet.dart b/lib/wallets/wallet/impl/bitcoincash_wallet.dart index 5edcbf9f7..4191052bc 100644 --- a/lib/wallets/wallet/impl/bitcoincash_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoincash_wallet.dart @@ -67,20 +67,19 @@ class BitcoincashWallet @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = - await mainDB - .getAddresses(walletId) - .filter() - .not() - .typeEqualTo(AddressType.nonWallet) - .and() - .group( - (q) => q - .subTypeEqualTo(AddressSubType.receiving) - .or() - .subTypeEqualTo(AddressSubType.change), - ) - .findAll(); + final allAddresses = await mainDB + .getAddresses(walletId) + .filter() + .not() + .typeEqualTo(AddressType.nonWallet) + .and() + .group( + (q) => q + .subTypeEqualTo(AddressSubType.receiving) + .or() + .subTypeEqualTo(AddressSubType.change), + ) + .findAll(); return allAddresses; } @@ -103,17 +102,15 @@ class BitcoincashWallet final List
allAddressesOld = await fetchAddressesForElectrumXScan(); - final Set receivingAddresses = - allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => convertAddressString(e.value)) - .toSet(); + final Set receivingAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => convertAddressString(e.value)) + .toSet(); - final Set changeAddresses = - allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => convertAddressString(e.value)) - .toSet(); + final Set changeAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => convertAddressString(e.value)) + .toSet(); final allAddressesSet = {...receivingAddresses, ...changeAddresses}; @@ -389,7 +386,7 @@ class BitcoincashWallet @override int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { - return vSize * (feeRatePerKB.toInt() / 1000).ceil(); + return (feeRatePerKB * BigInt.from(vSize) ~/ BigInt.from(1000)).toInt(); } @override diff --git a/lib/wallets/wallet/impl/dash_wallet.dart b/lib/wallets/wallet/impl/dash_wallet.dart index 9d39bd26f..a00faf77c 100644 --- a/lib/wallets/wallet/impl/dash_wallet.dart +++ b/lib/wallets/wallet/impl/dash_wallet.dart @@ -36,18 +36,17 @@ class DashWallet extends Bip39HDWallet @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = - await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); + final allAddresses = await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); return allAddresses; } @@ -60,16 +59,14 @@ class DashWallet extends Bip39HDWallet await fetchAddressesForElectrumXScan(); // Separate receiving and change addresses. - final Set receivingAddresses = - allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => e.value) - .toSet(); - final Set changeAddresses = - allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => e.value) - .toSet(); + final Set receivingAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set changeAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); // Remove duplicates. final allAddressesSet = {...receivingAddresses, ...changeAddresses}; @@ -83,11 +80,10 @@ class DashWallet extends Bip39HDWallet final List> allTransactions = []; for (final txHash in allTxHashes) { // Check for duplicates by searching for tx by tx_hash in db. - final storedTx = - await mainDB.isar.transactionV2s - .where() - .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) - .findFirst(); + final storedTx = await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); if (storedTx == null || storedTx.height == null || @@ -319,6 +315,6 @@ class DashWallet extends Bip39HDWallet @override int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { - return vSize * (feeRatePerKB.toInt() / 1000).ceil(); + return (feeRatePerKB * BigInt.from(vSize) ~/ BigInt.from(1000)).toInt(); } } diff --git a/lib/wallets/wallet/impl/dogecoin_wallet.dart b/lib/wallets/wallet/impl/dogecoin_wallet.dart index 01a1eed40..444b0bafa 100644 --- a/lib/wallets/wallet/impl/dogecoin_wallet.dart +++ b/lib/wallets/wallet/impl/dogecoin_wallet.dart @@ -38,18 +38,17 @@ class DogecoinWallet @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = - await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); + final allAddresses = await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); return allAddresses; } @@ -62,16 +61,14 @@ class DogecoinWallet await fetchAddressesForElectrumXScan(); // Separate receiving and change addresses. - final Set receivingAddresses = - allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => e.value) - .toSet(); - final Set changeAddresses = - allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => e.value) - .toSet(); + final Set receivingAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set changeAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); // Remove duplicates. final allAddressesSet = {...receivingAddresses, ...changeAddresses}; @@ -85,11 +82,10 @@ class DogecoinWallet final List> allTransactions = []; for (final txHash in allTxHashes) { // Check for duplicates by searching for tx by tx_hash in db. - final storedTx = - await mainDB.isar.transactionV2s - .where() - .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) - .findFirst(); + final storedTx = await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); if (storedTx == null || storedTx.height == null || @@ -323,6 +319,6 @@ class DogecoinWallet @override int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { - return vSize * (feeRatePerKB.toInt() / 1000).ceil(); + return (feeRatePerKB * BigInt.from(vSize) ~/ BigInt.from(1000)).toInt(); } } diff --git a/lib/wallets/wallet/impl/ecash_wallet.dart b/lib/wallets/wallet/impl/ecash_wallet.dart index 4a72b2b94..9e83afb70 100644 --- a/lib/wallets/wallet/impl/ecash_wallet.dart +++ b/lib/wallets/wallet/impl/ecash_wallet.dart @@ -55,16 +55,15 @@ class EcashWallet extends Bip39HDWallet @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = - await mainDB - .getAddresses(walletId) - .filter() - .not() - .typeEqualTo(AddressType.nonWallet) - .and() - .not() - .subTypeEqualTo(AddressSubType.nonWallet) - .findAll(); + final allAddresses = await mainDB + .getAddresses(walletId) + .filter() + .not() + .typeEqualTo(AddressType.nonWallet) + .and() + .not() + .subTypeEqualTo(AddressSubType.nonWallet) + .findAll(); return allAddresses; } @@ -87,17 +86,15 @@ class EcashWallet extends Bip39HDWallet final List
allAddressesOld = await fetchAddressesForElectrumXScan(); - final Set receivingAddresses = - allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => convertAddressString(e.value)) - .toSet(); + final Set receivingAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => convertAddressString(e.value)) + .toSet(); - final Set changeAddresses = - allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => convertAddressString(e.value)) - .toSet(); + final Set changeAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => convertAddressString(e.value)) + .toSet(); final allAddressesSet = {...receivingAddresses, ...changeAddresses}; @@ -108,11 +105,10 @@ class EcashWallet extends Bip39HDWallet final List> allTransactions = []; for (final txHash in allTxHashes) { - final storedTx = - await mainDB.isar.transactionV2s - .where() - .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) - .findFirst(); + final storedTx = await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); if (storedTx == null || storedTx.height == null || @@ -360,7 +356,7 @@ class EcashWallet extends Bip39HDWallet @override int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { - return vSize * (feeRatePerKB.toInt() / 1000).ceil(); + return (feeRatePerKB * BigInt.from(vSize) ~/ BigInt.from(1000)).toInt(); } @override diff --git a/lib/wallets/wallet/impl/fact0rn_wallet.dart b/lib/wallets/wallet/impl/fact0rn_wallet.dart index 0f6a93d0d..3ddd053db 100644 --- a/lib/wallets/wallet/impl/fact0rn_wallet.dart +++ b/lib/wallets/wallet/impl/fact0rn_wallet.dart @@ -35,18 +35,17 @@ class Fact0rnWallet @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = - await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); + final allAddresses = await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); return allAddresses; } @@ -59,16 +58,14 @@ class Fact0rnWallet await fetchAddressesForElectrumXScan(); // Separate receiving and change addresses. - final Set receivingAddresses = - allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => e.value) - .toSet(); - final Set changeAddresses = - allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => e.value) - .toSet(); + final Set receivingAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set changeAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); // Remove duplicates. final allAddressesSet = {...receivingAddresses, ...changeAddresses}; @@ -82,11 +79,10 @@ class Fact0rnWallet final List> allTransactions = []; for (final txHash in allTxHashes) { // Check for duplicates by searching for tx by tx_hash in db. - final storedTx = - await mainDB.isar.transactionV2s - .where() - .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) - .findFirst(); + final storedTx = await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); if (storedTx == null || storedTx.height == null || @@ -321,6 +317,6 @@ class Fact0rnWallet @override int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { - return vSize * (feeRatePerKB.toInt() / 1000).ceil(); + return (feeRatePerKB * BigInt.from(vSize) ~/ BigInt.from(1000)).toInt(); } } diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index 8c69f7970..bd2b3f70f 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -813,6 +813,6 @@ class FiroWallet extends Bip39HDWallet @override int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { - return vSize * (feeRatePerKB.toInt() / 1000).ceil(); + return (feeRatePerKB * BigInt.from(vSize) ~/ BigInt.from(1000)).toInt(); } } diff --git a/lib/wallets/wallet/impl/litecoin_wallet.dart b/lib/wallets/wallet/impl/litecoin_wallet.dart index db497a904..c9fa52a33 100644 --- a/lib/wallets/wallet/impl/litecoin_wallet.dart +++ b/lib/wallets/wallet/impl/litecoin_wallet.dart @@ -49,20 +49,19 @@ class LitecoinWallet @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = - await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.mweb) - .or() - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); + final allAddresses = await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.mweb) + .or() + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); return allAddresses; } @@ -75,16 +74,14 @@ class LitecoinWallet await fetchAddressesForElectrumXScan(); // Separate receiving and change addresses. - final Set receivingAddresses = - allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => e.value) - .toSet(); - final Set changeAddresses = - allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => e.value) - .toSet(); + final Set receivingAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set changeAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); // Remove duplicates. final allAddressesSet = {...receivingAddresses, ...changeAddresses}; @@ -102,11 +99,10 @@ class LitecoinWallet final List> allTransactions = []; for (final txHash in allTxHashes) { // Check for duplicates by searching for tx by tx_hash in db. - final storedTx = - await mainDB.isar.transactionV2s - .where() - .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) - .findFirst(); + final storedTx = await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); if (storedTx == null || storedTx.height == null || @@ -239,10 +235,9 @@ class LitecoinWallet final db = Drift.get(walletId); - final mwebUtxo = - await (db.select( - db.mwebUtxos, - )..where((e) => e.outputId.equals(outputId))).getSingleOrNull(); + final mwebUtxo = await (db.select( + db.mwebUtxos, + )..where((e) => e.outputId.equals(outputId))).getSingleOrNull(); final output = OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "mweb", @@ -283,13 +278,12 @@ class LitecoinWallet // Check for special Litecoin outputs like ordinals. if (outputs.isNotEmpty) { // may not catch every case but it is much quicker - final hasOrdinal = - await mainDB.isar.ordinals - .where() - .filter() - .walletIdEqualTo(walletId) - .utxoTXIDEqualTo(txData["txid"] as String) - .isNotEmpty(); + final hasOrdinal = await mainDB.isar.ordinals + .where() + .filter() + .walletIdEqualTo(walletId) + .utxoTXIDEqualTo(txData["txid"] as String) + .isNotEmpty(); if (hasOrdinal) { subType = TransactionSubType.ordinal; } else { @@ -384,7 +378,7 @@ class LitecoinWallet @override int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { - return vSize * (feeRatePerKB.toInt() / 1000).ceil(); + return (feeRatePerKB * BigInt.from(vSize) ~/ BigInt.from(1000)).toInt(); } // diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index a6dd6f74b..10ea40c5a 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -72,18 +72,17 @@ class NamecoinWallet @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = - await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); + final allAddresses = await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); return allAddresses; } @@ -140,9 +139,8 @@ class NamecoinWallet blockReason = "Contains name"; try { - final rawNameOP = - (output["scriptPubKey"]["nameOp"] as Map) - .cast(); + final rawNameOP = (output["scriptPubKey"]["nameOp"] as Map) + .cast(); otherDataString = jsonEncode({ UTXOOtherDataKeys.nameOpData: jsonEncode(rawNameOP), @@ -201,7 +199,7 @@ class NamecoinWallet @override int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { - return vSize * (feeRatePerKB.toInt() / 1000).ceil(); + return (feeRatePerKB * BigInt.from(vSize) ~/ BigInt.from(1000)).toInt(); } // TODO: Check if this is the correct formula for namecoin. @@ -227,16 +225,14 @@ class NamecoinWallet await fetchAddressesForElectrumXScan(); // Separate receiving and change addresses. - final Set receivingAddresses = - allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => e.value) - .toSet(); - final Set changeAddresses = - allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => e.value) - .toSet(); + final Set receivingAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set changeAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); // Remove duplicates. final allAddressesSet = {...receivingAddresses, ...changeAddresses}; @@ -250,11 +246,10 @@ class NamecoinWallet final List> allTransactions = []; for (final txHash in allTxHashes) { // Check for duplicates by searching for tx by tx_hash in db. - final storedTx = - await mainDB.isar.transactionV2s - .where() - .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) - .findFirst(); + final storedTx = await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); if (storedTx == null || storedTx.height == null || @@ -442,8 +437,11 @@ class NamecoinWallet ) async { // first check own utxos. Should only need to check NAME NEW here. // NAME UPDATE and NAME FIRST UPDATE will appear readable from electrumx - final utxos = - await mainDB.getUTXOs(walletId).filter().otherDataIsNotNull().findAll(); + final utxos = await mainDB + .getUTXOs(walletId) + .filter() + .otherDataIsNotNull() + .findAll(); for (final utxo in utxos) { final nameOp = getOpNameDataFrom(utxo); if (nameOp?.op == OpName.nameNew) { @@ -509,18 +507,17 @@ class NamecoinWallet try { final currentHeight = await chainHeight; // not ideal filtering - final utxos = - await mainDB - .getUTXOs(walletId) - .filter() - .otherDataIsNotNull() - .and() - .blockHeightIsNotNull() - .and() - .blockHeightGreaterThan(0) - .and() - .blockHeightLessThan(currentHeight - kNameWaitBlocks) - .findAll(); + final utxos = await mainDB + .getUTXOs(walletId) + .filter() + .otherDataIsNotNull() + .and() + .blockHeightIsNotNull() + .and() + .blockHeightGreaterThan(0) + .and() + .blockHeightLessThan(currentHeight - kNameWaitBlocks) + .findAll(); Logging.instance.t( "_unknownNameNewOutputs(count=${_unknownNameNewOutputs.length})" @@ -572,8 +569,9 @@ class NamecoinWallet data.salt, ); - String noteName = - data.name.startsWith("d/") ? data.name.substring(2) : data.name; + String noteName = data.name.startsWith("d/") + ? data.name.substring(2) + : data.name; if (!noteName.endsWith(".bit")) { noteName += ".bit"; } @@ -638,8 +636,10 @@ class NamecoinWallet assert(txData.recipients!.where((e) => !e.isChange).length == 1); if (!isForFeeCalcPurposesOnly) { - final nameAmount = - txData.recipients!.where((e) => !e.isChange).first.amount; + final nameAmount = txData.recipients! + .where((e) => !e.isChange) + .first + .amount; switch (txData.opNameState!.type) { case OpName.nameNew: @@ -664,10 +664,9 @@ class NamecoinWallet ); // TODO: [prio=high]: check this opt in rbf - final sequence = - this is RbfInterface && (this as RbfInterface).flagOptInRBF - ? 0xffffffff - 10 - : 0xffffffff - 1; + final sequence = this is RbfInterface && (this as RbfInterface).flagOptInRBF + ? 0xffffffff - 10 + : 0xffffffff - 1; // Add transaction inputs for (int i = 0; i < inputsWithKeys.length; i++) { @@ -737,10 +736,9 @@ class NamecoinWallet txid: inputsWithKeys[i].utxo.txid, vout: inputsWithKeys[i].utxo.vout, ), - addresses: - inputsWithKeys[i].utxo.address == null - ? [] - : [inputsWithKeys[i].utxo.address!], + addresses: inputsWithKeys[i].utxo.address == null + ? [] + : [inputsWithKeys[i].utxo.address!], valueStringSats: inputsWithKeys[i].utxo.value.toString(), witness: null, innerRedeemScriptAsm: null, @@ -872,9 +870,9 @@ class NamecoinWallet version: clTx.version, type: tempOutputs.map((e) => e.walletOwns).fold(true, (p, e) => p &= e) && - txData.paynymAccountLite == null - ? TransactionType.sentToSelf - : TransactionType.outgoing, + txData.paynymAccountLite == null + ? TransactionType.sentToSelf + : TransactionType.outgoing, subType: TransactionSubType.none, otherData: null, ), @@ -1023,20 +1021,19 @@ class NamecoinWallet final canCPFP = this is CpfpInterface && coinControl; - final spendableOutputs = - availableOutputs - .where( - (e) => - !e.isBlocked && - (e.used != true) && - (canCPFP || - e.isConfirmed( - currentChainHeight, - cryptoCurrency.minConfirms, - cryptoCurrency.minCoinbaseConfirms, - )), - ) - .toList(); + final spendableOutputs = availableOutputs + .where( + (e) => + !e.isBlocked && + (e.used != true) && + (canCPFP || + e.isConfirmed( + currentChainHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + )), + ) + .toList(); if (coinControl) { if (spendableOutputs.length < availableOutputs.length) { @@ -1118,24 +1115,22 @@ class NamecoinWallet final List recipientsAmtArray = [satoshiAmountToSend]; // gather required signing data - final inputsWithKeys = - (await addSigningKeys( - utxoObjectsToUse.map((e) => StandardInput(e)).toList(), - )).whereType().toList(); + final inputsWithKeys = (await addSigningKeys( + utxoObjectsToUse.map((e) => StandardInput(e)).toList(), + )).whereType().toList(); final int vSizeForOneOutput; try { - vSizeForOneOutput = - (await _createNameTx( - inputsWithKeys: inputsWithKeys, - isForFeeCalcPurposesOnly: true, - txData: txData.copyWith( - recipients: await helperRecipientsConvert( - [recipientAddress], - [satoshisBeingUsed], - ), - ), - )).vSize!; + vSizeForOneOutput = (await _createNameTx( + inputsWithKeys: inputsWithKeys, + isForFeeCalcPurposesOnly: true, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + [recipientAddress], + [satoshisBeingUsed], + ), + ), + )).vSize!; } catch (e, s) { Logging.instance.e("vSizeForOneOutput: $e", error: e, stackTrace: s); rethrow; @@ -1146,20 +1141,19 @@ class NamecoinWallet BigInt maxBI(BigInt a, BigInt b) => a > b ? a : b; try { - vSizeForTwoOutPuts = - (await _createNameTx( - inputsWithKeys: inputsWithKeys, - isForFeeCalcPurposesOnly: true, - txData: txData.copyWith( - recipients: await helperRecipientsConvert( - [recipientAddress, (await getCurrentChangeAddress())!.value], - [ - satoshiAmountToSend, - maxBI(BigInt.zero, satoshisBeingUsed - satoshiAmountToSend), - ], - ), - ), - )).vSize!; + vSizeForTwoOutPuts = (await _createNameTx( + inputsWithKeys: inputsWithKeys, + isForFeeCalcPurposesOnly: true, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + [recipientAddress, (await getCurrentChangeAddress())!.value], + [ + satoshiAmountToSend, + maxBI(BigInt.zero, satoshisBeingUsed - satoshiAmountToSend), + ], + ), + ), + )).vSize!; } catch (e, s) { Logging.instance.e("vSizeForTwoOutPuts: $e", error: e, stackTrace: s); rethrow; @@ -1170,18 +1164,18 @@ class NamecoinWallet satsPerVByte != null ? (satsPerVByte * vSizeForOneOutput) : estimateTxFee( - vSize: vSizeForOneOutput, - feeRatePerKB: selectedTxFeeRate, - ), + vSize: vSizeForOneOutput, + feeRatePerKB: selectedTxFeeRate, + ), ); // Assume 2 outputs, one for recipient and one for change final feeForTwoOutputs = BigInt.from( satsPerVByte != null ? (satsPerVByte * vSizeForTwoOutPuts) : estimateTxFee( - vSize: vSizeForTwoOutPuts, - feeRatePerKB: selectedTxFeeRate, - ), + vSize: vSizeForTwoOutPuts, + feeRatePerKB: selectedTxFeeRate, + ), ); Logging.instance.d( diff --git a/lib/wallets/wallet/impl/particl_wallet.dart b/lib/wallets/wallet/impl/particl_wallet.dart index eb9fb6043..6f2b9764e 100644 --- a/lib/wallets/wallet/impl/particl_wallet.dart +++ b/lib/wallets/wallet/impl/particl_wallet.dart @@ -45,18 +45,17 @@ class ParticlWallet @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = - await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); + final allAddresses = await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); return allAddresses; } @@ -115,7 +114,7 @@ class ParticlWallet @override int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { - return vSize * (feeRatePerKB.toInt() / 1000).ceil(); + return (feeRatePerKB * BigInt.from(vSize) ~/ BigInt.from(1000)).toInt(); } @override @@ -140,16 +139,14 @@ class ParticlWallet await fetchAddressesForElectrumXScan(); // Separate receiving and change addresses. - final Set receivingAddresses = - allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => e.value) - .toSet(); - final Set changeAddresses = - allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => e.value) - .toSet(); + final Set receivingAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set changeAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); // Remove duplicates. final allAddressesSet = {...receivingAddresses, ...changeAddresses}; @@ -163,11 +160,10 @@ class ParticlWallet final List> allTransactions = []; for (final txHash in allTxHashes) { // Check for duplicates by searching for tx by tx_hash in db. - final storedTx = - await mainDB.isar.transactionV2s - .where() - .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) - .findFirst(); + final storedTx = await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); if (storedTx == null || storedTx.height == null || @@ -382,31 +378,28 @@ class ParticlWallet switch (sd.derivePathType) { case DerivePathType.bip44: - data = - bitcoindart - .P2PKH( - data: bitcoindart.PaymentData(pubkey: pubKey), - network: convertedNetwork, - ) - .data; + data = bitcoindart + .P2PKH( + data: bitcoindart.PaymentData(pubkey: pubKey), + network: convertedNetwork, + ) + .data; break; case DerivePathType.bip49: - final p2wpkh = - bitcoindart - .P2WPKH( - data: bitcoindart.PaymentData(pubkey: pubKey), - network: convertedNetwork, - ) - .data; + final p2wpkh = bitcoindart + .P2WPKH( + data: bitcoindart.PaymentData(pubkey: pubKey), + network: convertedNetwork, + ) + .data; redeem = p2wpkh.output; - data = - bitcoindart - .P2SH( - data: bitcoindart.PaymentData(redeem: p2wpkh), - network: convertedNetwork, - ) - .data; + data = bitcoindart + .P2SH( + data: bitcoindart.PaymentData(redeem: p2wpkh), + network: convertedNetwork, + ) + .data; break; case DerivePathType.bip84: @@ -414,13 +407,12 @@ class ParticlWallet // prevOut: coinlib.OutPoint.fromHex(sd.utxo.txid, sd.utxo.vout), // publicKey: keys.publicKey, // ); - data = - bitcoindart - .P2WPKH( - data: bitcoindart.PaymentData(pubkey: pubKey), - network: convertedNetwork, - ) - .data; + data = bitcoindart + .P2WPKH( + data: bitcoindart.PaymentData(pubkey: pubKey), + network: convertedNetwork, + ) + .data; break; case DerivePathType.bip86: @@ -469,10 +461,9 @@ class ParticlWallet txid: insAndKeys[i].utxo.txid, vout: insAndKeys[i].utxo.vout, ), - addresses: - insAndKeys[i].utxo.address == null - ? [] - : [insAndKeys[i].utxo.address!], + addresses: insAndKeys[i].utxo.address == null + ? [] + : [insAndKeys[i].utxo.address!], valueStringSats: insAndKeys[i].utxo.value.toString(), witness: null, innerRedeemScriptAsm: null, diff --git a/lib/wallets/wallet/impl/peercoin_wallet.dart b/lib/wallets/wallet/impl/peercoin_wallet.dart index 8046f0d23..bcdb36c3e 100644 --- a/lib/wallets/wallet/impl/peercoin_wallet.dart +++ b/lib/wallets/wallet/impl/peercoin_wallet.dart @@ -37,18 +37,17 @@ class PeercoinWallet @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = - await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); + final allAddresses = await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); return allAddresses; } @@ -74,7 +73,7 @@ class PeercoinWallet /// we can just pretend vSize is size for peercoin @override int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { - return vSize * (feeRatePerKB.toInt() / 1000).ceil(); + return (feeRatePerKB * BigInt.from(vSize) ~/ BigInt.from(1000)).toInt(); } // =========================================================================== @@ -98,16 +97,14 @@ class PeercoinWallet await fetchAddressesForElectrumXScan(); // Separate receiving and change addresses. - final Set receivingAddresses = - allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => e.value) - .toSet(); - final Set changeAddresses = - allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => e.value) - .toSet(); + final Set receivingAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set changeAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); // Remove duplicates. final allAddressesSet = {...receivingAddresses, ...changeAddresses}; @@ -121,11 +118,10 @@ class PeercoinWallet final List> allTransactions = []; for (final txHash in allTxHashes) { // Check for duplicates by searching for tx by tx_hash in db. - final storedTx = - await mainDB.isar.transactionV2s - .where() - .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) - .findFirst(); + final storedTx = await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); if (storedTx == null || storedTx.height == null || From b3eb7dba7e94e6505dd79fc4243447f8c0e11478 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 26 Nov 2025 17:57:06 -0600 Subject: [PATCH 11/11] really quick and dirty order by rate swap options --- .../sub_widgets/exchange_provider_option.dart | 243 ++++++++-------- .../exchange_provider_options.dart | 87 +++--- .../sorted_exchange_providers.dart | 265 ++++++++++++++++++ 3 files changed, 429 insertions(+), 166 deletions(-) create mode 100644 lib/pages/exchange_view/sub_widgets/sorted_exchange_providers.dart diff --git a/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart index 8b56ae213..08d6d88d4 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart @@ -66,18 +66,16 @@ class _ExchangeOptionState extends ConsumerState { efCurrencyPairProvider.select((value) => value.receive), ); final reversed = ref.watch(efReversedProvider); - final amount = - reversed - ? ref.watch(efReceiveAmountProvider) - : ref.watch(efSendAmountProvider); + final amount = reversed + ? ref.watch(efReceiveAmountProvider) + : ref.watch(efSendAmountProvider); final data = ref.watch(efEstimatesListProvider(widget.exchange.name)); final estimates = data?.item1.value; - final pair = - sendCurrency != null && receivingCurrency != null - ? (from: sendCurrency, to: receivingCurrency) - : null; + final pair = sendCurrency != null && receivingCurrency != null + ? (from: sendCurrency, to: receivingCurrency) + : null; return AnimatedSize( duration: const Duration(milliseconds: 500), @@ -86,7 +84,7 @@ class _ExchangeOptionState extends ConsumerState { builder: (_) { if (ref.watch(efRefreshingProvider)) { // show loading - return _ProviderOption( + return ExchProviderOption( exchange: widget.exchange, estimate: null, pair: pair, @@ -108,10 +106,9 @@ class _ExchangeOptionState extends ConsumerState { int decimals; try { - decimals = - AppConfig.getCryptoCurrencyForTicker( - receivingCurrency.ticker, - )!.fractionDigits; + decimals = AppConfig.getCryptoCurrencyForTicker( + receivingCurrency.ticker, + )!.fractionDigits; } catch (_) { decimals = 8; // some reasonable alternative } @@ -161,23 +158,21 @@ class _ExchangeOptionState extends ConsumerState { return ConditionalParent( condition: i > 0, - builder: - (child) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - isDesktop - ? Container( - height: 1, - color: - Theme.of(context) - .extension()! - .background, - ) - : const SizedBox(height: 16), - child, - ], - ), - child: _ProviderOption( + builder: (child) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + isDesktop + ? Container( + height: 1, + color: Theme.of( + context, + ).extension()!.background, + ) + : const SizedBox(height: 16), + child, + ], + ), + child: ExchProviderOption( key: Key(widget.exchange.name + e.exchangeProvider), exchange: widget.exchange, pair: pair, @@ -209,26 +204,27 @@ class _ExchangeOptionState extends ConsumerState { } else if (data?.item1.value == null) { final rateType = ref.watch(efRateTypeProvider) == - ExchangeRateType.estimated - ? "estimated" - : "fixed"; + ExchangeRateType.estimated + ? "estimated" + : "fixed"; message ??= "Pair unavailable on $rateType rate flow"; } - return _ProviderOption( + return ExchProviderOption( exchange: widget.exchange, estimate: null, pair: pair, rateString: message ?? "Failed to fetch rate", - rateColor: - Theme.of(context).extension()!.textError, + rateColor: Theme.of( + context, + ).extension()!.textError, ); }, ); } } else { // show n/a - return _ProviderOption( + return ExchProviderOption( exchange: widget.exchange, estimate: null, pair: pair, @@ -241,8 +237,8 @@ class _ExchangeOptionState extends ConsumerState { } } -class _ProviderOption extends ConsumerStatefulWidget { - const _ProviderOption({ +class ExchProviderOption extends ConsumerStatefulWidget { + const ExchProviderOption({ super.key, required this.exchange, required this.estimate, @@ -262,10 +258,10 @@ class _ProviderOption extends ConsumerStatefulWidget { final Color? rateColor; @override - ConsumerState<_ProviderOption> createState() => _ProviderOptionState(); + ConsumerState createState() => _ProviderOptionState(); } -class _ProviderOptionState extends ConsumerState<_ProviderOption> { +class _ProviderOptionState extends ConsumerState { final isDesktop = Util.isDesktop; late final String _id; @@ -335,9 +331,8 @@ class _ProviderOptionState extends ConsumerState<_ProviderOption> { return ConditionalParent( condition: isDesktop, - builder: - (child) => - MouseRegion(cursor: SystemMouseCursors.click, child: child), + builder: (child) => + MouseRegion(cursor: SystemMouseCursors.click, child: child), child: GestureDetector( onTap: () { ref.read(efExchangeProvider.notifier).state = widget.exchange; @@ -347,8 +342,9 @@ class _ProviderOptionState extends ConsumerState<_ProviderOption> { child: Container( color: Colors.transparent, child: Padding( - padding: - isDesktop ? const EdgeInsets.all(16) : const EdgeInsets.all(0), + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -358,18 +354,18 @@ class _ProviderOptionState extends ConsumerState<_ProviderOption> { child: Padding( padding: EdgeInsets.only(top: isDesktop ? 20.0 : 15.0), child: Radio( - activeColor: - Theme.of( - context, - ).extension()!.radioButtonIconEnabled, + activeColor: Theme.of( + context, + ).extension()!.radioButtonIconEnabled, value: _id, groupValue: groupValue, onChanged: (_) { ref.read(efExchangeProvider.notifier).state = widget.exchange; ref - .read(efExchangeProviderNameProvider.notifier) - .state = widget.estimate?.exchangeProvider ?? + .read(efExchangeProviderNameProvider.notifier) + .state = + widget.estimate?.exchangeProvider ?? widget.exchange.name; }, ), @@ -383,47 +379,41 @@ class _ProviderOptionState extends ConsumerState<_ProviderOption> { height: isDesktop ? 32 : 24, child: widget.estimate?.exchangeProviderLogo != null && - widget - .estimate! - .exchangeProviderLogo! - .isNotEmpty - ? ClipRRect( - borderRadius: BorderRadius.circular(5), - child: Image.network( - widget.estimate!.exchangeProviderLogo!, - loadingBuilder: ( - context, - child, - loadingProgress, - ) { - if (loadingProgress == null) { - return child; - } else { - return const Center( - child: CircularProgressIndicator(), - ); - } - }, - errorBuilder: (context, error, stackTrace) { - return SvgPicture.asset( - Assets.exchange.getIconFor( - exchangeName: widget.exchange.name, - ), - width: isDesktop ? 32 : 24, - height: isDesktop ? 32 : 24, - ); - }, - width: isDesktop ? 32 : 24, - height: isDesktop ? 32 : 24, - ), - ) - : SvgPicture.asset( - Assets.exchange.getIconFor( - exchangeName: widget.exchange.name, - ), + widget.estimate!.exchangeProviderLogo!.isNotEmpty + ? ClipRRect( + borderRadius: BorderRadius.circular(5), + child: Image.network( + widget.estimate!.exchangeProviderLogo!, + loadingBuilder: + (context, child, loadingProgress) { + if (loadingProgress == null) { + return child; + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + errorBuilder: (context, error, stackTrace) { + return SvgPicture.asset( + Assets.exchange.getIconFor( + exchangeName: widget.exchange.name, + ), + width: isDesktop ? 32 : 24, + height: isDesktop ? 32 : 24, + ); + }, width: isDesktop ? 32 : 24, height: isDesktop ? 32 : 24, ), + ) + : SvgPicture.asset( + Assets.exchange.getIconFor( + exchangeName: widget.exchange.name, + ), + width: isDesktop ? 32 : 24, + height: isDesktop ? 32 : 24, + ), ), ), const SizedBox(width: 10), @@ -435,55 +425,54 @@ class _ProviderOptionState extends ConsumerState<_ProviderOption> { children: [ ConditionalParent( condition: _warnings.isNotEmpty, - builder: - (child) => Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - child, - CustomTextButton( - text: _warnings.first.value, - onTap: () { - _showNoSparkWarning(); - }, - ), - ], + builder: (child) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + child, + CustomTextButton( + text: _warnings.first.value, + onTap: () { + _showNoSparkWarning(); + }, ), + ], + ), child: Text( widget.estimate?.exchangeProvider ?? widget.exchange.name, style: STextStyles.titleBold12(context).copyWith( - color: - Theme.of( - context, - ).extension()!.textDark2, + color: Theme.of( + context, + ).extension()!.textDark2, ), ), ), widget.loadingString ? AnimatedText( - stringsToLoopThrough: const [ - "Loading", - "Loading.", - "Loading..", - "Loading...", - ], - style: STextStyles.itemSubtitle12(context).copyWith( - color: - Theme.of( - context, - ).extension()!.textSubtitle1, - ), - ) + stringsToLoopThrough: const [ + "Loading", + "Loading.", + "Loading..", + "Loading...", + ], + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of( + context, + ).extension()!.textSubtitle1, + ), + ) : Text( - widget.rateString, - style: STextStyles.itemSubtitle12(context).copyWith( - color: - widget.rateColor ?? - Theme.of( - context, - ).extension()!.textSubtitle1, + widget.rateString, + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: + widget.rateColor ?? + Theme.of(context) + .extension()! + .textSubtitle1, + ), ), - ), ], ), ), diff --git a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart index a2ef39305..e4c264e04 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart @@ -21,7 +21,7 @@ import '../../../themes/stack_colors.dart'; import '../../../utilities/prefs.dart'; import '../../../utilities/util.dart'; import '../../../widgets/rounded_white_container.dart'; -import 'exchange_provider_option.dart'; +import 'sorted_exchange_providers.dart'; class ExchangeProviderOptions extends ConsumerStatefulWidget { const ExchangeProviderOptions({ @@ -94,46 +94,55 @@ class _ExchangeProviderOptionsState return RoundedWhiteContainer( padding: isDesktop ? const EdgeInsets.all(0) : const EdgeInsets.all(12), - borderColor: - isDesktop - ? Theme.of(context).extension()!.background - : null, - child: Column( - children: [ - if (showChangeNow) - ExchangeOption( - exchange: ChangeNowExchange.instance, - fixedRate: widget.fixedRate, - reversed: widget.reversed, - ), - if (showChangeNow && showTrocador) - isDesktop - ? Container( - height: 1, - color: Theme.of(context).extension()!.background, - ) - : const SizedBox(height: 16), - if (showTrocador) - ExchangeOption( - fixedRate: widget.fixedRate, - reversed: widget.reversed, - exchange: TrocadorExchange.instance, - ), - if ((showChangeNow || showTrocador) && showNanswap) - isDesktop - ? Container( - height: 1, - color: Theme.of(context).extension()!.background, - ) - : const SizedBox(height: 16), - if (showNanswap) - ExchangeOption( - fixedRate: widget.fixedRate, - reversed: widget.reversed, - exchange: NanswapExchange.instance, - ), + borderColor: isDesktop + ? Theme.of(context).extension()!.background + : null, + child: SortedExchangeProviders( + exchangees: [ + if (showChangeNow) ChangeNowExchange.instance, + if (showTrocador) TrocadorExchange.instance, + if (showNanswap) NanswapExchange.instance, ], + fixedRate: widget.fixedRate, + reversed: widget.reversed, ), + + // Column( + // children: [ + // if (showChangeNow) + // ExchangeOption( + // exchange: ChangeNowExchange.instance, + // fixedRate: widget.fixedRate, + // reversed: widget.reversed, + // ), + // if (showChangeNow && showTrocador) + // isDesktop + // ? Container( + // height: 1, + // color: Theme.of(context).extension()!.background, + // ) + // : const SizedBox(height: 16), + // if (showTrocador) + // ExchangeOption( + // fixedRate: widget.fixedRate, + // reversed: widget.reversed, + // exchange: TrocadorExchange.instance, + // ), + // if ((showChangeNow || showTrocador) && showNanswap) + // isDesktop + // ? Container( + // height: 1, + // color: Theme.of(context).extension()!.background, + // ) + // : const SizedBox(height: 16), + // if (showNanswap) + // ExchangeOption( + // fixedRate: widget.fixedRate, + // reversed: widget.reversed, + // exchange: NanswapExchange.instance, + // ), + // ], + // ), ); } } diff --git a/lib/pages/exchange_view/sub_widgets/sorted_exchange_providers.dart b/lib/pages/exchange_view/sub_widgets/sorted_exchange_providers.dart new file mode 100644 index 000000000..c478f9ead --- /dev/null +++ b/lib/pages/exchange_view/sub_widgets/sorted_exchange_providers.dart @@ -0,0 +1,265 @@ +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:tuple/tuple.dart'; + +import '../../../app_config.dart'; +import '../../../models/exchange/response_objects/estimate.dart'; +import '../../../models/exchange/response_objects/range.dart'; +import '../../../providers/exchange/exchange_form_state_provider.dart'; +import '../../../providers/global/locale_provider.dart'; +import '../../../services/exchange/exchange.dart'; +import '../../../services/exchange/exchange_response.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/amount/amount_formatter.dart'; +import '../../../utilities/amount/amount_unit.dart'; +import '../../../utilities/enums/exchange_rate_type_enum.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/crypto_currency/crypto_currency.dart'; +import '../../../widgets/conditional_parent.dart'; +import '../../../widgets/loading_indicator.dart'; +import 'exchange_provider_option.dart'; + +class SortedExchangeProviders extends ConsumerStatefulWidget { + const SortedExchangeProviders({ + super.key, + required this.exchangees, + required this.fixedRate, + required this.reversed, + }); + + final List exchangees; + final bool fixedRate; + final bool reversed; + + @override + ConsumerState createState() => + _SortedExchangeProvidersState(); +} + +class _SortedExchangeProvidersState + extends ConsumerState { + final List<(Exchange, Tuple2>, Range?>?)> + dataList = []; + final List<(Exchange, List?)> estimates = []; + + List<(Exchange, Estimate?)> transform(Decimal amount, String rcvTicker) { + final List<(Exchange, Estimate?)> flattened = []; + + for (final s in estimates) { + if (s.$2 != null && s.$2!.isNotEmpty) { + for (final e in s.$2!) { + flattened.add((s.$1, e)); + } + } else { + flattened.add((s.$1, null)); + } + } + + flattened.sort((a, b) { + if (a.$2 == null && b.$2 == null) return 1; + if (a.$2 != null && b.$2 == null) return 0; + if (a.$2 == null && b.$2 != null) return 0; + + // or we get problems!!! + assert(a.$2!.reversed == b.$2!.reversed); + + return _getRate(a.$2!, amount, rcvTicker) > + _getRate(b.$2!, amount, rcvTicker) + ? 0 + : 1; + }); + + return flattened; + } + + Amount _getRate(Estimate e, Decimal amount, String rcvTicker) { + int decimals; + try { + decimals = AppConfig.getCryptoCurrencyForTicker( + rcvTicker, + )!.fractionDigits; + } catch (_) { + decimals = 8; // some reasonable alternative + } + Amount rate; + if (e.reversed) { + rate = (amount / e.estimatedAmount) + .toDecimal(scaleOnInfinitePrecision: 18) + .toAmount(fractionDigits: decimals); + } else { + rate = (e.estimatedAmount / amount) + .toDecimal(scaleOnInfinitePrecision: 18) + .toAmount(fractionDigits: decimals); + } + return rate; + } + + @override + Widget build(BuildContext context) { + final sendCurrency = ref.watch( + efCurrencyPairProvider.select((value) => value.send), + ); + final receivingCurrency = ref.watch( + efCurrencyPairProvider.select((value) => value.receive), + ); + final reversed = ref.watch(efReversedProvider); + final amount = reversed + ? ref.watch(efReceiveAmountProvider) + : ref.watch(efSendAmountProvider); + + dataList.clear(); + estimates.clear(); + for (final exchange in widget.exchangees) { + final data = ref.watch(efEstimatesListProvider(exchange.name)); + dataList.add((exchange, data)); + estimates.add((exchange, data?.item1.value)); + } + + // final data = ref.watch(efEstimatesListProvider(widget.exchange.name)); + // final estimates = data?.item1.value; + + final pair = sendCurrency != null && receivingCurrency != null + ? (from: sendCurrency, to: receivingCurrency) + : null; + + if (ref.watch(efRefreshingProvider)) { + return const LoadingIndicator(width: 48, height: 48); + } + + if (sendCurrency != null && + receivingCurrency != null && + amount != null && + amount > Decimal.zero) { + final estimates = transform(amount, receivingCurrency.ticker); + + if (estimates.isNotEmpty) { + return Column( + mainAxisSize: .min, + children: [ + for (int i = 0; i < estimates.length; i++) + Builder( + builder: (context) { + final e = estimates[i].$2; + + if (e == null) { + return Consumer( + builder: (_, ref, __) { + String? message; + + final data = dataList + .firstWhere((e) => identical(e.$1, estimates[i].$1)) + .$2; + + final range = data?.item2; + if (range != null) { + if (range.min != null && amount < range.min!) { + message ??= "Amount too small"; + } else if (range.max != null && amount > range.max!) { + message ??= "Amount too large"; + } + } else if (data?.item1.value == null) { + final rateType = + ref.watch(efRateTypeProvider) == + ExchangeRateType.estimated + ? "estimated" + : "fixed"; + message ??= "Pair unavailable on $rateType rate flow"; + } + + return ExchProviderOption( + exchange: estimates[i].$1, + estimate: null, + pair: pair, + rateString: message ?? "Failed to fetch rate", + rateColor: Theme.of( + context, + ).extension()!.textError, + ); + }, + ); + } + + final rate = _getRate(e, amount, receivingCurrency.ticker); + + CryptoCurrency? coin; + try { + coin = AppConfig.getCryptoCurrencyForTicker( + receivingCurrency.ticker, + ); + } catch (_) { + coin = null; + } + + final String rateString; + if (coin != null) { + rateString = + "1 ${sendCurrency.ticker.toUpperCase()} " + "~ ${ref.watch(pAmountFormatter(coin)).format(rate)}"; + } else { + final formatter = AmountFormatter( + unit: AmountUnit.normal, + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + coin: Bitcoin( + CryptoCurrencyNetwork.main, + ), // some sane default + maxDecimals: 8, // some sane default + ); + rateString = + "1 ${sendCurrency.ticker.toUpperCase()} " + "~ ${formatter.format(rate, withUnitName: false)}" + " ${receivingCurrency.ticker.toUpperCase()}"; + } + + return ConditionalParent( + condition: i > 0, + builder: (child) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + Util.isDesktop + ? Container( + height: 1, + color: Theme.of( + context, + ).extension()!.background, + ) + : const SizedBox(height: 16), + child, + ], + ), + child: ExchProviderOption( + key: Key(estimates[i].$1.name + e.exchangeProvider), + exchange: estimates[i].$1, + pair: pair, + estimate: e, + rateString: rateString, + kycRating: e.kycRating, + ), + ); + }, + ), + ], + ); + } + } + + return Column( + mainAxisSize: .min, + children: [ + ...widget.exchangees.map( + (e) => ExchProviderOption( + exchange: e, + estimate: null, + pair: pair, + rateString: "n/a", + ), + ), + ], + ); + } +}