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 : }
|