Line data Source code
1 : import 'package:walletkit_dart/src/crypto/utxo/utils/endpoint_utils.dart';
2 : import 'package:walletkit_dart/src/crypto/utxo/entities/electrum_peer.dart';
3 : import 'package:walletkit_dart/src/crypto/utxo/entities/transactions/electrum_transaction.dart';
4 : import 'package:walletkit_dart/src/crypto/utxo/utils/utxo_in_memory_cache.dart';
5 : import 'package:walletkit_dart/walletkit_dart.dart';
6 :
7 : import 'json_rpc_client.dart';
8 :
9 : class ElectrumXClient {
10 : final TcpJsonRpcClient _client;
11 :
12 13 : ElectrumXClient(this._client);
13 :
14 9 : Future<int?> getCurrentBlock() async {
15 18 : final response = await _client.sendRequest(
16 18 : {"method": "blockchain.headers.subscribe", "params": []},
17 : );
18 9 : if (response == null || response is! Json) return null;
19 9 : final block = response["height"];
20 : return block;
21 : }
22 :
23 0 : Future<List<ElectrumPeer>?> fetchPeers() async {
24 0 : final response = await _client.sendRequest(
25 0 : {"method": "server.peers.subscribe", "params": []},
26 : );
27 0 : if (response == null || response is! JsonList) return null;
28 :
29 0 : return [
30 0 : for (var [_, String host, [String version, ...args]] in response)
31 0 : if (args.whereType<String>().any((args) => args.startsWith("t")))
32 0 : ElectrumPeer.fromJson(
33 : host: host,
34 : version: version,
35 : args: args,
36 : )
37 : ];
38 : }
39 :
40 8 : Future<UTXOTransaction> getTransaction({
41 : required String txHash,
42 : required Iterable<AddressType> addressTypes,
43 : required UTXONetworkType type,
44 : required Iterable<NodeWithAddress> nodes,
45 : bool verbose = true,
46 : }) async {
47 16 : final cache = getUtxoInMemoryCache(type.coin);
48 8 : final cachedTx = cache.getTx(txHash: txHash);
49 : if (cachedTx != null) {
50 : return cachedTx;
51 : }
52 16 : assert(nodes.isNotEmpty, "Nodes must not be empty");
53 :
54 8 : Future<Json> blockchainTransactionGet(String txHash) async {
55 16 : final response = await _client.sendRequest(
56 8 : {
57 : "method": "blockchain.transaction.get",
58 8 : "params": [txHash, verbose]
59 : },
60 : );
61 8 : if (response == null || response is! Json) {
62 0 : throw Exception("Could not fetch transaction $txHash from ElectrumX");
63 : }
64 :
65 : return response;
66 : }
67 :
68 : ///
69 : /// Get Initial Transaction
70 : ///
71 : try {
72 8 : final mainTxJson = await blockchainTransactionGet(txHash);
73 :
74 : ///
75 : /// Get Transaction Inputs
76 : ///
77 : // final inputs = <(String, int)>[];
78 : // if (mainTxJson case {"vin": JsonList _inputs}) {
79 : // for (final {"txid": String txid, "vout": int index} in _inputs) {
80 : // inputs.add((txid, index));
81 : // }
82 : // }
83 : // final outputsFutures = [
84 : // for (final (hash, index) in inputs)
85 : // blockchainTransactionGet(hash).then((tx) {
86 : // if (tx case {"vout": JsonList _outputs}) {
87 : // return ElectrumOutput.fromJson(_outputs[index]);
88 : // }
89 : // })
90 : // ];
91 :
92 : // Logger.logWarning("Fetching ${outputsFutures.length} outputs for $txHash");
93 :
94 : // final outputs = await Future.wait(outputsFutures);
95 :
96 8 : final tx = UTXOTransaction.create(
97 : json: mainTxJson,
98 : addressTypes: addressTypes,
99 : type: type,
100 : nodes: nodes,
101 8 : spentOutputs: [],
102 : );
103 :
104 : /// P2SH Addresses ?
105 :
106 8 : cache.insertTxIfConfirmed(tx);
107 : return tx;
108 : } catch (e) {
109 4 : throw Exception("Could not fetch transaction $txHash from ElectrumX");
110 : }
111 : }
112 :
113 4 : Future<UTXOTransaction> getTransactionForSimulation({
114 : required String txHash,
115 : required Iterable<AddressType> addressTypes,
116 : required UTXONetworkType type,
117 : required Iterable<NodeWithAddress> nodes,
118 : bool verbose = true,
119 : }) async {
120 4 : Future<Json> blockchainTransactionGet(String txHash) async {
121 8 : final response = await _client.sendRequest(
122 4 : {
123 : "method": "blockchain.transaction.get",
124 4 : "params": [txHash, verbose]
125 : },
126 : );
127 4 : if (response == null || response is! Json) {
128 0 : throw Exception("Could not fetch transaction $txHash from ElectrumX");
129 : }
130 :
131 : return response;
132 : }
133 :
134 : ///
135 : /// Get Initial Transaction
136 : ///
137 4 : final mainTxJson = await blockchainTransactionGet(txHash);
138 :
139 : ///
140 : /// Get Transaction Inputs
141 : ///
142 4 : final inputs = <(String, int)>[];
143 8 : if (mainTxJson case {"vin": JsonList _inputs}) {
144 8 : for (final input in _inputs) {
145 17 : if (input case {"txid": String txid, "vout": int index})
146 3 : inputs.add((txid, index));
147 : }
148 : }
149 4 : final outputs = [
150 4 : for (final (hash, index) in inputs)
151 12 : await blockchainTransactionGet(hash).then((tx) {
152 6 : if (tx case {"vout": JsonList _outputs}) {
153 6 : return ElectrumOutput.fromJson(_outputs[index]);
154 : }
155 : })
156 4 : ].nonNulls;
157 :
158 4 : return UTXOTransaction.create(
159 : json: mainTxJson,
160 : addressTypes: addressTypes,
161 : type: type,
162 : nodes: nodes,
163 : spentOutputs: outputs,
164 : );
165 : }
166 :
167 : // do not use getRaw because it does not use an in-memory-cache !
168 6 : Future<String?> getRaw(
169 : String txHash, {
170 : bool verbose = false,
171 : }) async {
172 12 : final response = await _client.sendRequest(
173 6 : {
174 : "method": "blockchain.transaction.get",
175 6 : "params": [txHash, verbose]
176 : },
177 : );
178 6 : return response.toString();
179 : }
180 :
181 9 : Future<Set<ElectrumTransactionInfo>?> getHistory(
182 : String publicKeyScriptHash,
183 : ) async {
184 18 : final response = await _client.sendRequest(
185 9 : {
186 : "method": "blockchain.scripthash.get_history",
187 9 : "params": [publicKeyScriptHash]
188 : },
189 : );
190 9 : if (response == null || response is! List<dynamic>) return null;
191 : return {
192 18 : for (final json in response) ElectrumTransactionInfo.fromJson(json),
193 : };
194 : }
195 :
196 0 : Future<String> broadcastTransaction({required String rawTxHex}) async {
197 0 : final response = await _client.sendRawRequest(
198 0 : {
199 : "method": "blockchain.transaction.broadcast",
200 0 : "params": [rawTxHex]
201 : },
202 : );
203 : return response;
204 : }
205 :
206 1 : Future<double> estimateFee({required int blocks}) async {
207 2 : final response = await _client.sendRequest(
208 1 : {
209 : "method": "blockchain.estimatefee",
210 1 : "params": [blocks]
211 : },
212 : );
213 : final fee = response as double?;
214 1 : if (fee == null || fee == 0) throw Exception("Fee estimation failed");
215 : return fee;
216 : }
217 :
218 13 : Future<bool> disconnect() async {
219 26 : await _client.disconnect();
220 : return true;
221 : }
222 :
223 21 : String get host => _client.host;
224 : }
225 :
226 2 : Future<String> fetchRawTxByHash(
227 : String hash,
228 : UTXONetworkType networkType,
229 : ) async {
230 2 : final (result, _client, _) = await fetchFromRandomElectrumXNode(
231 2 : (client) async {
232 2 : return await client.getRaw(hash);
233 : },
234 : client: null,
235 2 : endpoints: networkType.endpoints,
236 2 : token: networkType.coin,
237 2 : timeout: Duration(seconds: 20),
238 : );
239 2 : await _client?.disconnect();
240 4 : if (result == null || result.isEmpty || result == "null") {
241 0 : throw Exception("No result for $hash");
242 : }
243 :
244 : return result;
245 : }
|