Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
261 changes: 216 additions & 45 deletions lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,30 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
Logging.instance.d("spendableSatoshiValue: $spendableSatoshiValue");
Logging.instance.d("satoshiAmountToSend: $satoshiAmountToSend");

// Use coinlib CoinSelection algorithms except for
// "coinControl", "SendAll", "MWEB", "overrideFeeAmount",
// because they do not need a selection or
// do not meet the requirements for the algorithms
final bool useOptimalSelection = !coinControl &&
!isSendAll &&
!isSendAllCoinControlUtxos &&
overrideFeeAmount == null &&
txData.type != TxType.mweb &&
txData.type != TxType.mwebPegOut &&
txData.type != TxType.mwebPegIn;

if (useOptimalSelection) {
return await _optimalCoinSelection(
txData: txData,
spendableOutputs: spendableOutputs.whereType<StandardInput>().toList(),
recipientAddress: recipientAddress,
satoshiAmountToSend: satoshiAmountToSend,
satsPerVByte: satsPerVByte,
feeRatePerKB: selectedTxFeeRate,
changeAddress: await changeAddress(),
);
}

BigInt satoshisBeingUsed = BigInt.zero;
int inputsBeingConsumed = 0;
final List<BaseInput> utxoObjectsToUse = [];
Expand Down Expand Up @@ -571,6 +595,197 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
);
}

coinlib.Input standardInputToCoinlibInput(
StandardInput input, {
int sequence = 0xffffffff,
}) {
final hash = Uint8List.fromList(
input.utxo.txid.toUint8ListFromHex.reversed.toList(),
);
final prevOut = coinlib.OutPoint(hash, input.utxo.vout);

switch (input.derivePathType) {
case DerivePathType.bip44:
case DerivePathType.bch44:
return coinlib.P2PKHInput(
prevOut: prevOut,
publicKey: input.key!.publicKey,
sequence: sequence,
);

// TODO: fix this as it is (probably) wrong!
case DerivePathType.bip49:
throw Exception("TODO p2sh");
// return coinlib.P2SHMultisigInput(
// prevOut: prevOut,
// program: coinlib.MultisigProgram.decompile(
// input.redeemScript!,
// ),
// sequence: sequence,
// );

case DerivePathType.bip84:
return coinlib.P2WPKHInput(
prevOut: prevOut,
publicKey: input.key!.publicKey,
sequence: sequence,
);

case DerivePathType.bip86:
return coinlib.TaprootKeyInput(prevOut: prevOut);

default:
throw UnsupportedError(
"Unknown derivation path type found: ${input.derivePathType}",
);
}
}

/// Helper that will convert BaseInput into InputCandidates
/// and use [coinlib.CoinSelection.optimal] to select the good candidates.
Future<TxData> _optimalCoinSelection({
required TxData txData,
required List<StandardInput> spendableOutputs,
required String recipientAddress,
required BigInt satoshiAmountToSend,
required int? satsPerVByte,
required BigInt feeRatePerKB,
required Address changeAddress,
}) async {
final List<BaseInput> candidateInputs =
await addSigningKeys(spendableOutputs);

final BigInt feePerKb = satsPerVByte != null
? BigInt.from(satsPerVByte * 1000)
: feeRatePerKB;

// minFee should be equal or above the Vsize of the tx, which should happen
// since coin selection algorithms will respect feeRatePerKB. So there is no
// need to define a minFee
final BigInt minFee = BigInt.zero;

final List<coinlib.InputCandidate> candidates = [];
final Map<int, BaseInput> candidateBaseInputs = {};

for (int i = 0; i < candidateInputs.length; i++) {

final baseInput = candidateInputs[i];

if (baseInput is! StandardInput) {
// This shouldn't be happening since only non MWEB inputs
// will be given to this helper
throw Exception(
'''
Unexpected input type ${baseInput.runtimeType}
only StandardInput are supported
''',
);
}

final input = standardInputToCoinlibInput(baseInput);

candidates.add(
coinlib.InputCandidate(input: input, value: baseInput.value),
);
candidateBaseInputs[i] = baseInput;
}

final coinlib.Address clRecipientAddress = coinlib.Address.fromString(
normalizeAddress(recipientAddress),
cryptoCurrency.networkParams,
);
final coinlib.Output recipientOutput = coinlib.Output.fromAddress(
satoshiAmountToSend,
clRecipientAddress,
);

final coinlib.Address clChangeAddress = coinlib.Address.fromString(
normalizeAddress(changeAddress.value),
cryptoCurrency.networkParams,
);

final coinlib.Program changeProgram = clChangeAddress.program;

final coinlib.CoinSelection selection =
coinlib.CoinSelection.optimal(
candidates: candidates,
recipients: [recipientOutput],
changeProgram: changeProgram,
feePerKb: feePerKb,
minFee: minFee,
minChange: cryptoCurrency.dustLimit.raw,
);

if (selection.tooLarge) {
throw Exception("Selected transaction would be too large");
}
if (!selection.ready) {
throw Exception("Selection of coins was not successful");
}

// Going back from InputCandidates to BaseInput
// This could be avoided since buildTransaction will do the exact opposite ?
final List<BaseInput> selectedBaseInputs = [];
for (final picked in selection.selected) {
final pickedTxid =
Uint8List.fromList(picked.input.prevOut.hash.reversed.toList()).toHex;
final pickedVout = picked.input.prevOut.n;
bool matched = false;
for (final entry in candidateBaseInputs.entries) {
final base = entry.value;
if (base is StandardInput &&
base.utxo.txid == pickedTxid &&
base.utxo.vout == pickedVout) {
selectedBaseInputs.add(base);
matched = true;
break;
}
}
if (!matched) {
throw Exception(
"Selected input not found among candidates (txid=$pickedTxid"
" vout=$pickedVout)",
);
}
}

Logging.instance.d(
"Optimal selection: picked ${selectedBaseInputs.length} input(s),"
" inputValue=${selection.inputValue}, fee=${selection.fee},"
" changeValue=${selection.changeValue},"
" signedSize=${selection.signedSize}",
);

/// Add the change if there is one
final List<String> recipientsArray = [recipientAddress];
final List<BigInt> recipientsAmtArray = [satoshiAmountToSend];
if (!selection.changeless) {
await checkChangeAddressForTransactions();
final freshChange = (await getCurrentChangeAddress())!;
recipientsArray.add(freshChange.value);
recipientsAmtArray.add(selection.changeValue);
}

final TxData txBuilt = await buildTransaction(
inputsWithKeys: selectedBaseInputs,
txData: txData.copyWith(
recipients: await helperRecipientsConvert(
recipientsArray,
recipientsAmtArray,
),
usedUTXOs: selectedBaseInputs,
),
);

return txBuilt.copyWith(
fee: Amount(
rawValue: selection.fee,
fractionDigits: cryptoCurrency.fractionDigits,
),
usedUTXOs: selectedBaseInputs,
);
}

Future<List<BaseInput>> addSigningKeys(List<BaseInput> utxosToUse) async {
// return data
final List<BaseInput> inputsWithKeys = [];
Expand Down Expand Up @@ -715,14 +930,6 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
),
);
} else if (data is StandardInput) {
final txid = data.utxo.txid;

final hash = Uint8List.fromList(
txid.toUint8ListFromHex.reversed.toList(),
);

final prevOutpoint = coinlib.OutPoint(hash, data.utxo.vout);

final prevOutput = coinlib.Output.fromAddress(
BigInt.from(data.utxo.value),
coinlib.Address.fromString(
Expand All @@ -733,43 +940,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>

prevOuts.add(prevOutput);

final coinlib.Input input;

switch (data.derivePathType) {
case DerivePathType.bip44:
case DerivePathType.bch44:
input = coinlib.P2PKHInput(
prevOut: prevOutpoint,
publicKey: data.key!.publicKey,
sequence: sequence,
);

// TODO: fix this as it is (probably) wrong!
case DerivePathType.bip49:
throw Exception("TODO p2sh");
// input = coinlib.P2SHMultisigInput(
// prevOut: prevOutpoint,
// program: coinlib.MultisigProgram.decompile(
// data.redeemScript!,
// ),
// sequence: sequence,
// );

case DerivePathType.bip84:
input = coinlib.P2WPKHInput(
prevOut: prevOutpoint,
publicKey: data.key!.publicKey,
sequence: sequence,
);

case DerivePathType.bip86:
input = coinlib.TaprootKeyInput(prevOut: prevOutpoint);

default:
throw UnsupportedError(
"Unknown derivation path type found: ${data.derivePathType}",
);
}
final input = standardInputToCoinlibInput(data, sequence: sequence);

if (input is! coinlib.WitnessInput) {
hasNonWitnessInput = true;
Expand Down
38 changes: 19 additions & 19 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -285,10 +285,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
charcode:
dependency: transitive
description:
Expand Down Expand Up @@ -349,9 +349,9 @@ packages:
dependency: "direct overridden"
description:
path: coinlib
ref: "5c59c7e7d120d9c981f23008fa03421d39fe8631"
resolved-ref: "5c59c7e7d120d9c981f23008fa03421d39fe8631"
url: "https://www.github.com/julian-CStack/coinlib"
ref: "390aa75277b56828879f13e0c8defa779544888e"
resolved-ref: "390aa75277b56828879f13e0c8defa779544888e"
url: "https://www.github.com/Cyrix126/coinlib"
source: git
version: "4.1.0"
coinlib_flutter:
Expand Down Expand Up @@ -1578,18 +1578,18 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
memoize:
dependency: transitive
description:
Expand All @@ -1602,10 +1602,10 @@ packages:
dependency: "direct main"
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
sha256: df0c643f44ad098eb37988027a8e2b2b5a031fd3977f06bbfd3a76637e8df739
url: "https://pub.dev"
source: hosted
version: "1.17.0"
version: "1.18.2"
mime:
dependency: transitive
description:
Expand Down Expand Up @@ -2276,26 +2276,26 @@ packages:
dependency: transitive
description:
name: test
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20"
url: "https://pub.dev"
source: hosted
version: "1.26.3"
version: "1.31.0"
test_api:
dependency: transitive
description:
name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
version: "0.7.7"
version: "0.7.11"
test_core:
dependency: transitive
description:
name: test_core
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34"
url: "https://pub.dev"
source: hosted
version: "0.6.12"
version: "0.6.17"
tezart:
dependency: "direct main"
description:
Expand Down Expand Up @@ -2486,10 +2486,10 @@ packages:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
sha256: "47a1b32ee755c3fcffa33db52a7258c137f97bdb2209a1075be847809fac4ccf"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.3.0"
very_good_analysis:
dependency: transitive
description:
Expand Down
Loading
Loading