fix bug that can cause transaction broadcast to fail in the case where two or more input utxos come from the same parent transaction

This commit is contained in:
julian 2023-03-09 13:49:39 -06:00
parent 260771061c
commit c04723840f
6 changed files with 676 additions and 1094 deletions

View file

@ -18,6 +18,7 @@ import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart';
import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; 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/paymint/fee_object_model.dart';
import 'package:stackwallet/models/signing_data.dart';
import 'package:stackwallet/services/coins/coin_service.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/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/refresh_percent_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); .log("Attempting to send all $coin", level: LogLevel.Info);
final int vSizeForOneOutput = (await buildTransaction( final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: [recipientAddress], recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1], satoshiAmounts: [satoshisBeingUsed - 1],
@ -2402,7 +2402,6 @@ class BitcoinWallet extends CoinServiceAPI
final int amount = satoshiAmountToSend - feeForOneOutput; final int amount = satoshiAmountToSend - feeForOneOutput;
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: [amount], satoshiAmounts: [amount],
@ -2413,7 +2412,7 @@ class BitcoinWallet extends CoinServiceAPI
"recipientAmt": amount, "recipientAmt": amount,
"fee": feeForOneOutput, "fee": feeForOneOutput,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} }
@ -2421,7 +2420,6 @@ class BitcoinWallet extends CoinServiceAPI
final int vSizeForOneOutput; final int vSizeForOneOutput;
try { try {
vSizeForOneOutput = (await buildTransaction( vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: [recipientAddress], recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1], satoshiAmounts: [satoshisBeingUsed - 1],
@ -2434,7 +2432,6 @@ class BitcoinWallet extends CoinServiceAPI
final int vSizeForTwoOutPuts; final int vSizeForTwoOutPuts;
try { try {
vSizeForTwoOutPuts = (await buildTransaction( vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: [ recipients: [
recipientAddress, recipientAddress,
@ -2505,7 +2502,6 @@ class BitcoinWallet extends CoinServiceAPI
Logging.instance Logging.instance
.log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2533,7 +2529,6 @@ class BitcoinWallet extends CoinServiceAPI
Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs',
level: LogLevel.Info); level: LogLevel.Info);
txn = await buildTransaction( txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2546,7 +2541,7 @@ class BitcoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0], "recipientAmt": recipientsAmtArray[0],
"fee": feeBeingPaid, "fee": feeBeingPaid,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} else { } else {
@ -2563,7 +2558,6 @@ class BitcoinWallet extends CoinServiceAPI
Logging.instance Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2574,7 +2568,7 @@ class BitcoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0], "recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend, "fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} }
@ -2593,7 +2587,6 @@ class BitcoinWallet extends CoinServiceAPI
Logging.instance Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2604,7 +2597,7 @@ class BitcoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0], "recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend, "fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} }
@ -2623,7 +2616,6 @@ class BitcoinWallet extends CoinServiceAPI
Logging.instance Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2634,7 +2626,7 @@ class BitcoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0], "recipientAmt": recipientsAmtArray[0],
"fee": feeForOneOutput, "fee": feeForOneOutput,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} else { } else {
@ -2660,241 +2652,129 @@ class BitcoinWallet extends CoinServiceAPI
} }
} }
Future<Map<String, dynamic>> fetchBuildTxData( Future<List<SigningData>> fetchBuildTxData(
List<isar_models.UTXO> utxosToUse, List<isar_models.UTXO> utxosToUse,
) async { ) async {
// return data // return data
Map<String, dynamic> results = {}; List<SigningData> signingData = [];
Map<String, List<String>> addressTxid = {};
// addresses to check
List<String> addressesP2PKH = [];
List<String> addressesP2SH = [];
List<String> addressesP2WPKH = [];
try { try {
// Populating the addresses to check // Populating the addresses to check
for (var i = 0; i < utxosToUse.length; i++) { for (var i = 0; i < utxosToUse.length; i++) {
if (utxosToUse[i].address == null) {
final txid = utxosToUse[i].txid; final txid = utxosToUse[i].txid;
final tx = await _cachedElectrumXClient.getTransaction( final tx = await _cachedElectrumXClient.getTransaction(
txHash: txid, txHash: txid,
coin: coin, coin: coin,
); );
for (final output in tx["vout"] as List) { for (final output in tx["vout"] as List) {
final n = output["n"]; final n = output["n"];
if (n != null && n == utxosToUse[i].vout) { if (n != null && n == utxosToUse[i].vout) {
final address = output["scriptPubKey"]["address"] as String; utxosToUse[i] = utxosToUse[i].copyWith(
if (!addressTxid.containsKey(address)) { address: output["scriptPubKey"]?["addresses"]?[0] as String? ??
addressTxid[address] = <String>[]; output["scriptPubKey"]["address"] as String,
);
} }
(addressTxid[address] as List).add(txid); }
switch (addressType(address: address)) { }
final derivePathType = addressType(address: utxosToUse[i].address!);
signingData.add(
SigningData(
derivePathType: derivePathType,
utxo: utxosToUse[i],
),
);
}
Map<DerivePathType, Map<String, dynamic>> receiveDerivations = {};
Map<DerivePathType, Map<String, dynamic>> changeDerivations = {};
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: case DerivePathType.bip44:
addressesP2PKH.add(address); data = P2PKH(
data: PaymentData(
pubkey: Format.stringToUint8List(pubKey),
),
network: _network,
).data;
redeemScript = null;
break; break;
case DerivePathType.bip49: case DerivePathType.bip49:
addressesP2SH.add(address); final p2wpkh = P2WPKH(
data: PaymentData(
pubkey: Format.stringToUint8List(pubKey),
),
network: _network,
).data;
redeemScript = p2wpkh.output;
data = P2SH(
data: PaymentData(redeem: p2wpkh),
network: _network,
).data;
break; break;
case DerivePathType.bip84: case DerivePathType.bip84:
addressesP2WPKH.add(address); data = P2WPKH(
data: PaymentData(
pubkey: Format.stringToUint8List(pubKey),
),
network: _network,
).data;
redeemScript = null;
break; break;
default: default:
throw Exception("DerivePathType unsupported"); throw Exception("DerivePathType unsupported");
} }
}
}
}
// p2pkh / bip44 final keyPair = ECPair.fromWIF(
final p2pkhLength = addressesP2PKH.length; wif,
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, network: _network,
).data;
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(
data: PaymentData(
pubkey: Format.stringToUint8List(
changeDerivation["pubKey"] as String)),
network: _network,
).data;
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) {
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++) { sd.redeemScript = redeemScript;
// receives sd.output = data.output;
final receiveDerivation = receiveDerivations[addressesP2WPKH[i]]; sd.keyPair = keyPair;
// 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)),
network: _network,
).data;
for (String tx in addressTxid[addressesP2WPKH[i]]!) {
results[tx] = {
"output": data.output,
"keyPair": ECPair.fromWIF(
changeDerivation["wif"] as String,
network: _network,
),
};
}
}
}
} }
} }
return results; return signingData;
} catch (e, s) { } catch (e, s) {
Logging.instance Logging.instance
.log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error);
@ -2904,8 +2784,7 @@ class BitcoinWallet extends CoinServiceAPI
/// Builds and signs a transaction /// Builds and signs a transaction
Future<Map<String, dynamic>> buildTransaction({ Future<Map<String, dynamic>> buildTransaction({
required List<isar_models.UTXO> utxosToUse, required List<SigningData> utxoSigningData,
required Map<String, dynamic> utxoSigningData,
required List<String> recipients, required List<String> recipients,
required List<int> satoshiAmounts, required List<int> satoshiAmounts,
}) async { }) async {
@ -2916,10 +2795,14 @@ class BitcoinWallet extends CoinServiceAPI
txb.setVersion(1); txb.setVersion(1);
// Add transaction inputs // Add transaction inputs
for (var i = 0; i < utxosToUse.length; i++) { for (var i = 0; i < utxoSigningData.length; i++) {
final txid = utxosToUse[i].txid; final txid = utxoSigningData[i].utxo.txid;
txb.addInput(txid, utxosToUse[i].vout, null, txb.addInput(
utxoSigningData[txid]["output"] as Uint8List); txid,
utxoSigningData[i].utxo.vout,
null,
utxoSigningData[i].output!,
);
} }
// Add transaction output // Add transaction output
@ -2929,13 +2812,12 @@ class BitcoinWallet extends CoinServiceAPI
try { try {
// Sign the transaction accordingly // Sign the transaction accordingly
for (var i = 0; i < utxosToUse.length; i++) { for (var i = 0; i < utxoSigningData.length; i++) {
final txid = utxosToUse[i].txid;
txb.sign( txb.sign(
vin: i, vin: i,
keyPair: utxoSigningData[txid]["keyPair"] as ECPair, keyPair: utxoSigningData[i].keyPair!,
witnessValue: utxosToUse[i].value, witnessValue: utxoSigningData[i].utxo.value,
redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, redeemScript: utxoSigningData[i].redeemScript,
); );
} }
} catch (e, s) { } catch (e, s) {

View file

@ -18,6 +18,7 @@ import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart';
import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; 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/paymint/fee_object_model.dart';
import 'package:stackwallet/models/signing_data.dart';
import 'package:stackwallet/services/coins/coin_service.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/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/refresh_percent_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); .log("Attempting to send all $coin", level: LogLevel.Info);
final int vSizeForOneOutput = (await buildTransaction( final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: [recipientAddress], recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1], satoshiAmounts: [satoshisBeingUsed - 1],
@ -2167,7 +2167,6 @@ class DogecoinWallet extends CoinServiceAPI
final int amount = satoshiAmountToSend - feeForOneOutput; final int amount = satoshiAmountToSend - feeForOneOutput;
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: [amount], satoshiAmounts: [amount],
@ -2178,19 +2177,17 @@ class DogecoinWallet extends CoinServiceAPI
"recipientAmt": amount, "recipientAmt": amount,
"fee": feeForOneOutput, "fee": feeForOneOutput,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} }
final int vSizeForOneOutput = (await buildTransaction( final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: [recipientAddress], recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1], satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int; ))["vSize"] as int;
final int vSizeForTwoOutPuts = (await buildTransaction( final int vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: [ recipients: [
recipientAddress, recipientAddress,
@ -2270,7 +2267,6 @@ class DogecoinWallet extends CoinServiceAPI
Logging.instance Logging.instance
.log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2298,7 +2294,6 @@ class DogecoinWallet extends CoinServiceAPI
Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs',
level: LogLevel.Info); level: LogLevel.Info);
txn = await buildTransaction( txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2311,7 +2306,7 @@ class DogecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0], "recipientAmt": recipientsAmtArray[0],
"fee": feeBeingPaid, "fee": feeBeingPaid,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} else { } else {
@ -2328,7 +2323,6 @@ class DogecoinWallet extends CoinServiceAPI
Logging.instance Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2339,7 +2333,7 @@ class DogecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0], "recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend, "fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} }
@ -2358,7 +2352,6 @@ class DogecoinWallet extends CoinServiceAPI
Logging.instance Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2369,7 +2362,7 @@ class DogecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0], "recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend, "fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} }
@ -2388,7 +2381,6 @@ class DogecoinWallet extends CoinServiceAPI
Logging.instance Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2399,7 +2391,7 @@ class DogecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0], "recipientAmt": recipientsAmtArray[0],
"fee": feeForOneOutput, "fee": feeForOneOutput,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} else { } else {
@ -2425,103 +2417,105 @@ class DogecoinWallet extends CoinServiceAPI
} }
} }
Future<Map<String, dynamic>> fetchBuildTxData( Future<List<SigningData>> fetchBuildTxData(
List<isar_models.UTXO> utxosToUse, List<isar_models.UTXO> utxosToUse,
) async { ) async {
// return data // return data
Map<String, dynamic> results = {}; List<SigningData> signingData = [];
Map<String, List<String>> addressTxid = {};
// addresses to check
List<String> addressesP2PKH = [];
try { try {
// Populating the addresses to check // Populating the addresses to check
for (var i = 0; i < utxosToUse.length; i++) { for (var i = 0; i < utxosToUse.length; i++) {
if (utxosToUse[i].address == null) {
final txid = utxosToUse[i].txid; final txid = utxosToUse[i].txid;
final tx = await _cachedElectrumXClient.getTransaction( final tx = await _cachedElectrumXClient.getTransaction(
txHash: txid, txHash: txid,
coin: coin, coin: coin,
); );
for (final output in tx["vout"] as List) { for (final output in tx["vout"] as List) {
final n = output["n"]; final n = output["n"];
if (n != null && n == utxosToUse[i].vout) { if (n != null && n == utxosToUse[i].vout) {
final address = output["scriptPubKey"]["addresses"][0] as String; utxosToUse[i] = utxosToUse[i].copyWith(
if (!addressTxid.containsKey(address)) { address: output["scriptPubKey"]?["addresses"]?[0] as String? ??
addressTxid[address] = <String>[]; output["scriptPubKey"]["address"] as String,
} );
(addressTxid[address] as List).add(txid);
switch (addressType(address: address)) {
case DerivePathType.bip44:
addressesP2PKH.add(address);
break;
default:
throw Exception("Unsupported DerivePathType");
}
} }
} }
} }
// p2pkh / bip44 final derivePathType = addressType(address: utxosToUse[i].address!);
final p2pkhLength = addressesP2PKH.length;
if (p2pkhLength > 0) { signingData.add(
final receiveDerivations = await _fetchDerivations( SigningData(
derivePathType: derivePathType,
utxo: utxosToUse[i],
),
);
}
Map<DerivePathType, Map<String, dynamic>> receiveDerivations = {};
Map<DerivePathType, Map<String, dynamic>> changeDerivations = {};
for (final sd in signingData) {
String? pubKey;
String? wif;
// fetch receiving derivations if null
receiveDerivations[sd.derivePathType] ??= await _fetchDerivations(
chain: 0, chain: 0,
derivePathType: DerivePathType.bip44, derivePathType: sd.derivePathType,
); );
final changeDerivations = await _fetchDerivations( final receiveDerivation =
chain: 1, receiveDerivations[sd.derivePathType]![sd.utxo.address!];
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) { if (receiveDerivation != null) {
final data = P2PKH( pubKey = receiveDerivation["pubKey"] as String;
data: PaymentData( wif = receiveDerivation["wif"] as String;
pubkey: Format.stringToUint8List(
receiveDerivation["pubKey"] as String)),
network: network,
).data;
for (String tx in addressTxid[addressesP2PKH[i]]!) {
results[tx] = {
"output": data.output,
"keyPair": ECPair.fromWIF(
receiveDerivation["wif"] as String,
network: network,
),
};
}
} else { } else {
// if its not a receive, check change // fetch change derivations if null
final changeDerivation = changeDerivations[addressesP2PKH[i]]; changeDerivations[sd.derivePathType] ??= await _fetchDerivations(
// if a match exists it will not be null chain: 1,
derivePathType: sd.derivePathType,
);
final changeDerivation =
changeDerivations[sd.derivePathType]![sd.utxo.address!];
if (changeDerivation != null) { if (changeDerivation != null) {
final data = P2PKH( 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( data: PaymentData(
pubkey: Format.stringToUint8List( pubkey: Format.stringToUint8List(pubKey),
changeDerivation["pubKey"] as String)), ),
network: network, network: network,
).data; ).data;
redeemScript = null;
break;
for (String tx in addressTxid[addressesP2PKH[i]]!) { default:
results[tx] = { throw Exception("DerivePathType unsupported");
"output": data.output, }
"keyPair": ECPair.fromWIF(
changeDerivation["wif"] as String, final keyPair = ECPair.fromWIF(
wif,
network: network, network: network,
), );
};
} sd.redeemScript = redeemScript;
} sd.output = data.output;
} sd.keyPair = keyPair;
} }
} }
return results; return signingData;
} catch (e, s) { } catch (e, s) {
Logging.instance Logging.instance
.log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error);
@ -2531,8 +2525,7 @@ class DogecoinWallet extends CoinServiceAPI
/// Builds and signs a transaction /// Builds and signs a transaction
Future<Map<String, dynamic>> buildTransaction({ Future<Map<String, dynamic>> buildTransaction({
required List<isar_models.UTXO> utxosToUse, required List<SigningData> utxoSigningData,
required Map<String, dynamic> utxoSigningData,
required List<String> recipients, required List<String> recipients,
required List<int> satoshiAmounts, required List<int> satoshiAmounts,
}) async { }) async {
@ -2543,10 +2536,14 @@ class DogecoinWallet extends CoinServiceAPI
txb.setVersion(1); txb.setVersion(1);
// Add transaction inputs // Add transaction inputs
for (var i = 0; i < utxosToUse.length; i++) { for (var i = 0; i < utxoSigningData.length; i++) {
final txid = utxosToUse[i].txid; final txid = utxoSigningData[i].utxo.txid;
txb.addInput(txid, utxosToUse[i].vout, null, txb.addInput(
utxoSigningData[txid]["output"] as Uint8List); txid,
utxoSigningData[i].utxo.vout,
null,
utxoSigningData[i].output!,
);
} }
// Add transaction output // Add transaction output
@ -2556,13 +2553,12 @@ class DogecoinWallet extends CoinServiceAPI
try { try {
// Sign the transaction accordingly // Sign the transaction accordingly
for (var i = 0; i < utxosToUse.length; i++) { for (var i = 0; i < utxoSigningData.length; i++) {
final txid = utxosToUse[i].txid;
txb.sign( txb.sign(
vin: i, vin: i,
keyPair: utxoSigningData[txid]["keyPair"] as ECPair, keyPair: utxoSigningData[i].keyPair!,
witnessValue: utxosToUse[i].value, witnessValue: utxoSigningData[i].utxo.value,
redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, redeemScript: utxoSigningData[i].redeemScript,
); );
} }
} catch (e, s) { } catch (e, s) {

View file

@ -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_coin.dart';
import 'package:stackwallet/models/lelantus_fee_data.dart'; import 'package:stackwallet/models/lelantus_fee_data.dart';
import 'package:stackwallet/models/paymint/fee_object_model.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/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/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/refresh_percent_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/constants.dart';
import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/default_nodes.dart';
import 'package:stackwallet/utilities/enums/coin_enum.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/enums/fee_rate_type_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/format.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); .log("Attempting to send all $coin", level: LogLevel.Info);
final int vSizeForOneOutput = (await buildTransaction( final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: [_recipientAddress], recipients: [_recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1], satoshiAmounts: [satoshisBeingUsed - 1],
@ -1383,7 +1384,6 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive {
final int amount = satoshiAmountToSend - feeForOneOutput; final int amount = satoshiAmountToSend - feeForOneOutput;
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: [amount], satoshiAmounts: [amount],
@ -1399,13 +1399,11 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive {
} }
final int vSizeForOneOutput = (await buildTransaction( final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: [_recipientAddress], recipients: [_recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1], satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int; ))["vSize"] as int;
final int vSizeForTwoOutPuts = (await buildTransaction( final int vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: [ recipients: [
_recipientAddress, _recipientAddress,
@ -1484,7 +1482,6 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive {
Logging.instance Logging.instance
.log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -1512,7 +1509,6 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive {
Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs',
level: LogLevel.Info); level: LogLevel.Info);
txn = await buildTransaction( txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -1541,7 +1537,6 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive {
Logging.instance Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -1570,7 +1565,6 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive {
Logging.instance Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -1599,7 +1593,6 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive {
Logging.instance Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -1629,119 +1622,126 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive {
} }
} }
Future<Map<String, dynamic>> fetchBuildTxData( Future<List<SigningData>> fetchBuildTxData(
List<isar_models.UTXO> utxosToUse, List<isar_models.UTXO> utxosToUse,
) async { ) async {
// return data // return data
Map<String, dynamic> results = {}; List<SigningData> signingData = [];
Map<String, List<String>> addressTxid = {};
// addresses to check
List<String> addresses = [];
try { try {
// Populating the addresses to check // Populating the addresses to check
for (var i = 0; i < utxosToUse.length; i++) { for (var i = 0; i < utxosToUse.length; i++) {
if (utxosToUse[i].address == null) {
final txid = utxosToUse[i].txid; final txid = utxosToUse[i].txid;
final tx = await _cachedElectrumXClient.getTransaction( final tx = await _cachedElectrumXClient.getTransaction(
txHash: txid, txHash: txid,
coin: coin, coin: coin,
); );
for (final output in tx["vout"] as List) { for (final output in tx["vout"] as List) {
final n = output["n"]; final n = output["n"];
if (n != null && n == utxosToUse[i].vout) { if (n != null && n == utxosToUse[i].vout) {
final address = output["scriptPubKey"]["addresses"][0] as String; utxosToUse[i] = utxosToUse[i].copyWith(
address: output["scriptPubKey"]?["addresses"]?[0] as String? ??
if (!addressTxid.containsKey(address)) { output["scriptPubKey"]["address"] as String,
addressTxid[address] = <String>[]; );
}
(addressTxid[address] as List).add(txid);
addresses.add(address);
} }
} }
} }
// p2pkh / bip44 signingData.add(
final addressesLength = addresses.length; SigningData(
if (addressesLength > 0) { derivePathType: DerivePathType.bip44,
final receiveDerivationsString = utxo: utxosToUse[i],
await _secureStore.read(key: "${walletId}_receiveDerivations"); ),
final receiveDerivations = Map<String, dynamic>.from( );
jsonDecode(receiveDerivationsString ?? "{}") as Map); }
final changeDerivationsString = Map<DerivePathType, Map<String, dynamic>> receiveDerivations = {};
await _secureStore.read(key: "${walletId}_changeDerivations"); Map<DerivePathType, Map<String, dynamic>> changeDerivations = {};
final changeDerivations = Map<String, dynamic>.from(
jsonDecode(changeDerivationsString ?? "{}") as Map);
for (int i = 0; i < addressesLength; i++) { for (final sd in signingData) {
// receives String? pubKey;
String? wif;
// fetch receiving derivations if null
receiveDerivations[sd.derivePathType] ??= Map<String, dynamic>.from(
jsonDecode((await _secureStore.read(
key: "${walletId}_receiveDerivations",
)) ??
"{}") as Map,
);
dynamic receiveDerivation; dynamic receiveDerivation;
for (int j = 0;
for (int j = 0; j < receiveDerivations.length; j++) { j < receiveDerivations[sd.derivePathType]!.length &&
if (receiveDerivations["$j"]["address"] == addresses[i]) { receiveDerivation == null;
receiveDerivation = receiveDerivations["$j"]; j++) {
if (receiveDerivations[sd.derivePathType]!["$j"]["address"] ==
sd.utxo.address!) {
receiveDerivation = receiveDerivations[sd.derivePathType]!["$j"];
} }
} }
// receiveDerivation = receiveDerivations[addresses[i]];
// if a match exists it will not be null
if (receiveDerivation != null) { if (receiveDerivation != null) {
final data = P2PKH( pubKey = receiveDerivation["publicKey"] as String;
data: PaymentData( wif = receiveDerivation["wif"] as String;
pubkey: Format.stringToUint8List(
receiveDerivation["publicKey"] as String)),
network: _network,
).data;
for (String tx in addressTxid[addresses[i]]!) {
results[tx] = {
"output": data.output,
"keyPair": ECPair.fromWIF(
receiveDerivation["wif"] as String,
network: _network,
),
};
}
} else { } else {
// if its not a receive, check change // fetch change derivations if null
changeDerivations[sd.derivePathType] ??= Map<String, dynamic>.from(
jsonDecode((await _secureStore.read(
key: "${walletId}_changeDerivations",
)) ??
"{}") as Map,
);
dynamic changeDerivation; dynamic changeDerivation;
for (int j = 0;
for (int j = 0; j < changeDerivations.length; j++) { j < changeDerivations[sd.derivePathType]!.length &&
if (changeDerivations["$j"]["address"] == addresses[i]) { changeDerivation == null;
changeDerivation = changeDerivations["$j"]; j++) {
if (changeDerivations[sd.derivePathType]!["$j"]["address"] ==
sd.utxo.address!) {
changeDerivation = changeDerivations[sd.derivePathType]!["$j"];
} }
} }
// final changeDerivation = changeDerivations[addresses[i]];
// if a match exists it will not be null
if (changeDerivation != null) { if (changeDerivation != null) {
final data = P2PKH( pubKey = changeDerivation["publicKey"] 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( data: PaymentData(
pubkey: Format.stringToUint8List( pubkey: Format.stringToUint8List(pubKey),
changeDerivation["publicKey"] as String)), ),
network: _network, network: _network,
).data; ).data;
redeemScript = null;
break;
for (String tx in addressTxid[addresses[i]]!) { default:
results[tx] = { throw Exception("DerivePathType unsupported");
"output": data.output, }
"keyPair": ECPair.fromWIF(
changeDerivation["wif"] as String, final keyPair = ECPair.fromWIF(
wif,
network: _network, network: _network,
), );
};
} sd.redeemScript = redeemScript;
} sd.output = data.output;
} sd.keyPair = keyPair;
} }
} }
return results; return signingData;
} catch (e, s) { } catch (e, s) {
Logging.instance Logging.instance
.log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); .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 /// Builds and signs a transaction
Future<Map<String, dynamic>> buildTransaction({ Future<Map<String, dynamic>> buildTransaction({
required List<isar_models.UTXO> utxosToUse, required List<SigningData> utxoSigningData,
required Map<String, dynamic> utxoSigningData,
required List<String> recipients, required List<String> recipients,
required List<int> satoshiAmounts, required List<int> satoshiAmounts,
}) async { }) async {
@ -1763,10 +1762,14 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive {
txb.setVersion(1); txb.setVersion(1);
// Add transaction inputs // Add transaction inputs
for (var i = 0; i < utxosToUse.length; i++) { for (var i = 0; i < utxoSigningData.length; i++) {
final txid = utxosToUse[i].txid; final txid = utxoSigningData[i].utxo.txid;
txb.addInput(txid, utxosToUse[i].vout, null, txb.addInput(
utxoSigningData[txid]["output"] as Uint8List); txid,
utxoSigningData[i].utxo.vout,
null,
utxoSigningData[i].output!,
);
} }
// Add transaction output // Add transaction output
@ -1776,13 +1779,12 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive {
try { try {
// Sign the transaction accordingly // Sign the transaction accordingly
for (var i = 0; i < utxosToUse.length; i++) { for (var i = 0; i < utxoSigningData.length; i++) {
final txid = utxosToUse[i].txid;
txb.sign( txb.sign(
vin: i, vin: i,
keyPair: utxoSigningData[txid]["keyPair"] as ECPair, keyPair: utxoSigningData[i].keyPair!,
witnessValue: utxosToUse[i].value, witnessValue: utxoSigningData[i].utxo.value,
redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, redeemScript: utxoSigningData[i].redeemScript,
); );
} }
} catch (e, s) { } catch (e, s) {

View file

@ -17,6 +17,7 @@ import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; 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/paymint/fee_object_model.dart';
import 'package:stackwallet/models/signing_data.dart';
import 'package:stackwallet/services/coins/coin_service.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/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/refresh_percent_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); .log("Attempting to send all $coin", level: LogLevel.Info);
final int vSizeForOneOutput = (await buildTransaction( final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: [recipientAddress], recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1], satoshiAmounts: [satoshisBeingUsed - 1],
@ -2356,7 +2356,6 @@ class LitecoinWallet extends CoinServiceAPI
final int amount = satoshiAmountToSend - feeForOneOutput; final int amount = satoshiAmountToSend - feeForOneOutput;
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: [amount], satoshiAmounts: [amount],
@ -2367,19 +2366,17 @@ class LitecoinWallet extends CoinServiceAPI
"recipientAmt": amount, "recipientAmt": amount,
"fee": feeForOneOutput, "fee": feeForOneOutput,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} }
final int vSizeForOneOutput = (await buildTransaction( final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: [recipientAddress], recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1], satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int; ))["vSize"] as int;
final int vSizeForTwoOutPuts = (await buildTransaction( final int vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: [ recipients: [
recipientAddress, recipientAddress,
@ -2445,7 +2442,6 @@ class LitecoinWallet extends CoinServiceAPI
Logging.instance Logging.instance
.log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2473,7 +2469,6 @@ class LitecoinWallet extends CoinServiceAPI
Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs',
level: LogLevel.Info); level: LogLevel.Info);
txn = await buildTransaction( txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2486,7 +2481,7 @@ class LitecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0], "recipientAmt": recipientsAmtArray[0],
"fee": feeBeingPaid, "fee": feeBeingPaid,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} else { } else {
@ -2503,7 +2498,6 @@ class LitecoinWallet extends CoinServiceAPI
Logging.instance Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2514,7 +2508,7 @@ class LitecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0], "recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend, "fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} }
@ -2533,7 +2527,6 @@ class LitecoinWallet extends CoinServiceAPI
Logging.instance Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2544,7 +2537,7 @@ class LitecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0], "recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend, "fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} }
@ -2563,7 +2556,6 @@ class LitecoinWallet extends CoinServiceAPI
Logging.instance Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2574,7 +2566,7 @@ class LitecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0], "recipientAmt": recipientsAmtArray[0],
"fee": feeForOneOutput, "fee": feeForOneOutput,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} else { } else {
@ -2600,245 +2592,131 @@ class LitecoinWallet extends CoinServiceAPI
} }
} }
Future<Map<String, dynamic>> fetchBuildTxData( Future<List<SigningData>> fetchBuildTxData(
List<isar_models.UTXO> utxosToUse, List<isar_models.UTXO> utxosToUse,
) async { ) async {
// return data // return data
Map<String, dynamic> results = {}; List<SigningData> signingData = [];
Map<String, List<String>> addressTxid = {};
// addresses to check
List<String> addressesP2PKH = [];
List<String> addressesP2SH = [];
List<String> addressesP2WPKH = [];
try { try {
// Populating the addresses to check // Populating the addresses to check
for (var i = 0; i < utxosToUse.length; i++) { for (var i = 0; i < utxosToUse.length; i++) {
if (utxosToUse[i].address == null) {
final txid = utxosToUse[i].txid; final txid = utxosToUse[i].txid;
final tx = await _cachedElectrumXClient.getTransaction( final tx = await _cachedElectrumXClient.getTransaction(
txHash: txid, txHash: txid,
coin: coin, coin: coin,
); );
for (final output in tx["vout"] as List) { for (final output in tx["vout"] as List) {
final n = output["n"]; final n = output["n"];
if (n != null && n == utxosToUse[i].vout) { if (n != null && n == utxosToUse[i].vout) {
final address = output["scriptPubKey"]["addresses"][0] as String; utxosToUse[i] = utxosToUse[i].copyWith(
if (!addressTxid.containsKey(address)) { address: output["scriptPubKey"]?["addresses"]?[0] as String? ??
addressTxid[address] = <String>[]; output["scriptPubKey"]["address"] as String,
);
} }
(addressTxid[address] as List).add(txid); }
switch (addressType(address: address)) { }
final derivePathType = addressType(address: utxosToUse[i].address!);
signingData.add(
SigningData(
derivePathType: derivePathType,
utxo: utxosToUse[i],
),
);
}
Map<DerivePathType, Map<String, dynamic>> receiveDerivations = {};
Map<DerivePathType, Map<String, dynamic>> changeDerivations = {};
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: case DerivePathType.bip44:
addressesP2PKH.add(address); data = P2PKH(
data: PaymentData(
pubkey: Format.stringToUint8List(pubKey),
),
network: _network,
).data;
redeemScript = null;
break; break;
case DerivePathType.bip49: case DerivePathType.bip49:
addressesP2SH.add(address); final p2wpkh = P2WPKH(
data: PaymentData(
pubkey: Format.stringToUint8List(pubKey),
),
network: _network,
overridePrefix: _network.bech32!,
).data;
redeemScript = p2wpkh.output;
data = P2SH(
data: PaymentData(redeem: p2wpkh),
network: _network,
).data;
break; break;
case DerivePathType.bip84: case DerivePathType.bip84:
addressesP2WPKH.add(address); data = P2WPKH(
data: PaymentData(
pubkey: Format.stringToUint8List(pubKey),
),
network: _network,
overridePrefix: _network.bech32!,
).data;
redeemScript = null;
break; break;
default: default:
throw Exception("DerivePathType unsupported"); throw Exception("DerivePathType unsupported");
} }
}
}
}
// p2pkh / bip44 final keyPair = ECPair.fromWIF(
final p2pkhLength = addressesP2PKH.length; wif,
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, network: _network,
).data;
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(
data: PaymentData(
pubkey: Format.stringToUint8List(
changeDerivation["pubKey"] as String)),
network: _network,
).data;
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) {
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++) { sd.redeemScript = redeemScript;
// receives sd.output = data.output;
final receiveDerivation = receiveDerivations[addressesP2WPKH[i]]; sd.keyPair = keyPair;
// 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,
),
};
}
} 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;
for (String tx in addressTxid[addressesP2WPKH[i]]!) {
results[tx] = {
"output": data.output,
"keyPair": ECPair.fromWIF(
changeDerivation["wif"] as String,
network: _network,
),
};
}
}
}
} }
} }
return results; return signingData;
} catch (e, s) { } catch (e, s) {
Logging.instance Logging.instance
.log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error);
@ -2848,8 +2726,7 @@ class LitecoinWallet extends CoinServiceAPI
/// Builds and signs a transaction /// Builds and signs a transaction
Future<Map<String, dynamic>> buildTransaction({ Future<Map<String, dynamic>> buildTransaction({
required List<isar_models.UTXO> utxosToUse, required List<SigningData> utxoSigningData,
required Map<String, dynamic> utxoSigningData,
required List<String> recipients, required List<String> recipients,
required List<int> satoshiAmounts, required List<int> satoshiAmounts,
}) async { }) async {
@ -2860,10 +2737,15 @@ class LitecoinWallet extends CoinServiceAPI
txb.setVersion(1); txb.setVersion(1);
// Add transaction inputs // Add transaction inputs
for (var i = 0; i < utxosToUse.length; i++) { for (var i = 0; i < utxoSigningData.length; i++) {
final txid = utxosToUse[i].txid; final txid = utxoSigningData[i].utxo.txid;
txb.addInput(txid, utxosToUse[i].vout, null, txb.addInput(
utxoSigningData[txid]["output"] as Uint8List, _network.bech32!); txid,
utxoSigningData[i].utxo.vout,
null,
utxoSigningData[i].output!,
_network.bech32!,
);
} }
// Add transaction output // Add transaction output
@ -2873,14 +2755,14 @@ class LitecoinWallet extends CoinServiceAPI
try { try {
// Sign the transaction accordingly // Sign the transaction accordingly
for (var i = 0; i < utxosToUse.length; i++) { for (var i = 0; i < utxoSigningData.length; i++) {
final txid = utxosToUse[i].txid;
txb.sign( txb.sign(
vin: i, vin: i,
keyPair: utxoSigningData[txid]["keyPair"] as ECPair, keyPair: utxoSigningData[i].keyPair!,
witnessValue: utxosToUse[i].value, witnessValue: utxoSigningData[i].utxo.value,
redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, redeemScript: utxoSigningData[i].redeemScript,
overridePrefix: _network.bech32!); overridePrefix: _network.bech32!,
);
} }
} catch (e, s) { } catch (e, s) {
Logging.instance.log("Caught exception while signing transaction: $e\n$s", Logging.instance.log("Caught exception while signing transaction: $e\n$s",

View file

@ -17,6 +17,7 @@ import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; 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/paymint/fee_object_model.dart';
import 'package:stackwallet/models/signing_data.dart';
import 'package:stackwallet/services/coins/coin_service.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/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/refresh_percent_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); .log("Attempting to send all $coin", level: LogLevel.Info);
final int vSizeForOneOutput = (await buildTransaction( final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: [recipientAddress], recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1], satoshiAmounts: [satoshisBeingUsed - 1],
@ -2349,7 +2349,6 @@ class NamecoinWallet extends CoinServiceAPI
final int amount = satoshiAmountToSend - feeForOneOutput; final int amount = satoshiAmountToSend - feeForOneOutput;
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: [amount], satoshiAmounts: [amount],
@ -2360,19 +2359,17 @@ class NamecoinWallet extends CoinServiceAPI
"recipientAmt": amount, "recipientAmt": amount,
"fee": feeForOneOutput, "fee": feeForOneOutput,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} }
final int vSizeForOneOutput = (await buildTransaction( final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: [recipientAddress], recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1], satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int; ))["vSize"] as int;
final int vSizeForTwoOutPuts = (await buildTransaction( final int vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: [ recipients: [
recipientAddress, recipientAddress,
@ -2438,7 +2435,6 @@ class NamecoinWallet extends CoinServiceAPI
Logging.instance Logging.instance
.log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2466,7 +2462,6 @@ class NamecoinWallet extends CoinServiceAPI
Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs',
level: LogLevel.Info); level: LogLevel.Info);
txn = await buildTransaction( txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2479,7 +2474,7 @@ class NamecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0], "recipientAmt": recipientsAmtArray[0],
"fee": feeBeingPaid, "fee": feeBeingPaid,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} else { } else {
@ -2496,7 +2491,6 @@ class NamecoinWallet extends CoinServiceAPI
Logging.instance Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2507,7 +2501,7 @@ class NamecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0], "recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend, "fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} }
@ -2526,7 +2520,6 @@ class NamecoinWallet extends CoinServiceAPI
Logging.instance Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2537,7 +2530,7 @@ class NamecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0], "recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend, "fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} }
@ -2556,7 +2549,6 @@ class NamecoinWallet extends CoinServiceAPI
Logging.instance Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2567,7 +2559,7 @@ class NamecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0], "recipientAmt": recipientsAmtArray[0],
"fee": feeForOneOutput, "fee": feeForOneOutput,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} else { } else {
@ -2593,248 +2585,129 @@ class NamecoinWallet extends CoinServiceAPI
} }
} }
Future<Map<String, dynamic>> fetchBuildTxData( Future<List<SigningData>> fetchBuildTxData(
List<isar_models.UTXO> utxosToUse, List<isar_models.UTXO> utxosToUse,
) async { ) async {
// return data // return data
Map<String, dynamic> results = {}; List<SigningData> signingData = [];
Map<String, List<String>> addressTxid = {};
// addresses to check
List<String> addressesP2PKH = [];
List<String> addressesP2SH = [];
List<String> addressesP2WPKH = [];
Logging.instance.log("utxos: $utxosToUse", level: LogLevel.Info);
try { try {
// Populating the addresses to check // Populating the addresses to check
for (var i = 0; i < utxosToUse.length; i++) { for (var i = 0; i < utxosToUse.length; i++) {
if (utxosToUse[i].address == null) {
final txid = utxosToUse[i].txid; final txid = utxosToUse[i].txid;
final tx = await _cachedElectrumXClient.getTransaction( final tx = await _cachedElectrumXClient.getTransaction(
txHash: txid, txHash: txid,
coin: coin, coin: coin,
); );
Logging.instance.log("tx: ${json.encode(tx)}",
level: LogLevel.Info, printFullLength: true);
for (final output in tx["vout"] as List) { for (final output in tx["vout"] as List) {
final n = output["n"]; final n = output["n"];
if (n != null && n == utxosToUse[i].vout) { if (n != null && n == utxosToUse[i].vout) {
final address = output["scriptPubKey"]["address"] as String; utxosToUse[i] = utxosToUse[i].copyWith(
if (!addressTxid.containsKey(address)) { address: output["scriptPubKey"]?["addresses"]?[0] as String? ??
addressTxid[address] = <String>[]; output["scriptPubKey"]["address"] as String,
);
} }
(addressTxid[address] as List).add(txid); }
switch (addressType(address: address)) { }
final derivePathType = addressType(address: utxosToUse[i].address!);
signingData.add(
SigningData(
derivePathType: derivePathType,
utxo: utxosToUse[i],
),
);
}
Map<DerivePathType, Map<String, dynamic>> receiveDerivations = {};
Map<DerivePathType, Map<String, dynamic>> changeDerivations = {};
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: case DerivePathType.bip44:
addressesP2PKH.add(address); data = P2PKH(
data: PaymentData(
pubkey: Format.stringToUint8List(pubKey),
),
network: _network,
).data;
redeemScript = null;
break; break;
case DerivePathType.bip49: case DerivePathType.bip49:
addressesP2SH.add(address); final p2wpkh = P2WPKH(
data: PaymentData(
pubkey: Format.stringToUint8List(pubKey),
),
network: _network,
overridePrefix: _network.bech32!,
).data;
redeemScript = p2wpkh.output;
data = P2SH(data: PaymentData(redeem: p2wpkh), network: _network)
.data;
break; break;
case DerivePathType.bip84: case DerivePathType.bip84:
addressesP2WPKH.add(address); data = P2WPKH(
data: PaymentData(
pubkey: Format.stringToUint8List(pubKey),
),
network: _network,
overridePrefix: _network.bech32!,
).data;
redeemScript = null;
break; break;
default: default:
throw Exception("DerivePathType unsupported"); throw Exception("DerivePathType unsupported");
} }
}
}
}
// p2pkh / bip44 final keyPair = ECPair.fromWIF(
final p2pkhLength = addressesP2PKH.length; wif,
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, network: _network,
).data;
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(
data: PaymentData(
pubkey: Format.stringToUint8List(
changeDerivation["pubKey"] as String)),
network: _network,
).data;
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) {
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++) { sd.redeemScript = redeemScript;
// receives sd.output = data.output;
final receiveDerivation = receiveDerivations[addressesP2WPKH[i]]; sd.keyPair = keyPair;
// 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,
),
};
}
} 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!)
.data;
for (String tx in addressTxid[addressesP2WPKH[i]]!) {
results[tx] = {
"output": data.output,
"keyPair": ECPair.fromWIF(
changeDerivation["wif"] as String,
network: _network,
),
};
}
}
}
} }
} }
return results; return signingData;
} catch (e, s) { } catch (e, s) {
Logging.instance Logging.instance
.log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error);
@ -2844,8 +2717,7 @@ class NamecoinWallet extends CoinServiceAPI
/// Builds and signs a transaction /// Builds and signs a transaction
Future<Map<String, dynamic>> buildTransaction({ Future<Map<String, dynamic>> buildTransaction({
required List<isar_models.UTXO> utxosToUse, required List<SigningData> utxoSigningData,
required Map<String, dynamic> utxoSigningData,
required List<String> recipients, required List<String> recipients,
required List<int> satoshiAmounts, required List<int> satoshiAmounts,
}) async { }) async {
@ -2856,10 +2728,15 @@ class NamecoinWallet extends CoinServiceAPI
txb.setVersion(2); txb.setVersion(2);
// Add transaction inputs // Add transaction inputs
for (var i = 0; i < utxosToUse.length; i++) { for (var i = 0; i < utxoSigningData.length; i++) {
final txid = utxosToUse[i].txid; final txid = utxoSigningData[i].utxo.txid;
txb.addInput(txid, utxosToUse[i].vout, null, txb.addInput(
utxoSigningData[txid]["output"] as Uint8List, namecoin.bech32!); txid,
utxoSigningData[i].utxo.vout,
null,
utxoSigningData[i].output!,
_network.bech32!,
);
} }
// Add transaction output // Add transaction output
@ -2869,14 +2746,15 @@ class NamecoinWallet extends CoinServiceAPI
try { try {
// Sign the transaction accordingly // Sign the transaction accordingly
for (var i = 0; i < utxosToUse.length; i++) { for (var i = 0; i < utxoSigningData.length; i++) {
final txid = utxosToUse[i].txid; final txid = utxoSigningData[i].utxo.txid;
txb.sign( txb.addInput(
vin: i, txid,
keyPair: utxoSigningData[txid]["keyPair"] as ECPair, utxoSigningData[i].utxo.vout,
witnessValue: utxosToUse[i].value, null,
redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, utxoSigningData[i].output!,
overridePrefix: namecoin.bech32!); _network.bech32!,
);
} }
} catch (e, s) { } catch (e, s) {
Logging.instance.log("Caught exception while signing transaction: $e\n$s", Logging.instance.log("Caught exception while signing transaction: $e\n$s",

View file

@ -17,6 +17,7 @@ import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; 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/paymint/fee_object_model.dart';
import 'package:stackwallet/models/signing_data.dart';
import 'package:stackwallet/services/coins/coin_service.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/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/refresh_percent_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); .log("Attempting to send all $coin", level: LogLevel.Info);
final int vSizeForOneOutput = (await buildTransaction( final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: [recipientAddress], recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1], satoshiAmounts: [satoshisBeingUsed - 1],
@ -2510,7 +2510,6 @@ class ParticlWallet extends CoinServiceAPI
final int amount = satoshiAmountToSend - feeForOneOutput; final int amount = satoshiAmountToSend - feeForOneOutput;
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: [amount], satoshiAmounts: [amount],
@ -2521,19 +2520,17 @@ class ParticlWallet extends CoinServiceAPI
"recipientAmt": amount, "recipientAmt": amount,
"fee": feeForOneOutput, "fee": feeForOneOutput,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} }
final int vSizeForOneOutput = (await buildTransaction( final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: [recipientAddress], recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1], satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int; ))["vSize"] as int;
final int vSizeForTwoOutPuts = (await buildTransaction( final int vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: [ recipients: [
recipientAddress, recipientAddress,
@ -2599,7 +2596,6 @@ class ParticlWallet extends CoinServiceAPI
Logging.instance Logging.instance
.log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2627,7 +2623,6 @@ class ParticlWallet extends CoinServiceAPI
Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs',
level: LogLevel.Info); level: LogLevel.Info);
txn = await buildTransaction( txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2640,7 +2635,7 @@ class ParticlWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0], "recipientAmt": recipientsAmtArray[0],
"fee": feeBeingPaid, "fee": feeBeingPaid,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} else { } else {
@ -2657,7 +2652,6 @@ class ParticlWallet extends CoinServiceAPI
Logging.instance Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2668,7 +2662,7 @@ class ParticlWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0], "recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend, "fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} }
@ -2687,7 +2681,6 @@ class ParticlWallet extends CoinServiceAPI
Logging.instance Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2698,7 +2691,7 @@ class ParticlWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0], "recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend, "fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} }
@ -2717,7 +2710,6 @@ class ParticlWallet extends CoinServiceAPI
Logging.instance Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
dynamic txn = await buildTransaction( dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData, utxoSigningData: utxoSigningData,
recipients: recipientsArray, recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray, satoshiAmounts: recipientsAmtArray,
@ -2728,7 +2720,7 @@ class ParticlWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0], "recipientAmt": recipientsAmtArray[0],
"fee": feeForOneOutput, "fee": feeForOneOutput,
"vSize": txn["vSize"], "vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse, "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(),
}; };
return transactionObject; return transactionObject;
} else { } else {
@ -2754,168 +2746,115 @@ class ParticlWallet extends CoinServiceAPI
} }
} }
Future<Map<String, dynamic>> fetchBuildTxData( Future<List<SigningData>> fetchBuildTxData(
List<isar_models.UTXO> utxosToUse, List<isar_models.UTXO> utxosToUse,
) async { ) async {
// return data // return data
Map<String, dynamic> results = {}; List<SigningData> signingData = [];
Map<String, List<String>> addressTxid = {};
// addresses to check
List<String> addressesP2PKH = [];
List<String> addressesP2WPKH = [];
try { try {
// Populating the addresses to check // Populating the addresses to check
for (var i = 0; i < utxosToUse.length; i++) { for (var i = 0; i < utxosToUse.length; i++) {
if (utxosToUse[i].address == null) {
final txid = utxosToUse[i].txid; final txid = utxosToUse[i].txid;
final tx = await _cachedElectrumXClient.getTransaction( final tx = await _cachedElectrumXClient.getTransaction(
txHash: txid, txHash: txid,
coin: coin, coin: coin,
); );
for (final output in tx["vout"] as List) { for (final output in tx["vout"] as List) {
final n = output["n"]; final n = output["n"];
if (n != null && n == utxosToUse[i].vout) { if (n != null && n == utxosToUse[i].vout) {
final address = output["scriptPubKey"]["addresses"][0] as String; utxosToUse[i] = utxosToUse[i].copyWith(
if (!addressTxid.containsKey(address)) { address: output["scriptPubKey"]?["addresses"]?[0] as String? ??
addressTxid[address] = <String>[]; output["scriptPubKey"]["address"] as String,
);
} }
(addressTxid[address] as List).add(txid); }
switch (addressType(address: address)) { }
final derivePathType = addressType(address: utxosToUse[i].address!);
signingData.add(
SigningData(
derivePathType: derivePathType,
utxo: utxosToUse[i],
),
);
}
Map<DerivePathType, Map<String, dynamic>> receiveDerivations = {};
Map<DerivePathType, Map<String, dynamic>> changeDerivations = {};
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: case DerivePathType.bip44:
addressesP2PKH.add(address); data = P2PKH(
data: PaymentData(
pubkey: Format.stringToUint8List(pubKey),
),
network: _network,
).data;
redeemScript = null;
break; break;
case DerivePathType.bip84: case DerivePathType.bip84:
addressesP2WPKH.add(address); data = P2WPKH(
data: PaymentData(
pubkey: Format.stringToUint8List(pubKey),
),
network: _network,
).data;
redeemScript = null;
break; break;
default: default:
throw Exception( throw Exception("DerivePathType unsupported");
"DerivePathType ${addressType(address: address)} not supported");
}
}
}
} }
// p2pkh / bip44 final keyPair = ECPair.fromWIF(
final p2pkhLength = addressesP2PKH.length; wif,
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, network: _network,
).data;
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(
data: PaymentData(
pubkey: Format.stringToUint8List(
changeDerivation["pubKey"] as String)),
network: _network,
).data;
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++) { sd.redeemScript = redeemScript;
// receives sd.output = data.output;
final receiveDerivation = receiveDerivations[addressesP2WPKH[i]]; sd.keyPair = keyPair;
// 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]]!) { return signingData;
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)),
network: _network,
).data;
for (String tx in addressTxid[addressesP2WPKH[i]]!) {
results[tx] = {
"output": data.output,
"keyPair": ECPair.fromWIF(
changeDerivation["wif"] as String,
network: _network,
),
};
}
}
}
}
}
Logging.instance.log("FETCHED TX BUILD DATA IS -----$results",
level: LogLevel.Info, printFullLength: true);
return results;
} catch (e, s) { } catch (e, s) {
Logging.instance Logging.instance
.log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error);
@ -2925,8 +2864,7 @@ class ParticlWallet extends CoinServiceAPI
/// Builds and signs a transaction /// Builds and signs a transaction
Future<Map<String, dynamic>> buildTransaction({ Future<Map<String, dynamic>> buildTransaction({
required List<isar_models.UTXO> utxosToUse, required List<SigningData> utxoSigningData,
required Map<String, dynamic> utxoSigningData,
required List<String> recipients, required List<String> recipients,
required List<int> satoshiAmounts, required List<int> satoshiAmounts,
}) async { }) async {
@ -2940,11 +2878,15 @@ class ParticlWallet extends CoinServiceAPI
txb.setVersion(160); txb.setVersion(160);
// Add transaction inputs // Add transaction inputs
for (var i = 0; i < utxosToUse.length; i++) { for (var i = 0; i < utxoSigningData.length; i++) {
final txid = utxosToUse[i].txid; final txid = utxoSigningData[i].utxo.txid;
txb.addInput(
txb.addInput(txid, utxosToUse[i].vout, null, txid,
utxoSigningData[txid]["output"] as Uint8List, ''); utxoSigningData[i].utxo.vout,
null,
utxoSigningData[i].output!,
'',
);
} }
// Add transaction output // Add transaction output
@ -2954,13 +2896,13 @@ class ParticlWallet extends CoinServiceAPI
try { try {
// Sign the transaction accordingly // Sign the transaction accordingly
for (var i = 0; i < utxosToUse.length; i++) { for (var i = 0; i < utxoSigningData.length; i++) {
final txid = utxosToUse[i].txid;
txb.sign( txb.sign(
vin: i, vin: i,
keyPair: utxoSigningData[txid]["keyPair"] as ECPair, keyPair: utxoSigningData[i].keyPair!,
witnessValue: utxosToUse[i].value, witnessValue: utxoSigningData[i].utxo.value,
redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?); redeemScript: utxoSigningData[i].redeemScript,
);
} }
} catch (e, s) { } catch (e, s) {
Logging.instance.log("Caught exception while signing transaction: $e\n$s", Logging.instance.log("Caught exception while signing transaction: $e\n$s",