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.8 % 124 63
Test Date: 2025-07-02 01:23:33 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              :         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 ?? Uint8List(0),
     121            3 :           v: response['v'].toString().toInt,
     122            3 :           r: response['r'].toString().toBigInt,
     123            3 :           s: response['s'].toString().toBigInt,
     124              :         ),
     125            0 :       _ => throw UnsupportedError('Unsupported transaction type: $type'),
     126              :     };
     127              :   }
     128              : 
     129            0 :   Future<Json> getBlockByNumber(int blockNumber) async {
     130            0 :     final response = await _call<Json>(
     131              :       'eth_getBlockByNumber',
     132            0 :       args: [blockNumber.toHexWithPrefix, false],
     133              :     );
     134              : 
     135              :     return response;
     136              :   }
     137              : 
     138            0 :   Future<String> sendRawTransaction(String rawTx) async {
     139            0 :     final response = await _call<String>(
     140              :       'eth_sendRawTransaction',
     141            0 :       args: [rawTx],
     142              :     );
     143              : 
     144              :     return response;
     145              :   }
     146              : 
     147              :   // Future<BigInt> estimateZkSyncFee({
     148              :   //   required String from,
     149              :   //   required String to,
     150              :   //   String? data,
     151              :   // }) async {
     152              :   //   final body = [
     153              :   //     {
     154              :   //       'from': from,
     155              :   //       'to': to,
     156              :   //       'data': data ?? "0x",
     157              :   //     },
     158              :   //   ];
     159              : 
     160              :   //   final response = await _call('zks_estimateFee', args: body);
     161              : 
     162              :   //   final gaslimit = int.parse(
     163              :   //     response['gas_limit'].toString().replaceAll("0x", ""),
     164              :   //     radix: 16,
     165              :   //   );
     166              :   //   return BigInt.from(gaslimit);
     167              :   // }
     168              : 
     169              :   ///
     170              :   /// Returns the balance of the account of given address in wei.
     171              :   ///
     172            2 :   Future<BigInt> getBalance(String address) async {
     173            2 :     final response = await _call<String>(
     174              :       'eth_getBalance',
     175            2 :       args: [address, 'latest'],
     176              :     );
     177            2 :     final balance = response.toBigIntOrNull;
     178            0 :     if (balance == null) throw Exception('Could not parse balance');
     179              :     return balance;
     180              :   }
     181              : 
     182              :   ///
     183              :   /// Returns the current block number.
     184              :   ///
     185            5 :   Future<int> getBlockNumber() async {
     186            5 :     final response = await _call<String>('eth_blockNumber');
     187            5 :     final blockNumber = response.toBigIntOrNull;
     188            0 :     if (blockNumber == null) throw Exception('Could not parse block number');
     189            5 :     return blockNumber.toInt();
     190              :   }
     191              : 
     192              :   ///
     193              :   /// Returns the Logs
     194              :   ///
     195            0 :   Future<JsonListNested> getLogs({
     196              :     required String address,
     197              :     required List<String?> topics,
     198              :     required dynamic fromBlock,
     199              :     required dynamic toBlock,
     200              :   }) async {
     201            0 :     final response = await _call<JsonList>(
     202              :       'eth_getLogs',
     203            0 :       args: [
     204            0 :         {
     205              :           'address': address,
     206              :           'topics': topics,
     207            0 :           'fromBlock': dynToHex(fromBlock),
     208            0 :           'toBlock': dynToHex(toBlock),
     209              :         }
     210              :       ],
     211              :     );
     212              : 
     213            0 :     return response.cast<Json>();
     214              :   }
     215              : 
     216              :   ///
     217              :   /// zeniq_queryTxByAddr
     218              :   ///
     219            0 :   Future<JsonList> queryTxByAddr({
     220              :     required String address,
     221              :     required dynamic startBlock,
     222              :     required dynamic endBlock,
     223              :     num maxTx = 10000,
     224              :   }) async {
     225            0 :     final response = await _call<JsonList>(
     226              :       'zeniq_queryTxByAddr',
     227            0 :       args: [
     228              :         address,
     229            0 :         dynToHex(startBlock),
     230            0 :         dynToHex(endBlock),
     231            0 :         maxTx.toHexWithPrefix,
     232              :       ],
     233              :     );
     234              : 
     235              :     return response;
     236              :   }
     237              : 
     238              :   ///
     239              :   /// Get the transaction receipt
     240              :   ///
     241            0 :   Future<Json?> getTransactionReceipt(String txHash) async {
     242            0 :     final response = await _call<Json?>(
     243              :       'eth_getTransactionReceipt',
     244            0 :       args: [txHash],
     245              :     );
     246              : 
     247            0 :     return response ?? {};
     248              :   }
     249              : 
     250              :   ///
     251              :   /// Estimate Time to be included in the next block
     252              :   ///
     253              : 
     254              :   ///
     255              :   /// Get Timestamp for block
     256              :   ///
     257            0 :   Future<int> getBlockTimestamp(int blockNumber) async {
     258            0 :     final response = await _call(
     259              :       'eth_getBlockByNumber',
     260            0 :       args: [blockNumber.toHexWithPrefix, false],
     261              :     );
     262              : 
     263              :     if (response
     264            0 :         case {
     265            0 :           "timestamp": String timestamp_s,
     266              :         }) {
     267            0 :       final timestamp = timestamp_s.toBigIntOrNull;
     268            0 :       if (timestamp == null) throw Exception('Could not parse timestamp');
     269            0 :       return timestamp.toInt();
     270              :     }
     271              : 
     272            0 :     throw UnimplementedError();
     273              :   }
     274              : 
     275              :   ///
     276              :   /// Get Gas Price
     277              :   ///
     278            1 :   Future<BigInt> getGasPrice() async {
     279            1 :     final response = await _call<String>('eth_gasPrice');
     280              : 
     281            1 :     final gasPrice = response.toBigIntOrNull;
     282            0 :     if (gasPrice == null) throw Exception('Could not parse gas price');
     283              :     return gasPrice;
     284              :   }
     285              : 
     286            1 :   Future<BigInt> getPriorityFee() async {
     287              :     // Direct RPC call to get suggested priority fee
     288            1 :     final response = await _call<String>(
     289              :       'eth_maxPriorityFeePerGas',
     290              :     );
     291              : 
     292            1 :     final priorityFee = response.toBigIntOrNull;
     293            0 :     if (priorityFee == null) throw Exception('Could not parse priority fee');
     294              :     return priorityFee;
     295              :   }
     296              : 
     297              :   ///
     298              :   /// Estimate Gas Fee
     299              :   ///
     300            1 :   Future<BigInt> estimateGasLimit({
     301              :     String? from,
     302              :     required String to,
     303              :     BigInt? amount,
     304              :     BigInt? gasPrice,
     305              :     String? data,
     306              :   }) async {
     307              :     try {
     308            1 :       final response = await _call<String>(
     309              :         'eth_estimateGas',
     310            1 :         args: [
     311            1 :           {
     312            1 :             if (from != null) 'from': from,
     313            1 :             'to': to,
     314            2 :             if (gasPrice != null) 'gasPrice': gasPrice.toHexWithPrefix,
     315            1 :             if (data != null) 'data': data,
     316            2 :             if (amount != null) 'value': amount.toHexWithPrefix,
     317              :           }
     318              :         ],
     319              :       );
     320              : 
     321            1 :       final gasFee = response.toBigIntOrNull;
     322            0 :       if (gasFee == null) throw Exception('Could not parse gas fee');
     323              :       return gasFee;
     324              :     } catch (e) {
     325            1 :       Logger.logError(
     326              :         e,
     327              :         hint: 'estimateGasLimit failed - falling back to hardcoded gasLimit',
     328              :       );
     329            1 :       return BigInt.from(95000);
     330              :     }
     331              :   }
     332              : }
     333              : 
     334            0 : dynamic dynToHex(dynamic value) {
     335            0 :   if (value is int) return value.toHexWithPrefix;
     336            0 :   if (value is String) return value;
     337              : 
     338            0 :   throw Exception('Could not convert $value to hex');
     339              : }
     340              : 
     341              : class RateLimitingException implements Exception {
     342              :   final String message;
     343              : 
     344            0 :   RateLimitingException(this.message);
     345              : 
     346            0 :   @override
     347              :   String toString() {
     348            0 :     return 'RateLimitingException: $message';
     349              :   }
     350              : }
        

Generated by: LCOV version 2.0-1