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

Generated by: LCOV version 2.0-1