Skip to content

Commit 07908c2

Browse files
author
Cyrix126
committed
feat: use CoinSelection class from coinlib for coin selection
Replace the legacy FIFO algorithm used so far, except for cases that can not be treated by new coin selection algorithms (mweb input, override fee, send all, coin control)
1 parent 037e13f commit 07908c2

2 files changed

Lines changed: 218 additions & 47 deletions

File tree

lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart

Lines changed: 216 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,30 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
223223
Logging.instance.d("spendableSatoshiValue: $spendableSatoshiValue");
224224
Logging.instance.d("satoshiAmountToSend: $satoshiAmountToSend");
225225

226+
// Use coinlib CoinSelection algorithms except for
227+
// "coinControl", "SendAll", "MWEB", "overrideFeeAmount",
228+
// because they do not need a selection or
229+
// do not meet the requirements for the algorithms
230+
final bool useOptimalSelection = !coinControl &&
231+
!isSendAll &&
232+
!isSendAllCoinControlUtxos &&
233+
overrideFeeAmount == null &&
234+
txData.type != TxType.mweb &&
235+
txData.type != TxType.mwebPegOut &&
236+
txData.type != TxType.mwebPegIn;
237+
238+
if (useOptimalSelection) {
239+
return await _optimalCoinSelection(
240+
txData: txData,
241+
spendableOutputs: spendableOutputs.whereType<StandardInput>().toList(),
242+
recipientAddress: recipientAddress,
243+
satoshiAmountToSend: satoshiAmountToSend,
244+
satsPerVByte: satsPerVByte,
245+
feeRatePerKB: selectedTxFeeRate,
246+
changeAddress: await changeAddress(),
247+
);
248+
}
249+
226250
BigInt satoshisBeingUsed = BigInt.zero;
227251
int inputsBeingConsumed = 0;
228252
final List<BaseInput> utxoObjectsToUse = [];
@@ -571,6 +595,197 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
571595
);
572596
}
573597

598+
coinlib.Input standardInputToCoinlibInput(
599+
StandardInput input, {
600+
int sequence = 0xffffffff,
601+
}) {
602+
final hash = Uint8List.fromList(
603+
input.utxo.txid.toUint8ListFromHex.reversed.toList(),
604+
);
605+
final prevOut = coinlib.OutPoint(hash, input.utxo.vout);
606+
607+
switch (input.derivePathType) {
608+
case DerivePathType.bip44:
609+
case DerivePathType.bch44:
610+
return coinlib.P2PKHInput(
611+
prevOut: prevOut,
612+
publicKey: input.key!.publicKey,
613+
sequence: sequence,
614+
);
615+
616+
// TODO: fix this as it is (probably) wrong!
617+
case DerivePathType.bip49:
618+
throw Exception("TODO p2sh");
619+
// return coinlib.P2SHMultisigInput(
620+
// prevOut: prevOut,
621+
// program: coinlib.MultisigProgram.decompile(
622+
// input.redeemScript!,
623+
// ),
624+
// sequence: sequence,
625+
// );
626+
627+
case DerivePathType.bip84:
628+
return coinlib.P2WPKHInput(
629+
prevOut: prevOut,
630+
publicKey: input.key!.publicKey,
631+
sequence: sequence,
632+
);
633+
634+
case DerivePathType.bip86:
635+
return coinlib.TaprootKeyInput(prevOut: prevOut);
636+
637+
default:
638+
throw UnsupportedError(
639+
"Unknown derivation path type found: ${input.derivePathType}",
640+
);
641+
}
642+
}
643+
644+
/// Helper that will convert BaseInput into InputCandidates
645+
/// and use [coinlib.CoinSelection.optimal] to select the good candidates.
646+
Future<TxData> _optimalCoinSelection({
647+
required TxData txData,
648+
required List<StandardInput> spendableOutputs,
649+
required String recipientAddress,
650+
required BigInt satoshiAmountToSend,
651+
required int? satsPerVByte,
652+
required BigInt feeRatePerKB,
653+
required Address changeAddress,
654+
}) async {
655+
final List<BaseInput> candidateInputs =
656+
await addSigningKeys(spendableOutputs);
657+
658+
final BigInt feePerKb = satsPerVByte != null
659+
? BigInt.from(satsPerVByte * 1000)
660+
: feeRatePerKB;
661+
662+
// minFee should be equal or above the Vsize of the tx, which should happen
663+
// since coin selection algorithms will respect feeRatePerKB. So there is no
664+
// need to define a minFee
665+
final BigInt minFee = BigInt.zero;
666+
667+
final List<coinlib.InputCandidate> candidates = [];
668+
final Map<int, BaseInput> candidateBaseInputs = {};
669+
670+
for (int i = 0; i < candidateInputs.length; i++) {
671+
672+
final baseInput = candidateInputs[i];
673+
674+
if (baseInput is! StandardInput) {
675+
// This shouldn't be happening since only non MWEB inputs
676+
// will be given to this helper
677+
throw Exception(
678+
'''
679+
Unexpected input type ${baseInput.runtimeType}
680+
only StandardInput are supported
681+
''',
682+
);
683+
}
684+
685+
final input = standardInputToCoinlibInput(baseInput);
686+
687+
candidates.add(
688+
coinlib.InputCandidate(input: input, value: baseInput.value),
689+
);
690+
candidateBaseInputs[i] = baseInput;
691+
}
692+
693+
final coinlib.Address clRecipientAddress = coinlib.Address.fromString(
694+
normalizeAddress(recipientAddress),
695+
cryptoCurrency.networkParams,
696+
);
697+
final coinlib.Output recipientOutput = coinlib.Output.fromAddress(
698+
satoshiAmountToSend,
699+
clRecipientAddress,
700+
);
701+
702+
final coinlib.Address clChangeAddress = coinlib.Address.fromString(
703+
normalizeAddress(changeAddress.value),
704+
cryptoCurrency.networkParams,
705+
);
706+
707+
final coinlib.Program changeProgram = clChangeAddress.program;
708+
709+
final coinlib.CoinSelection selection =
710+
coinlib.CoinSelection.optimal(
711+
candidates: candidates,
712+
recipients: [recipientOutput],
713+
changeProgram: changeProgram,
714+
feePerKb: feePerKb,
715+
minFee: minFee,
716+
minChange: cryptoCurrency.dustLimit.raw,
717+
);
718+
719+
if (selection.tooLarge) {
720+
throw Exception("Selected transaction would be too large");
721+
}
722+
if (!selection.ready) {
723+
throw Exception("Selection of coins was not successful");
724+
}
725+
726+
// Going back from InputCandidates to BaseInput
727+
// This could be avoided since buildTransaction will do the exact opposite ?
728+
final List<BaseInput> selectedBaseInputs = [];
729+
for (final picked in selection.selected) {
730+
final pickedTxid =
731+
Uint8List.fromList(picked.input.prevOut.hash.reversed.toList()).toHex;
732+
final pickedVout = picked.input.prevOut.n;
733+
bool matched = false;
734+
for (final entry in candidateBaseInputs.entries) {
735+
final base = entry.value;
736+
if (base is StandardInput &&
737+
base.utxo.txid == pickedTxid &&
738+
base.utxo.vout == pickedVout) {
739+
selectedBaseInputs.add(base);
740+
matched = true;
741+
break;
742+
}
743+
}
744+
if (!matched) {
745+
throw Exception(
746+
"Selected input not found among candidates (txid=$pickedTxid"
747+
" vout=$pickedVout)",
748+
);
749+
}
750+
}
751+
752+
Logging.instance.d(
753+
"Optimal selection: picked ${selectedBaseInputs.length} input(s),"
754+
" inputValue=${selection.inputValue}, fee=${selection.fee},"
755+
" changeValue=${selection.changeValue},"
756+
" signedSize=${selection.signedSize}",
757+
);
758+
759+
/// Add the change if there is one
760+
final List<String> recipientsArray = [recipientAddress];
761+
final List<BigInt> recipientsAmtArray = [satoshiAmountToSend];
762+
if (!selection.changeless) {
763+
await checkChangeAddressForTransactions();
764+
final freshChange = (await getCurrentChangeAddress())!;
765+
recipientsArray.add(freshChange.value);
766+
recipientsAmtArray.add(selection.changeValue);
767+
}
768+
769+
final TxData txBuilt = await buildTransaction(
770+
inputsWithKeys: selectedBaseInputs,
771+
txData: txData.copyWith(
772+
recipients: await helperRecipientsConvert(
773+
recipientsArray,
774+
recipientsAmtArray,
775+
),
776+
usedUTXOs: selectedBaseInputs,
777+
),
778+
);
779+
780+
return txBuilt.copyWith(
781+
fee: Amount(
782+
rawValue: selection.fee,
783+
fractionDigits: cryptoCurrency.fractionDigits,
784+
),
785+
usedUTXOs: selectedBaseInputs,
786+
);
787+
}
788+
574789
Future<List<BaseInput>> addSigningKeys(List<BaseInput> utxosToUse) async {
575790
// return data
576791
final List<BaseInput> inputsWithKeys = [];
@@ -715,14 +930,6 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
715930
),
716931
);
717932
} else if (data is StandardInput) {
718-
final txid = data.utxo.txid;
719-
720-
final hash = Uint8List.fromList(
721-
txid.toUint8ListFromHex.reversed.toList(),
722-
);
723-
724-
final prevOutpoint = coinlib.OutPoint(hash, data.utxo.vout);
725-
726933
final prevOutput = coinlib.Output.fromAddress(
727934
BigInt.from(data.utxo.value),
728935
coinlib.Address.fromString(
@@ -733,43 +940,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
733940

734941
prevOuts.add(prevOutput);
735942

736-
final coinlib.Input input;
737-
738-
switch (data.derivePathType) {
739-
case DerivePathType.bip44:
740-
case DerivePathType.bch44:
741-
input = coinlib.P2PKHInput(
742-
prevOut: prevOutpoint,
743-
publicKey: data.key!.publicKey,
744-
sequence: sequence,
745-
);
746-
747-
// TODO: fix this as it is (probably) wrong!
748-
case DerivePathType.bip49:
749-
throw Exception("TODO p2sh");
750-
// input = coinlib.P2SHMultisigInput(
751-
// prevOut: prevOutpoint,
752-
// program: coinlib.MultisigProgram.decompile(
753-
// data.redeemScript!,
754-
// ),
755-
// sequence: sequence,
756-
// );
757-
758-
case DerivePathType.bip84:
759-
input = coinlib.P2WPKHInput(
760-
prevOut: prevOutpoint,
761-
publicKey: data.key!.publicKey,
762-
sequence: sequence,
763-
);
764-
765-
case DerivePathType.bip86:
766-
input = coinlib.TaprootKeyInput(prevOut: prevOutpoint);
767-
768-
default:
769-
throw UnsupportedError(
770-
"Unknown derivation path type found: ${data.derivePathType}",
771-
);
772-
}
943+
final input = standardInputToCoinlibInput(data, sequence: sequence);
773944

774945
if (input is! coinlib.WitnessInput) {
775946
hasNonWitnessInput = true;

scripts/app_config/templates/pubspec.template.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,9 +316,9 @@ dependency_overrides:
316316
# coinlib_flutter requires this
317317
coinlib:
318318
git:
319-
url: https://www.github.com/julian-CStack/coinlib
319+
url: https://www.github.com/Cyrix126/coinlib
320320
path: coinlib
321-
ref: 5c59c7e7d120d9c981f23008fa03421d39fe8631
321+
ref: 390aa75277b56828879f13e0c8defa779544888e
322322

323323
bip47:
324324
git:

0 commit comments

Comments
 (0)