LCOV - code coverage report
Current view: top level - crypto/utxo/utils - send.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 33.2 % 232 77
Test Date: 2025-07-02 01:23:33 Functions: - 0 0

            Line data    Source code
       1              : import 'dart:convert';
       2              : import 'dart:typed_data';
       3              : 
       4              : import 'package:bip32/bip32.dart';
       5              : import 'package:convert/convert.dart';
       6              : import 'package:walletkit_dart/src/common/logger.dart';
       7              : import 'package:walletkit_dart/src/crypto/utxo/entities/payments/input_selection.dart';
       8              : import 'package:walletkit_dart/src/crypto/utxo/entities/payments/p2h.dart';
       9              : import 'package:walletkit_dart/src/crypto/utxo/entities/raw_transaction/input.dart';
      10              : import 'package:walletkit_dart/src/crypto/utxo/entities/raw_transaction/output.dart';
      11              : import 'package:walletkit_dart/src/crypto/utxo/repositories/electrum_json_rpc_client.dart';
      12              : import 'package:walletkit_dart/src/crypto/utxo/utils/endpoint_utils.dart';
      13              : import 'package:walletkit_dart/src/utils/der.dart';
      14              : import 'package:walletkit_dart/src/utils/int.dart';
      15              : import 'package:walletkit_dart/walletkit_dart.dart';
      16              : 
      17              : ///
      18              : /// Useful: https://btcinformation.org/en/developer-reference#raw-transaction-format
      19              : /// https://github.com/bitcoin/bips/blob/master/bip-0144.mediawiki
      20              : ///
      21              : 
      22            0 : RawTransaction buildUnsignedTransaction({
      23              :   required TransferIntent<UtxoFeeInformation> intent,
      24              :   required UTXONetworkType networkType,
      25              :   required HDWalletPath walletPath,
      26              :   required Iterable<UTXOTransaction> txList,
      27              :   required Amount feePerByte,
      28              :   required Iterable<String> changeAddresses,
      29              : 
      30              :   /// Pre chosen UTXOs to deterministly choose the UTXOs
      31              :   /// if null, the UTXOs will be chosen randomly
      32              :   List<ElectrumOutput>? preChosenUTXOs,
      33              : }) {
      34            0 :   if (txList.isEmpty) {
      35            0 :     throw SendFailure("No transactions");
      36              :   }
      37              : 
      38            0 :   var targetValue = intent.amount.value;
      39              : 
      40            0 :   if (targetValue < BigInt.zero) {
      41            0 :     throw SendFailure("targetValue < 0");
      42              :   }
      43              : 
      44            0 :   if (targetValue < networkType.dustTreshhold.legacy.toBI &&
      45            0 :       walletPath.purpose != HDWalletPurpose.BIP84) {
      46            0 :     throw SendFailure(
      47            0 :       "targetValue < DUST_THRESHOLD: ${networkType.dustTreshhold.legacy}",
      48              :     );
      49              :   }
      50            0 :   if (walletPath.purpose == HDWalletPurpose.BIP84 &&
      51            0 :       targetValue < networkType.dustTreshhold.segwit.toBI) {
      52            0 :     throw SendFailure(
      53            0 :       "targetValue < DUST_THRESHOLD_BIP84: ${networkType.dustTreshhold.segwit}",
      54              :     );
      55              :   }
      56              : 
      57            0 :   final allUTXOs = extractUTXOs(txList: txList);
      58              : 
      59            0 :   if (allUTXOs.isEmpty) {
      60              :     throw const SendFailure("no UTXOs"); // should be never reached
      61              :   }
      62              : 
      63              :   const lockTime = 0;
      64              :   const validFrom = 0; // EC8 specific
      65              :   const validUntil = 0; // EC8 specific
      66            0 :   final version = networkType.txVersion;
      67              : 
      68              :   final chosenUTXOs = preChosenUTXOs ??
      69            0 :       singleRandomDrawUTXOSelection(
      70            0 :         allUTXOs.keys.toList(),
      71              :         targetValue,
      72              :       );
      73              : 
      74            0 :   Logger.log("Chosen UTXOs: ${chosenUTXOs}");
      75              : 
      76            0 :   var chosenUTXOsMap = {
      77            0 :     for (final utxo in chosenUTXOs) utxo: allUTXOs[utxo]!,
      78              :   };
      79              : 
      80            0 :   var (totalInputValue, inputMap) = buildInputs(chosenUTXOsMap, networkType);
      81              : 
      82            0 :   if (totalInputValue < targetValue) {
      83            0 :     throw SendFailure("Not enough funds to pay targetValue $targetValue");
      84              :   }
      85            0 :   if (inputMap.keys.isEmpty) {
      86            0 :     throw SendFailure("No inputs");
      87              :   }
      88              : 
      89            0 :   final targetAddress = intent.recipient;
      90              : 
      91            0 :   final changeAddress = findUnusedAddress(
      92              :     addresses: changeAddresses,
      93              :     txs: txList,
      94              :   );
      95              : 
      96              :   ///
      97              :   /// Build Dummy TX
      98              :   ///
      99              : 
     100            0 :   final dummyOutputs = buildOutputs(
     101            0 :     recipient: intent.recipient,
     102              :     value: targetValue,
     103              :     changeAddress: changeAddress,
     104            0 :     changeValue: BigInt.one,
     105              :     networkType: networkType,
     106              :   );
     107              : 
     108            0 :   var dummyTx = buildDummyTx(
     109              :     networkType: networkType,
     110              :     walletPath: walletPath,
     111              :     inputMap: inputMap,
     112              :     dummyOutputs: dummyOutputs,
     113              :   );
     114              : 
     115              :   ///
     116              :   /// Build Outputs again with the estimated size
     117              :   ///
     118              : 
     119            0 :   var estimatedFee = calculateFee(tx: dummyTx, feePerByte: feePerByte);
     120              : 
     121            0 :   var changeValue = totalInputValue - targetValue - estimatedFee;
     122              : 
     123            0 :   if (changeValue < BigInt.zero) {
     124            0 :     targetValue -= changeValue.abs();
     125            0 :     if (targetValue < networkType.dustTreshhold.legacy.toBI) {
     126              :       /// Ad addidional UTXO to cover the fee
     127            0 :       targetValue = intent.amount.value;
     128            0 :       final additionalUTXO = fillUpToTargetAmount(
     129              :         chosenUTXOs,
     130            0 :         allUTXOs.keys.toList(),
     131            0 :         targetValue + estimatedFee * BigInt.two,
     132              :       );
     133              : 
     134            0 :       chosenUTXOsMap = {
     135            0 :         for (final utxo in additionalUTXO) utxo: allUTXOs[utxo]!,
     136              :       };
     137              : 
     138            0 :       (totalInputValue, inputMap) = buildInputs(chosenUTXOsMap, networkType);
     139              : 
     140            0 :       dummyTx = buildDummyTx(
     141              :         networkType: networkType,
     142              :         walletPath: walletPath,
     143              :         inputMap: inputMap,
     144              :         dummyOutputs: dummyOutputs,
     145              :       );
     146              : 
     147            0 :       estimatedFee = calculateFee(tx: dummyTx, feePerByte: feePerByte);
     148              :     }
     149              : 
     150            0 :     changeValue = totalInputValue - targetValue - estimatedFee;
     151            0 :     if (changeValue < BigInt.zero)
     152            0 :       throw SendFailure("Not enough funds to pay targetValue $targetValue");
     153              :   }
     154              : 
     155              :   assert(
     156            0 :     totalInputValue == targetValue + changeValue + estimatedFee,
     157              :     "Total Input Value does not match Total Output Value",
     158              :   );
     159              : 
     160            0 :   Logger.log("Estimated Fee: $estimatedFee");
     161              : 
     162            0 :   final outputs = buildOutputs(
     163              :     recipient: targetAddress,
     164              :     value: targetValue,
     165              :     changeAddress: changeAddress,
     166              :     changeValue: changeValue,
     167              :     networkType: networkType,
     168              :   );
     169              : 
     170              :   ///
     171              :   /// Build final transaction
     172              :   ///
     173              : 
     174            0 :   var tx = RawTransaction.build(
     175              :     version: version,
     176              :     lockTime: lockTime,
     177              :     validFrom: validFrom,
     178              :     validUntil: validUntil,
     179              :     inputMap: inputMap,
     180              :     outputs: outputs,
     181              :   );
     182              : 
     183            0 :   if (tx.totalOutputValue + estimatedFee != totalInputValue) {
     184            0 :     throw SendFailure(
     185              :       "Total Output Value does not match Total Input Value",
     186              :     );
     187              :   }
     188              : 
     189              :   return tx;
     190              : }
     191              : 
     192              : typedef DummyTxInfo = ({RawTransaction dummyRawTx, List<ElectrumOutput> chosenUTXOs});
     193              : 
     194              : ///
     195              : /// Creates a dummy transaction to estimate the size of the transaction and hence the fee
     196              : /// Also returns the chosen UTXOs so that they can be used to create the real transaction with the same UTXOs
     197              : /// Includes a safety margin so that changes in the Amount dont lead to a different fee
     198              : ///
     199            0 : DummyTxInfo buildDummyTxFromScratch({
     200              :   required TransferIntent intent,
     201              :   required UTXONetworkType networkType,
     202              :   required HDWalletPath walletPath,
     203              :   required Iterable<UTXOTransaction> txList,
     204              :   required List<String> changeAddresses,
     205              : }) {
     206            0 :   final allUTXOs = extractUTXOs(txList: txList);
     207              : 
     208            0 :   final chosenUTXOs = singleRandomDrawUTXOSelection(
     209            0 :     allUTXOs.keys.toList(),
     210            0 :     intent.amount.value,
     211              :   );
     212              : 
     213            0 :   final chosenUTXOsMap = {
     214            0 :     for (final utxo in chosenUTXOs) utxo: allUTXOs[utxo]!,
     215              :   };
     216              : 
     217            0 :   final (_, inputMap) = buildInputs(chosenUTXOsMap, networkType);
     218              : 
     219            0 :   final changeAddress = findUnusedAddress(
     220              :     addresses: changeAddresses,
     221              :     txs: txList,
     222              :   );
     223              : 
     224            0 :   final dummyOutputs = buildOutputs(
     225            0 :     recipient: intent.recipient,
     226            0 :     value: intent.amount.value,
     227              :     changeAddress: changeAddress,
     228            0 :     changeValue: BigInt.one,
     229              :     networkType: networkType,
     230              :   );
     231              : 
     232            0 :   final dummyTx = buildDummyTx(
     233              :     networkType: networkType,
     234              :     walletPath: walletPath,
     235              :     inputMap: inputMap,
     236              :     dummyOutputs: dummyOutputs,
     237              :   );
     238              : 
     239              :   return (dummyRawTx: dummyTx, chosenUTXOs: chosenUTXOs);
     240              : }
     241              : 
     242            0 : RawTransaction buildDummyTx({
     243              :   required UTXONetworkType networkType,
     244              :   required HDWalletPath walletPath,
     245              :   required Map<ElectrumOutput, Input> inputMap,
     246              :   required List<Output> dummyOutputs,
     247              : }) {
     248            0 :   final dummySeed = helloSeed;
     249              : 
     250            0 :   var dummyTx = RawTransaction.build(
     251              :     version: 0,
     252              :     lockTime: 0,
     253              :     validFrom: 0,
     254              :     validUntil: 0,
     255              :     inputMap: inputMap,
     256              :     outputs: dummyOutputs,
     257            0 :   ).sign(
     258              :     seed: dummySeed,
     259              :     networkType: networkType,
     260              :     walletPath: walletPath,
     261              :   );
     262              : 
     263              :   return dummyTx;
     264              : }
     265              : 
     266            4 : List<Input> signInputs({
     267              :   required Map<ElectrumOutput, Input> inputs,
     268              :   required HDWalletPath walletPath,
     269              :   required UTXONetworkType networkType,
     270              :   required RawTransaction tx,
     271              :   required Uint8List seed,
     272              : }) {
     273            4 :   final signedInputs = <Input>[];
     274              : 
     275           12 :   for (var i = 0; i < inputs.length; i++) {
     276            8 :     final entry = inputs.entries.elementAt(i);
     277            4 :     final input = entry.value;
     278            4 :     final output = entry.key;
     279            8 :     var bip32Node = output.node.bip32Node;
     280              : 
     281            4 :     if (bip32Node == null || bip32Node.isNeutered()) {
     282            0 :       if (output.belongsToUs) {
     283            0 :         bip32Node = deriveChildNodeFromPath(
     284              :           seed: seed,
     285            0 :           childDerivationPath: output.node.derivationPath,
     286              :           networkType: networkType,
     287              :           walletPath: walletPath,
     288              :         );
     289              :       } else
     290            0 :         throw SendFailure("Can't sign input without node: $output $input");
     291              :     }
     292              : 
     293           10 :     if (tx is BTCRawTransaction && output.scriptPubKey.isSegwit) {
     294            1 :       final witnessSript = createScriptWitness(
     295              :         tx: tx,
     296              :         i: i,
     297              :         output: output,
     298              :         networkType: networkType,
     299              :         node: bip32Node,
     300              :       );
     301              : 
     302            2 :       signedInputs.add(input.addScript(wittnessScript: witnessSript));
     303              :       continue;
     304              :     }
     305              : 
     306            4 :     final scriptSig = createScriptSignature(
     307              :       tx: tx,
     308              :       i: i,
     309              :       output: output,
     310            4 :       walletPurpose: walletPath.purpose,
     311              :       networkType: networkType,
     312              :       node: bip32Node,
     313              :     );
     314              : 
     315            8 :     signedInputs.add(input.addScript(scriptSig: scriptSig));
     316              :   }
     317              : 
     318              :   return signedInputs;
     319              : }
     320              : 
     321            4 : Uint8List createScriptSignature({
     322              :   required RawTransaction tx,
     323              :   required int i,
     324              :   required ElectrumOutput output,
     325              :   required HDWalletPurpose walletPurpose,
     326              :   required UTXONetworkType networkType,
     327              :   required BIP32 node,
     328              : }) {
     329            8 :   final hashType = networkType.sighash.all;
     330            8 :   final prevScriptPubKey = output.scriptPubKey.lockingScript;
     331              : 
     332              :   final sigHash = switch (networkType) {
     333           12 :     BITCOINCASH_NETWORK() || ZENIQ_NETWORK() when tx is BTCRawTransaction => tx.bip143sigHash(
     334              :         index: i,
     335              :         prevScriptPubKey: prevScriptPubKey,
     336              :         output: output,
     337              :         hashType: hashType,
     338              :       ),
     339           10 :     LITECOIN_NETWORK() || BITCOIN_NETWORK() || EUROCOIN_NETWORK() => tx.legacySigHash(
     340              :         index: i,
     341              :         prevScriptPubKey: prevScriptPubKey,
     342              :         hashType: hashType,
     343              :       ),
     344            0 :     _ => throw SendFailure("Could not find sigHash for networkType $networkType"),
     345              :   };
     346              : 
     347            4 :   final sig = signInput(bip32: node, sigHash: sigHash);
     348              : 
     349            4 :   final scriptSig = encodeSignature(sig, hashType);
     350              : 
     351            4 :   final unlockingScript = constructScriptSig(
     352              :     walletPurpose: walletPurpose,
     353              :     signature: scriptSig,
     354            4 :     publicKey: node.publicKey,
     355              :   );
     356              : 
     357              :   return unlockingScript;
     358              : }
     359              : 
     360            1 : Uint8List createScriptWitness({
     361              :   required BTCRawTransaction tx,
     362              :   required int i,
     363              :   required ElectrumOutput output,
     364              :   required UTXONetworkType networkType,
     365              :   required BIP32 node,
     366              : }) {
     367            2 :   final hashType = networkType.sighash.all;
     368            2 :   final prevScriptPubKey = output.scriptPubKey.lockingScript;
     369              : 
     370            3 :   assert(output.scriptPubKey.isSegwit);
     371              : 
     372            1 :   final sigHash = tx.bip143sigHash(
     373              :     index: i,
     374              :     prevScriptPubKey: prevScriptPubKey,
     375              :     output: output,
     376              :     hashType: hashType,
     377              :   );
     378              : 
     379            1 :   final sig = signInput(bip32: node, sigHash: sigHash);
     380              : 
     381            1 :   final scriptSig = encodeSignature(sig, hashType);
     382              : 
     383            1 :   final pubkey = node.publicKey;
     384              : 
     385            1 :   return [
     386              :     0x02,
     387            1 :     scriptSig.length,
     388            1 :     ...scriptSig,
     389            1 :     pubkey.length,
     390            1 :     ...pubkey,
     391            1 :   ].toUint8List;
     392              : }
     393              : 
     394            4 : (BigInt, Map<ElectrumOutput, Input>) buildInputs(
     395              :   Map<ElectrumOutput, UTXOTransaction> utxos,
     396              :   UTXONetworkType networkType,
     397              : ) {
     398              :   final usedUTXO = <String>{};
     399            4 :   final inputs = <ElectrumOutput, Input>{};
     400            4 :   var totalInputValue = BigInt.zero;
     401              : 
     402            8 :   for (final uTXOEntry in utxos.entries) {
     403            4 :     final uTXO = uTXOEntry.key;
     404            4 :     final uTXOTx = uTXOEntry.value;
     405              : 
     406            4 :     final hash = uTXOTx.id;
     407              : 
     408            8 :     inputs[uTXO] = buildInput(
     409              :       txidHex: hash,
     410              :       usedUTXO: usedUTXO,
     411              :       utxo: uTXO,
     412              :       networkType: networkType,
     413              :     );
     414              : 
     415            8 :     totalInputValue += uTXO.value;
     416              :   }
     417              : 
     418              :   return (totalInputValue, inputs);
     419              : }
     420              : 
     421            1 : List<Output> buildOutputs({
     422              :   required String recipient,
     423              :   required BigInt value,
     424              :   required String? changeAddress,
     425              :   required BigInt changeValue,
     426              :   required UTXONetworkType networkType,
     427              : }) {
     428            1 :   return [
     429            1 :     buildOutput(recipient, value, networkType),
     430            2 :     if (changeAddress != null && changeValue != BigInt.zero)
     431            1 :       buildOutput(changeAddress, changeValue, networkType),
     432              :   ];
     433              : }
     434              : 
     435            4 : Input buildInput({
     436              :   required String txidHex,
     437              :   required Set<String> usedUTXO,
     438              :   required ElectrumOutput utxo,
     439              :   required UTXONetworkType networkType,
     440              : }) {
     441            4 :   final vout = utxo.n;
     442            4 :   final txid = Uint8List.fromList(
     443           12 :     hex.decode(txidHex).reversed.toList(),
     444              :   ); // Use 'txid' instead of 'hash'
     445              : 
     446            4 :   final prevTxOut = '$txidHex:$vout';
     447              : 
     448            4 :   if (usedUTXO.contains(prevTxOut)) {
     449              :     throw const SendFailure("double spend");
     450              :   }
     451              : 
     452              :   /// Check if utxo has a ScriptSig => Input should also have a ScriptSig
     453              :   /// Check if utxo has a WitnessScript => Input should also have a WitnessScript
     454              :   ///
     455              : 
     456              :   return switch (networkType) {
     457           14 :     BITCOIN_NETWORK() || BITCOINCASH_NETWORK() || ZENIQ_NETWORK() || LITECOIN_NETWORK() => BTCInput(
     458              :         txid: txid,
     459              :         vout: vout,
     460            3 :         value: utxo.value,
     461            6 :         prevScriptPubKey: utxo.scriptPubKey.lockingScript,
     462              :       ),
     463            2 :     EUROCOIN_NETWORK() => EC8Input(
     464              :         txid: txid,
     465              :         vout: vout,
     466            1 :         value: utxo.value,
     467            2 :         prevScriptPubKey: utxo.scriptPubKey.lockingScript,
     468              :       ),
     469              :   };
     470              : }
     471              : 
     472            1 : Output buildOutput(String address, BigInt value, UTXONetworkType networkType) {
     473            2 :   final lockingScript = P2Hash(address).publicKeyScript;
     474              : 
     475              :   return switch (networkType) {
     476            1 :     BITCOIN_NETWORK() ||
     477            1 :     BITCOINCASH_NETWORK() ||
     478            1 :     ZENIQ_NETWORK() ||
     479            0 :     LITECOIN_NETWORK() =>
     480            1 :       BTCOutput(
     481              :         value: value,
     482              :         scriptPubKey: lockingScript,
     483              :       ),
     484            0 :     EUROCOIN_NETWORK() => EC8Output(
     485              :         value: value,
     486              :         scriptPubKey: lockingScript,
     487              :       ),
     488              :   };
     489              : }
     490              : 
     491            0 : Future<String> broadcastTransaction({
     492              :   required String rawTxHex,
     493              :   required UTXONetworkType type,
     494              : }) async {
     495            0 :   final (result, client, error) = await fetchFromRandomElectrumXNode(
     496            0 :     (client) async {
     497            0 :       final broadcastResult = await client.broadcastTransaction(rawTxHex: rawTxHex);
     498              :       return broadcastResult;
     499              :     },
     500              :     client: null,
     501            0 :     token: type.coin,
     502            0 :     endpoints: type.endpoints,
     503              :   );
     504              : 
     505            0 :   final host = "${client?.host}:${client?.port}";
     506              : 
     507              :   if (result == null) {
     508            0 :     throw SendFailure("Broadcasting failed for $host: ${error?.message}");
     509              :   }
     510              : 
     511            0 :   final json = jsonDecode(result);
     512              : 
     513            0 :   if (result.contains('error')) {
     514            0 :     if (json case {"error": {"error": {"code": int code, "message": String message}}}) {
     515            0 :       throw SendFailure("$host $code $message");
     516              :     }
     517            0 :     if (json case {"error": {"code": int code, "message": String message}}) {
     518            0 :       throw SendFailure("$host $code $message");
     519              :     }
     520            0 :     throw SendFailure("Unknown error for $host: $result");
     521              :   }
     522              : 
     523            0 :   if (result.contains('result') == false) {
     524            0 :     throw SendFailure("Unknown error for $host: $result");
     525              :   }
     526              : 
     527            0 :   final hash = json['result'];
     528              : 
     529              :   return hash;
     530              : }
     531              : 
     532              : ///
     533              : /// For a given [hash] and [serializedTx] we check if the transaction is already in the mempool
     534              : /// If not we rebroadcast the transaction until at least half of the nodes have the transaction
     535              : ///
     536            0 : Future<bool> rebroadcastTransaction({
     537              :   required String hash,
     538              :   required String serializedTx,
     539              :   required UTXONetworkType type,
     540              :   Duration delay = const Duration(seconds: 5),
     541              : }) async {
     542            0 :   await Future.delayed(delay);
     543              : 
     544            0 :   final clients = await Future.wait(
     545            0 :     [
     546            0 :       for (final endpoint in type.endpoints)
     547            0 :         createElectrumXClient(
     548              :           endpoint: endpoint.$1,
     549              :           port: endpoint.$2,
     550            0 :           token: type.coin,
     551              :         ),
     552              :     ],
     553            0 :   ).then(
     554            0 :     (clients) => clients.whereType<ElectrumXClient>(),
     555              :   );
     556              : 
     557              :   while (true) {
     558              :     int rebroadcastCount = 0;
     559              :     Set<ElectrumXClient> clientsForRebroadcast = {};
     560              : 
     561            0 :     Future<void> testEndpoint(ElectrumXClient client) async {
     562            0 :       final (rawTx, error) = await fetchFromNode(
     563            0 :         (client) => client.getRaw(hash),
     564              :         client: client,
     565              :       );
     566              : 
     567              :       if (error != null) {
     568            0 :         clientsForRebroadcast.add(client);
     569              :         return;
     570              :       }
     571              : 
     572            0 :       if (rawTx == serializedTx) {
     573            0 :         rebroadcastCount++;
     574              :       }
     575              :     }
     576              : 
     577            0 :     await Future.wait(
     578            0 :       [
     579            0 :         for (final client in clients) testEndpoint(client),
     580              :       ],
     581              :     );
     582              : 
     583            0 :     if (rebroadcastCount > type.endpoints.length / 2) {
     584              :       break;
     585              :     }
     586              : 
     587            0 :     Logger.log(
     588            0 :       "Rebroadcasting: $hash for ${clientsForRebroadcast.length} endpoints",
     589              :     );
     590              : 
     591            0 :     for (final client in clientsForRebroadcast) {
     592            0 :       final (result, _) = await fetchFromNode(
     593            0 :         (client) => client.broadcastTransaction(rawTxHex: serializedTx),
     594              :         client: client,
     595              :       );
     596              :       if (result == null) continue;
     597            0 :       final json = jsonDecode(result);
     598            0 :       final hasResult = json.containsKey('result');
     599            0 :       final hasError = json.containsKey('error');
     600              :       if (hasResult) {
     601            0 :         final _hash = json['result'];
     602            0 :         Logger.log("Rebroadcasted: $_hash");
     603            0 :         assert(_hash == hash);
     604              :       }
     605              :       if (hasError) {
     606            0 :         final error = json['error'];
     607            0 :         Logger.logWarning("Error rebroadcasting: $error");
     608              :       }
     609              :     }
     610              : 
     611            0 :     await Future.delayed(delay);
     612              :   }
     613              : 
     614            0 :   await Future.wait([for (final client in clients) client.disconnect()]);
     615              : 
     616              :   return true;
     617              : }
     618              : 
     619            4 : Uint8List signInput({
     620              :   required BIP32 bip32,
     621              :   required Uint8List sigHash,
     622              : }) {
     623              :   try {
     624            4 :     return bip32.sign(sigHash);
     625              :   } catch (e) {
     626            0 :     throw SendFailure("signing failed $e");
     627              :   }
     628              : }
     629              : 
     630            4 : Uint8List constructScriptSig({
     631              :   required HDWalletPurpose walletPurpose,
     632              :   required Uint8List signature,
     633              :   required Uint8List publicKey,
     634              :   Uint8List? redeemScript, // Required for BIP49 (P2SH-P2WPKH)
     635              : }) =>
     636              :     switch (walletPurpose) {
     637           12 :       HDWalletPurpose.NO_STRUCTURE || HDWalletPurpose.BIP44 => Uint8List.fromList([
     638            4 :           signature.length,
     639            4 :           ...signature,
     640            4 :           publicKey.length,
     641            4 :           ...publicKey,
     642              :         ]),
     643            0 :       HDWalletPurpose.BIP49 => Uint8List.fromList([
     644              :           0x00,
     645            0 :           signature.length,
     646            0 :           ...signature,
     647            0 :           redeemScript!.length,
     648            0 :           ...redeemScript,
     649              :         ]),
     650              : 
     651              :       /// Should never be called as it is handled in constructWitnessScript
     652            0 :       HDWalletPurpose.BIP84 => Uint8List.fromList([
     653              :           0x00,
     654            0 :           signature.length,
     655            0 :           ...signature,
     656            0 :           publicKey.length,
     657            0 :           ...publicKey,
     658              :         ]),
     659              :     };
     660              : 
     661            0 : BigInt calculateFee({
     662              :   required RawTransaction tx,
     663              :   required Amount feePerByte,
     664              : }) {
     665              :   return switch (tx) {
     666            0 :     EC8RawTransaction _ => calculateFeeEC8(tx: tx),
     667            0 :     _ => tx.size.toBI * feePerByte.value,
     668              :   };
     669              : }
     670              : 
     671              : const int max_cheap_tx_weight = 15000;
     672              : 
     673            0 : BigInt calculateFeeEC8({
     674              :   required RawTransaction tx,
     675              : }) {
     676            0 :   var fee = 1000.toBI; // Base fee
     677              : 
     678            0 :   final outputLength = tx.outputs.length;
     679              : 
     680            0 :   if (outputLength > 2) {
     681            0 :     fee += 1000.toBI * (outputLength - 2).toBI;
     682              :   }
     683              : 
     684            0 :   if (tx.weight > max_cheap_tx_weight.toBI) {
     685            0 :     fee += 1000.toBI * ((tx.weight + 999.toBI) / 1000.toBI).toBI;
     686              :   }
     687              : 
     688            0 :   assert(fee % 1000.toBI == 0.toBI);
     689              : 
     690              :   return fee;
     691              : }
        

Generated by: LCOV version 2.0-1