From d458a4265c4ee5ca12e6b1ba6499fc62a52baae2 Mon Sep 17 00:00:00 2001 From: Julian Date: Mon, 31 Aug 2026 13:39:40 -0600 Subject: [PATCH 1/4] flutter_libsparkmobile dependency update --- .../spark_interface.dart | 66 +++++++++++++++++-- .../interfaces/lib_spark_interface.dart | 10 ++- pubspec.lock | 4 +- .../templates/pubspec.template.yaml | 2 +- test/wallets/spark_name_fee_test.dart | 39 +++++++++++ ...IRO_lib_spark_interface_impl.template.dart | 19 +++++- 6 files changed, 129 insertions(+), 11 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 0f71059a6b..f19878666d 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -56,6 +56,24 @@ const OP_SPARKNAMEID = 0xe1; const OP_DROP = 0x75; const _maxSingleInputSparkTransactions = 50; +const _sparkTxVersion = 3; +const _transactionSpark = 9; +const _transactionSparkV2 = 11; +const _sparkChaumV2ActivationHeight = 1371000; + +@visibleForTesting +({int transactionType, int spendVersion}) sparkH2ParametersForNextBlock({ + required CryptoCurrencyNetwork network, + required int nextBlockHeight, +}) { + final useChaumV2 = + network == CryptoCurrencyNetwork.main && + nextBlockHeight >= _sparkChaumV2ActivationHeight; + return ( + transactionType: useChaumV2 ? _transactionSparkV2 : _transactionSpark, + spendVersion: useChaumV2 ? 2 : 1, + ); +} int _compareSparkCoinsForSingleInputSpend(SparkCoin a, SparkCoin b) { int result = b.value.compareTo(a.value); @@ -512,6 +530,11 @@ mixin SparkInterface final root = await getRootHDNode(); final privateKey = root.derivePath(sparkDerivationPath).privateKey.data; + final chainTipHeight = await fetchChainHeight(); + final spendVersion = sparkH2ParametersForNextBlock( + network: cryptoCurrency.network, + nextBlockHeight: chainTipHeight + 1, + ).spendVersion; BigInt singleInputFee = BigInt.from( await _asyncSparkFeesWrapper( privateKeyHex: privateKey.toHex, @@ -522,6 +545,7 @@ mixin SparkInterface privateRecipientsCount: 1, utxoNum: 0, additionalTxSize: 0, + spendVersion: spendVersion, ), ); @@ -610,7 +634,11 @@ mixin SparkInterface .privateKey .data .toHex; - final lockTime = await chainHeight; + final lockTime = await fetchChainHeight(); + final sparkParameters = sparkH2ParametersForNextBlock( + network: cryptoCurrency.network, + nextBlockHeight: lockTime + 1, + ); if (txData.sparkNameInfo != null) { final spend = await _prepareSingleSparkSpend( @@ -619,6 +647,8 @@ mixin SparkInterface subtractFeeFromAmount: false, privateKeyHex: privateKeyHex, lockTime: lockTime, + transactionType: sparkParameters.transactionType, + spendVersion: sparkParameters.spendVersion, ); return spend.copyWith(sparkSpends: [spend]); } @@ -637,6 +667,8 @@ mixin SparkInterface subtractFeeFromAmount: true, privateKeyHex: privateKeyHex, lockTime: lockTime, + transactionType: sparkParameters.transactionType, + spendVersion: sparkParameters.spendVersion, ); return spend.copyWith(sparkSpends: [spend]); } @@ -682,6 +714,7 @@ mixin SparkInterface privateRecipientsCount: privateRecipientCount, utxoNum: transparentRecipientCount, additionalTxSize: 0, + spendVersion: sparkParameters.spendVersion, ), ); feeCache[key] = fee; @@ -738,6 +771,8 @@ mixin SparkInterface subtractFeeFromAmount: false, privateKeyHex: privateKeyHex, lockTime: lockTime, + transactionType: sparkParameters.transactionType, + spendVersion: sparkParameters.spendVersion, ); if (spend.fee?.raw != plan.fee) { throw Exception("Spark transaction fee changed during creation."); @@ -772,6 +807,8 @@ mixin SparkInterface required bool subtractFeeFromAmount, required String privateKeyHex, required int lockTime, + required int transactionType, + required int spendVersion, }) async { if (!(txData.recipients?.isNotEmpty == true || txData.sparkRecipients?.isNotEmpty == true)) { @@ -889,7 +926,7 @@ mixin SparkInterface final txb = btc.TransactionBuilder(network: _bitcoinDartNetwork); txb.setLockTime(lockTime); - txb.setVersion(3 | (9 << 16)); + txb.setVersion(_sparkTxVersion | (transactionType << 16)); List? recipientsWithFeeSubtracted; List<({String address, Amount amount, String memo, bool isChange})>? @@ -910,6 +947,7 @@ mixin SparkInterface privateRecipientsCount: (txData.sparkRecipients?.length ?? 0), utxoNum: recipientCount, additionalTxSize: 0, // name script size + spendVersion: spendVersion, ); estimatedFee = BigInt.from(estFee); } else { @@ -1031,7 +1069,8 @@ mixin SparkInterface sparkNameValidityBlocks: txData.sparkNameInfo!.validBlocks, name: txData.sparkNameInfo!.name, additionalInfo: txData.sparkNameInfo!.additionalInfo, - scalarHex: extractedTx.getId(), + ownershipDigest: extractedTx.getId(), + spendVersion: spendVersion, privateKeyHex: privateKeyHex, spendKeyIndex: sparkIndex, diversifier: txData.sparkNameInfo!.sparkAddress.derivationIndex, @@ -1041,6 +1080,12 @@ mixin SparkInterface ); } + final extensionCommitment = spendVersion == 2 && noProofNameTxData != null + ? libSpark.getSparkNameCommitment( + serializedSparkNameData: noProofNameTxData.script, + ) + : Uint8List(32); + final spend = await computeWithLibSparkLogging(_createSparkSend, ( privateKeyHex: privateKeyHex, index: sparkIndex, @@ -1076,6 +1121,8 @@ mixin SparkInterface additionalTxSize: txData.sparkNameInfo == null ? 0 : noProofNameTxData!.size + sparkNameFeeScriptSizeDelta, + spendVersion: spendVersion, + extensionCommitment: extensionCommitment, )); if (spend.usedCoins.length != 1) { @@ -1108,7 +1155,8 @@ mixin SparkInterface sparkNameValidityBlocks: txData.sparkNameInfo!.validBlocks, name: txData.sparkNameInfo!.name, additionalInfo: txData.sparkNameInfo!.additionalInfo, - scalarHex: hash, + ownershipDigest: hash, + spendVersion: spendVersion, privateKeyHex: privateKeyHex, spendKeyIndex: sparkIndex, diversifier: txData.sparkNameInfo!.sparkAddress.derivationIndex, @@ -1118,7 +1166,7 @@ mixin SparkInterface ); break; } catch (e) { - if (e.toString() != "Exception: hash fail") { + if (spendVersion == 2 || e.toString() != "Exception: hash fail") { rethrow; } hashFailSafe++; @@ -2865,6 +2913,8 @@ _createSparkSend( List<({int setId, Uint8List blockHash})> idAndBlockHashes, Uint8List txHash, int additionalTxSize, + int spendVersion, + Uint8List extensionCommitment, }) args, ) async { @@ -2878,6 +2928,8 @@ _createSparkSend( idAndBlockHashes: args.idAndBlockHashes, txHash: args.txHash, additionalTxSize: args.additionalTxSize, + spendVersion: args.spendVersion, + extensionCommitment: args.extensionCommitment, ); return spend; @@ -2933,6 +2985,7 @@ Future _asyncSparkFeesWrapper({ required int privateRecipientsCount, required int utxoNum, required int additionalTxSize, + required int spendVersion, }) async { return await computeWithLibSparkLogging(_estSparkFeeComputeFunc, ( privateKeyHex: privateKeyHex, @@ -2943,6 +2996,7 @@ Future _asyncSparkFeesWrapper({ privateRecipientsCount: privateRecipientsCount, utxoNum: utxoNum, additionalTxSize: additionalTxSize, + spendVersion: spendVersion, )); } @@ -2956,6 +3010,7 @@ int _estSparkFeeComputeFunc( int privateRecipientsCount, int utxoNum, int additionalTxSize, + int spendVersion, }) args, ) { @@ -2968,6 +3023,7 @@ int _estSparkFeeComputeFunc( privateRecipientsCount: args.privateRecipientsCount, utxoNum: args.utxoNum, additionalTxSize: args.additionalTxSize, + spendVersion: args.spendVersion, ); return est; diff --git a/lib/wl_gen/interfaces/lib_spark_interface.dart b/lib/wl_gen/interfaces/lib_spark_interface.dart index 92404f57f8..44650b86e8 100644 --- a/lib/wl_gen/interfaces/lib_spark_interface.dart +++ b/lib/wl_gen/interfaces/lib_spark_interface.dart @@ -35,7 +35,8 @@ abstract class LibSparkInterface { required int sparkNameValidityBlocks, required String name, required String additionalInfo, - required String scalarHex, + required String ownershipDigest, + required int spendVersion, required String privateKeyHex, required int spendKeyIndex, required int diversifier, @@ -44,6 +45,10 @@ abstract class LibSparkInterface { required bool ignoreProof, }); + Uint8List getSparkNameCommitment({ + required Uint8List serializedSparkNameData, + }); + List<({Uint8List scriptPubKey, int amount, bool subtractFeeFromAmount})> createSparkMintRecipients({ required List<({String sparkAddress, int value, String memo})> outputs, @@ -128,6 +133,8 @@ abstract class LibSparkInterface { required List<({int setId, Uint8List blockHash})> idAndBlockHashes, required Uint8List txHash, required int additionalTxSize, + required int spendVersion, + required Uint8List extensionCommitment, }); int estimateSparkFee({ @@ -147,6 +154,7 @@ abstract class LibSparkInterface { required int privateRecipientsCount, required int utxoNum, required int additionalTxSize, + required int spendVersion, }); } diff --git a/pubspec.lock b/pubspec.lock index 2c042130bd..a187d14bb2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1019,8 +1019,8 @@ packages: dependency: "direct main" description: path: "." - ref: "783bd00f0114b007f7ef97017cd10ad263ed452c" - resolved-ref: "783bd00f0114b007f7ef97017cd10ad263ed452c" + ref: c164f003f6ddb29837901f48e3c0f80073fbcd09 + resolved-ref: c164f003f6ddb29837901f48e3c0f80073fbcd09 url: "https://github.com/cypherstack/flutter_libsparkmobile.git" source: git version: "0.1.0" diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml index cd53bf1fad..b1ab0afdff 100644 --- a/scripts/app_config/templates/pubspec.template.yaml +++ b/scripts/app_config/templates/pubspec.template.yaml @@ -44,7 +44,7 @@ dependencies: # flutter_libsparkmobile: # git: # url: https://github.com/cypherstack/flutter_libsparkmobile.git -# ref: 783bd00f0114b007f7ef97017cd10ad263ed452c +# ref: c164f003f6ddb29837901f48e3c0f80073fbcd09 # %%END_ENABLE_FIRO%% # %%ENABLE_EPIC%% diff --git a/test/wallets/spark_name_fee_test.dart b/test/wallets/spark_name_fee_test.dart index 0608fa9274..fc11c7909f 100644 --- a/test/wallets/spark_name_fee_test.dart +++ b/test/wallets/spark_name_fee_test.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; void main() { @@ -42,4 +43,42 @@ void main() { isTrue, ); }); + + group('Spark H2 activation', () { + test('mainnet uses V1 before the activation block', () { + expect( + sparkH2ParametersForNextBlock( + network: CryptoCurrencyNetwork.main, + nextBlockHeight: 1370999, + ), + (transactionType: 9, spendVersion: 1), + ); + }); + + test('mainnet uses V2 at activation and later', () { + for (final nextBlockHeight in [1371000, 1371001]) { + expect( + sparkH2ParametersForNextBlock( + network: CryptoCurrencyNetwork.main, + nextBlockHeight: nextBlockHeight, + ), + (transactionType: 11, spendVersion: 2), + ); + } + }); + + test('non-mainnet networks remain on V1', () { + for (final network in CryptoCurrencyNetwork.values.where( + (network) => network != CryptoCurrencyNetwork.main, + )) { + expect( + sparkH2ParametersForNextBlock( + network: network, + nextBlockHeight: 1371000, + ), + (transactionType: 9, spendVersion: 1), + ); + } + }); + }); } diff --git a/tool/wl_templates/FIRO_lib_spark_interface_impl.template.dart b/tool/wl_templates/FIRO_lib_spark_interface_impl.template.dart index f2998be060..23b01b635b 100644 --- a/tool/wl_templates/FIRO_lib_spark_interface_impl.template.dart +++ b/tool/wl_templates/FIRO_lib_spark_interface_impl.template.dart @@ -101,7 +101,8 @@ class _LibSparkInterfaceImpl extends LibSparkInterface { required int sparkNameValidityBlocks, required String name, required String additionalInfo, - required String scalarHex, + required String ownershipDigest, + required int spendVersion, required String privateKeyHex, required int spendKeyIndex, required int diversifier, @@ -112,7 +113,8 @@ class _LibSparkInterfaceImpl extends LibSparkInterface { sparkNameValidityBlocks: sparkNameValidityBlocks, name: name, additionalInfo: additionalInfo, - scalarHex: scalarHex, + ownershipDigest: ownershipDigest, + spendVersion: spendVersion, privateKeyHex: privateKeyHex, spendKeyIndex: spendKeyIndex, diversifier: diversifier, @@ -121,6 +123,13 @@ class _LibSparkInterfaceImpl extends LibSparkInterface { ignoreProof: ignoreProof, ); + @override + Uint8List getSparkNameCommitment({ + required Uint8List serializedSparkNameData, + }) => LibSpark.getSparkNameCommitment( + serializedSparkNameData: serializedSparkNameData, + ); + @override List<({int amount, Uint8List scriptPubKey, bool subtractFeeFromAmount})> createSparkMintRecipients({ @@ -288,6 +297,8 @@ class _LibSparkInterfaceImpl extends LibSparkInterface { required List<({Uint8List blockHash, int setId})> idAndBlockHashes, required Uint8List txHash, required int additionalTxSize, + required int spendVersion, + required Uint8List extensionCommitment, }) => LibSpark.createSparkSendTransaction( index: index, privateKeyHex: privateKeyHex, @@ -298,6 +309,8 @@ class _LibSparkInterfaceImpl extends LibSparkInterface { idAndBlockHashes: idAndBlockHashes, txHash: txHash, additionalTxSize: additionalTxSize, + spendVersion: spendVersion, + extensionCommitment: extensionCommitment, ); @override @@ -318,6 +331,7 @@ class _LibSparkInterfaceImpl extends LibSparkInterface { required int privateRecipientsCount, required int utxoNum, required int additionalTxSize, + required int spendVersion, }) => LibSpark.estimateSparkFee( privateKeyHex: privateKeyHex, sendAmount: sendAmount, @@ -326,6 +340,7 @@ class _LibSparkInterfaceImpl extends LibSparkInterface { privateRecipientsCount: privateRecipientsCount, utxoNum: utxoNum, additionalTxSize: additionalTxSize, + spendVersion: spendVersion, index: index, ); } From 27d00b05bd8517b4e1570bb734436a81b782c9e5 Mon Sep 17 00:00:00 2001 From: Julian Date: Tue, 1 Sep 2026 11:58:21 -0600 Subject: [PATCH 2/4] partial revert of SIST in prep for h2 fork --- .../exchange_step_views/step_4_view.dart | 11 +- lib/pages/exchange_view/send_from_view.dart | 19 +- lib/wallets/wallet/impl/firo_wallet.dart | 11 +- .../spark_interface.dart | 206 ++++++++++++------ .../interfaces/lib_spark_interface.dart | 42 +++- pubspec.lock | 4 +- .../templates/pubspec.template.yaml | 2 +- test/wallets/spark_name_fee_test.dart | 46 ++-- ...IRO_lib_spark_interface_impl.template.dart | 40 +++- 9 files changed, 279 insertions(+), 102 deletions(-) diff --git a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart index a48b85b23c..896feb74be 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart @@ -288,9 +288,14 @@ class _Step4ViewState extends ConsumerState { ); if (wallet is FiroWallet && !firoPublicSend) { - throw Exception( - "Sending private Firo funds to an exchange address is temporarily " - "unavailable.", + txDataFuture = wallet.prepareSendSpark( + txData: TxData( + recipients: [recipient], + note: + "${model.trade!.payInCurrency.toUpperCase()}/" + "${model.trade!.payOutCurrency.toUpperCase()} exchange", + ), + requireChaumV2: true, ); } else { final memo = wallet.info.coin is Stellar diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 1e3555c655..9fe4121ee0 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -293,6 +293,7 @@ class _SendFromCardState extends ConsumerState { ), ); } else { + final firoWallet = wallet as FiroWallet; // otherwise do firo send based on balance selected if (shouldSendPublicFiroFunds) { txDataFuture = wallet.prepareSend( @@ -302,9 +303,21 @@ class _SendFromCardState extends ConsumerState { ), ); } else { - throw Exception( - "Sending private Firo funds to an exchange address is " - "temporarily unavailable.", + txDataFuture = firoWallet.prepareSendSpark( + txData: TxData( + recipients: recipient.addressType == .spark ? null : [recipient], + sparkRecipients: recipient.addressType == .spark + ? [ + ( + address: recipient.address, + amount: recipient.amount, + memo: "", + isChange: false, + ), + ] + : null, + ), + requireChaumV2: true, ); } } diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index 48970b5152..34c68c2e5a 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -95,6 +95,9 @@ class MasternodeInfo { final kMasterNodeValue = Decimal.fromInt(1000); // full value (not sats) +const _zeroTxid = + "0000000000000000000000000000000000000000000000000000000000000000"; + class FiroWallet extends Bip39HDWallet with ElectrumXInterface, @@ -278,7 +281,10 @@ class FiroWallet extends Bip39HDWallet final txid = map["txid"] as String?; final vout = map["vout"] as int?; - if (coinbase == null && txid != null && vout != null) { + if (coinbase == null && + txid != null && + vout != null && + txid != _zeroTxid) { txInputTxidsSet.add(txid); } } @@ -992,7 +998,8 @@ class FiroWallet extends Bip39HDWallet ).raw.toInt(); if (collateralUtxo.value != expectedCollateralRaw) { throw Exception( - "Collateral outpoint must be exactly ${kMasterNodeValue.toString()} FIRO.", + "Collateral outpoint must be exactly " + "${kMasterNodeValue.toString()} FIRO.", ); } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index f19878666d..193bb133f1 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -56,25 +56,33 @@ const OP_SPARKNAMEID = 0xe1; const OP_DROP = 0x75; const _maxSingleInputSparkTransactions = 50; -const _sparkTxVersion = 3; -const _transactionSpark = 9; -const _transactionSparkV2 = 11; const _sparkChaumV2ActivationHeight = 1371000; @visibleForTesting -({int transactionType, int spendVersion}) sparkH2ParametersForNextBlock({ +LibSparkSpendVersion sparkSpendVersionForNextBlock({ required CryptoCurrencyNetwork network, required int nextBlockHeight, }) { - final useChaumV2 = - network == CryptoCurrencyNetwork.main && - nextBlockHeight >= _sparkChaumV2ActivationHeight; - return ( - transactionType: useChaumV2 ? _transactionSparkV2 : _transactionSpark, - spendVersion: useChaumV2 ? 2 : 1, + if (network != .main) return .chaumV1; + + return libSpark.getSpendVersionForBlockHeight( + nextBlockHeight: nextBlockHeight, + chaumV2ActivationHeight: _sparkChaumV2ActivationHeight, ); } +@visibleForTesting +bool isChaumV2SparkTransactionVersion(int transactionVersion) => + transactionVersion == LibSparkSpendVersion.chaumV2.transactionVersion; + +LibSparkNameProofInput _sparkNameProofInput({ + required LibSparkSpendVersion spendVersion, + required String inputHex, +}) => switch (spendVersion) { + .chaumV1 => .chaumV1(scalarHex: inputHex), + .chaumV2 => .chaumV2(ownershipDigest: inputHex), +}; + int _compareSparkCoinsForSingleInputSpend(SparkCoin a, SparkCoin b) { int result = b.value.compareTo(a.value); if (result != 0) return result; @@ -519,6 +527,44 @@ mixin SparkInterface ); } + final root = await getRootHDNode(); + final privateKey = root.derivePath(sparkDerivationPath).privateKey.data; + final chainTipHeight = await fetchChainHeight(); + final spendVersion = sparkSpendVersionForNextBlock( + network: cryptoCurrency.network, + nextBlockHeight: chainTipHeight + 1, + ); + + if (spendVersion.allowsMultipleInputs) { + final serializedCoins = coins + .map( + (e) => ( + serializedCoin: e.serializedCoinB64!, + serializedCoinContext: e.contextB64!, + groupId: e.groupId, + height: e.height!, + ), + ) + .toList(); + int estimate = await _asyncSparkFeesWrapper( + privateKeyHex: privateKey.toHex, + index: sparkIndex, + sendAmount: amount.raw.toInt(), + subtractFeeFromAmount: true, + serializedCoins: serializedCoins, + privateRecipientsCount: 1, + utxoNum: 0, + additionalTxSize: 0, + spendVersion: spendVersion, + ); + if (estimate < 0) estimate = 0; + + return Amount( + rawValue: BigInt.from(estimate), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + final serializedCoins = [ ( serializedCoin: coins.first.serializedCoinB64!, @@ -527,14 +573,6 @@ mixin SparkInterface height: coins.first.height!, ), ]; - - final root = await getRootHDNode(); - final privateKey = root.derivePath(sparkDerivationPath).privateKey.data; - final chainTipHeight = await fetchChainHeight(); - final spendVersion = sparkH2ParametersForNextBlock( - network: cryptoCurrency.network, - nextBlockHeight: chainTipHeight + 1, - ).spendVersion; BigInt singleInputFee = BigInt.from( await _asyncSparkFeesWrapper( privateKeyHex: privateKey.toHex, @@ -580,7 +618,10 @@ mixin SparkInterface } /// Spark to Spark/Transparent (spend) creation - Future prepareSendSpark({required TxData txData}) async { + Future prepareSendSpark({ + required TxData txData, + bool requireChaumV2 = false, + }) async { if (isViewOnly) { throw Exception("Spending is not supported for view only wallets"); } @@ -595,6 +636,18 @@ mixin SparkInterface throw Exception("Recipient has invalid amount."); } + final lockTime = await fetchChainHeight(); + final spendVersion = sparkSpendVersionForNextBlock( + network: cryptoCurrency.network, + nextBlockHeight: lockTime + 1, + ); + if (requireChaumV2 && !spendVersion.allowsMultipleInputs) { + throw Exception( + "Sending private Firo funds to an exchange address is temporarily " + "unavailable.", + ); + } + final coins = await mainDB.isar.sparkCoins .where() .walletIdEqualToAnyLTagHash(walletId) @@ -634,21 +687,26 @@ mixin SparkInterface .privateKey .data .toHex; - final lockTime = await fetchChainHeight(); - final sparkParameters = sparkH2ParametersForNextBlock( - network: cryptoCurrency.network, - nextBlockHeight: lockTime + 1, - ); + + if (spendVersion.allowsMultipleInputs) { + return _prepareSparkSpend( + txData: txData, + coins: coins, + subtractFeeFromAmount: isSendAll, + privateKeyHex: privateKeyHex, + lockTime: lockTime, + spendVersion: spendVersion, + ); + } if (txData.sparkNameInfo != null) { - final spend = await _prepareSingleSparkSpend( + final spend = await _prepareSparkSpend( txData: txData, - coin: coins.first, + coins: [coins.first], subtractFeeFromAmount: false, privateKeyHex: privateKeyHex, lockTime: lockTime, - transactionType: sparkParameters.transactionType, - spendVersion: sparkParameters.spendVersion, + spendVersion: spendVersion, ); return spend.copyWith(sparkSpends: [spend]); } @@ -661,14 +719,13 @@ mixin SparkInterface ); } - final spend = await _prepareSingleSparkSpend( + final spend = await _prepareSparkSpend( txData: txData, - coin: coins.single, + coins: [coins.single], subtractFeeFromAmount: true, privateKeyHex: privateKeyHex, lockTime: lockTime, - transactionType: sparkParameters.transactionType, - spendVersion: sparkParameters.spendVersion, + spendVersion: spendVersion, ); return spend.copyWith(sparkSpends: [spend]); } @@ -714,7 +771,7 @@ mixin SparkInterface privateRecipientsCount: privateRecipientCount, utxoNum: transparentRecipientCount, additionalTxSize: 0, - spendVersion: sparkParameters.spendVersion, + spendVersion: spendVersion, ), ); feeCache[key] = fee; @@ -762,17 +819,16 @@ mixin SparkInterface } } - final spend = await _prepareSingleSparkSpend( + final spend = await _prepareSparkSpend( txData: txData.copyWith( recipients: batchTransparentRecipients, sparkRecipients: batchPrivateRecipients, ), - coin: coins[plan.coinIndex], + coins: [coins[plan.coinIndex]], subtractFeeFromAmount: false, privateKeyHex: privateKeyHex, lockTime: lockTime, - transactionType: sparkParameters.transactionType, - spendVersion: sparkParameters.spendVersion, + spendVersion: spendVersion, ); if (spend.fee?.raw != plan.fee) { throw Exception("Spark transaction fee changed during creation."); @@ -801,15 +857,17 @@ mixin SparkInterface ); } - Future _prepareSingleSparkSpend({ + Future _prepareSparkSpend({ required TxData txData, - required SparkCoin coin, + required List coins, required bool subtractFeeFromAmount, required String privateKeyHex, required int lockTime, - required int transactionType, - required int spendVersion, + required LibSparkSpendVersion spendVersion, }) async { + if (coins.isEmpty) { + throw Exception("No spendable Spark coins found"); + } if (!(txData.recipients?.isNotEmpty == true || txData.sparkRecipients?.isNotEmpty == true)) { throw Exception("No recipients provided."); @@ -856,7 +914,6 @@ mixin SparkInterface ); final txAmount = transparentSumOut + sparkSumOut; - final coins = [coin]; final isSendAll = subtractFeeFromAmount; // prepare coin data for ffi @@ -926,7 +983,7 @@ mixin SparkInterface final txb = btc.TransactionBuilder(network: _bitcoinDartNetwork); txb.setLockTime(lockTime); - txb.setVersion(_sparkTxVersion | (transactionType << 16)); + txb.setVersion(spendVersion.transactionVersion); List? recipientsWithFeeSubtracted; List<({String address, Amount amount, String memo, bool isChange})>? @@ -1069,8 +1126,10 @@ mixin SparkInterface sparkNameValidityBlocks: txData.sparkNameInfo!.validBlocks, name: txData.sparkNameInfo!.name, additionalInfo: txData.sparkNameInfo!.additionalInfo, - ownershipDigest: extractedTx.getId(), - spendVersion: spendVersion, + proofInput: _sparkNameProofInput( + spendVersion: spendVersion, + inputHex: extractedTx.getId(), + ), privateKeyHex: privateKeyHex, spendKeyIndex: sparkIndex, diversifier: txData.sparkNameInfo!.sparkAddress.derivationIndex, @@ -1080,11 +1139,12 @@ mixin SparkInterface ); } - final extensionCommitment = spendVersion == 2 && noProofNameTxData != null + final extensionCommitment = + spendVersion == .chaumV2 && noProofNameTxData != null ? libSpark.getSparkNameCommitment( serializedSparkNameData: noProofNameTxData.script, ) - : Uint8List(32); + : null; final spend = await computeWithLibSparkLogging(_createSparkSend, ( privateKeyHex: privateKeyHex, @@ -1125,7 +1185,8 @@ mixin SparkInterface extensionCommitment: extensionCommitment, )); - if (spend.usedCoins.length != 1) { + if (spend.usedCoins.isEmpty || + (!spendVersion.allowsMultipleInputs && spend.usedCoins.length != 1)) { throw Exception("Unable to create a single-input Spark transaction."); } @@ -1155,8 +1216,10 @@ mixin SparkInterface sparkNameValidityBlocks: txData.sparkNameInfo!.validBlocks, name: txData.sparkNameInfo!.name, additionalInfo: txData.sparkNameInfo!.additionalInfo, - ownershipDigest: hash, - spendVersion: spendVersion, + proofInput: _sparkNameProofInput( + spendVersion: spendVersion, + inputHex: hash, + ), privateKeyHex: privateKeyHex, spendKeyIndex: sparkIndex, diversifier: txData.sparkNameInfo!.sparkAddress.derivationIndex, @@ -1166,7 +1229,8 @@ mixin SparkInterface ); break; } catch (e) { - if (spendVersion == 2 || e.toString() != "Exception: hash fail") { + if (spendVersion == .chaumV2 || + e.toString() != "Exception: hash fail") { rethrow; } hashFailSafe++; @@ -1236,7 +1300,8 @@ mixin SparkInterface ); } } - if (usedSparkCoins.length != 1) { + if (usedSparkCoins.isEmpty || + (!spendVersion.allowsMultipleInputs && usedSparkCoins.length != 1)) { throw Exception("Unable to create a single-input Spark transaction."); } @@ -1278,16 +1343,27 @@ mixin SparkInterface (e) => e.raw == null || e.usedSparkCoins == null || - e.usedSparkCoins!.length != 1, + e.usedSparkCoins!.isEmpty, )) { - throw Exception( - "Refusing to broadcast a non-single-input Spark transaction.", - ); + throw Exception("Refusing to broadcast an invalid Spark transaction."); + } + for (final transaction in transactions) { + if (transaction.usedSparkCoins!.length > 1) { + final transactionVersion = btc.Transaction.fromHex( + transaction.raw!, + ).version; + if (!isChaumV2SparkTransactionVersion(transactionVersion)) { + throw Exception( + "Refusing to broadcast a multi-input Chaum V1 transaction.", + ); + } + } } final coinIds = transactions - .map((e) => e.usedSparkCoins!.single.lTagHash) - .toSet(); - if (coinIds.length != transactions.length) { + .expand((e) => e.usedSparkCoins!) + .map((e) => e.lTagHash) + .toList(growable: false); + if (coinIds.toSet().length != coinIds.length) { throw Exception( "A Spark coin cannot be used by multiple transactions.", ); @@ -1325,6 +1401,10 @@ mixin SparkInterface } } + if (txData.sparkSpends == null) { + return confirmed.single; + } + return txData.copyWith( txHash: confirmed.first.txHash, txid: confirmed.first.txid, @@ -2913,8 +2993,8 @@ _createSparkSend( List<({int setId, Uint8List blockHash})> idAndBlockHashes, Uint8List txHash, int additionalTxSize, - int spendVersion, - Uint8List extensionCommitment, + LibSparkSpendVersion spendVersion, + Uint8List? extensionCommitment, }) args, ) async { @@ -2985,7 +3065,7 @@ Future _asyncSparkFeesWrapper({ required int privateRecipientsCount, required int utxoNum, required int additionalTxSize, - required int spendVersion, + required LibSparkSpendVersion spendVersion, }) async { return await computeWithLibSparkLogging(_estSparkFeeComputeFunc, ( privateKeyHex: privateKeyHex, @@ -3010,7 +3090,7 @@ int _estSparkFeeComputeFunc( int privateRecipientsCount, int utxoNum, int additionalTxSize, - int spendVersion, + LibSparkSpendVersion spendVersion, }) args, ) { diff --git a/lib/wl_gen/interfaces/lib_spark_interface.dart b/lib/wl_gen/interfaces/lib_spark_interface.dart index 44650b86e8..06ddf07c49 100644 --- a/lib/wl_gen/interfaces/lib_spark_interface.dart +++ b/lib/wl_gen/interfaces/lib_spark_interface.dart @@ -4,6 +4,34 @@ import 'package:logger/logger.dart'; export '../generated/lib_spark_interface_impl.dart'; +enum LibSparkSpendVersion { + chaumV1(transactionType: 9), + chaumV2(transactionType: 11); + + const LibSparkSpendVersion({required this.transactionType}); + + static const int baseTransactionVersion = 3; + final int transactionType; + + int get transactionVersion => + baseTransactionVersion | (transactionType << 16); + + bool get allowsMultipleInputs => this == chaumV2; +} + +final class LibSparkNameProofInput { + const LibSparkNameProofInput.chaumV1({required String scalarHex}) + : spendVersion = .chaumV1, + inputHex = scalarHex; + + const LibSparkNameProofInput.chaumV2({required String ownershipDigest}) + : spendVersion = .chaumV2, + inputHex = ownershipDigest; + + final LibSparkSpendVersion spendVersion; + final String inputHex; +} + abstract class LibSparkInterface { const LibSparkInterface(); @@ -31,12 +59,16 @@ abstract class LibSparkInterface { bool isTestNet = false, }); + LibSparkSpendVersion getSpendVersionForBlockHeight({ + required int nextBlockHeight, + required int chaumV2ActivationHeight, + }); + ({Uint8List script, int size}) createSparkNameScript({ required int sparkNameValidityBlocks, required String name, required String additionalInfo, - required String ownershipDigest, - required int spendVersion, + required LibSparkNameProofInput proofInput, required String privateKeyHex, required int spendKeyIndex, required int diversifier, @@ -133,8 +165,8 @@ abstract class LibSparkInterface { required List<({int setId, Uint8List blockHash})> idAndBlockHashes, required Uint8List txHash, required int additionalTxSize, - required int spendVersion, - required Uint8List extensionCommitment, + required LibSparkSpendVersion spendVersion, + Uint8List? extensionCommitment, }); int estimateSparkFee({ @@ -154,7 +186,7 @@ abstract class LibSparkInterface { required int privateRecipientsCount, required int utxoNum, required int additionalTxSize, - required int spendVersion, + required LibSparkSpendVersion spendVersion, }); } diff --git a/pubspec.lock b/pubspec.lock index a187d14bb2..10c5f6f4c8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1019,8 +1019,8 @@ packages: dependency: "direct main" description: path: "." - ref: c164f003f6ddb29837901f48e3c0f80073fbcd09 - resolved-ref: c164f003f6ddb29837901f48e3c0f80073fbcd09 + ref: "1c902b020647302376275c454d01cc64cf9f8956" + resolved-ref: "1c902b020647302376275c454d01cc64cf9f8956" url: "https://github.com/cypherstack/flutter_libsparkmobile.git" source: git version: "0.1.0" diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml index b1ab0afdff..c566a1f59e 100644 --- a/scripts/app_config/templates/pubspec.template.yaml +++ b/scripts/app_config/templates/pubspec.template.yaml @@ -44,7 +44,7 @@ dependencies: # flutter_libsparkmobile: # git: # url: https://github.com/cypherstack/flutter_libsparkmobile.git -# ref: c164f003f6ddb29837901f48e3c0f80073fbcd09 +# ref: 1c902b020647302376275c454d01cc64cf9f8956 # %%END_ENABLE_FIRO%% # %%ENABLE_EPIC%% diff --git a/test/wallets/spark_name_fee_test.dart b/test/wallets/spark_name_fee_test.dart index fc11c7909f..331cc6a884 100644 --- a/test/wallets/spark_name_fee_test.dart +++ b/test/wallets/spark_name_fee_test.dart @@ -4,6 +4,7 @@ import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; +import 'package:stackwallet/wl_gen/interfaces/lib_spark_interface.dart'; void main() { test('Spark Name validation rejects underscores before construction', () { @@ -46,24 +47,26 @@ void main() { group('Spark H2 activation', () { test('mainnet uses V1 before the activation block', () { - expect( - sparkH2ParametersForNextBlock( - network: CryptoCurrencyNetwork.main, - nextBlockHeight: 1370999, - ), - (transactionType: 9, spendVersion: 1), + final version = sparkSpendVersionForNextBlock( + network: CryptoCurrencyNetwork.main, + nextBlockHeight: 1370999, ); + + expect(version, LibSparkSpendVersion.chaumV1); + expect(version.allowsMultipleInputs, isFalse); + expect(version.transactionVersion, 3 | (9 << 16)); }); test('mainnet uses V2 at activation and later', () { for (final nextBlockHeight in [1371000, 1371001]) { - expect( - sparkH2ParametersForNextBlock( - network: CryptoCurrencyNetwork.main, - nextBlockHeight: nextBlockHeight, - ), - (transactionType: 11, spendVersion: 2), + final version = sparkSpendVersionForNextBlock( + network: CryptoCurrencyNetwork.main, + nextBlockHeight: nextBlockHeight, ); + + expect(version, LibSparkSpendVersion.chaumV2); + expect(version.allowsMultipleInputs, isTrue); + expect(version.transactionVersion, 3 | (11 << 16)); } }); @@ -72,13 +75,28 @@ void main() { (network) => network != CryptoCurrencyNetwork.main, )) { expect( - sparkH2ParametersForNextBlock( + sparkSpendVersionForNextBlock( network: network, nextBlockHeight: 1371000, ), - (transactionType: 9, spendVersion: 1), + LibSparkSpendVersion.chaumV1, ); } }); + + test('only the Chaum V2 transaction version permits multiple inputs', () { + expect( + isChaumV2SparkTransactionVersion( + LibSparkSpendVersion.chaumV1.transactionVersion, + ), + isFalse, + ); + expect( + isChaumV2SparkTransactionVersion( + LibSparkSpendVersion.chaumV2.transactionVersion, + ), + isTrue, + ); + }); }); } diff --git a/tool/wl_templates/FIRO_lib_spark_interface_impl.template.dart b/tool/wl_templates/FIRO_lib_spark_interface_impl.template.dart index 23b01b635b..5408f11fa6 100644 --- a/tool/wl_templates/FIRO_lib_spark_interface_impl.template.dart +++ b/tool/wl_templates/FIRO_lib_spark_interface_impl.template.dart @@ -96,13 +96,27 @@ class _LibSparkInterfaceImpl extends LibSparkInterface { isTestNet: isTestNet, ); + @override + LibSparkSpendVersion getSpendVersionForBlockHeight({ + required int nextBlockHeight, + required int chaumV2ActivationHeight, + }) { + final version = SparkSpendVersion.forBlockHeight( + nextBlockHeight: nextBlockHeight, + chaumV2ActivationHeight: chaumV2ActivationHeight, + ); + return switch (version) { + .chaumV1 => .chaumV1, + .chaumV2 => .chaumV2, + }; + } + @override ({Uint8List script, int size}) createSparkNameScript({ required int sparkNameValidityBlocks, required String name, required String additionalInfo, - required String ownershipDigest, - required int spendVersion, + required LibSparkNameProofInput proofInput, required String privateKeyHex, required int spendKeyIndex, required int diversifier, @@ -113,8 +127,10 @@ class _LibSparkInterfaceImpl extends LibSparkInterface { sparkNameValidityBlocks: sparkNameValidityBlocks, name: name, additionalInfo: additionalInfo, - ownershipDigest: ownershipDigest, - spendVersion: spendVersion, + proofInput: switch (proofInput.spendVersion) { + .chaumV1 => .chaumV1(scalarHex: proofInput.inputHex), + .chaumV2 => .chaumV2(ownershipDigest: proofInput.inputHex), + }, privateKeyHex: privateKeyHex, spendKeyIndex: spendKeyIndex, diversifier: diversifier, @@ -297,8 +313,8 @@ class _LibSparkInterfaceImpl extends LibSparkInterface { required List<({Uint8List blockHash, int setId})> idAndBlockHashes, required Uint8List txHash, required int additionalTxSize, - required int spendVersion, - required Uint8List extensionCommitment, + required LibSparkSpendVersion spendVersion, + Uint8List? extensionCommitment, }) => LibSpark.createSparkSendTransaction( index: index, privateKeyHex: privateKeyHex, @@ -309,7 +325,10 @@ class _LibSparkInterfaceImpl extends LibSparkInterface { idAndBlockHashes: idAndBlockHashes, txHash: txHash, additionalTxSize: additionalTxSize, - spendVersion: spendVersion, + spendVersion: switch (spendVersion) { + .chaumV1 => .chaumV1, + .chaumV2 => .chaumV2, + }, extensionCommitment: extensionCommitment, ); @@ -331,7 +350,7 @@ class _LibSparkInterfaceImpl extends LibSparkInterface { required int privateRecipientsCount, required int utxoNum, required int additionalTxSize, - required int spendVersion, + required LibSparkSpendVersion spendVersion, }) => LibSpark.estimateSparkFee( privateKeyHex: privateKeyHex, sendAmount: sendAmount, @@ -340,7 +359,10 @@ class _LibSparkInterfaceImpl extends LibSparkInterface { privateRecipientsCount: privateRecipientsCount, utxoNum: utxoNum, additionalTxSize: additionalTxSize, - spendVersion: spendVersion, + spendVersion: switch (spendVersion) { + .chaumV1 => .chaumV1, + .chaumV2 => .chaumV2, + }, index: index, ); } From c6de6d3c2e2cb8afe08191e645366cf6990e93ae Mon Sep 17 00:00:00 2001 From: Julian Date: Tue, 1 Sep 2026 12:54:03 -0600 Subject: [PATCH 3/4] update flutter_libsparkmobile to make use of flutter native assets --- .github/workflows/build.yaml | 18 +++---- Dockerfile | 6 +-- pubspec.lock | 48 +++++++++++++++++-- .../app_config/templates/linux/CMakeLists.txt | 6 +++ .../templates/pubspec.template.yaml | 4 +- .../templates/windows/CMakeLists.txt | 6 +++ 6 files changed, 70 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e716b930ca..e4f1868062 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -254,7 +254,7 @@ jobs: - uses: subosito/flutter-action@v2 with: - flutter-version: '3.44.8' + flutter-version: '3.44.9' channel: 'stable' - uses: actions/setup-go@v6 @@ -373,7 +373,7 @@ jobs: - uses: subosito/flutter-action@v2 with: - flutter-version: '3.44.8' + flutter-version: '3.44.9' channel: 'stable' - uses: actions/setup-go@v6 @@ -455,7 +455,7 @@ jobs: - uses: subosito/flutter-action@v2 with: - flutter-version: '3.44.8' + flutter-version: '3.44.9' channel: 'stable' - uses: actions/setup-go@v6 @@ -882,7 +882,7 @@ jobs: - uses: subosito/flutter-action@v2 with: - flutter-version: '3.44.8' + flutter-version: '3.44.9' channel: 'stable' - uses: actions/setup-go@v6 @@ -971,7 +971,7 @@ jobs: - uses: subosito/flutter-action@v2 with: - flutter-version: '3.44.8' + flutter-version: '3.44.9' channel: 'stable' - uses: actions/setup-go@v6 @@ -1046,7 +1046,7 @@ jobs: - uses: subosito/flutter-action@v2 with: - flutter-version: '3.44.8' + flutter-version: '3.44.9' channel: 'stable' - uses: actions/setup-go@v6 @@ -1340,7 +1340,7 @@ jobs: - uses: subosito/flutter-action@v2 with: - flutter-version: '3.44.8' + flutter-version: '3.44.9' channel: 'stable' - uses: actions/setup-go@v6 @@ -1429,7 +1429,7 @@ jobs: - uses: subosito/flutter-action@v2 with: - flutter-version: '3.44.8' + flutter-version: '3.44.9' channel: 'stable' - uses: actions/setup-go@v6 @@ -1506,7 +1506,7 @@ jobs: - uses: subosito/flutter-action@v2 with: - flutter-version: '3.44.8' + flutter-version: '3.44.9' channel: 'stable' - uses: actions/setup-go@v6 diff --git a/Dockerfile b/Dockerfile index 72e8cd3e2c..e657c616d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -83,7 +83,7 @@ RUN curl -fsSL https://go.dev/dl/go1.24.13.linux-amd64.tar.gz -o /tmp/go.tar.gz ENV FLUTTER_HOME=/opt/flutter \ PATH=/opt/flutter/bin:/opt/flutter/bin/cache/dart-sdk/bin:$PATH -RUN git clone --depth 1 --branch 3.44.8 https://github.com/flutter/flutter.git "$FLUTTER_HOME" \ +RUN git clone --depth 1 --branch 3.44.9 https://github.com/flutter/flutter.git "$FLUTTER_HOME" \ && git config --global --add safe.directory '*' \ && flutter config --no-analytics \ && flutter precache --linux --android \ @@ -168,7 +168,7 @@ RUN curl -fsSL https://go.dev/dl/go1.24.13.linux-amd64.tar.gz -o /tmp/go.tar.gz ENV FLUTTER_HOME=/opt/flutter \ PATH=/opt/flutter/bin:/opt/flutter/bin/cache/dart-sdk/bin:$PATH -RUN git clone --depth 1 --branch 3.44.8 https://github.com/flutter/flutter.git "$FLUTTER_HOME" \ +RUN git clone --depth 1 --branch 3.44.9 https://github.com/flutter/flutter.git "$FLUTTER_HOME" \ && git config --global --add safe.directory '*' \ && flutter config --no-analytics \ && flutter precache --android \ @@ -200,7 +200,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ ENV FLUTTER_HOME=/opt/flutter \ PATH=/opt/flutter/bin:/opt/flutter/bin/cache/dart-sdk/bin:$PATH -RUN git clone --depth 1 --branch 3.44.8 https://github.com/flutter/flutter.git "$FLUTTER_HOME" \ +RUN git clone --depth 1 --branch 3.44.9 https://github.com/flutter/flutter.git "$FLUTTER_HOME" \ && git config --global --add safe.directory '*' \ && flutter config --no-analytics \ && flutter precache --linux \ diff --git a/pubspec.lock b/pubspec.lock index 10c5f6f4c8..6164f3dbbf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -281,6 +281,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.1" + change_case: + dependency: transitive + description: + name: change_case + sha256: e41ef3df58521194ef8d7649928954805aeb08061917cf658322305e61568003 + url: "https://pub.dev" + source: hosted + version: "2.2.0" characters: dependency: transitive description: @@ -329,6 +337,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" code_builder: dependency: transitive description: @@ -1019,8 +1035,8 @@ packages: dependency: "direct main" description: path: "." - ref: "1c902b020647302376275c454d01cc64cf9f8956" - resolved-ref: "1c902b020647302376275c454d01cc64cf9f8956" + ref: e017e62766908e9714c5309761183e8be8b37799 + resolved-ref: e017e62766908e9714c5309761183e8be8b37799 url: "https://github.com/cypherstack/flutter_libsparkmobile.git" source: git version: "0.1.0" @@ -1303,6 +1319,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + hooks: + dependency: transitive + description: + name: hooks + sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" + url: "https://pub.dev" + source: hosted + version: "1.0.3" html: dependency: "direct main" description: @@ -1664,6 +1688,14 @@ packages: url: "https://github.com/cypherstack/nanodart" source: git version: "2.0.1" + native_toolchain_cmake: + dependency: transitive + description: + name: native_toolchain_cmake + sha256: "8ba223410102665483e873b83d2156720233a6a0181c07f9dc41cb2e961336b0" + url: "https://pub.dev" + source: hosted + version: "0.3.2" nm: dependency: transitive description: @@ -1968,6 +2000,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" retry: dependency: transitive description: @@ -2669,5 +2709,5 @@ packages: source: hosted version: "0.2.4" sdks: - dart: ">=3.12.0 <4.0.0" - flutter: ">=3.44.0 <4.0.0" + dart: ">=3.12.2 <4.0.0" + flutter: ">=3.44.9 <4.0.0" diff --git a/scripts/app_config/templates/linux/CMakeLists.txt b/scripts/app_config/templates/linux/CMakeLists.txt index d1c69c17fe..d36efad397 100644 --- a/scripts/app_config/templates/linux/CMakeLists.txt +++ b/scripts/app_config/templates/linux/CMakeLists.txt @@ -193,6 +193,12 @@ foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) COMPONENT Runtime) endforeach(bundled_library) +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml index c566a1f59e..a9161185f8 100644 --- a/scripts/app_config/templates/pubspec.template.yaml +++ b/scripts/app_config/templates/pubspec.template.yaml @@ -15,7 +15,7 @@ version: PLACEHOLDER_V+PLACEHOLDER_B environment: sdk: ">=3.12.0 <4.0.0" - flutter: ^3.44.0 + flutter: ^3.44.9 dependencies: flutter: @@ -44,7 +44,7 @@ dependencies: # flutter_libsparkmobile: # git: # url: https://github.com/cypherstack/flutter_libsparkmobile.git -# ref: 1c902b020647302376275c454d01cc64cf9f8956 +# ref: e017e62766908e9714c5309761183e8be8b37799 # %%END_ENABLE_FIRO%% # %%ENABLE_EPIC%% diff --git a/scripts/app_config/templates/windows/CMakeLists.txt b/scripts/app_config/templates/windows/CMakeLists.txt index a33fe23bb5..d152222b2c 100644 --- a/scripts/app_config/templates/windows/CMakeLists.txt +++ b/scripts/app_config/templates/windows/CMakeLists.txt @@ -99,6 +99,12 @@ if(PLUGIN_BUNDLED_LIBRARIES) COMPONENT Runtime) endif() +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") From 04ee64966f9db4d662613d5cc371e77589214c4a Mon Sep 17 00:00:00 2001 From: Julian Date: Tue, 1 Sep 2026 13:17:05 -0600 Subject: [PATCH 4/4] fix send to spark name gray screen (nav error) --- lib/pages/send_view/confirm_transaction_view.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index b54b3b071e..ed4e967792 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -764,7 +764,11 @@ class _ConfirmTransactionViewState Text( widget.isPaynymTransaction ? widget.txData.paynymAccountLite!.nymName - : widget.txData.recipients?.first.address ?? + : widget + .txData + .recipients + ?.firstOrNull + ?.address ?? widget .txData .sparkRecipients! @@ -1110,7 +1114,11 @@ class _ConfirmTransactionViewState // TODO: [prio=med] spark transaction specifics - better handling widget.isPaynymTransaction ? widget.txData.paynymAccountLite!.nymName - : widget.txData.recipients?.first.address ?? + : widget + .txData + .recipients + ?.firstOrNull + ?.address ?? widget .txData .sparkRecipients!