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

Generated by: LCOV version 2.0-1