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