@@ -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 ;
0 commit comments