Line data Source code
1 : import 'dart:typed_data';
2 : import 'package:collection/collection.dart';
3 : import 'package:walletkit_dart/src/common/isolate_service.dart';
4 : import 'package:walletkit_dart/src/common/logger.dart';
5 : import 'package:walletkit_dart/src/crypto/utxo/entities/payments/p2h.dart';
6 : import 'package:walletkit_dart/src/crypto/utxo/repositories/electrum_json_rpc_client.dart';
7 : import 'package:walletkit_dart/src/crypto/utxo/utils/endpoint_utils.dart';
8 : import 'package:walletkit_dart/src/crypto/utxo/entities/transactions/electrum_transaction.dart';
9 : import 'package:bip32/bip32.dart' as bip32;
10 : import 'package:walletkit_dart/walletkit_dart.dart';
11 :
12 : const kAddressesUpperLimit = 10000;
13 : const kEmptyLimit = 20;
14 :
15 : /// Txs; Receive Addresses; Change Addresses
16 : typedef UTXOTxInfo = (Set<UTXOTransaction>, Iterable<NodeWithAddress>);
17 :
18 : typedef ElectrumXResult = (Set<ElectrumTransactionInfo>?, ElectrumXClient);
19 :
20 : typedef SearchTransactionResult = (
21 : Set<ElectrumTransactionInfo>,
22 : Set<NodeWithAddress>
23 : );
24 :
25 7 : Future<UTXOTxInfo> fetchMissingUTXOTransactions({
26 : required Set<UTXOTransaction> cachedTransactions,
27 : required List<NodeWithAddress> cachedNodes,
28 : required Set<ElectrumTransactionInfo> allTxs,
29 : required Iterable<NodeWithAddress> nodes,
30 : required CoinEntity coin,
31 : required UTXONetworkType networkType,
32 : required Iterable<AddressType> addressTypes,
33 : required List<(String, int)> endpoints,
34 : required Stopwatch watch,
35 : }) async {
36 : /// Filter out all transactions that are already cached
37 7 : final newTxs = allTxs.where(
38 7 : (tx) {
39 7 : final isCached = cachedTransactions.any((cTx) => cTx.id == tx.hash);
40 :
41 7 : return isCached == false;
42 : },
43 : );
44 :
45 7 : final pendingTxs = allTxs.where(
46 7 : (tx) {
47 7 : final cTx = cachedTransactions.singleWhereOrNull(
48 0 : (cTx) => cTx.id == tx.hash,
49 : );
50 : if (cTx == null) return false;
51 :
52 0 : return cTx.isPending || cTx is NotAvaialableUTXOTransaction;
53 : },
54 : );
55 28 : Logger.log("Found ${pendingTxs.length} pending TXs for ${coin.symbol}");
56 28 : Logger.log("Found ${newTxs.length} new TXs for ${coin.symbol}");
57 :
58 : ///
59 : /// Fetch UTXO Details for all new transactions
60 : ///
61 7 : var newUtxoTxs = await computeMissingUTXODetails(
62 : txList: newTxs,
63 : nodes: nodes,
64 : type: networkType,
65 : endpoints: endpoints,
66 : addressTypes: addressTypes,
67 : );
68 :
69 : ///
70 : /// Fetch Parent Transactions of P2SH Inputs to get the redeem script and in hence the address
71 : ///
72 7 : final unsupported = newUtxoTxs.where(
73 21 : (tx) => tx.sender == ADDRESS_NOT_SUPPORTED,
74 : );
75 7 : final updatedUTXOs = <UTXOTransaction>[];
76 11 : for (final tx in unsupported) {
77 8 : final firstInput = tx.inputs.firstOrNull;
78 : if (firstInput == null) continue;
79 :
80 : /// Coinbase Parent TX
81 4 : if (firstInput.isCoinbase) {
82 : continue;
83 : }
84 :
85 4 : final outputIndex = firstInput.vout!;
86 4 : final parentTxHash = firstInput.txid;
87 : if (parentTxHash == null) continue;
88 :
89 : // Check if Parent TX is already in the list
90 : var parentTx = newUtxoTxs
91 20 : .singleWhereOrNull((element) => element.id == firstInput.txid);
92 :
93 4 : final (_tx, _, _) = await fetchFromRandomElectrumXNode(
94 4 : (client) {
95 4 : return client.getTransaction(
96 : addressTypes: addressTypes,
97 : nodes: nodes,
98 : type: networkType,
99 : txHash: parentTxHash,
100 : );
101 : },
102 : client: null,
103 4 : endpoints: networkType.endpoints,
104 : token: coin,
105 : );
106 :
107 : parentTx ??= _tx;
108 :
109 : if (parentTx == null) {
110 0 : Logger.logWarning(
111 0 : "Parent TX $parentTxHash not found for ${tx.hash}",
112 : );
113 : continue;
114 : }
115 :
116 8 : final output = parentTx.outputs[outputIndex];
117 :
118 4 : final address = output.getAddress(networkType);
119 :
120 8 : updatedUTXOs.add(tx.copyWith(sender: address));
121 : }
122 : // Replace the unsupported transactions with the updated ones
123 14 : newUtxoTxs = newUtxoTxs.map((tx) {
124 7 : final updatedTx = updatedUTXOs.singleWhereOrNull(
125 16 : (upTx) => upTx.id == tx.id,
126 : );
127 : return updatedTx ?? tx;
128 : });
129 :
130 : ///
131 : /// Fetch UTXO Details for all pending transactions and replace them
132 : ///
133 7 : final pendingUtxoTxs = await computeMissingUTXODetails(
134 : txList: pendingTxs,
135 : nodes: nodes,
136 : type: networkType,
137 : endpoints: endpoints,
138 : addressTypes: addressTypes,
139 : );
140 :
141 7 : var utxoTXs = {
142 : ...newUtxoTxs,
143 7 : ...cachedTransactions,
144 : };
145 :
146 : /// Replace the pending transactions with the updated ones
147 14 : utxoTXs = utxoTXs.map((tx) {
148 7 : final pendingTx = pendingUtxoTxs.singleWhereOrNull(
149 0 : (element) => element.id == tx.id,
150 : );
151 : return pendingTx ?? tx;
152 7 : }).toSet();
153 :
154 : ///
155 : /// Mark Spent Outputs
156 : ///
157 14 : for (final tx in utxoTXs) {
158 14 : for (final input in tx.inputs) {
159 : final outputs = utxoTXs
160 35 : .singleWhereOrNull((element) => element.id == input.txid)
161 7 : ?.outputs;
162 :
163 7 : if (input.isCoinbase) continue;
164 7 : final index = input.vout!;
165 14 : if (outputs == null || outputs.length <= index) {
166 : continue;
167 : }
168 21 : outputs[index] = outputs[index].copyWith(spent: true);
169 : }
170 : }
171 :
172 7 : watch.stop();
173 21 : Logger.logFetch("Fetched TXs in ${watch.elapsed}");
174 :
175 21 : final sortedTxs = utxoTXs.sortedBy<GenericTransaction>((tx) => tx).toSet();
176 :
177 : return (sortedTxs, nodes);
178 : }
179 :
180 : /// Fetches UTXO Transactions for a given ePubKey
181 : /// if [purpose] is not provied the returned [nodes] cant be used to derive the master node (used in Proof of Payment)
182 4 : Future<UTXOTxInfo> fetchUTXOTransactionsFromEpubKey({
183 : required Iterable<AddressType> addressTypes,
184 : required UTXONetworkType networkType,
185 : required String ePubKey,
186 : required HDWalletPurpose purpose,
187 : Set<UTXOTransaction> cachedTransactions = const {},
188 : List<NodeWithAddress> cachedNodes = const [],
189 : int minEndpoints = 2,
190 : Duration maxLatency = const Duration(milliseconds: 800),
191 : }) async {
192 8 : final watch = Stopwatch()..start();
193 :
194 4 : final endpoints = await getBestHealthEndpointsWithRetry(
195 4 : endpointPool: networkType.endpoints,
196 4 : token: networkType.coin,
197 : maxLatency: maxLatency,
198 : min: minEndpoints,
199 : );
200 :
201 4 : final isolateManager = IsolateManager();
202 :
203 : ///
204 : /// Search for Receive and Change Addresses
205 : ///
206 :
207 4 : final masterNode = await isolateManager.executeTask(
208 4 : IsolateTask(
209 4 : task: (arg) {
210 4 : return deriveMasterNodeFromExtendedKeyWithCheck(
211 : ePubKey: arg.$1,
212 : networkType: arg.$2,
213 : purpose: arg.$3,
214 : );
215 : },
216 : argument: (ePubKey, networkType, purpose),
217 : ),
218 : );
219 :
220 4 : final (allTxs, nodes) = await searchTransactionsForWalletType(
221 : masterNode: masterNode,
222 : purpose: purpose,
223 : addressTypes: addressTypes,
224 : networkType: networkType,
225 : endpoints: endpoints,
226 : cachedNodes: cachedNodes,
227 : isolateManager: isolateManager,
228 : );
229 :
230 4 : isolateManager.dispose();
231 :
232 4 : return fetchMissingUTXOTransactions(
233 : allTxs: allTxs,
234 : nodes: nodes,
235 : cachedNodes: cachedNodes,
236 : cachedTransactions: cachedTransactions,
237 : addressTypes: addressTypes,
238 4 : coin: networkType.coin,
239 : endpoints: endpoints,
240 : networkType: networkType,
241 : watch: watch,
242 : );
243 : }
244 :
245 5 : Future<UTXOTxInfo> fetchUTXOTransactions({
246 : required Iterable<HDWalletPath> walletTypes,
247 : required Iterable<AddressType> addressTypes,
248 : required UTXONetworkType networkType,
249 : required Uint8List seed,
250 : Set<UTXOTransaction> cachedTransactions = const {},
251 : List<NodeWithAddress> cachedNodes = const [],
252 : int minEndpoints = 2,
253 : Duration maxLatency = const Duration(milliseconds: 800),
254 : }) async {
255 10 : final watch = Stopwatch()..start();
256 :
257 5 : final endpoints = await getBestHealthEndpointsWithRetry(
258 5 : endpointPool: networkType.endpoints,
259 5 : token: networkType.coin,
260 : maxLatency: maxLatency,
261 : min: minEndpoints,
262 : );
263 :
264 5 : print(
265 5 : "Selected ${endpoints.map(
266 10 : (e) => "$e",
267 5 : )}",
268 : );
269 :
270 5 : final isolateManager = IsolateManager();
271 :
272 : ///
273 : /// Search for Receive and Change Addresses
274 : ///
275 :
276 10 : final (allTxs, nodes) = await Future.wait([
277 5 : for (final walletType in walletTypes)
278 5 : () async {
279 5 : final masterNode = await isolateManager.executeTask(
280 5 : IsolateTask(
281 5 : task: (arg) {
282 5 : return deriveMasterNodeFromSeed(seed: arg.$1, walletPath: arg.$2);
283 : },
284 : argument: (seed, walletType),
285 : ),
286 : );
287 5 : return searchTransactionsForWalletType(
288 : masterNode: masterNode,
289 5 : purpose: walletType.purpose,
290 : addressTypes: addressTypes,
291 : networkType: networkType,
292 : endpoints: endpoints,
293 : cachedNodes: cachedNodes,
294 : isolateManager: isolateManager,
295 : );
296 5 : }.call()
297 10 : ]).then((value) => (
298 15 : value.expand((element) => element.$1).toSet(),
299 15 : value.expand((element) => element.$2).toSet()
300 : ));
301 :
302 5 : isolateManager.dispose();
303 :
304 5 : return fetchMissingUTXOTransactions(
305 : cachedTransactions: cachedTransactions,
306 : cachedNodes: cachedNodes,
307 : allTxs: allTxs,
308 : nodes: nodes,
309 5 : coin: networkType.coin,
310 : networkType: networkType,
311 : addressTypes: addressTypes,
312 : endpoints: endpoints,
313 : watch: watch,
314 : );
315 : }
316 :
317 7 : Future<(Set<ElectrumTransactionInfo>, Set<NodeWithAddress>)>
318 : searchTransactionsForWalletType({
319 : required BipNode masterNode,
320 : required HDWalletPurpose? purpose,
321 : required Iterable<AddressType> addressTypes,
322 : required UTXONetworkType networkType,
323 : required List<(String, int)> endpoints,
324 : required List<NodeWithAddress> cachedNodes,
325 : required IsolateManager isolateManager,
326 : }) async {
327 7 : final receiveTxsFuture = searchForTransactions(
328 : masterNode: masterNode,
329 : chainIndex: EXTERNAL_CHAIN_INDEX,
330 : addressTypes: addressTypes,
331 : walletPurpose: purpose,
332 : networkType: networkType,
333 : endpoints: endpoints,
334 : cachedNodes: cachedNodes,
335 : isolateManager: isolateManager,
336 : );
337 :
338 7 : final changeTxsFuture = searchForTransactions(
339 : masterNode: masterNode,
340 : chainIndex: INTERNAL_CHAIN_INDEX,
341 : addressTypes: addressTypes,
342 : walletPurpose: purpose,
343 : networkType: networkType,
344 : endpoints: endpoints,
345 : cachedNodes: cachedNodes,
346 : isolateManager: isolateManager,
347 : );
348 :
349 14 : final allTxsResult = await Future.wait([receiveTxsFuture, changeTxsFuture]);
350 21 : final allTxs = allTxsResult.expand((element) => element.$1).toSet();
351 21 : final nodes = allTxsResult.expand((element) => element.$2).toSet();
352 :
353 : return (allTxs, nodes);
354 : }
355 :
356 7 : Future<(Set<ElectrumTransactionInfo>, List<NodeWithAddress>)>
357 : searchForTransactions({
358 : required bip32.BIP32 masterNode,
359 : required int chainIndex,
360 : required Iterable<AddressType> addressTypes,
361 : required HDWalletPurpose? walletPurpose,
362 : required UTXONetworkType networkType,
363 : required List<(String, int)> endpoints,
364 : required List<NodeWithAddress> cachedNodes,
365 : required IsolateManager isolateManager,
366 : int emptyLimit = kEmptyLimit,
367 : }) async {
368 14 : final watch = Stopwatch()..start();
369 :
370 : int emptyCount = 0;
371 :
372 : final txs0 = <ElectrumTransactionInfo>{};
373 :
374 7 : final clients = await createClients(
375 : endpoints: endpoints,
376 7 : token: networkType.coin,
377 : );
378 7 : final batchSize = clients.length;
379 7 : final nodes = <NodeWithAddress>[];
380 :
381 7 : Logger.logFetch(
382 7 : "Fetching transactions from $batchSize ElectrumX Nodes",
383 : );
384 :
385 7 : if (batchSize == 0) {
386 0 : Logger.logWarning("No ElectrumX Nodes available for $networkType");
387 : return (txs0, nodes);
388 : }
389 :
390 21 : for (var index = 0; index * batchSize < kAddressesUpperLimit; index++) {
391 28 : final indexes = List.generate(batchSize, (i) => index * batchSize + i);
392 7 : final newIndexes = indexes.where(
393 14 : (i) => !cachedNodes.any(
394 0 : (cNode) => cNode.index == i && cNode.chainIndex == chainIndex,
395 : ),
396 : );
397 :
398 : final List<NodeWithAddress> newNodes;
399 7 : if (newIndexes.isEmpty) {
400 0 : newNodes = [];
401 : } else {
402 7 : newNodes = await isolateManager.executeTask(
403 7 : IsolateTask(
404 14 : task: (arg) => [
405 7 : for (var index in indexes)
406 7 : deriveChildNode(
407 : masterNode: masterNode,
408 : chainIndex: chainIndex,
409 : index: index,
410 : networkType: networkType,
411 : addressTypes: addressTypes,
412 : walletPurpose: walletPurpose,
413 : ),
414 : ],
415 : argument: null,
416 : ),
417 : );
418 : }
419 :
420 7 : final batchNodes = [
421 : ...newNodes,
422 7 : ...cachedNodes.where(
423 0 : (cNode) =>
424 0 : indexes.contains(cNode.index) && cNode.chainIndex == chainIndex,
425 : ),
426 : ];
427 :
428 7 : final futures = [
429 14 : for (int i = 0; i < batchSize; i++)
430 7 : fetchFromRandomElectrumXNode(
431 7 : (client) async {
432 7 : final txsInfos = [
433 7 : for (final type in addressTypes)
434 21 : if (batchNodes[i].addresses.containsKey(type))
435 14 : await client.getHistory(
436 35 : P2Hash(batchNodes[i].addresses[type]!).publicKeyScriptHash,
437 : )
438 : ];
439 :
440 : return txsInfos
441 14 : .expand((e) => e ?? <ElectrumTransactionInfo>{})
442 7 : .whereType<ElectrumTransactionInfo>()
443 7 : .toSet();
444 : },
445 7 : endpoints: networkType.endpoints,
446 7 : client: clients[i],
447 7 : token: networkType.coin,
448 : ),
449 : ];
450 :
451 7 : final results = await Future.wait(futures);
452 : final batchTxs = <ElectrumTransactionInfo>{};
453 :
454 14 : for (int i = 0; i < batchSize; i++) {
455 7 : final (txs, client, error) = results[i];
456 : if (error != null) continue;
457 7 : if (txs == null || txs.isEmpty) continue;
458 7 : if (client != null) clients[i] = client;
459 :
460 7 : batchTxs.addAll(txs);
461 : }
462 :
463 7 : nodes.addAll(batchNodes);
464 :
465 7 : if (batchTxs.isEmpty) {
466 7 : emptyCount += batchSize;
467 7 : if (emptyCount >= kEmptyLimit) {
468 21 : Logger.log("Abort Search after ${index + batchSize} addresses");
469 : break;
470 : }
471 : continue;
472 : }
473 :
474 7 : txs0.addAll(batchTxs);
475 : emptyCount = 0;
476 : }
477 :
478 : ///
479 : /// Disconnect Clients
480 : ///
481 14 : await Future.wait([
482 14 : for (final client in clients) client.disconnect(),
483 : ]);
484 :
485 7 : watch.stop();
486 7 : Logger.logFetch(
487 21 : "Fetched ${txs0.length} TXs in ${watch.elapsed} $chainIndex",
488 : );
489 :
490 : return (txs0, nodes);
491 : }
492 :
493 1 : Future<Amount> estimateFeeForPriority({
494 : required int blocks,
495 : required UTXONetworkType network,
496 : required ElectrumXClient? initalClient,
497 : }) async {
498 1 : final (fee, _, _) = await fetchFromRandomElectrumXNode(
499 2 : (client) => client.estimateFee(blocks: blocks),
500 : client: initalClient,
501 1 : endpoints: network.endpoints,
502 1 : token: network.coin,
503 : );
504 :
505 0 : if (fee == null) throw Exception("Fee estimation failed");
506 :
507 1 : final feePerKb = Amount.convert(value: fee, decimals: 8);
508 :
509 2 : final feePerB = feePerKb / Amount.from(value: 1000, decimals: 0);
510 :
511 : return feePerB;
512 : }
513 :
514 1 : Future<UtxoNetworkFees> getNetworkFees({
515 : required UTXONetworkType network,
516 : double multiplier = 1.0,
517 : }) async {
518 2 : final blockInOneHour = 3600 ~/ network.blockTime;
519 3 : final blocksTillTomorrow = 24 * 3600 ~/ network.blockTime;
520 :
521 1 : final client = await getBestHealthEndpointsWithRetry(
522 1 : endpointPool: network.endpoints,
523 1 : token: network.coin,
524 : max: 1,
525 : min: 1,
526 : )
527 1 : .then(
528 2 : (endpoints) => endpoints.first,
529 : )
530 1 : .then(
531 2 : (endpoint) => createElectrumXClient(
532 : endpoint: endpoint.$1,
533 : port: endpoint.$2,
534 1 : token: network.coin,
535 : ),
536 : );
537 :
538 1 : final next = await estimateFeeForPriority(
539 : blocks: 1,
540 : network: network,
541 : initalClient: client,
542 : );
543 :
544 1 : final second = await estimateFeeForPriority(
545 : blocks: 2,
546 : network: network,
547 : initalClient: client,
548 : );
549 :
550 1 : final hour = await estimateFeeForPriority(
551 : blocks: blockInOneHour,
552 : network: network,
553 : initalClient: client,
554 : );
555 :
556 1 : final day = await estimateFeeForPriority(
557 : blocks: blocksTillTomorrow,
558 : network: network,
559 : initalClient: client,
560 : );
561 :
562 1 : client?.disconnect();
563 :
564 1 : return UtxoNetworkFees(
565 1 : nextBlock: next.multiplyAndCeil(multiplier),
566 1 : secondBlock: second.multiplyAndCeil(multiplier),
567 1 : hour: hour.multiplyAndCeil(multiplier),
568 1 : day: day.multiplyAndCeil(multiplier),
569 : );
570 : }
571 :
572 7 : Future<Iterable<UTXOTransaction>> computeMissingUTXODetails({
573 : required Iterable<ElectrumTransactionInfo> txList,
574 : required Iterable<NodeWithAddress> nodes,
575 : required Iterable<AddressType> addressTypes,
576 : required UTXONetworkType type,
577 : required List<(String, int)> endpoints,
578 : }) async {
579 7 : final coin = type.coin;
580 14 : if (txList.isEmpty) return [];
581 :
582 14 : final watch = Stopwatch()..start();
583 7 : final clients = await createClients(endpoints: endpoints, token: coin);
584 :
585 7 : final batchSize = clients.length;
586 :
587 7 : if (batchSize == 0) {
588 0 : throw Exception(
589 0 : "No clients available for fetching UTXO details. for token: ${coin.symbol}",
590 : );
591 : }
592 :
593 7 : final pool = List<ElectrumTransactionInfo>.from(txList);
594 :
595 7 : final nodeMap = <String, int>{};
596 :
597 7 : Future<Iterable<UTXOTransaction>> fetchFromPool(
598 : ElectrumXClient initalClient,
599 : ) async {
600 7 : final txs = <UTXOTransaction>[];
601 : var client = initalClient;
602 7 : while (pool.isNotEmpty) {
603 7 : final txInfo = pool.removeLast();
604 :
605 7 : final (tx, newClient, _) = await fetchFromRandomElectrumXNode(
606 7 : (client) {
607 7 : return client.getTransaction(
608 7 : txHash: txInfo.hash,
609 : addressTypes: addressTypes,
610 : nodes: nodes,
611 : type: type,
612 : );
613 : },
614 : client: client,
615 : endpoints: endpoints,
616 : token: coin,
617 7 : timeout: Duration(seconds: 5),
618 : );
619 :
620 : if (tx == null) {
621 0 : Logger.logWarning(
622 0 : "Failed to fetch TX ${txInfo.hash} from ${client.host}");
623 0 : txs.add(txInfo.getNotAvailableUTXOTransaction(type.coin));
624 : continue;
625 : }
626 :
627 : if (newClient != null) client = newClient;
628 35 : nodeMap[client.host] = (nodeMap[client.host] ?? 0) + 1;
629 7 : txs.add(tx);
630 : }
631 :
632 : return txs;
633 : }
634 :
635 7 : final futures = [
636 14 : for (final client in clients) fetchFromPool(client),
637 : ];
638 :
639 7 : final results = await Future.wait(futures);
640 :
641 21 : final txs = results.expand((e) => e).toList();
642 :
643 7 : watch.stop();
644 7 : Logger.logFetch(
645 21 : "Fetched ${txs.length} transactions in ${watch.elapsed}",
646 : );
647 :
648 : ///
649 : /// Disconnect Clients
650 : ///
651 14 : await Future.wait([
652 14 : for (final client in clients) client.disconnect(),
653 : ]);
654 :
655 : return txs;
656 : }
657 :
658 : ///
659 : /// Returns a map of UTXOs which belong to us and are unspent and their corresponding transactions
660 : ///
661 4 : Map<ElectrumOutput, UTXOTransaction> extractUTXOs({
662 : required Iterable<UTXOTransaction> txList,
663 : }) {
664 4 : Map<ElectrumOutput, UTXOTransaction> utxoMap = {};
665 8 : for (final tx in txList) {
666 8 : for (final ElectrumOutput output in tx.outputs) {
667 16 : if (output.spent == false && output.belongsToUs == true) {
668 2 : utxoMap[output] = tx;
669 : }
670 : }
671 : }
672 : return utxoMap;
673 : }
674 :
675 : ///
676 : /// Returns a map of UTXOs which belong to us and are unspent and their corresponding transactions
677 : ///
678 0 : Map<ElectrumOutput, UTXOTransaction> extractAllUTXOs({
679 : required Iterable<UTXOTransaction> txList,
680 : bool own = true,
681 : }) {
682 0 : Map<ElectrumOutput, UTXOTransaction> utxoMap = {};
683 0 : for (final tx in txList) {
684 0 : for (final ElectrumOutput output in tx.outputs) {
685 : if (own) {
686 0 : if (output.belongsToUs == true) {
687 0 : utxoMap[output] = tx;
688 : }
689 : } else {
690 0 : utxoMap[output] = tx;
691 : }
692 : }
693 : }
694 : return utxoMap;
695 : }
696 :
697 : ///
698 : /// Returns a map of UTXOs which belong to us and are unspent and their corresponding transactions
699 : ///
700 0 : List<ElectrumOutput> getSpendableOutputs(
701 : {required List<UTXOTransaction> txList}) =>
702 0 : [
703 0 : for (final tx in txList)
704 0 : for (final ElectrumOutput output in tx.outputs)
705 0 : if (output.spent == false && output.belongsToUs == true) output
706 : ];
707 :
708 4 : BigInt computeBalanceFromUTXOs({
709 : required Iterable<UTXOTransaction> txList,
710 : }) {
711 4 : BigInt balance = BigInt.zero;
712 8 : for (final tx in txList) {
713 8 : for (final ElectrumOutput output in tx.outputs) {
714 16 : if (output.spent == false && output.belongsToUs == true) {
715 4 : balance += output.value;
716 : }
717 : }
718 : }
719 : return balance;
720 : }
721 :
722 4 : BigInt computeBalanceFromVisualList({
723 : required Iterable<UTXOTransaction> txList,
724 : }) {
725 4 : BigInt balance = BigInt.zero;
726 8 : for (final tx in txList) {
727 8 : if (tx.transferMethod == TransactionTransferMethod.receive) {
728 8 : balance += tx.value;
729 : }
730 :
731 8 : if (tx.transferMethod == TransactionTransferMethod.send) {
732 8 : balance -= tx.value;
733 : }
734 : }
735 : return balance;
736 : }
737 :
738 8 : TransactionTransferMethod determineSendDirection({
739 : required List<ElectrumInput> inputs,
740 : required List<ElectrumOutput> outputs,
741 : required Iterable<NodeWithAddress> nodes,
742 : required UTXONetworkType type,
743 : required Iterable<AddressType> addressTypes,
744 : }) {
745 : bool anyInputsAreOurs;
746 : try {
747 16 : anyInputsAreOurs = inputs.any((input) {
748 8 : final inputAddress = input.getAddresses(
749 : addressTypes: addressTypes,
750 : networkType: type,
751 : );
752 16 : return nodes.addresses.any(
753 16 : (nodeAddress) => inputAddress.contains(nodeAddress),
754 : );
755 : });
756 : } catch (e) {
757 : anyInputsAreOurs = false;
758 : }
759 :
760 24 : final anyOutputsAreOurs = outputs.any((output) => output.belongsToUs);
761 :
762 16 : final outputsHaveReceive = outputs.any((output) {
763 8 : final outputAddresses = output.getAddresses(
764 : addressTypes: addressTypes,
765 : networkType: type,
766 : );
767 24 : return nodes.receiveNodes.addresses.any(
768 16 : (nodeAddress) => outputAddresses.contains(nodeAddress),
769 : );
770 : });
771 :
772 16 : final outputsHaveChange = outputs.any((output) {
773 8 : final outputAddresses = output.getAddresses(
774 : addressTypes: addressTypes,
775 : networkType: type,
776 : );
777 24 : return nodes.changeNodes.addresses.any(
778 16 : (nodeAddress) => outputAddresses.contains(nodeAddress),
779 : );
780 : });
781 :
782 : return switch ((anyInputsAreOurs, anyOutputsAreOurs)) {
783 16 : (true, true) when outputsHaveReceive => TransactionTransferMethod.own,
784 14 : (true, true) when outputsHaveChange && outputs.length == 1 =>
785 : TransactionTransferMethod.own,
786 : (true, true) => TransactionTransferMethod.send,
787 6 : (true, false) => TransactionTransferMethod.send,
788 8 : (false, true) => TransactionTransferMethod.receive,
789 : _ => TransactionTransferMethod.unknown,
790 : };
791 : }
792 :
793 8 : List<ElectrumOutput> findOurOwnCoins(
794 : List<ElectrumOutput> outputs,
795 : Iterable<NodeWithAddress> nodes,
796 : Iterable<AddressType> addressTypes,
797 : UTXONetworkType type,
798 : ) {
799 8 : final outputs0 = <ElectrumOutput>[];
800 16 : for (final vout in outputs) {
801 8 : final outputAddresses = vout.getAddresses(
802 : networkType: type,
803 : addressTypes: addressTypes,
804 : );
805 :
806 8 : final node = nodes.singleWhereOrNull(
807 24 : (node) => node.addressesList.any(
808 16 : (nodeAddress) => outputAddresses.contains(nodeAddress),
809 : ),
810 : );
811 : final belongsToUs = node != null;
812 8 : outputs0.add(
813 8 : vout.copyWith(
814 : belongsToUs: belongsToUs,
815 : node: node,
816 : ),
817 : );
818 : }
819 : return outputs0;
820 : }
821 :
822 8 : (BigInt, BigInt) determineTransactionValue(
823 : List<ElectrumOutput> outputs,
824 : TransactionTransferMethod transferMethod,
825 : Iterable<NodeWithAddress> nodes,
826 : UTXONetworkType type,
827 : ) {
828 : final ourValue = switch (transferMethod) {
829 16 : TransactionTransferMethod.receive => outputs.fold(
830 8 : BigInt.zero,
831 8 : (prev, output) {
832 8 : if (output.belongsToUs) {
833 16 : return prev + output.value;
834 : }
835 : return prev;
836 : },
837 : ),
838 7 : TransactionTransferMethod.own => outputs
839 7 : .singleWhereOrNull(
840 21 : (output) => output.node is ReceiveNode,
841 : )
842 7 : ?.value ??
843 6 : BigInt.from(-1),
844 14 : TransactionTransferMethod.send => outputs.fold(
845 7 : BigInt.zero,
846 7 : (prev, output) {
847 7 : if (!output.belongsToUs) {
848 14 : return prev + output.value;
849 : }
850 : return prev;
851 : },
852 : ),
853 15 : TransactionTransferMethod.unknown => BigInt.from(-1),
854 : };
855 8 : final totalValue = outputs.fold(
856 8 : BigInt.zero,
857 24 : (prev, output) => prev + output.value,
858 : );
859 :
860 : return (ourValue, totalValue);
861 : }
862 :
863 8 : String? determineTransactionTarget(
864 : List<ElectrumOutput> outputs,
865 : TransactionTransferMethod transferMethod,
866 : UTXONetworkType type,
867 : AddressType addressType,
868 : ) {
869 8 : final mainOutput = _findMainOutput(outputs, transferMethod);
870 8 : return mainOutput?.getAddress(type, addressType: addressType);
871 : }
872 :
873 8 : ElectrumOutput? _findMainOutput(
874 : List<ElectrumOutput> outputs,
875 : TransactionTransferMethod transferMethod,
876 : ) {
877 : final voutListFull = outputs;
878 :
879 : final isMainOutputOurOwn =
880 8 : transferMethod == TransactionTransferMethod.receive ||
881 7 : transferMethod == TransactionTransferMethod.own;
882 : final voutList =
883 40 : voutListFull.where((v) => v.belongsToUs == isMainOutputOurOwn).toList();
884 8 : if (voutList.isEmpty) {
885 : return null;
886 : }
887 :
888 8 : ElectrumOutput highestVout = voutList[0];
889 16 : for (final v in voutList) {
890 8 : final valueCoin = v.value;
891 16 : if (valueCoin > highestVout.value) {
892 : highestVout = v;
893 : }
894 : }
895 : return highestVout;
896 : }
|