diff --git a/lib/services/coins/bitcoin/bitcoin_wallet.dart b/lib/services/coins/bitcoin/bitcoin_wallet.dart index 8d1be5291..235422e2e 100644 --- a/lib/services/coins/bitcoin/bitcoin_wallet.dart +++ b/lib/services/coins/bitcoin/bitcoin_wallet.dart @@ -18,6 +18,7 @@ import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; @@ -2384,7 +2385,6 @@ class BitcoinWallet extends CoinServiceAPI .log("Attempting to send all $coin", level: LogLevel.Info); final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], @@ -2402,7 +2402,6 @@ class BitcoinWallet extends CoinServiceAPI final int amount = satoshiAmountToSend - feeForOneOutput; dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: [amount], @@ -2413,7 +2412,7 @@ class BitcoinWallet extends CoinServiceAPI "recipientAmt": amount, "fee": feeForOneOutput, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2421,7 +2420,6 @@ class BitcoinWallet extends CoinServiceAPI final int vSizeForOneOutput; try { vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], @@ -2434,7 +2432,6 @@ class BitcoinWallet extends CoinServiceAPI final int vSizeForTwoOutPuts; try { vSizeForTwoOutPuts = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [ recipientAddress, @@ -2505,7 +2502,6 @@ class BitcoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2533,7 +2529,6 @@ class BitcoinWallet extends CoinServiceAPI Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2546,7 +2541,7 @@ class BitcoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": feeBeingPaid, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -2563,7 +2558,6 @@ class BitcoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2574,7 +2568,7 @@ class BitcoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2593,7 +2587,6 @@ class BitcoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2604,7 +2597,7 @@ class BitcoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2623,7 +2616,6 @@ class BitcoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2634,7 +2626,7 @@ class BitcoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": feeForOneOutput, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -2660,241 +2652,129 @@ class BitcoinWallet extends CoinServiceAPI } } - Future> fetchBuildTxData( + Future> fetchBuildTxData( List utxosToUse, ) async { // return data - Map results = {}; - Map> addressTxid = {}; - - // addresses to check - List addressesP2PKH = []; - List addressesP2SH = []; - List addressesP2WPKH = []; + List signingData = []; try { // Populating the addresses to check for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - final tx = await _cachedElectrumXClient.getTransaction( - txHash: txid, - coin: coin, - ); - - for (final output in tx["vout"] as List) { - final n = output["n"]; - if (n != null && n == utxosToUse[i].vout) { - final address = output["scriptPubKey"]["address"] as String; - if (!addressTxid.containsKey(address)) { - addressTxid[address] = []; - } - (addressTxid[address] as List).add(txid); - switch (addressType(address: address)) { - case DerivePathType.bip44: - addressesP2PKH.add(address); - break; - case DerivePathType.bip49: - addressesP2SH.add(address); - break; - case DerivePathType.bip84: - addressesP2WPKH.add(address); - break; - default: - throw Exception("DerivePathType unsupported"); + if (utxosToUse[i].address == null) { + final txid = utxosToUse[i].txid; + final tx = await _cachedElectrumXClient.getTransaction( + txHash: txid, + coin: coin, + ); + for (final output in tx["vout"] as List) { + final n = output["n"]; + if (n != null && n == utxosToUse[i].vout) { + utxosToUse[i] = utxosToUse[i].copyWith( + address: output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]["address"] as String, + ); } } } + + final derivePathType = addressType(address: utxosToUse[i].address!); + + signingData.add( + SigningData( + derivePathType: derivePathType, + utxo: utxosToUse[i], + ), + ); } - // p2pkh / bip44 - final p2pkhLength = addressesP2PKH.length; - if (p2pkhLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip44, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip44, - ); - for (int i = 0; i < p2pkhLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2PKH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final data = P2PKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network, - ).data; + Map> receiveDerivations = {}; + Map> changeDerivations = {}; - for (String tx in addressTxid[addressesP2PKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, - ), - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2PKH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { - final data = P2PKH( + for (final sd in signingData) { + String? pubKey; + String? wif; + + // fetch receiving derivations if null + receiveDerivations[sd.derivePathType] ??= await _fetchDerivations( + chain: 0, + derivePathType: sd.derivePathType, + ); + final receiveDerivation = + receiveDerivations[sd.derivePathType]![sd.utxo.address!]; + + if (receiveDerivation != null) { + pubKey = receiveDerivation["pubKey"] as String; + wif = receiveDerivation["wif"] as String; + } else { + // fetch change derivations if null + changeDerivations[sd.derivePathType] ??= await _fetchDerivations( + chain: 1, + derivePathType: sd.derivePathType, + ); + final changeDerivation = + changeDerivations[sd.derivePathType]![sd.utxo.address!]; + if (changeDerivation != null) { + pubKey = changeDerivation["pubKey"] as String; + wif = changeDerivation["wif"] as String; + } + } + + if (wif != null && pubKey != null) { + final PaymentData data; + final Uint8List? redeemScript; + + switch (sd.derivePathType) { + case DerivePathType.bip44: + data = P2PKH( data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), + pubkey: Format.stringToUint8List(pubKey), + ), network: _network, ).data; + redeemScript = null; + break; - for (String tx in addressTxid[addressesP2PKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - }; - } - } - } - } - } - - // p2sh / bip49 - final p2shLength = addressesP2SH.length; - if (p2shLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip49, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip49, - ); - for (int i = 0; i < p2shLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2SH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final p2wpkh = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network) - .data; - - final redeemScript = p2wpkh.output; - - final data = - P2SH(data: PaymentData(redeem: p2wpkh), network: _network).data; - - for (String tx in addressTxid[addressesP2SH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, - ), - "redeemScript": redeemScript, - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2SH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { + case DerivePathType.bip49: final p2wpkh = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), - network: _network) - .data; - - final redeemScript = p2wpkh.output; - - final data = - P2SH(data: PaymentData(redeem: p2wpkh), network: _network) - .data; - - for (String tx in addressTxid[addressesP2SH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - "redeemScript": redeemScript, - }; - } - } - } - } - } - - // p2wpkh / bip84 - final p2wpkhLength = addressesP2WPKH.length; - if (p2wpkhLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip84, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip84, - ); - - for (int i = 0; i < p2wpkhLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2WPKH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final data = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network, - ).data; - - for (String tx in addressTxid[addressesP2WPKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, - ), - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2WPKH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { - final data = P2WPKH( data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), + pubkey: Format.stringToUint8List(pubKey), + ), network: _network, ).data; + redeemScript = p2wpkh.output; + data = P2SH( + data: PaymentData(redeem: p2wpkh), + network: _network, + ).data; + break; - for (String tx in addressTxid[addressesP2WPKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - }; - } - } + case DerivePathType.bip84: + data = P2WPKH( + data: PaymentData( + pubkey: Format.stringToUint8List(pubKey), + ), + network: _network, + ).data; + redeemScript = null; + break; + + default: + throw Exception("DerivePathType unsupported"); } + + final keyPair = ECPair.fromWIF( + wif, + network: _network, + ); + + sd.redeemScript = redeemScript; + sd.output = data.output; + sd.keyPair = keyPair; } } - return results; + return signingData; } catch (e, s) { Logging.instance .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); @@ -2904,8 +2784,7 @@ class BitcoinWallet extends CoinServiceAPI /// Builds and signs a transaction Future> buildTransaction({ - required List utxosToUse, - required Map utxoSigningData, + required List utxoSigningData, required List recipients, required List satoshiAmounts, }) async { @@ -2916,10 +2795,14 @@ class BitcoinWallet extends CoinServiceAPI txb.setVersion(1); // Add transaction inputs - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - txb.addInput(txid, utxosToUse[i].vout, null, - utxoSigningData[txid]["output"] as Uint8List); + for (var i = 0; i < utxoSigningData.length; i++) { + final txid = utxoSigningData[i].utxo.txid; + txb.addInput( + txid, + utxoSigningData[i].utxo.vout, + null, + utxoSigningData[i].output!, + ); } // Add transaction output @@ -2929,13 +2812,12 @@ class BitcoinWallet extends CoinServiceAPI try { // Sign the transaction accordingly - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; + for (var i = 0; i < utxoSigningData.length; i++) { txb.sign( vin: i, - keyPair: utxoSigningData[txid]["keyPair"] as ECPair, - witnessValue: utxosToUse[i].value, - redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, + keyPair: utxoSigningData[i].keyPair!, + witnessValue: utxoSigningData[i].utxo.value, + redeemScript: utxoSigningData[i].redeemScript, ); } } catch (e, s) { diff --git a/lib/services/coins/dogecoin/dogecoin_wallet.dart b/lib/services/coins/dogecoin/dogecoin_wallet.dart index 22c4db183..137b5a2be 100644 --- a/lib/services/coins/dogecoin/dogecoin_wallet.dart +++ b/lib/services/coins/dogecoin/dogecoin_wallet.dart @@ -18,6 +18,7 @@ import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; @@ -2152,7 +2153,6 @@ class DogecoinWallet extends CoinServiceAPI .log("Attempting to send all $coin", level: LogLevel.Info); final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], @@ -2167,7 +2167,6 @@ class DogecoinWallet extends CoinServiceAPI final int amount = satoshiAmountToSend - feeForOneOutput; dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: [amount], @@ -2178,19 +2177,17 @@ class DogecoinWallet extends CoinServiceAPI "recipientAmt": amount, "fee": feeForOneOutput, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], ))["vSize"] as int; final int vSizeForTwoOutPuts = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [ recipientAddress, @@ -2270,7 +2267,6 @@ class DogecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2298,7 +2294,6 @@ class DogecoinWallet extends CoinServiceAPI Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2311,7 +2306,7 @@ class DogecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": feeBeingPaid, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -2328,7 +2323,6 @@ class DogecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2339,7 +2333,7 @@ class DogecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2358,7 +2352,6 @@ class DogecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2369,7 +2362,7 @@ class DogecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2388,7 +2381,6 @@ class DogecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2399,7 +2391,7 @@ class DogecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": feeForOneOutput, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -2425,103 +2417,105 @@ class DogecoinWallet extends CoinServiceAPI } } - Future> fetchBuildTxData( + Future> fetchBuildTxData( List utxosToUse, ) async { // return data - Map results = {}; - Map> addressTxid = {}; - - // addresses to check - List addressesP2PKH = []; + List signingData = []; try { // Populating the addresses to check for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - final tx = await _cachedElectrumXClient.getTransaction( - txHash: txid, - coin: coin, - ); - - for (final output in tx["vout"] as List) { - final n = output["n"]; - if (n != null && n == utxosToUse[i].vout) { - final address = output["scriptPubKey"]["addresses"][0] as String; - if (!addressTxid.containsKey(address)) { - addressTxid[address] = []; - } - (addressTxid[address] as List).add(txid); - switch (addressType(address: address)) { - case DerivePathType.bip44: - addressesP2PKH.add(address); - break; - default: - throw Exception("Unsupported DerivePathType"); + if (utxosToUse[i].address == null) { + final txid = utxosToUse[i].txid; + final tx = await _cachedElectrumXClient.getTransaction( + txHash: txid, + coin: coin, + ); + for (final output in tx["vout"] as List) { + final n = output["n"]; + if (n != null && n == utxosToUse[i].vout) { + utxosToUse[i] = utxosToUse[i].copyWith( + address: output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]["address"] as String, + ); } } } + + final derivePathType = addressType(address: utxosToUse[i].address!); + + signingData.add( + SigningData( + derivePathType: derivePathType, + utxo: utxosToUse[i], + ), + ); } - // p2pkh / bip44 - final p2pkhLength = addressesP2PKH.length; - if (p2pkhLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip44, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip44, - ); - for (int i = 0; i < p2pkhLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2PKH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final data = P2PKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: network, - ).data; + Map> receiveDerivations = {}; + Map> changeDerivations = {}; - for (String tx in addressTxid[addressesP2PKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: network, - ), - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2PKH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { - final data = P2PKH( + for (final sd in signingData) { + String? pubKey; + String? wif; + + // fetch receiving derivations if null + receiveDerivations[sd.derivePathType] ??= await _fetchDerivations( + chain: 0, + derivePathType: sd.derivePathType, + ); + final receiveDerivation = + receiveDerivations[sd.derivePathType]![sd.utxo.address!]; + + if (receiveDerivation != null) { + pubKey = receiveDerivation["pubKey"] as String; + wif = receiveDerivation["wif"] as String; + } else { + // fetch change derivations if null + changeDerivations[sd.derivePathType] ??= await _fetchDerivations( + chain: 1, + derivePathType: sd.derivePathType, + ); + final changeDerivation = + changeDerivations[sd.derivePathType]![sd.utxo.address!]; + if (changeDerivation != null) { + pubKey = changeDerivation["pubKey"] as String; + wif = changeDerivation["wif"] as String; + } + } + + if (wif != null && pubKey != null) { + final PaymentData data; + final Uint8List? redeemScript; + + switch (sd.derivePathType) { + case DerivePathType.bip44: + data = P2PKH( data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), + pubkey: Format.stringToUint8List(pubKey), + ), network: network, ).data; + redeemScript = null; + break; - for (String tx in addressTxid[addressesP2PKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: network, - ), - }; - } - } + default: + throw Exception("DerivePathType unsupported"); } + + final keyPair = ECPair.fromWIF( + wif, + network: network, + ); + + sd.redeemScript = redeemScript; + sd.output = data.output; + sd.keyPair = keyPair; } } - return results; + return signingData; } catch (e, s) { Logging.instance .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); @@ -2531,8 +2525,7 @@ class DogecoinWallet extends CoinServiceAPI /// Builds and signs a transaction Future> buildTransaction({ - required List utxosToUse, - required Map utxoSigningData, + required List utxoSigningData, required List recipients, required List satoshiAmounts, }) async { @@ -2543,10 +2536,14 @@ class DogecoinWallet extends CoinServiceAPI txb.setVersion(1); // Add transaction inputs - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - txb.addInput(txid, utxosToUse[i].vout, null, - utxoSigningData[txid]["output"] as Uint8List); + for (var i = 0; i < utxoSigningData.length; i++) { + final txid = utxoSigningData[i].utxo.txid; + txb.addInput( + txid, + utxoSigningData[i].utxo.vout, + null, + utxoSigningData[i].output!, + ); } // Add transaction output @@ -2556,13 +2553,12 @@ class DogecoinWallet extends CoinServiceAPI try { // Sign the transaction accordingly - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; + for (var i = 0; i < utxoSigningData.length; i++) { txb.sign( vin: i, - keyPair: utxoSigningData[txid]["keyPair"] as ECPair, - witnessValue: utxosToUse[i].value, - redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, + keyPair: utxoSigningData[i].keyPair!, + witnessValue: utxoSigningData[i].utxo.value, + redeemScript: utxoSigningData[i].redeemScript, ); } } catch (e, s) { diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 1ea8ddf13..85560d88a 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -18,6 +18,7 @@ import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/lelantus_coin.dart'; import 'package:stackwallet/models/lelantus_fee_data.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; @@ -36,6 +37,7 @@ import 'package:stackwallet/utilities/bip32_utils.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/enums/derive_path_type_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/format.dart'; @@ -1367,7 +1369,6 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { .log("Attempting to send all $coin", level: LogLevel.Info); final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [_recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], @@ -1383,7 +1384,6 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { final int amount = satoshiAmountToSend - feeForOneOutput; dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: [amount], @@ -1399,13 +1399,11 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { } final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [_recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], ))["vSize"] as int; final int vSizeForTwoOutPuts = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [ _recipientAddress, @@ -1484,7 +1482,6 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { Logging.instance .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -1512,7 +1509,6 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -1541,7 +1537,6 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -1570,7 +1565,6 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -1599,7 +1593,6 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -1629,119 +1622,126 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { } } - Future> fetchBuildTxData( + Future> fetchBuildTxData( List utxosToUse, ) async { // return data - Map results = {}; - Map> addressTxid = {}; - - // addresses to check - List addresses = []; + List signingData = []; try { // Populating the addresses to check for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - final tx = await _cachedElectrumXClient.getTransaction( - txHash: txid, - coin: coin, + if (utxosToUse[i].address == null) { + final txid = utxosToUse[i].txid; + final tx = await _cachedElectrumXClient.getTransaction( + txHash: txid, + coin: coin, + ); + for (final output in tx["vout"] as List) { + final n = output["n"]; + if (n != null && n == utxosToUse[i].vout) { + utxosToUse[i] = utxosToUse[i].copyWith( + address: output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]["address"] as String, + ); + } + } + } + + signingData.add( + SigningData( + derivePathType: DerivePathType.bip44, + utxo: utxosToUse[i], + ), + ); + } + + Map> receiveDerivations = {}; + Map> changeDerivations = {}; + + for (final sd in signingData) { + String? pubKey; + String? wif; + + // fetch receiving derivations if null + receiveDerivations[sd.derivePathType] ??= Map.from( + jsonDecode((await _secureStore.read( + key: "${walletId}_receiveDerivations", + )) ?? + "{}") as Map, ); - for (final output in tx["vout"] as List) { - final n = output["n"]; - if (n != null && n == utxosToUse[i].vout) { - final address = output["scriptPubKey"]["addresses"][0] as String; - - if (!addressTxid.containsKey(address)) { - addressTxid[address] = []; - } - (addressTxid[address] as List).add(txid); - - addresses.add(address); + dynamic receiveDerivation; + for (int j = 0; + j < receiveDerivations[sd.derivePathType]!.length && + receiveDerivation == null; + j++) { + if (receiveDerivations[sd.derivePathType]!["$j"]["address"] == + sd.utxo.address!) { + receiveDerivation = receiveDerivations[sd.derivePathType]!["$j"]; } } - } - // p2pkh / bip44 - final addressesLength = addresses.length; - if (addressesLength > 0) { - final receiveDerivationsString = - await _secureStore.read(key: "${walletId}_receiveDerivations"); - final receiveDerivations = Map.from( - jsonDecode(receiveDerivationsString ?? "{}") as Map); + if (receiveDerivation != null) { + pubKey = receiveDerivation["publicKey"] as String; + wif = receiveDerivation["wif"] as String; + } else { + // fetch change derivations if null + changeDerivations[sd.derivePathType] ??= Map.from( + jsonDecode((await _secureStore.read( + key: "${walletId}_changeDerivations", + )) ?? + "{}") as Map, + ); - final changeDerivationsString = - await _secureStore.read(key: "${walletId}_changeDerivations"); - final changeDerivations = Map.from( - jsonDecode(changeDerivationsString ?? "{}") as Map); - - for (int i = 0; i < addressesLength; i++) { - // receives - - dynamic receiveDerivation; - - for (int j = 0; j < receiveDerivations.length; j++) { - if (receiveDerivations["$j"]["address"] == addresses[i]) { - receiveDerivation = receiveDerivations["$j"]; + dynamic changeDerivation; + for (int j = 0; + j < changeDerivations[sd.derivePathType]!.length && + changeDerivation == null; + j++) { + if (changeDerivations[sd.derivePathType]!["$j"]["address"] == + sd.utxo.address!) { + changeDerivation = changeDerivations[sd.derivePathType]!["$j"]; } } - // receiveDerivation = receiveDerivations[addresses[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final data = P2PKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["publicKey"] as String)), - network: _network, - ).data; + if (changeDerivation != null) { + pubKey = changeDerivation["publicKey"] as String; + wif = changeDerivation["wif"] as String; + } + } - for (String tx in addressTxid[addresses[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, - ), - }; - } - } else { - // if its not a receive, check change + if (wif != null && pubKey != null) { + final PaymentData data; + final Uint8List? redeemScript; - dynamic changeDerivation; - - for (int j = 0; j < changeDerivations.length; j++) { - if (changeDerivations["$j"]["address"] == addresses[i]) { - changeDerivation = changeDerivations["$j"]; - } - } - - // final changeDerivation = changeDerivations[addresses[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { - final data = P2PKH( + switch (sd.derivePathType) { + case DerivePathType.bip44: + data = P2PKH( data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["publicKey"] as String)), + pubkey: Format.stringToUint8List(pubKey), + ), network: _network, ).data; + redeemScript = null; + break; - for (String tx in addressTxid[addresses[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - }; - } - } + default: + throw Exception("DerivePathType unsupported"); } + + final keyPair = ECPair.fromWIF( + wif, + network: _network, + ); + + sd.redeemScript = redeemScript; + sd.output = data.output; + sd.keyPair = keyPair; } } - return results; + return signingData; } catch (e, s) { Logging.instance .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); @@ -1751,8 +1751,7 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { /// Builds and signs a transaction Future> buildTransaction({ - required List utxosToUse, - required Map utxoSigningData, + required List utxoSigningData, required List recipients, required List satoshiAmounts, }) async { @@ -1763,10 +1762,14 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { txb.setVersion(1); // Add transaction inputs - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - txb.addInput(txid, utxosToUse[i].vout, null, - utxoSigningData[txid]["output"] as Uint8List); + for (var i = 0; i < utxoSigningData.length; i++) { + final txid = utxoSigningData[i].utxo.txid; + txb.addInput( + txid, + utxoSigningData[i].utxo.vout, + null, + utxoSigningData[i].output!, + ); } // Add transaction output @@ -1776,13 +1779,12 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { try { // Sign the transaction accordingly - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; + for (var i = 0; i < utxoSigningData.length; i++) { txb.sign( vin: i, - keyPair: utxoSigningData[txid]["keyPair"] as ECPair, - witnessValue: utxosToUse[i].value, - redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, + keyPair: utxoSigningData[i].keyPair!, + witnessValue: utxoSigningData[i].utxo.value, + redeemScript: utxoSigningData[i].redeemScript, ); } } catch (e, s) { diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index 409c169c2..fa4c0e22d 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -17,6 +17,7 @@ import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; @@ -2338,7 +2339,6 @@ class LitecoinWallet extends CoinServiceAPI .log("Attempting to send all $coin", level: LogLevel.Info); final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], @@ -2356,7 +2356,6 @@ class LitecoinWallet extends CoinServiceAPI final int amount = satoshiAmountToSend - feeForOneOutput; dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: [amount], @@ -2367,19 +2366,17 @@ class LitecoinWallet extends CoinServiceAPI "recipientAmt": amount, "fee": feeForOneOutput, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], ))["vSize"] as int; final int vSizeForTwoOutPuts = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [ recipientAddress, @@ -2445,7 +2442,6 @@ class LitecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2473,7 +2469,6 @@ class LitecoinWallet extends CoinServiceAPI Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2486,7 +2481,7 @@ class LitecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": feeBeingPaid, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -2503,7 +2498,6 @@ class LitecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2514,7 +2508,7 @@ class LitecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2533,7 +2527,6 @@ class LitecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2544,7 +2537,7 @@ class LitecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2563,7 +2556,6 @@ class LitecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2574,7 +2566,7 @@ class LitecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": feeForOneOutput, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -2600,245 +2592,131 @@ class LitecoinWallet extends CoinServiceAPI } } - Future> fetchBuildTxData( + Future> fetchBuildTxData( List utxosToUse, ) async { // return data - Map results = {}; - Map> addressTxid = {}; - - // addresses to check - List addressesP2PKH = []; - List addressesP2SH = []; - List addressesP2WPKH = []; + List signingData = []; try { // Populating the addresses to check for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - final tx = await _cachedElectrumXClient.getTransaction( - txHash: txid, - coin: coin, - ); - - for (final output in tx["vout"] as List) { - final n = output["n"]; - if (n != null && n == utxosToUse[i].vout) { - final address = output["scriptPubKey"]["addresses"][0] as String; - if (!addressTxid.containsKey(address)) { - addressTxid[address] = []; - } - (addressTxid[address] as List).add(txid); - switch (addressType(address: address)) { - case DerivePathType.bip44: - addressesP2PKH.add(address); - break; - case DerivePathType.bip49: - addressesP2SH.add(address); - break; - case DerivePathType.bip84: - addressesP2WPKH.add(address); - break; - default: - throw Exception("DerivePathType unsupported"); + if (utxosToUse[i].address == null) { + final txid = utxosToUse[i].txid; + final tx = await _cachedElectrumXClient.getTransaction( + txHash: txid, + coin: coin, + ); + for (final output in tx["vout"] as List) { + final n = output["n"]; + if (n != null && n == utxosToUse[i].vout) { + utxosToUse[i] = utxosToUse[i].copyWith( + address: output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]["address"] as String, + ); } } } + + final derivePathType = addressType(address: utxosToUse[i].address!); + + signingData.add( + SigningData( + derivePathType: derivePathType, + utxo: utxosToUse[i], + ), + ); } - // p2pkh / bip44 - final p2pkhLength = addressesP2PKH.length; - if (p2pkhLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip44, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip44, - ); - for (int i = 0; i < p2pkhLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2PKH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final data = P2PKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network, - ).data; + Map> receiveDerivations = {}; + Map> changeDerivations = {}; - for (String tx in addressTxid[addressesP2PKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, - ), - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2PKH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { - final data = P2PKH( + for (final sd in signingData) { + String? pubKey; + String? wif; + + // fetch receiving derivations if null + receiveDerivations[sd.derivePathType] ??= await _fetchDerivations( + chain: 0, + derivePathType: sd.derivePathType, + ); + final receiveDerivation = + receiveDerivations[sd.derivePathType]![sd.utxo.address!]; + + if (receiveDerivation != null) { + pubKey = receiveDerivation["pubKey"] as String; + wif = receiveDerivation["wif"] as String; + } else { + // fetch change derivations if null + changeDerivations[sd.derivePathType] ??= await _fetchDerivations( + chain: 1, + derivePathType: sd.derivePathType, + ); + final changeDerivation = + changeDerivations[sd.derivePathType]![sd.utxo.address!]; + if (changeDerivation != null) { + pubKey = changeDerivation["pubKey"] as String; + wif = changeDerivation["wif"] as String; + } + } + + if (wif != null && pubKey != null) { + final PaymentData data; + final Uint8List? redeemScript; + + switch (sd.derivePathType) { + case DerivePathType.bip44: + data = P2PKH( data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), + pubkey: Format.stringToUint8List(pubKey), + ), network: _network, ).data; + redeemScript = null; + break; - for (String tx in addressTxid[addressesP2PKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - }; - } - } - } - } - } - - // p2sh / bip49 - final p2shLength = addressesP2SH.length; - if (p2shLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip49, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip49, - ); - for (int i = 0; i < p2shLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2SH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final p2wpkh = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network, - overridePrefix: _network.bech32!) - .data; - - final redeemScript = p2wpkh.output; - - final data = - P2SH(data: PaymentData(redeem: p2wpkh), network: _network).data; - - for (String tx in addressTxid[addressesP2SH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, - ), - "redeemScript": redeemScript, - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2SH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { + case DerivePathType.bip49: final p2wpkh = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), - network: _network, - overridePrefix: _network.bech32!) - .data; - - final redeemScript = p2wpkh.output; - - final data = - P2SH(data: PaymentData(redeem: p2wpkh), network: _network) - .data; - - for (String tx in addressTxid[addressesP2SH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - "redeemScript": redeemScript, - }; - } - } - } - } - } - - // p2wpkh / bip84 - final p2wpkhLength = addressesP2WPKH.length; - if (p2wpkhLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip84, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip84, - ); - - for (int i = 0; i < p2wpkhLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2WPKH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final data = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network, - overridePrefix: _network.bech32!) - .data; - - for (String tx in addressTxid[addressesP2WPKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, + data: PaymentData( + pubkey: Format.stringToUint8List(pubKey), ), - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2WPKH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { - final data = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), - network: _network, - overridePrefix: _network.bech32!) - .data; + network: _network, + overridePrefix: _network.bech32!, + ).data; + redeemScript = p2wpkh.output; + data = P2SH( + data: PaymentData(redeem: p2wpkh), + network: _network, + ).data; + break; - for (String tx in addressTxid[addressesP2WPKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - }; - } - } + case DerivePathType.bip84: + data = P2WPKH( + data: PaymentData( + pubkey: Format.stringToUint8List(pubKey), + ), + network: _network, + overridePrefix: _network.bech32!, + ).data; + redeemScript = null; + break; + + default: + throw Exception("DerivePathType unsupported"); } + + final keyPair = ECPair.fromWIF( + wif, + network: _network, + ); + + sd.redeemScript = redeemScript; + sd.output = data.output; + sd.keyPair = keyPair; } } - return results; + return signingData; } catch (e, s) { Logging.instance .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); @@ -2848,8 +2726,7 @@ class LitecoinWallet extends CoinServiceAPI /// Builds and signs a transaction Future> buildTransaction({ - required List utxosToUse, - required Map utxoSigningData, + required List utxoSigningData, required List recipients, required List satoshiAmounts, }) async { @@ -2860,10 +2737,15 @@ class LitecoinWallet extends CoinServiceAPI txb.setVersion(1); // Add transaction inputs - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - txb.addInput(txid, utxosToUse[i].vout, null, - utxoSigningData[txid]["output"] as Uint8List, _network.bech32!); + for (var i = 0; i < utxoSigningData.length; i++) { + final txid = utxoSigningData[i].utxo.txid; + txb.addInput( + txid, + utxoSigningData[i].utxo.vout, + null, + utxoSigningData[i].output!, + _network.bech32!, + ); } // Add transaction output @@ -2873,14 +2755,14 @@ class LitecoinWallet extends CoinServiceAPI try { // Sign the transaction accordingly - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; + for (var i = 0; i < utxoSigningData.length; i++) { txb.sign( - vin: i, - keyPair: utxoSigningData[txid]["keyPair"] as ECPair, - witnessValue: utxosToUse[i].value, - redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, - overridePrefix: _network.bech32!); + vin: i, + keyPair: utxoSigningData[i].keyPair!, + witnessValue: utxoSigningData[i].utxo.value, + redeemScript: utxoSigningData[i].redeemScript, + overridePrefix: _network.bech32!, + ); } } catch (e, s) { Logging.instance.log("Caught exception while signing transaction: $e\n$s", diff --git a/lib/services/coins/namecoin/namecoin_wallet.dart b/lib/services/coins/namecoin/namecoin_wallet.dart index 4e0bb99db..cc254ca31 100644 --- a/lib/services/coins/namecoin/namecoin_wallet.dart +++ b/lib/services/coins/namecoin/namecoin_wallet.dart @@ -17,6 +17,7 @@ import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; @@ -2331,7 +2332,6 @@ class NamecoinWallet extends CoinServiceAPI .log("Attempting to send all $coin", level: LogLevel.Info); final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], @@ -2349,7 +2349,6 @@ class NamecoinWallet extends CoinServiceAPI final int amount = satoshiAmountToSend - feeForOneOutput; dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: [amount], @@ -2360,19 +2359,17 @@ class NamecoinWallet extends CoinServiceAPI "recipientAmt": amount, "fee": feeForOneOutput, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], ))["vSize"] as int; final int vSizeForTwoOutPuts = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [ recipientAddress, @@ -2438,7 +2435,6 @@ class NamecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2466,7 +2462,6 @@ class NamecoinWallet extends CoinServiceAPI Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2479,7 +2474,7 @@ class NamecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": feeBeingPaid, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -2496,7 +2491,6 @@ class NamecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2507,7 +2501,7 @@ class NamecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2526,7 +2520,6 @@ class NamecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2537,7 +2530,7 @@ class NamecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2556,7 +2549,6 @@ class NamecoinWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2567,7 +2559,7 @@ class NamecoinWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": feeForOneOutput, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -2593,248 +2585,129 @@ class NamecoinWallet extends CoinServiceAPI } } - Future> fetchBuildTxData( + Future> fetchBuildTxData( List utxosToUse, ) async { // return data - Map results = {}; - Map> addressTxid = {}; - - // addresses to check - List addressesP2PKH = []; - List addressesP2SH = []; - List addressesP2WPKH = []; - Logging.instance.log("utxos: $utxosToUse", level: LogLevel.Info); + List signingData = []; try { // Populating the addresses to check for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - final tx = await _cachedElectrumXClient.getTransaction( - txHash: txid, - coin: coin, - ); - Logging.instance.log("tx: ${json.encode(tx)}", - level: LogLevel.Info, printFullLength: true); - - for (final output in tx["vout"] as List) { - final n = output["n"]; - if (n != null && n == utxosToUse[i].vout) { - final address = output["scriptPubKey"]["address"] as String; - if (!addressTxid.containsKey(address)) { - addressTxid[address] = []; - } - (addressTxid[address] as List).add(txid); - switch (addressType(address: address)) { - case DerivePathType.bip44: - addressesP2PKH.add(address); - break; - case DerivePathType.bip49: - addressesP2SH.add(address); - break; - case DerivePathType.bip84: - addressesP2WPKH.add(address); - break; - default: - throw Exception("DerivePathType unsupported"); + if (utxosToUse[i].address == null) { + final txid = utxosToUse[i].txid; + final tx = await _cachedElectrumXClient.getTransaction( + txHash: txid, + coin: coin, + ); + for (final output in tx["vout"] as List) { + final n = output["n"]; + if (n != null && n == utxosToUse[i].vout) { + utxosToUse[i] = utxosToUse[i].copyWith( + address: output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]["address"] as String, + ); } } } + + final derivePathType = addressType(address: utxosToUse[i].address!); + + signingData.add( + SigningData( + derivePathType: derivePathType, + utxo: utxosToUse[i], + ), + ); } - // p2pkh / bip44 - final p2pkhLength = addressesP2PKH.length; - if (p2pkhLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip44, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip44, - ); - for (int i = 0; i < p2pkhLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2PKH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final data = P2PKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network, - ).data; + Map> receiveDerivations = {}; + Map> changeDerivations = {}; - for (String tx in addressTxid[addressesP2PKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, - ), - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2PKH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { - final data = P2PKH( + for (final sd in signingData) { + String? pubKey; + String? wif; + + // fetch receiving derivations if null + receiveDerivations[sd.derivePathType] ??= await _fetchDerivations( + chain: 0, + derivePathType: sd.derivePathType, + ); + final receiveDerivation = + receiveDerivations[sd.derivePathType]![sd.utxo.address!]; + + if (receiveDerivation != null) { + pubKey = receiveDerivation["pubKey"] as String; + wif = receiveDerivation["wif"] as String; + } else { + // fetch change derivations if null + changeDerivations[sd.derivePathType] ??= await _fetchDerivations( + chain: 1, + derivePathType: sd.derivePathType, + ); + final changeDerivation = + changeDerivations[sd.derivePathType]![sd.utxo.address!]; + if (changeDerivation != null) { + pubKey = changeDerivation["pubKey"] as String; + wif = changeDerivation["wif"] as String; + } + } + + if (wif != null && pubKey != null) { + final PaymentData data; + final Uint8List? redeemScript; + + switch (sd.derivePathType) { + case DerivePathType.bip44: + data = P2PKH( data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), + pubkey: Format.stringToUint8List(pubKey), + ), network: _network, ).data; + redeemScript = null; + break; - for (String tx in addressTxid[addressesP2PKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - }; - } - } - } - } - } - - // p2sh / bip49 - final p2shLength = addressesP2SH.length; - if (p2shLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip49, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip49, - ); - for (int i = 0; i < p2shLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2SH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final p2wpkh = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network, - overridePrefix: namecoin.bech32!) - .data; - - final redeemScript = p2wpkh.output; - - final data = - P2SH(data: PaymentData(redeem: p2wpkh), network: _network).data; - - for (String tx in addressTxid[addressesP2SH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, - ), - "redeemScript": redeemScript, - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2SH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { + case DerivePathType.bip49: final p2wpkh = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), - network: _network, - overridePrefix: namecoin.bech32!) - .data; - - final redeemScript = p2wpkh.output; - - final data = - P2SH(data: PaymentData(redeem: p2wpkh), network: _network) - .data; - - for (String tx in addressTxid[addressesP2SH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - "redeemScript": redeemScript, - }; - } - } - } - } - } - - // p2wpkh / bip84 - final p2wpkhLength = addressesP2WPKH.length; - if (p2wpkhLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip84, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip84, - ); - - for (int i = 0; i < p2wpkhLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2WPKH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final data = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network, - overridePrefix: namecoin.bech32!) - .data; - - for (String tx in addressTxid[addressesP2WPKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, + data: PaymentData( + pubkey: Format.stringToUint8List(pubKey), ), - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2WPKH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { - final data = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), - network: _network, - overridePrefix: namecoin.bech32!) + network: _network, + overridePrefix: _network.bech32!, + ).data; + redeemScript = p2wpkh.output; + data = P2SH(data: PaymentData(redeem: p2wpkh), network: _network) .data; + break; - for (String tx in addressTxid[addressesP2WPKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - }; - } - } + case DerivePathType.bip84: + data = P2WPKH( + data: PaymentData( + pubkey: Format.stringToUint8List(pubKey), + ), + network: _network, + overridePrefix: _network.bech32!, + ).data; + redeemScript = null; + break; + + default: + throw Exception("DerivePathType unsupported"); } + + final keyPair = ECPair.fromWIF( + wif, + network: _network, + ); + + sd.redeemScript = redeemScript; + sd.output = data.output; + sd.keyPair = keyPair; } } - return results; + return signingData; } catch (e, s) { Logging.instance .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); @@ -2844,8 +2717,7 @@ class NamecoinWallet extends CoinServiceAPI /// Builds and signs a transaction Future> buildTransaction({ - required List utxosToUse, - required Map utxoSigningData, + required List utxoSigningData, required List recipients, required List satoshiAmounts, }) async { @@ -2856,10 +2728,15 @@ class NamecoinWallet extends CoinServiceAPI txb.setVersion(2); // Add transaction inputs - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - txb.addInput(txid, utxosToUse[i].vout, null, - utxoSigningData[txid]["output"] as Uint8List, namecoin.bech32!); + for (var i = 0; i < utxoSigningData.length; i++) { + final txid = utxoSigningData[i].utxo.txid; + txb.addInput( + txid, + utxoSigningData[i].utxo.vout, + null, + utxoSigningData[i].output!, + _network.bech32!, + ); } // Add transaction output @@ -2869,14 +2746,15 @@ class NamecoinWallet extends CoinServiceAPI try { // Sign the transaction accordingly - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - txb.sign( - vin: i, - keyPair: utxoSigningData[txid]["keyPair"] as ECPair, - witnessValue: utxosToUse[i].value, - redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, - overridePrefix: namecoin.bech32!); + for (var i = 0; i < utxoSigningData.length; i++) { + final txid = utxoSigningData[i].utxo.txid; + txb.addInput( + txid, + utxoSigningData[i].utxo.vout, + null, + utxoSigningData[i].output!, + _network.bech32!, + ); } } catch (e, s) { Logging.instance.log("Caught exception while signing transaction: $e\n$s", diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index b67771a30..560f58532 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -17,6 +17,7 @@ import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; @@ -2492,7 +2493,6 @@ class ParticlWallet extends CoinServiceAPI .log("Attempting to send all $coin", level: LogLevel.Info); final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], @@ -2510,7 +2510,6 @@ class ParticlWallet extends CoinServiceAPI final int amount = satoshiAmountToSend - feeForOneOutput; dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: [amount], @@ -2521,19 +2520,17 @@ class ParticlWallet extends CoinServiceAPI "recipientAmt": amount, "fee": feeForOneOutput, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } final int vSizeForOneOutput = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [recipientAddress], satoshiAmounts: [satoshisBeingUsed - 1], ))["vSize"] as int; final int vSizeForTwoOutPuts = (await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: [ recipientAddress, @@ -2599,7 +2596,6 @@ class ParticlWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2627,7 +2623,6 @@ class ParticlWallet extends CoinServiceAPI Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2640,7 +2635,7 @@ class ParticlWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": feeBeingPaid, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -2657,7 +2652,6 @@ class ParticlWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2668,7 +2662,7 @@ class ParticlWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2687,7 +2681,6 @@ class ParticlWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2698,7 +2691,7 @@ class ParticlWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": satoshisBeingUsed - satoshiAmountToSend, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } @@ -2717,7 +2710,6 @@ class ParticlWallet extends CoinServiceAPI Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); dynamic txn = await buildTransaction( - utxosToUse: utxoObjectsToUse, utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: recipientsAmtArray, @@ -2728,7 +2720,7 @@ class ParticlWallet extends CoinServiceAPI "recipientAmt": recipientsAmtArray[0], "fee": feeForOneOutput, "vSize": txn["vSize"], - "usedUTXOs": utxoObjectsToUse, + "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), }; return transactionObject; } else { @@ -2754,168 +2746,115 @@ class ParticlWallet extends CoinServiceAPI } } - Future> fetchBuildTxData( + Future> fetchBuildTxData( List utxosToUse, ) async { // return data - Map results = {}; - Map> addressTxid = {}; - - // addresses to check - List addressesP2PKH = []; - List addressesP2WPKH = []; + List signingData = []; try { // Populating the addresses to check for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - final tx = await _cachedElectrumXClient.getTransaction( - txHash: txid, - coin: coin, - ); - - for (final output in tx["vout"] as List) { - final n = output["n"]; - if (n != null && n == utxosToUse[i].vout) { - final address = output["scriptPubKey"]["addresses"][0] as String; - if (!addressTxid.containsKey(address)) { - addressTxid[address] = []; - } - (addressTxid[address] as List).add(txid); - switch (addressType(address: address)) { - case DerivePathType.bip44: - addressesP2PKH.add(address); - break; - case DerivePathType.bip84: - addressesP2WPKH.add(address); - break; - default: - throw Exception( - "DerivePathType ${addressType(address: address)} not supported"); + if (utxosToUse[i].address == null) { + final txid = utxosToUse[i].txid; + final tx = await _cachedElectrumXClient.getTransaction( + txHash: txid, + coin: coin, + ); + for (final output in tx["vout"] as List) { + final n = output["n"]; + if (n != null && n == utxosToUse[i].vout) { + utxosToUse[i] = utxosToUse[i].copyWith( + address: output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]["address"] as String, + ); } } } + + final derivePathType = addressType(address: utxosToUse[i].address!); + + signingData.add( + SigningData( + derivePathType: derivePathType, + utxo: utxosToUse[i], + ), + ); } - // p2pkh / bip44 - final p2pkhLength = addressesP2PKH.length; - if (p2pkhLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip44, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip44, - ); - for (int i = 0; i < p2pkhLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2PKH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final data = P2PKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network, - ).data; + Map> receiveDerivations = {}; + Map> changeDerivations = {}; - for (String tx in addressTxid[addressesP2PKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, - ), - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2PKH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { - final data = P2PKH( + for (final sd in signingData) { + String? pubKey; + String? wif; + + // fetch receiving derivations if null + receiveDerivations[sd.derivePathType] ??= await _fetchDerivations( + chain: 0, + derivePathType: sd.derivePathType, + ); + final receiveDerivation = + receiveDerivations[sd.derivePathType]![sd.utxo.address!]; + + if (receiveDerivation != null) { + pubKey = receiveDerivation["pubKey"] as String; + wif = receiveDerivation["wif"] as String; + } else { + // fetch change derivations if null + changeDerivations[sd.derivePathType] ??= await _fetchDerivations( + chain: 1, + derivePathType: sd.derivePathType, + ); + final changeDerivation = + changeDerivations[sd.derivePathType]![sd.utxo.address!]; + if (changeDerivation != null) { + pubKey = changeDerivation["pubKey"] as String; + wif = changeDerivation["wif"] as String; + } + } + + if (wif != null && pubKey != null) { + final PaymentData data; + final Uint8List? redeemScript; + + switch (sd.derivePathType) { + case DerivePathType.bip44: + data = P2PKH( data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), + pubkey: Format.stringToUint8List(pubKey), + ), network: _network, ).data; + redeemScript = null; + break; - for (String tx in addressTxid[addressesP2PKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - }; - } - } - } - } - } - - // p2wpkh / bip84 - final p2wpkhLength = addressesP2WPKH.length; - if (p2wpkhLength > 0) { - final receiveDerivations = await _fetchDerivations( - chain: 0, - derivePathType: DerivePathType.bip84, - ); - final changeDerivations = await _fetchDerivations( - chain: 1, - derivePathType: DerivePathType.bip84, - ); - - for (int i = 0; i < p2wpkhLength; i++) { - // receives - final receiveDerivation = receiveDerivations[addressesP2WPKH[i]]; - // if a match exists it will not be null - if (receiveDerivation != null) { - final data = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network, - ).data; - - for (String tx in addressTxid[addressesP2WPKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - receiveDerivation["wif"] as String, - network: _network, - ), - }; - } - } else { - // if its not a receive, check change - final changeDerivation = changeDerivations[addressesP2WPKH[i]]; - // if a match exists it will not be null - if (changeDerivation != null) { - final data = P2WPKH( + case DerivePathType.bip84: + data = P2WPKH( data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), + pubkey: Format.stringToUint8List(pubKey), + ), network: _network, ).data; + redeemScript = null; + break; - for (String tx in addressTxid[addressesP2WPKH[i]]!) { - results[tx] = { - "output": data.output, - "keyPair": ECPair.fromWIF( - changeDerivation["wif"] as String, - network: _network, - ), - }; - } - } + default: + throw Exception("DerivePathType unsupported"); } + + final keyPair = ECPair.fromWIF( + wif, + network: _network, + ); + + sd.redeemScript = redeemScript; + sd.output = data.output; + sd.keyPair = keyPair; } } - Logging.instance.log("FETCHED TX BUILD DATA IS -----$results", - level: LogLevel.Info, printFullLength: true); - return results; + + return signingData; } catch (e, s) { Logging.instance .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); @@ -2925,8 +2864,7 @@ class ParticlWallet extends CoinServiceAPI /// Builds and signs a transaction Future> buildTransaction({ - required List utxosToUse, - required Map utxoSigningData, + required List utxoSigningData, required List recipients, required List satoshiAmounts, }) async { @@ -2940,11 +2878,15 @@ class ParticlWallet extends CoinServiceAPI txb.setVersion(160); // Add transaction inputs - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; - - txb.addInput(txid, utxosToUse[i].vout, null, - utxoSigningData[txid]["output"] as Uint8List, ''); + for (var i = 0; i < utxoSigningData.length; i++) { + final txid = utxoSigningData[i].utxo.txid; + txb.addInput( + txid, + utxoSigningData[i].utxo.vout, + null, + utxoSigningData[i].output!, + '', + ); } // Add transaction output @@ -2954,13 +2896,13 @@ class ParticlWallet extends CoinServiceAPI try { // Sign the transaction accordingly - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].txid; + for (var i = 0; i < utxoSigningData.length; i++) { txb.sign( - vin: i, - keyPair: utxoSigningData[txid]["keyPair"] as ECPair, - witnessValue: utxosToUse[i].value, - redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?); + vin: i, + keyPair: utxoSigningData[i].keyPair!, + witnessValue: utxoSigningData[i].utxo.value, + redeemScript: utxoSigningData[i].redeemScript, + ); } } catch (e, s) { Logging.instance.log("Caught exception while signing transaction: $e\n$s",