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