Line data Source code
1 : import 'dart:async';
2 : import 'dart:typed_data';
3 : import 'package:walletkit_dart/src/common/logger.dart';
4 : import 'package:walletkit_dart/src/crypto/evm/entities/block_number.dart';
5 : import 'package:walletkit_dart/src/crypto/evm/repositories/rpc/queued_rpc_interface.dart';
6 : import 'package:walletkit_dart/src/utils/int.dart';
7 : import 'package:walletkit_dart/walletkit_dart.dart';
8 :
9 : const type2Multiplier = 1.5;
10 :
11 : final class EvmRpcInterface {
12 : final EVMNetworkType type;
13 : final Map<int, int> blockTimestampCache = {};
14 : final Map<String, ConfirmationStatus> txStatusCache = {};
15 : final RpcManager _manager;
16 :
17 0 : Future<void> get refreshFuture => _manager.refreshFuture;
18 :
19 : ///
20 : /// [clients] - A list of clients to use for the manager
21 : /// [useQueuedManager] - If true, the manager will use a QueuedRpcManager and requests will be queued
22 : /// [awaitRefresh] - If true, the manager will wait for the clients to be refreshed before performing a task
23 : /// [refreshIntervall] - The rate at which the clients are refreshed if null the clients will only be refreshed once
24 : /// [eagerError] - If true a task will throw the first error it encounters, if false it will try all clients before throwing an error
25 : ///
26 5 : EvmRpcInterface({
27 : bool useQueuedManager = true,
28 : bool awaitRefresh = true,
29 : Duration? refreshIntervall,
30 : bool eagerError = false,
31 : RefreshType refreshType = RefreshType.onTask,
32 : required List<EvmRpcClient> clients,
33 : required this.type,
34 : }) : _manager = useQueuedManager
35 4 : ? QueuedRpcManager(
36 : awaitRefresh: awaitRefresh,
37 : clientRefreshRate: refreshIntervall,
38 : allClients: clients,
39 : eagerError: eagerError,
40 : refreshType: refreshType,
41 : )
42 3 : : SimpleRpcManager(
43 : awaitRefresh: awaitRefresh,
44 : clientRefreshRate: refreshIntervall,
45 : allClients: clients,
46 : eagerError: eagerError,
47 : refreshType: refreshType,
48 : );
49 :
50 5 : Future<T> performTask<T>(
51 : Future<T> Function(EvmRpcClient client) task, {
52 : Duration timeout = const Duration(seconds: 30),
53 : int? maxTries,
54 : }) =>
55 15 : _manager.performTask(task, timeout: timeout, maxTries: maxTries).then(
56 5 : (valueOrError) {
57 5 : return valueOrError.when(
58 10 : value: (value) => value.value,
59 0 : error: (error) => throw Exception(error),
60 : );
61 : },
62 : );
63 :
64 0 : Future<R> performTaskForClients<T, R>(
65 : Future<T> Function(EvmRpcClient) task, {
66 : required R Function(
67 : List<ValueOrError<T, EvmRpcClient>> results,
68 : ) consilidate,
69 : Duration timeout = const Duration(seconds: 30),
70 : int maxTriesPerClient = 2,
71 : int minClients = 2,
72 : int? maxClients,
73 : bool enforceParallel = false,
74 : }) =>
75 0 : _manager.performTaskForClients(
76 : task,
77 : consilidate: consilidate,
78 : timeout: timeout,
79 : maxTriesPerClient: maxTriesPerClient,
80 : maxClients: maxClients,
81 : minClients: minClients,
82 : enforceParallel: enforceParallel,
83 : );
84 :
85 : ///
86 : /// eth_call
87 : ///
88 3 : Future<String> call({
89 : String? sender,
90 : required String contractAddress,
91 : required Uint8List data,
92 : BlockNum? atBlock,
93 : }) {
94 3 : return performTask(
95 6 : (client) => client.call(
96 : sender: sender,
97 : contractAddress: contractAddress,
98 : data: data,
99 : atBlock: atBlock,
100 : ),
101 : );
102 : }
103 :
104 : ///
105 : /// Fetch Balance
106 : ///
107 2 : Future<Amount> fetchBalance({
108 : required String address,
109 : }) async {
110 6 : final balance = await performTask((client) => client.getBalance(address));
111 2 : return Amount(
112 : value: balance,
113 6 : decimals: type.coin.decimals,
114 : );
115 : }
116 :
117 : ///
118 : /// Fetch Token Balance
119 : ///
120 1 : Future<Amount> fetchTokenBalance(
121 : String address,
122 : ERC20Entity token,
123 : ) async {
124 1 : final erc20Contract = ERC20Contract(
125 1 : contractAddress: token.contractAddress,
126 : rpc: this,
127 : );
128 1 : final balance = await erc20Contract.getBalance(address);
129 2 : return Amount(value: balance, decimals: token.decimals);
130 : }
131 :
132 : ///
133 : /// Fetch Balance of ERC1155 Token
134 : ///
135 1 : Future<Amount> fetchERC1155BalanceOfToken({
136 : required String address,
137 : required BigInt tokenID,
138 : required String contractAddress,
139 : }) async {
140 1 : final erc1155Contract = ERC1155Contract(
141 : contractAddress: contractAddress,
142 : rpc: this,
143 : );
144 1 : final balance = await erc1155Contract.balanceOf(
145 : address: address,
146 : tokenID: tokenID,
147 : );
148 :
149 1 : return Amount(value: balance, decimals: 0);
150 : }
151 :
152 : ///
153 : /// Fetch Batch Balance of ERC1155 Tokens
154 : ///
155 1 : Future<List<BigInt>> fetchERC1155BatchBalanceOfTokens({
156 : required List<String> accounts,
157 : required List<BigInt> tokenIDs,
158 : required String contractAddress,
159 : }) async {
160 1 : final erc1155Contract = ERC1155Contract(
161 : contractAddress: contractAddress,
162 : rpc: this,
163 : );
164 :
165 1 : final balances = await erc1155Contract.balanceOfBatch(
166 : accounts: accounts,
167 : tokenIDs: tokenIDs,
168 : );
169 :
170 : return balances;
171 : }
172 :
173 : ///
174 : /// Fetch Uri of ERC115 Token
175 : ///
176 1 : Future<String> fetchERC1155UriOfToken({
177 : required BigInt tokenID,
178 : required String contractAddress,
179 : }) async {
180 1 : final erc1155Contract = ERC1155Contract(
181 : contractAddress: contractAddress,
182 : rpc: this,
183 : );
184 :
185 1 : final uri = await erc1155Contract.getUri(
186 : tokenID: tokenID,
187 : );
188 :
189 : return uri;
190 : }
191 :
192 1 : Future<(Amount, int)> estimateNetworkFees({
193 : required String recipient,
194 : required String sender,
195 : required Uint8List? data,
196 : required BigInt? value,
197 : }) async {
198 1 : final gasPrice = await getGasPrice();
199 1 : final gasLimit = await estimateGasLimit(
200 : recipient: recipient,
201 : sender: sender,
202 : data: data,
203 : value: value,
204 : gasPrice: gasPrice,
205 : );
206 :
207 1 : return (Amount(value: gasPrice, decimals: 18), gasLimit);
208 : }
209 :
210 : ///
211 : /// Get Gas Price
212 : ///
213 1 : Future<BigInt> getGasPrice() async {
214 1 : return await performTask(
215 2 : (client) => client.getGasPrice(),
216 : );
217 : }
218 :
219 : ///
220 : /// Get Gas Price
221 : ///
222 3 : Future<Amount> getGasPriceAmount() => getGasPrice().then(
223 2 : (value) => Amount(value: value, decimals: 18),
224 : );
225 :
226 : ///
227 : /// Get Transaction Count (Nonce)
228 : ///
229 0 : Future<BigInt> getTransactionCount(String address) async {
230 0 : return await performTask(
231 0 : (client) => client.getTransactionCount(address),
232 : );
233 : }
234 :
235 : ///
236 : /// Get Transaction By Hash
237 : ///
238 1 : Future<RawEvmTransaction> getTransactionByHash(String hash) async {
239 1 : return await performTask(
240 2 : (client) => client.getTransactionByHash(hash),
241 : );
242 : }
243 :
244 : ///
245 : /// Send Currency
246 : ///
247 0 : Future<String> sendCoin({
248 : required TransferIntent<EvmFeeInformation> intent,
249 : required String from,
250 : required Uint8List privateKey,
251 : }) async {
252 0 : final tx = await buildTransaction(
253 : sender: from,
254 0 : recipient: intent.recipient,
255 : privateKey: privateKey,
256 0 : feeInfo: intent.feeInfo,
257 0 : data: intent.encodedMemo,
258 0 : value: intent.amount.value,
259 0 : accessList: intent.accessList,
260 : );
261 0 : final balance = await fetchBalance(address: toChecksumAddress(from)).then(
262 0 : (amount) => amount.value,
263 : );
264 :
265 0 : if (balance < tx.gasFee + tx.value) {
266 0 : throw WKFailure("Insufficient funds to pay native gas fee");
267 : }
268 :
269 0 : return await sendRawTransaction(tx.serialized.toHex);
270 : }
271 :
272 : ///
273 : /// Send ERC20 Token
274 : ///
275 0 : Future<String> sendERC20Token({
276 : required TransferIntent<EvmFeeInformation> intent,
277 : required String from,
278 : required Uint8List privateKey,
279 : }) async {
280 0 : assert(intent.token is ERC20Entity);
281 0 : assert(intent.memo == null);
282 :
283 0 : final erc20 = intent.token as ERC20Entity;
284 0 : final tokenContractAddress = erc20.contractAddress;
285 :
286 0 : final erc20Contract = ERC20Contract(
287 : contractAddress: tokenContractAddress,
288 : rpc: this,
289 : );
290 :
291 0 : return erc20Contract.transfer(
292 : privateKey: privateKey,
293 : sender: from,
294 0 : to: intent.recipient,
295 0 : value: intent.amount.value,
296 0 : feeInfo: intent.feeInfo,
297 0 : accessList: intent.accessList,
298 : );
299 : }
300 :
301 : ///
302 : /// Send ERC1155 Token
303 : ///
304 0 : Future<String> sendERC1155Token({
305 : required TransferIntent<EvmFeeInformation> intent,
306 : required String contractAddress,
307 : required BigInt tokenID,
308 : required String from,
309 : required Uint8List privateKey,
310 : }) async {
311 0 : final erc1155Contract = ERC1155Contract(
312 : contractAddress: contractAddress,
313 : rpc: this,
314 : );
315 :
316 0 : return erc1155Contract.safeTransferFrom(
317 : sender: from,
318 0 : to: intent.recipient,
319 : tokenID: tokenID,
320 0 : amount: intent.amount.value,
321 : privateKey: privateKey,
322 0 : feeInfo: intent.feeInfo,
323 0 : accessList: intent.accessList,
324 : );
325 : }
326 :
327 1 : Future<Amount> getPriorityFee() async {
328 1 : final priorityFee = await performTask(
329 2 : (client) => client.getPriorityFee(),
330 : );
331 :
332 1 : return Amount(value: priorityFee, decimals: 9);
333 : }
334 :
335 1 : Future<EvmType2GasPrice> getType2GasPrice() async {
336 1 : final maxFeePerGas = await getGasPriceAmount();
337 1 : final maxPriorityFeePerGas = await getPriorityFee();
338 :
339 1 : return EvmType2GasPrice(
340 2 : maxFeePerGas: maxFeePerGas.multiplyAndCeil(type2Multiplier) + maxPriorityFeePerGas,
341 : maxPriorityFeePerGas: maxPriorityFeePerGas,
342 : );
343 : }
344 :
345 1 : Future<(int gasLimit, EvmGasPrice gasPrice)> fetchNetworkFees({
346 : EvmFeeInformation? existing,
347 : required String recipient,
348 : required String sender,
349 : required Uint8List? data,
350 : required BigInt? value,
351 : }) async {
352 0 : var gasLimit = existing?.gasLimit;
353 : try {
354 1 : gasLimit ??= await estimateGasLimit(
355 : recipient: recipient,
356 : sender: sender,
357 : data: data,
358 : value: value,
359 : );
360 : } catch (e) {
361 0 : Logger.logError(e, hint: "Gas estimation failed");
362 :
363 : // Only Debug
364 0 : assert(true, "Gas estimation failed");
365 :
366 0 : gasLimit = 1E6.toInt();
367 : }
368 :
369 0 : final EvmGasPrice gasPrice = switch (existing?.gasPrice) {
370 1 : EvmLegacyGasPrice feeInfo => feeInfo,
371 1 : EvmType2GasPrice feeInfo => feeInfo,
372 3 : null when type.useEIP1559 => await getType2GasPrice(),
373 0 : null => EvmLegacyGasPrice(
374 0 : gasPrice: await getGasPriceAmount(),
375 : ),
376 : };
377 :
378 : return (gasLimit, gasPrice);
379 : }
380 :
381 : ///
382 : /// Used to create a raw Transactions
383 : /// Fetches the gasPrice and gasLimit from the network
384 : /// Fetches the nonce from the network
385 : /// If Transaction Type is not provided, it will use Legacy
386 : ///
387 0 : Future<RawEvmTransaction> buildUnsignedTransaction({
388 : required String sender,
389 : required String recipient,
390 : required EvmFeeInformation? feeInfo,
391 : required Uint8List? data,
392 : required BigInt? value,
393 : List<AccessListItem>? accessList,
394 : }) async {
395 0 : final (gasLimit, gasPrice) = await fetchNetworkFees(
396 : recipient: recipient,
397 : sender: sender,
398 : data: data,
399 : value: value,
400 : existing: feeInfo,
401 : );
402 :
403 0 : final nonce = await performTask(
404 0 : (client) => client.getTransactionCount(sender),
405 : );
406 :
407 : return switch (gasPrice) {
408 0 : EvmType2GasPrice fee => RawEVMTransactionType2.unsigned(
409 : nonce: nonce,
410 0 : maxFeePerGas: fee.maxFeePerGas.value,
411 0 : maxPriorityFeePerGas: fee.maxPriorityFeePerGas.value,
412 0 : gasLimit: gasLimit.toBI,
413 : to: recipient,
414 0 : value: value ?? BigInt.zero,
415 0 : data: data ?? Uint8List(0),
416 0 : accessList: accessList ?? [],
417 0 : chainId: type.chainId,
418 : ),
419 0 : EvmLegacyGasPrice fee => accessList != null
420 0 : ? RawEVMTransactionType1.unsigned(
421 : nonce: nonce,
422 0 : gasPrice: fee.gasPrice.value,
423 0 : gasLimit: gasLimit.toBI,
424 : to: recipient,
425 0 : value: value ?? BigInt.zero,
426 0 : data: data ?? Uint8List(0),
427 : accessList: accessList,
428 0 : chainId: type.chainId,
429 : )
430 0 : : RawEVMTransactionType0.unsigned(
431 : nonce: nonce,
432 0 : gasPrice: fee.gasPrice.value,
433 0 : gasLimit: gasLimit.toBI,
434 : to: recipient,
435 0 : value: value ?? BigInt.zero,
436 0 : data: data ?? Uint8List(0),
437 : ),
438 : };
439 : }
440 :
441 : ///
442 : /// Used to create a raw Transactions
443 : /// Fetches the gasPrice and gasLimit from the network
444 : /// Fetches the nonce from the network
445 : /// Signs the transaction
446 : ///
447 0 : Future<RawEvmTransaction> buildTransaction({
448 : required String sender,
449 : required String recipient,
450 : required Uint8List privateKey,
451 : required EvmFeeInformation? feeInfo,
452 : required Uint8List? data,
453 : required BigInt? value,
454 : List<AccessListItem>? accessList,
455 : }) async {
456 0 : final unsignedTx = await buildUnsignedTransaction(
457 : sender: sender,
458 : recipient: recipient,
459 : feeInfo: feeInfo,
460 : data: data,
461 : value: value,
462 : accessList: accessList,
463 : );
464 :
465 0 : final signature = Signature.createSignature(
466 : switch (unsignedTx) {
467 0 : RawEVMTransactionType0() => unsignedTx.serializedUnsigned(type.chainId),
468 0 : RawEVMTransactionType1() => unsignedTx.serializedUnsigned,
469 0 : RawEVMTransactionType2() => unsignedTx.serializedUnsigned,
470 : },
471 : txType: switch (unsignedTx) {
472 0 : RawEVMTransactionType0() => TransactionType.Legacy,
473 0 : RawEVMTransactionType1() => TransactionType.Type1,
474 0 : RawEVMTransactionType2() => TransactionType.Type2,
475 : },
476 : privateKey,
477 0 : chainId: type.chainId,
478 : );
479 :
480 0 : final signedTx = unsignedTx.addSignature(signature);
481 :
482 : return signedTx;
483 : }
484 :
485 0 : Future<String> sendRawTransaction(String serializedTransactionHex) {
486 0 : serializedTransactionHex = serializedTransactionHex.startsWith("0x")
487 : ? serializedTransactionHex
488 0 : : "0x$serializedTransactionHex";
489 0 : return performTaskForClients(
490 0 : (client) => client.sendRawTransaction(serializedTransactionHex),
491 : minClients: 1,
492 : maxTriesPerClient: 1,
493 : maxClients: 5,
494 : enforceParallel: true,
495 0 : consilidate: (resultsWithErrors) {
496 : final results =
497 0 : resultsWithErrors.whereType<Value<String, EvmRpcClient>>().map((v) => v.value);
498 :
499 0 : if (results.isEmpty) {
500 0 : throw Exception(
501 0 : "No client was able to send the transaction: ${results}",
502 : );
503 : }
504 :
505 0 : final hashMap = results.fold<Map<String, int>>(
506 0 : {},
507 0 : (acc, hash) {
508 0 : acc[hash] = (acc[hash] ?? 0) + 1;
509 : return acc;
510 : },
511 : );
512 :
513 0 : final hash = hashMap.entries.reduce(
514 0 : (a, b) => a.value > b.value ? a : b,
515 : );
516 :
517 0 : return hash.key;
518 : },
519 : );
520 : }
521 :
522 0 : Future<String> buildAndBroadcastTransaction({
523 : required String sender,
524 : required String recipient,
525 : required Uint8List privateKey,
526 : required EvmFeeInformation? feeInfo,
527 : required Uint8List? data,
528 : required BigInt? value,
529 : List<AccessListItem>? accessList,
530 : }) async {
531 0 : final signedTx = await buildTransaction(
532 : sender: sender,
533 : recipient: recipient,
534 : privateKey: privateKey,
535 : feeInfo: feeInfo,
536 : data: data,
537 : value: value,
538 : accessList: accessList,
539 : );
540 :
541 0 : final result = await sendRawTransaction(signedTx.serialized.toHex);
542 :
543 : return result;
544 : }
545 :
546 0 : Future<String> readContract({
547 : required String contractAddress,
548 : required LocalContractFunctionWithValues function,
549 : }) async {
550 : assert(
551 0 : function.stateMutability == StateMutability.view ||
552 0 : function.stateMutability == StateMutability.pure,
553 : "Invalid function",
554 : );
555 :
556 0 : final data = function.buildDataField();
557 :
558 0 : return await call(
559 : contractAddress: contractAddress,
560 : data: data,
561 : );
562 : }
563 :
564 : ///
565 : /// Interact with Contract
566 : ///
567 0 : Future<String> interactWithContract({
568 : required String contractAddress,
569 : required LocalContractFunctionWithValues function,
570 : required String sender,
571 : required Uint8List privateKey,
572 : required EvmFeeInformation? feeInfo,
573 : BigInt? value,
574 : }) async {
575 0 : final valid = switch ((function.stateMutability, value)) {
576 0 : (StateMutability.nonpayable, BigInt? value) =>
577 0 : value == null || value == BigInt.zero, // If nonpayable, value must be 0 or null
578 0 : (StateMutability.payable, BigInt? value) =>
579 0 : value != null && value != BigInt.zero, // If payable, value must be set
580 : _ => false,
581 : };
582 0 : assert(valid, "Invalid value for state mutability of function");
583 :
584 0 : final data = function.buildDataField();
585 :
586 0 : return await buildAndBroadcastTransaction(
587 : sender: sender,
588 : recipient: contractAddress,
589 : privateKey: privateKey,
590 : feeInfo: feeInfo,
591 : data: data,
592 0 : value: value ?? BigInt.zero,
593 : );
594 : }
595 :
596 1 : Future<int> estimateGasLimit({
597 : required String sender,
598 : required String recipient,
599 : Uint8List? data,
600 : BigInt? value,
601 : BigInt? gasPrice,
602 : }) async {
603 2 : final dataHex = data != null ? "0x${data.toHex}" : null;
604 :
605 1 : return await performTask(
606 1 : (client) => client
607 1 : .estimateGasLimit(
608 : from: sender,
609 : to: recipient,
610 : data: dataHex,
611 : amount: value,
612 : gasPrice: gasPrice,
613 : )
614 1 : .then(
615 2 : (value) => value.toInt(),
616 : ),
617 : );
618 : }
619 :
620 : ///
621 : /// Send ERC721
622 : ///
623 0 : Future<String> sendERC721Nft({
624 : required String recipient,
625 : required String from,
626 : required int tokenId,
627 : required String contractAddress,
628 : required Uint8List privateKey,
629 : }) async {
630 0 : final function = LocalContractFunctionWithValues(
631 : name: "transferFrom",
632 0 : parameters: [
633 0 : FunctionParamWithValue(
634 : name: "from",
635 0 : type: FunctionParamAddress(),
636 : value: from,
637 : ),
638 0 : FunctionParamWithValue(
639 : name: "to",
640 0 : type: FunctionParamAddress(),
641 : value: recipient,
642 : ),
643 0 : FunctionParamWithValue(
644 : name: "tokenId",
645 0 : type: FunctionParamInt(),
646 : value: tokenId,
647 : ),
648 : ],
649 : stateMutability: StateMutability.nonpayable,
650 0 : outputTypes: [],
651 : );
652 :
653 0 : return await interactWithContract(
654 : contractAddress: contractAddress,
655 : function: function,
656 : sender: from,
657 : privateKey: privateKey,
658 : feeInfo: null,
659 : );
660 : }
661 :
662 0 : Future<ConfirmationStatus> getConfirmationStatus(String hash) async {
663 0 : if (txStatusCache[hash] == null || txStatusCache[hash] == ConfirmationStatus.pending) {
664 0 : final json = await performTask(
665 0 : (client) => client.getTransactionReceipt(hash),
666 : );
667 0 : txStatusCache[hash] = _confirmationStatusFromJson(json ?? {});
668 : }
669 0 : return txStatusCache[hash]!;
670 : }
671 :
672 0 : ConfirmationStatus _confirmationStatusFromJson(Json json) {
673 : if (json
674 : case {
675 0 : "status": String status_s,
676 : }) {
677 0 : final status = status_s.toBigIntOrNull;
678 0 : if (status == null) throw Exception('Could not parse status');
679 0 : if (status == BigInt.from(0)) return ConfirmationStatus.failed;
680 0 : if (status == BigInt.from(1)) return ConfirmationStatus.confirmed;
681 : }
682 :
683 : return ConfirmationStatus.pending;
684 : }
685 :
686 : ///
687 : /// Get Current Block
688 : ///
689 0 : Future<Json> getCurrentBlock() async {
690 0 : final blockNumber = await getBlockNumber();
691 0 : return await performTask(
692 0 : (client) => client.getBlockByNumber(blockNumber),
693 : );
694 : }
695 :
696 : ///
697 : /// Get Block Number
698 : ///
699 1 : Future<int> getBlockNumber() async {
700 1 : return await performTask(
701 2 : (client) => client.getBlockNumber(),
702 : );
703 : }
704 :
705 0 : Future<bool> waitForTxConfirmation(
706 : String hash, {
707 : Duration interval = const Duration(seconds: 5),
708 : }) async {
709 : while (true) {
710 0 : await Future.delayed(interval);
711 :
712 0 : final receipt = await performTask(
713 0 : (client) => client.getTransactionReceipt(hash),
714 : );
715 :
716 0 : switch (receipt?['status']) {
717 0 : case '0x1':
718 : return true;
719 0 : case '0x0':
720 : return false;
721 : default:
722 : }
723 : }
724 : }
725 :
726 1 : Future<String?> resolveENS({
727 : required String name,
728 : required String contractAddress,
729 : }) async {
730 1 : name = name.toLowerCase();
731 1 : final contract = EnsRegistryContract(
732 : rpc: this,
733 : contractAddress: contractAddress,
734 : );
735 :
736 1 : final resolverAddress = await contract.resolver(name: name);
737 :
738 : if (resolverAddress == null) {
739 : return null;
740 : }
741 :
742 1 : final resolver = EnsResolverContract(
743 : contractAddress: resolverAddress,
744 : rpc: this,
745 : );
746 :
747 1 : final addr = await resolver.addr(name: name);
748 :
749 : return addr;
750 : }
751 : }
|