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 : }
|