LCOV - code coverage report
Current view: top level - crypto/evm/repositories/rpc - evm_client.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 50.4 % 125 63
Test Date: 2025-04-01 01:23:07 Functions: - 0 0

            Line data    Source code
       1              : import 'dart:typed_data';
       2              : 
       3              : import 'package:walletkit_dart/src/common/http_client.dart';
       4              : import 'package:walletkit_dart/src/common/logger.dart';
       5              : import 'package:walletkit_dart/src/crypto/evm/entities/block_number.dart';
       6              : import 'package:walletkit_dart/walletkit_dart.dart';
       7              : 
       8              : const erc20TransferSig = "a9059cbb";
       9              : 
      10              : base class EvmRpcClient {
      11              :   final JsonRPC _rpc;
      12              :   final Duration rateLimitTimeout;
      13              :   final void Function(Object e, StackTrace s, String url)? onRpcError;
      14              : 
      15            0 :   @override
      16            0 :   String toString() => 'EvmRpcClient{rpcUrl: $rpcUrl}';
      17              : 
      18            1 :   EvmRpcClient.withRPC(JsonRPC rpc)
      19              :       : _rpc = rpc,
      20              :         rateLimitTimeout = const Duration(seconds: 30),
      21              :         this.onRpcError = null;
      22              : 
      23            4 :   EvmRpcClient(
      24              :     String rpcUrl, {
      25              :     this.rateLimitTimeout = const Duration(seconds: 30),
      26              :     this.onRpcError,
      27            8 :   }) : _rpc = JsonRPC(rpcUrl, HTTPService.client);
      28              : 
      29           15 :   String get rpcUrl => _rpc.url;
      30            0 :   HTTPClient get httpClient => HTTPService.client;
      31              : 
      32              :   DateTime? lastFailedTime;
      33              : 
      34            5 :   bool isRateLimited() {
      35            5 :     if (lastFailedTime == null) return false;
      36            0 :     return DateTime.now().difference(lastFailedTime!) < rateLimitTimeout;
      37              :   }
      38              : 
      39            5 :   Future<T> _call<T>(String function, {List<dynamic>? args}) async {
      40            5 :     if (isRateLimited()) {
      41            0 :       throw RateLimitingException('RPC $rpcUrl is rate limited');
      42              :     }
      43              : 
      44              :     try {
      45           10 :       final response = await _rpc.call(function, args);
      46            5 :       final result = response.result as T;
      47              :       return result;
      48            2 :     } on RPCError catch (e, s) {
      49            3 :       if (e.errorCode == -32600) {
      50            0 :         lastFailedTime = DateTime.now();
      51            0 :         throw RateLimitingException("Rate limited");
      52              :       }
      53              : 
      54            1 :       if (onRpcError != null) {
      55            0 :         onRpcError!(e, s, rpcUrl);
      56              :       }
      57              : 
      58            1 :       Logger.logError(e, s: s, hint: 'EvmRpcClient RPCError');
      59              :       rethrow;
      60              :     } catch (e, s) {
      61            2 :       Logger.logError(e, s: s, hint: 'EvmRpcClient');
      62            2 :       if (onRpcError != null) {
      63            0 :         onRpcError!(e, s, rpcUrl);
      64              :       }
      65              :       rethrow;
      66              :     }
      67              :   }
      68              : 
      69            3 :   Future<String> call({
      70              :     String? sender,
      71              :     required String contractAddress,
      72              :     required Uint8List data,
      73              :     BlockNum? atBlock,
      74              :   }) async {
      75            3 :     final response = await _call<String>(
      76              :       'eth_call',
      77            3 :       args: [
      78            3 :         {
      79            0 :           if (sender != null) 'from': sender,
      80            3 :           'to': contractAddress,
      81            9 :           'data': "0x${data.toHex}",
      82              :         },
      83            0 :         atBlock?.toBlockParam() ?? 'latest',
      84              :       ],
      85              :     );
      86              : 
      87              :     return response;
      88              :   }
      89              : 
      90            0 :   Future<BigInt> getTransactionCount(String address) async {
      91            0 :     final response = await _call<String>(
      92              :       'eth_getTransactionCount',
      93            0 :       args: [address, 'latest'],
      94              :     );
      95              : 
      96            0 :     final count = response.toBigIntOrNull;
      97            0 :     if (count == null) throw Exception('Could not parse transaction count');
      98              :     return count;
      99              :   }
     100              : 
     101            1 :   Future<RawEvmTransaction> getTransactionByHash(
     102              :     String messageHash, [
     103              :     int? chainId,
     104              :   ]) async {
     105            1 :     final response = await _call<Json>(
     106              :       'eth_getTransactionByHash',
     107            1 :       args: [messageHash],
     108              :     );
     109              : 
     110            3 :     final type_i = response['type'].toString().toInt;
     111            2 :     final type = TransactionType.fromInt(type_i.toInt());
     112              : 
     113              :     return switch (type) {
     114            2 :       TransactionType.Legacy => RawEVMTransactionType0(
     115            3 :           nonce: response['nonce'].toString().toBigInt,
     116            3 :           gasPrice: response['gasPrice'].toString().toBigInt,
     117            3 :           gasLimit: response['gas'].toString().toBigInt,
     118            1 :           to: response['to'],
     119            3 :           value: response['value'].toString().toBigInt,
     120            3 :           data: response['input'].toString().hexToBytesWithPrefixOrNull ??
     121            0 :               Uint8List(0),
     122            3 :           v: response['v'].toString().toInt,
     123            3 :           r: response['r'].toString().toBigInt,
     124            3 :           s: response['s'].toString().toBigInt,
     125              :         ),
     126            0 :       _ => throw UnsupportedError('Unsupported transaction type: $type'),
     127              :     };
     128              :   }
     129              : 
     130            0 :   Future<Json> getBlockByNumber(int blockNumber) async {
     131            0 :     final response = await _call<Json>(
     132              :       'eth_getBlockByNumber',
     133            0 :       args: [blockNumber.toHexWithPrefix, false],
     134              :     );
     135              : 
     136              :     return response;
     137              :   }
     138              : 
     139            0 :   Future<String> sendRawTransaction(String rawTx) async {
     140            0 :     final response = await _call<String>(
     141              :       'eth_sendRawTransaction',
     142            0 :       args: [rawTx],
     143              :     );
     144              : 
     145              :     return response;
     146              :   }
     147              : 
     148              :   // Future<BigInt> estimateZkSyncFee({
     149              :   //   required String from,
     150              :   //   required String to,
     151              :   //   String? data,
     152              :   // }) async {
     153              :   //   final body = [
     154              :   //     {
     155              :   //       'from': from,
     156              :   //       'to': to,
     157              :   //       'data': data ?? "0x",
     158              :   //     },
     159              :   //   ];
     160              : 
     161              :   //   final response = await _call('zks_estimateFee', args: body);
     162              : 
     163              :   //   final gaslimit = int.parse(
     164              :   //     response['gas_limit'].toString().replaceAll("0x", ""),
     165              :   //     radix: 16,
     166              :   //   );
     167              :   //   return BigInt.from(gaslimit);
     168              :   // }
     169              : 
     170              :   ///
     171              :   /// Returns the balance of the account of given address in wei.
     172              :   ///
     173            2 :   Future<BigInt> getBalance(String address) async {
     174            2 :     final response = await _call<String>(
     175              :       'eth_getBalance',
     176            2 :       args: [address, 'latest'],
     177              :     );
     178            2 :     final balance = response.toBigIntOrNull;
     179            0 :     if (balance == null) throw Exception('Could not parse balance');
     180              :     return balance;
     181              :   }
     182              : 
     183              :   ///
     184              :   /// Returns the current block number.
     185              :   ///
     186            5 :   Future<int> getBlockNumber() async {
     187            5 :     final response = await _call<String>('eth_blockNumber');
     188            5 :     final blockNumber = response.toBigIntOrNull;
     189            0 :     if (blockNumber == null) throw Exception('Could not parse block number');
     190            5 :     return blockNumber.toInt();
     191              :   }
     192              : 
     193              :   ///
     194              :   /// Returns the Logs
     195              :   ///
     196            0 :   Future<JsonListNested> getLogs({
     197              :     required String address,
     198              :     required List<String?> topics,
     199              :     required dynamic fromBlock,
     200              :     required dynamic toBlock,
     201              :   }) async {
     202            0 :     final response = await _call<JsonList>(
     203              :       'eth_getLogs',
     204            0 :       args: [
     205            0 :         {
     206              :           'address': address,
     207              :           'topics': topics,
     208            0 :           'fromBlock': dynToHex(fromBlock),
     209            0 :           'toBlock': dynToHex(toBlock),
     210              :         }
     211              :       ],
     212              :     );
     213              : 
     214            0 :     return response.cast<Json>();
     215              :   }
     216              : 
     217              :   ///
     218              :   /// zeniq_queryTxByAddr
     219              :   ///
     220            0 :   Future<JsonList> queryTxByAddr({
     221              :     required String address,
     222              :     required dynamic startBlock,
     223              :     required dynamic endBlock,
     224              :     num maxTx = 10000,
     225              :   }) async {
     226            0 :     final response = await _call<JsonList>(
     227              :       'zeniq_queryTxByAddr',
     228            0 :       args: [
     229              :         address,
     230            0 :         dynToHex(startBlock),
     231            0 :         dynToHex(endBlock),
     232            0 :         maxTx.toHexWithPrefix,
     233              :       ],
     234              :     );
     235              : 
     236              :     return response;
     237              :   }
     238              : 
     239              :   ///
     240              :   /// Get the transaction receipt
     241              :   ///
     242            0 :   Future<Json?> getTransactionReceipt(String txHash) async {
     243            0 :     final response = await _call<Json?>(
     244              :       'eth_getTransactionReceipt',
     245            0 :       args: [txHash],
     246              :     );
     247              : 
     248            0 :     return response ?? {};
     249              :   }
     250              : 
     251              :   ///
     252              :   /// Estimate Time to be included in the next block
     253              :   ///
     254              : 
     255              :   ///
     256              :   /// Get Timestamp for block
     257              :   ///
     258            0 :   Future<int> getBlockTimestamp(int blockNumber) async {
     259            0 :     final response = await _call(
     260              :       'eth_getBlockByNumber',
     261            0 :       args: [blockNumber.toHexWithPrefix, false],
     262              :     );
     263              : 
     264              :     if (response
     265            0 :         case {
     266            0 :           "timestamp": String timestamp_s,
     267              :         }) {
     268            0 :       final timestamp = timestamp_s.toBigIntOrNull;
     269            0 :       if (timestamp == null) throw Exception('Could not parse timestamp');
     270            0 :       return timestamp.toInt();
     271              :     }
     272              : 
     273            0 :     throw UnimplementedError();
     274              :   }
     275              : 
     276              :   ///
     277              :   /// Get Gas Price
     278              :   ///
     279            1 :   Future<BigInt> getGasPrice() async {
     280            1 :     final response = await _call<String>('eth_gasPrice');
     281              : 
     282            1 :     final gasPrice = response.toBigIntOrNull;
     283            0 :     if (gasPrice == null) throw Exception('Could not parse gas price');
     284              :     return gasPrice;
     285              :   }
     286              : 
     287            1 :   Future<BigInt> getPriorityFee() async {
     288              :     // Direct RPC call to get suggested priority fee
     289            1 :     final response = await _call<String>(
     290              :       'eth_maxPriorityFeePerGas',
     291              :     );
     292              : 
     293            1 :     final priorityFee = response.toBigIntOrNull;
     294            0 :     if (priorityFee == null) throw Exception('Could not parse priority fee');
     295              :     return priorityFee;
     296              :   }
     297              : 
     298              :   ///
     299              :   /// Estimate Gas Fee
     300              :   ///
     301            1 :   Future<BigInt> estimateGasLimit({
     302              :     String? from,
     303              :     required String to,
     304              :     BigInt? amount,
     305              :     BigInt? gasPrice,
     306              :     String? data,
     307              :   }) async {
     308              :     try {
     309            1 :       final response = await _call<String>(
     310              :         'eth_estimateGas',
     311            1 :         args: [
     312            1 :           {
     313            1 :             if (from != null) 'from': from,
     314            1 :             'to': to,
     315            2 :             if (gasPrice != null) 'gasPrice': gasPrice.toHexWithPrefix,
     316            1 :             if (data != null) 'data': data,
     317            2 :             if (amount != null) 'value': amount.toHexWithPrefix,
     318              :           }
     319              :         ],
     320              :       );
     321              : 
     322            1 :       final gasFee = response.toBigIntOrNull;
     323            0 :       if (gasFee == null) throw Exception('Could not parse gas fee');
     324              :       return gasFee;
     325              :     } catch (e) {
     326            1 :       Logger.logError(
     327              :         e,
     328              :         hint: 'estimateGasLimit failed - falling back to hardcoded gasLimit',
     329              :       );
     330            1 :       return BigInt.from(95000);
     331              :     }
     332              :   }
     333              : }
     334              : 
     335            0 : dynamic dynToHex(dynamic value) {
     336            0 :   if (value is int) return value.toHexWithPrefix;
     337            0 :   if (value is String) return value;
     338              : 
     339            0 :   throw Exception('Could not convert $value to hex');
     340              : }
     341              : 
     342              : class RateLimitingException implements Exception {
     343              :   final String message;
     344              : 
     345            0 :   RateLimitingException(this.message);
     346              : 
     347            0 :   @override
     348              :   String toString() {
     349            0 :     return 'RateLimitingException: $message';
     350              :   }
     351              : }
        

Generated by: LCOV version 2.0-1