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