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