From 656b3017540492031dfafa41db58dc36d94ad107 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Mon, 4 Dec 2023 16:12:52 -0600 Subject: [PATCH 01/13] remove unnecessary toHex cleaning up diff for stashing etc purposes --- .../wallet/wallet_mixin_interfaces/spark_interface.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index eb2363df7..f50c91781 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -4,7 +4,6 @@ import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; -import 'package:stackwallet/utilities/extensions/impl/uint8_list.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/isar/models/spark_coin.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; @@ -205,12 +204,11 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { SparkCoin( walletId: walletId, type: coinType, - isUsed: spentCoinTags - .contains(coin.lTagHash!.toHex), // TODO: is hex right? + isUsed: spentCoinTags.contains(coin.lTagHash!), address: coin.address!, txHash: txHash, valueIntString: coin.value!.toString(), - lTagHash: coin.lTagHash!.toHex, // TODO: is hex right? + lTagHash: coin.lTagHash!, tag: coin.tag, memo: coin.memo, serial: coin.serial, From eaf14c2e8af4916f3969ca15eb2795b1636fe040 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 5 Dec 2023 00:00:30 -0600 Subject: [PATCH 02/13] hardcode key from test --- .../wallet/wallet_mixin_interfaces/spark_interface.dart | 8 ++++++-- 1 file changed, 6 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 f50c91781..0b44e083b 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -152,8 +152,12 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // have not yet parsed. Future refreshSparkData() async { try { - final privateKeyHex = "TODO"; - final index = 0; + const privateKeyHex = + "8acecb5844fbcb700706a90385d20e4752ec8aecb2d13b8f03d685374e2539b2"; + // This corresponds to the `jazz settle...` test mnemonic included in the + // plugin integration test. TODO move to test. + const index = 1; + // final latestSparkCoinId = await electrumXClient.getSparkLatestCoinId(); From 0b0774b0b8822a187c2083df582c854c9693174f Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 5 Dec 2023 00:00:58 -0600 Subject: [PATCH 03/13] testnet --- .../wallet/wallet_mixin_interfaces/spark_interface.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 0b44e083b..7814f5528 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -178,8 +178,9 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // find our coins final List myCoins = []; - for (final data - in List>.from(anonymitySet["coin"] as List)) { + // for (final data + // in List>.from(anonymitySet["coin"] as List)) { + for (final data in anonymitySet["coins"] as List) { if (data.length != 2) { throw Exception("Unexpected serialized coin info found"); } @@ -188,9 +189,10 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { final txHash = data.last; final coin = LibSpark.identifyAndRecoverCoin( - serializedCoin, + "$serializedCoin", privateKeyHex: privateKeyHex, index: index, + isTestNet: true, ); // its ours From 658901ff038893f954d69f558603b5099995da5c Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 5 Dec 2023 16:55:38 -0600 Subject: [PATCH 04/13] WIP spark scanning --- .../spark_interface.dart | 112 +++++++++++------- 1 file changed, 66 insertions(+), 46 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 7814f5528..1521361aa 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -4,6 +4,7 @@ import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/isar/models/spark_coin.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; @@ -75,7 +76,12 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { const index = 1; final root = await getRootHDNode(); - const derivationPath = "$kSparkBaseDerivationPath$index"; + final String derivationPath; + if (cryptoCurrency.network == CryptoCurrencyNetwork.test) { + derivationPath = "$kSparkBaseDerivationPathTestnet$index"; + } else { + derivationPath = "$kSparkBaseDerivationPath$index"; + } final keys = root.derivePath(derivationPath); final String addressString = await LibSpark.getAddress( @@ -151,13 +157,20 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // recoverSparkWallet but only fetch and check anonymity set data that we // have not yet parsed. Future refreshSparkData() async { + final sparkAddresses = await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.spark) + .findAll(); + + final Set paths = + sparkAddresses.map((e) => e.derivationPath!.value).toSet(); + try { - const privateKeyHex = - "8acecb5844fbcb700706a90385d20e4752ec8aecb2d13b8f03d685374e2539b2"; - // This corresponds to the `jazz settle...` test mnemonic included in the - // plugin integration test. TODO move to test. const index = 1; - // + + final root = await getRootHDNode(); final latestSparkCoinId = await electrumXClient.getSparkLatestCoinId(); @@ -178,51 +191,58 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // find our coins final List myCoins = []; - // for (final data - // in List>.from(anonymitySet["coin"] as List)) { - for (final data in anonymitySet["coins"] as List) { - if (data.length != 2) { - throw Exception("Unexpected serialized coin info found"); - } - final serializedCoin = data.first; - final txHash = data.last; + for (final path in paths) { + final keys = root.derivePath(path); - final coin = LibSpark.identifyAndRecoverCoin( - "$serializedCoin", - privateKeyHex: privateKeyHex, - index: index, - isTestNet: true, - ); + final privateKeyHex = keys.privateKey.data.toHex; - // its ours - if (coin != null) { - final SparkCoinType coinType; - switch (coin.type.value) { - case 0: - coinType = SparkCoinType.mint; - case 1: - coinType = SparkCoinType.spend; - default: - throw Exception("Unknown spark coin type detected"); + for (final dynData in anonymitySet["coins"] as List) { + final data = List.from(dynData as List); + + if (data.length != 2) { + throw Exception("Unexpected serialized coin info found"); } - myCoins.add( - SparkCoin( - walletId: walletId, - type: coinType, - isUsed: spentCoinTags.contains(coin.lTagHash!), - address: coin.address!, - txHash: txHash, - valueIntString: coin.value!.toString(), - lTagHash: coin.lTagHash!, - tag: coin.tag, - memo: coin.memo, - serial: coin.serial, - serialContext: coin.serialContext, - diversifierIntString: coin.diversifier!.toString(), - encryptedDiversifier: coin.encryptedDiversifier, - ), + + final serializedCoin = data.first; + final txHash = data.last; + + final coin = LibSpark.identifyAndRecoverCoin( + serializedCoin, + privateKeyHex: privateKeyHex, + index: index, + isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test, ); + + // its ours + if (coin != null) { + final SparkCoinType coinType; + switch (coin.type.value) { + case 0: + coinType = SparkCoinType.mint; + case 1: + coinType = SparkCoinType.spend; + default: + throw Exception("Unknown spark coin type detected"); + } + myCoins.add( + SparkCoin( + walletId: walletId, + type: coinType, + isUsed: spentCoinTags.contains(coin.lTagHash!), + address: coin.address!, + txHash: txHash, + valueIntString: coin.value!.toString(), + lTagHash: coin.lTagHash!, + tag: coin.tag, + memo: coin.memo, + serial: coin.serial, + serialContext: coin.serialContext, + diversifierIntString: coin.diversifier!.toString(), + encryptedDiversifier: coin.encryptedDiversifier, + ), + ); + } } } From 56e11400a275db4fb0a4cfa9358f36f2da83f22a Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 5 Dec 2023 14:44:50 -0600 Subject: [PATCH 05/13] WIP spark scanning txhash correction --- .../wallet/wallet_mixin_interfaces/spark_interface.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 1521361aa..cf276b664 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; @@ -205,7 +206,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } final serializedCoin = data.first; - final txHash = data.last; + final txHash = base64ToReverseHex(data.last); final coin = LibSpark.identifyAndRecoverCoin( serializedCoin, @@ -332,3 +333,9 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { await normalBalanceFuture; } } + +String base64ToReverseHex(String source) => + base64Decode(LineSplitter.split(source).join()) + .reversed + .map((e) => e.toRadixString(16).padLeft(2, '0')) + .join(); From 095bfc2ff3ea424a8eb4c044f587a783d0a9fcb7 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 7 Dec 2023 10:56:45 -0600 Subject: [PATCH 06/13] WIP spark mint transaction --- .../wallet_mixin_interfaces/spark_interface.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index cf276b664..133a09787 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -318,6 +318,18 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // first then we generate spark related data. And we sign like regular // transactions at the end. + List serialContext = []; + + final mintRecipients = LibSpark.createSparkMintRecipients( + outputs: txData.utxos! + .map((e) => ( + sparkAddress: e.address!, + value: e.value, + memo: "Stackwallet spark mint" + )) + .toList(), + serialContext: Uint8List.fromList(serialContext), + ); throw UnimplementedError(); } From 2e19dd8545b927b656774e0cd659b768acaa4b37 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 7 Dec 2023 10:57:54 -0600 Subject: [PATCH 07/13] WIP spark mint transaction fix --- .../wallet/wallet_mixin_interfaces/spark_interface.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 133a09787..2e0d0f2c0 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -321,10 +321,10 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { List serialContext = []; final mintRecipients = LibSpark.createSparkMintRecipients( - outputs: txData.utxos! + outputs: txData.recipients! .map((e) => ( - sparkAddress: e.address!, - value: e.value, + sparkAddress: e.address, + value: e.amount.raw.toInt(), memo: "Stackwallet spark mint" )) .toList(), From dd01444ff5ec9ac73843c6f8429d36dfbe209ecf Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 7 Dec 2023 14:46:46 -0600 Subject: [PATCH 08/13] add refresh spark data hidden button --- .../global_settings_view/hidden_settings.dart | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 04bdc18e3..d14a6fd5b 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -23,8 +23,10 @@ import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/onetime_popups/tor_has_been_add_dialog.dart'; @@ -552,6 +554,66 @@ class HiddenSettings extends StatelessWidget { ); }, ), + const SizedBox( + height: 12, + ), + Consumer( + builder: (_, ref, __) { + return GestureDetector( + onTap: () async { + try { + // Run refreshSparkData. + // + // Search wallets for a Firo testnet wallet. + for (final wallet + in ref.read(pWallets).wallets) { + if (!(wallet.info.coin == + Coin.firoTestNet)) { + continue; + } + // This is a Firo testnet wallet. + final walletId = wallet.info.walletId; + + // // Search for `circle chunk...` mnemonic. + // final potentialWallet = + // ref.read(pWallets).getWallet(walletId) + // as MnemonicInterface; + // final mnemonic = await potentialWallet + // .getMnemonicAsWords(); + // if (!(mnemonic[0] == "circle" && + // mnemonic[1] == "chunk)")) { + // // That ain't it. Skip this one. + // return; + // } + // Hardcode key in refreshSparkData instead. + + // Get a Spark interface. + final fusionWallet = ref + .read(pWallets) + .getWallet(walletId) as SparkInterface; + + // Refresh Spark data. + await fusionWallet.refreshSparkData(); + + // We only need to run this once. + break; + } + } catch (e, s) { + print("$e\n$s"); + } + }, + child: RoundedWhiteContainer( + child: Text( + "Refresh Spark wallet", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ); + }, + ), // const SizedBox( // height: 12, // ), From f30e9966553a5da2b52b3b1a16a96049273c5e53 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 7 Dec 2023 14:55:40 -0600 Subject: [PATCH 09/13] dummy hidden settings prepare spark mint button --- .../global_settings_view/hidden_settings.dart | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index d14a6fd5b..a430f3f63 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -26,6 +26,7 @@ import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; @@ -614,6 +615,55 @@ class HiddenSettings extends StatelessWidget { ); }, ), + const SizedBox( + height: 12, + ), + Consumer( + builder: (_, ref, __) { + return GestureDetector( + onTap: () async { + try { + // Run prepareSparkMintTransaction. + for (final wallet + in ref.read(pWallets).wallets) { + // Prepare tx with a Firo testnet wallet. + if (!(wallet.info.coin == + Coin.firoTestNet)) { + continue; + } + final walletId = wallet.info.walletId; + + // Get a Spark interface. + final fusionWallet = ref + .read(pWallets) + .getWallet(walletId) as SparkInterface; + + // Make a dummy TxData. + TxData txData = TxData(); // TODO + + await fusionWallet + .prepareSparkMintTransaction( + txData: txData); + + // We only need to run this once. + break; + } + } catch (e, s) { + print("$e\n$s"); + } + }, + child: RoundedWhiteContainer( + child: Text( + "Prepare Spark mint transaction", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ); + }, + ), // const SizedBox( // height: 12, // ), From 5567d96f5a8690e94278fe7630e8302f0fbb92bd Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 7 Dec 2023 15:05:27 -0600 Subject: [PATCH 10/13] confirmSparkMintTransaction --- .../spark_interface.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 2e0d0f2c0..7424047d6 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -333,6 +333,23 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { throw UnimplementedError(); } + /// Broadcast a tx and TODO update Spark balance. + Future confirmSparkMintTransaction({required TxData txData}) async { + // Broadcast tx. + final txid = await electrumXClient.broadcastTransaction( + rawTx: txData.raw!, + ); + + // Check txid. + assert(txid == txData.txid!); + + // TODO update spark balance. + + return txData.copyWith( + txid: txid, + ); + } + @override Future updateBalance() async { // call to super to update transparent balance (and lelantus balance if From 5f4ef72e6409854a2b5b1ac0f07497d0673aff3d Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 7 Dec 2023 15:58:23 -0600 Subject: [PATCH 11/13] validation in prepareSparkMintTransaction and TODOs --- .../spark_interface.dart | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 7424047d6..13bfc2e5f 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -300,12 +300,12 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } } - /// Transparent to Spark (mint) transaction creation + /// Transparent to Spark (mint) transaction creation. Future prepareSparkMintTransaction({required TxData txData}) async { // https://docs.google.com/document/d/1RG52GoYTZDvKlZz_3G4sQu-PpT6JWSZGHLNswWcrE3o/edit // this kind of transaction is generated like a regular transaction, but in - // place of regulart outputs we put spark outputs, so for that we call + // place of regular outputs we put spark outputs, so for that we call // createSparkMintRecipients function, we get spark related data, // everything else we do like for regular transaction, and we put CRecipient // object as a tx outputs, we need to keep the order.. @@ -318,8 +318,35 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // first then we generate spark related data. And we sign like regular // transactions at the end. - List serialContext = []; + // Validate inputs. + // There should be at least one recipient. + if (txData.recipients == null || txData.recipients!.isEmpty) { + throw Exception("No recipients provided"); + } + + // For now let's limit to one recipient. + if (txData.recipients!.length > 1) { + throw Exception("Only one recipient supported"); + } + + // Limit outputs per tx to 16. + // + // See SPARK_OUT_LIMIT_PER_TX at https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L16 + // TODO limit outputs to 16. + + // Limit spend value per tx to 1000000000000 satoshis. + // + // 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 + // TODO limit spend value per tx to 1000000000000 satoshis. + + // Create the serial context. + List serialContext = []; + // TODO set serialContext to the serialized inputs. + + // Create outputs. final mintRecipients = LibSpark.createSparkMintRecipients( outputs: txData.recipients! .map((e) => ( @@ -330,6 +357,9 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { .toList(), serialContext: Uint8List.fromList(serialContext), ); + + // TODO Add outputs to txData. + throw UnimplementedError(); } From 04bceb17553912719acac795183d43838691f38d Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 13 Dec 2023 20:12:12 -0600 Subject: [PATCH 12/13] prepareSparkMintTransaction i/o validation (WIP) --- .../spark_interface.dart | 103 ++++++++++++++---- 1 file changed, 79 insertions(+), 24 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 60eafbffa..4d454f265 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -563,52 +563,106 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } /// Transparent to Spark (mint) transaction creation. + /// + /// See https://docs.google.com/document/d/1RG52GoYTZDvKlZz_3G4sQu-PpT6JWSZGHLNswWcrE3o Future prepareSparkMintTransaction({required TxData txData}) async { - // https://docs.google.com/document/d/1RG52GoYTZDvKlZz_3G4sQu-PpT6JWSZGHLNswWcrE3o/edit - - // this kind of transaction is generated like a regular transaction, but in - // place of regular outputs we put spark outputs, so for that we call - // createSparkMintRecipients function, we get spark related data, - // everything else we do like for regular transaction, and we put CRecipient - // object as a tx outputs, we need to keep the order.. - // First we pass spark::MintedCoinData>, has following members, Address - // which is any spark address, amount (v) how much we want to send, and - // memo which can be any string with 32 length (any string we want to send - // to receiver), serial_context is a byte array, which should be unique for - // each transaction, and for that we serialize and put all inputs into - // serial_context vector. So we construct the input part of the transaction - // first then we generate spark related data. And we sign like regular - // transactions at the end. + // "this kind of transaction is generated like a regular transaction, but in + // place of [regular] outputs we put spark outputs... we construct the input + // part of the transaction first then we generate spark related data [and] + // we sign like regular transactions at the end." // Validate inputs. - // There should be at least one recipient. - if (txData.recipients == null || txData.recipients!.isEmpty) { - throw Exception("No recipients provided"); + // There should be at least one input. + if (txData.utxos == null || txData.utxos!.isEmpty) { + throw Exception("No inputs provided."); } - // For now let's limit to one recipient. + // For now let's limit to one input. + if (txData.utxos!.length > 1) { + throw Exception("Only one input supported."); + // TODO remove and test with multiple inputs. + } + + // Validate individual inputs. + for (final utxo in txData.utxos!) { + // Input amount must be greater than zero. + if (utxo.value == 0) { + throw Exception("Input value cannot be zero."); + } + + // Input value must be greater than dust limit. + if (BigInt.from(utxo.value) < cryptoCurrency.dustLimit.raw) { + throw Exception("Input value below dust limit."); + } + } + + // Validate outputs. + + // There should be at least one output. + if (txData.recipients == null || txData.recipients!.isEmpty) { + throw Exception("No recipients provided."); + } + + // For now let's limit to one output. if (txData.recipients!.length > 1) { - throw Exception("Only one recipient supported"); + throw Exception("Only one recipient supported."); + // TODO remove and test with multiple recipients. } // Limit outputs per tx to 16. // // See SPARK_OUT_LIMIT_PER_TX at https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L16 - // TODO limit outputs to 16. + if (txData.recipients!.length > 16) { + throw Exception("Too many recipients."); + } // Limit spend value per tx to 1000000000000 satoshis. // // 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 - // TODO limit spend value per tx to 1000000000000 satoshis. + // + // This will be added to and checked as we validate outputs. + Amount amountSent = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + + // Validate individual outputs. + for (final recipient in txData.recipients!) { + // Output amount must be greater than zero. + if (recipient.amount.raw == BigInt.zero) { + throw Exception("Output amount cannot be zero."); + // Could refactor this for loop to use an index and remove this output. + } + + // Output amount must be greater than dust limit. + if (recipient.amount < cryptoCurrency.dustLimit) { + throw Exception("Output below dust limit."); + } + + // Do not add outputs that would exceed the spend limit. + amountSent += recipient.amount; + if (amountSent.raw > BigInt.from(1000000000000)) { + throw Exception( + "Spend limit exceeded (10,000 FIRO per tx).", + ); + } + } + + // TODO create a transaction builder and add inputs. // Create the serial context. + // + // "...serial_context is a byte array, which should be unique for each + // transaction, and for that we serialize and put all inputs into + // serial_context vector. So we construct the input part of the transaction + // first then we generate spark related data." List serialContext = []; // TODO set serialContext to the serialized inputs. - // Create outputs. + // Create mint recipients. final mintRecipients = LibSpark.createSparkMintRecipients( outputs: txData.recipients! .map((e) => ( @@ -618,9 +672,10 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { )) .toList(), serialContext: Uint8List.fromList(serialContext), + // generate: true // TODO is this needed? ); - // TODO Add outputs to txData. + // TODO finish. throw UnimplementedError(); } From 1d6ca55a36d32d83ff16d3acb85dc364cd5e23f5 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 13 Dec 2023 20:25:13 -0600 Subject: [PATCH 13/13] add WIP transaction builder --- .../spark_interface.dart | 61 ++++++++++++++++--- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 4d454f265..2f37f3f8c 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -624,7 +624,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // 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 // // This will be added to and checked as we validate outputs. - Amount amountSent = Amount( + Amount totalAmount = Amount( rawValue: BigInt.zero, fractionDigits: cryptoCurrency.fractionDigits, ); @@ -643,24 +643,61 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } // Do not add outputs that would exceed the spend limit. - amountSent += recipient.amount; - if (amountSent.raw > BigInt.from(1000000000000)) { + totalAmount += recipient.amount; + if (totalAmount.raw > BigInt.from(1000000000000)) { throw Exception( "Spend limit exceeded (10,000 FIRO per tx).", ); } } - // TODO create a transaction builder and add inputs. + // Create a transaction builder and set locktime and version. + final txb = btc.TransactionBuilder( + network: btc.NetworkType( + messagePrefix: cryptoCurrency.networkParams.messagePrefix, + bech32: cryptoCurrency.networkParams.bech32Hrp, + bip32: btc.Bip32Type( + public: cryptoCurrency.networkParams.pubHDPrefix, + private: cryptoCurrency.networkParams.privHDPrefix, + ), + pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, + scriptHash: cryptoCurrency.networkParams.p2shPrefix, + wif: cryptoCurrency.networkParams.wifPrefix, + ), + ); + txb.setLockTime(await chainHeight); + txb.setVersion(3 | (9 << 16)); + + // Create a mint script. + final mintScript = bscript.compile([ + 0xd1, // OP_SPARKMINT. + Uint8List(0), + ]); + + // Add inputs. + for (final utxo in txData.utxos!) { + txb.addInput( + utxo.txid, + utxo.vout, + 0xffffffff, + mintScript, + ); + } // Create the serial context. // // "...serial_context is a byte array, which should be unique for each // transaction, and for that we serialize and put all inputs into - // serial_context vector. So we construct the input part of the transaction - // first then we generate spark related data." + // serial_context vector." List serialContext = []; - // TODO set serialContext to the serialized inputs. + for (final utxo in txData.utxos!) { + serialContext.addAll( + bscript.compile([ + utxo.txid, + utxo.vout, + ]), + ); + } // Create mint recipients. final mintRecipients = LibSpark.createSparkMintRecipients( @@ -675,7 +712,15 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // generate: true // TODO is this needed? ); - // TODO finish. + // Add mint output(s). + for (final mint in mintRecipients) { + txb.addOutput( + mint.scriptPubKey, + mint.amount, + ); + } + + // TODO Sign the transaction. throw UnimplementedError(); }