1
+import 'dart:convert';
2
+
3
+import 'package:cake_wallet/generated/i18n.dart';
4
+import 'package:cake_wallet/reactions/wallet_connect.dart';
5
+import 'package:eth_sig_util/eth_sig_util.dart';
6
+import 'package:eth_sig_util/util/utils.dart';
7
+import 'package:flutter/material.dart';
8
+import 'package:http/http.dart' as http;
9
+import 'package:reown_walletkit/reown_walletkit.dart';
10
+
11
+import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
12
+import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/eth/evm_chain_id.dart';
13
+import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/eth/evm_supported_methods.dart';
14
+import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
15
+import 'package:cake_wallet/src/screens/wallet_connect/models/wc_connection_model.dart';
16
+import 'package:cake_wallet/src/screens/wallet_connect/utils/eth_utils.dart';
17
+import 'package:cake_wallet/src/screens/wallet_connect/utils/method_utils.dart';
18
+import 'package:cake_wallet/store/app_store.dart';
19
+import 'package:cake_wallet/.secrets.g.dart' as secrets;
20
+
21
+class EvmChainServiceImpl {
22
+ Map<String, dynamic Function(String, dynamic)> get sessionRequestHandlers => {
23
+ EVMSupportedMethods.ethSign.name: ethSign,
24
+ EVMSupportedMethods.ethSignTransaction.name: ethSignTransaction,
25
+ EVMSupportedMethods.ethSignTypedData.name: ethSignTypedData,
26
+ EVMSupportedMethods.ethSignTypedDataV4.name: ethSignTypedDataV4,
27
+ };
28
+
29
+ Map<String, dynamic Function(String, dynamic)> get methodRequestHandlers => {
30
+ EVMSupportedMethods.personalSign.name: personalSign,
31
+ EVMSupportedMethods.ethSendTransaction.name: ethSendTransaction,
32
+ };
33
+
34
+ EvmChainServiceImpl({
35
+ required this.reference,
36
+ required this.appStore,
37
+ required this.wcKeyService,
38
+ required this.bottomSheetService,
39
+ required this.walletKit,
40
+ Web3Client? web3Client,
41
+ }) : ethClient = web3Client ??
42
+ Web3Client(
43
+ appStore.settingsStore.getCurrentNode(appStore.wallet!.type).uri.toString(),
44
+ http.Client(),
45
+ ) {
46
+ for (final event in EventsConstants.allEvents) {
47
+ walletKit.registerEventEmitter(
48
+ chainId: getChainId(),
49
+ event: event,
50
+ );
51
+ }
52
+
53
+ for (var handler in methodRequestHandlers.entries) {
54
+ walletKit.registerRequestHandler(
55
+ chainId: getChainId(),
56
+ method: handler.key,
57
+ handler: handler.value,
58
+ );
59
+ }
60
+ for (var handler in sessionRequestHandlers.entries) {
61
+ walletKit.registerRequestHandler(
62
+ chainId: getChainId(),
63
+ method: handler.key,
64
+ handler: handler.value,
65
+ );
66
+ }
67
+
68
+ walletKit.onSessionRequest.subscribe(_onSessionRequest);
69
+ }
70
+
71
+ final AppStore appStore;
72
+ final EVMChainId reference;
73
+ final Web3Client ethClient;
74
+ final ReownWalletKit walletKit;
75
+ final WalletConnectKeyService wcKeyService;
76
+ final BottomSheetService bottomSheetService;
77
+
78
+ String getChainId() => reference.chain();
79
+
80
+ Future<void> personalSign(String topic, dynamic parameters) async {
81
+ debugPrint('personalSign request: $parameters');
82
+
83
+ final pRequest = walletKit.pendingRequests.getAll().last;
84
+ final address = EthUtils.getAddressFromSessionRequest(pRequest);
85
+ final data = EthUtils.getDataFromSessionRequest(pRequest);
86
+ final message = EthUtils.getUtf8Message(data.toString());
87
+ var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0');
88
+
89
+ final isApproved = await MethodsUtils.requestApproval(
90
+ message,
91
+ method: pRequest.method,
92
+ chainId: pRequest.chainId,
93
+ address: address,
94
+ transportType: pRequest.transportType.name,
95
+ verifyContext: pRequest.verifyContext,
96
+ );
97
+
98
+ if (isApproved) {
99
+ try {
100
+ // Load the private key
101
+ final keys = wcKeyService.getKeysForChain(appStore.wallet!);
102
+ final credentials = EthPrivateKey.fromHex(keys[0].privateKey);
103
+
104
+ final signature = credentials.signPersonalMessageToUint8List(
105
+ utf8.encode(message),
106
+ );
107
+ final signedTx = bytesToHex(signature, include0x: true);
108
+
109
+ isValidSignature(signedTx, message, credentials.address.hex);
110
+
111
+ response = response.copyWith(result: signedTx);
112
+ } catch (e) {
113
+ debugPrint('personalSign error $e');
114
+ final error = Errors.getSdkError(Errors.MALFORMED_REQUEST_PARAMS);
115
+ response = response.copyWith(
116
+ error: JsonRpcError(code: error.code, message: error.message),
117
+ );
118
+ }
119
+ } else {
120
+ final error = Errors.getSdkError(Errors.USER_REJECTED);
121
+ response = response.copyWith(
122
+ error: JsonRpcError(code: error.code, message: error.message),
123
+ );
124
+ }
125
+
126
+ _handleResponseForTopic(topic, response);
127
+ }
128
+
129
+ Future<void> ethSign(String topic, dynamic parameters) async {
130
+ debugPrint('ethSign request: $parameters');
131
+
132
+ final pRequest = walletKit.pendingRequests.getAll().last;
133
+ final address = EthUtils.getAddressFromSessionRequest(pRequest);
134
+ final data = EthUtils.getDataFromSessionRequest(pRequest);
135
+ final message = EthUtils.getUtf8Message(data.toString());
136
+ var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0');
137
+
138
+ final isApproved = await MethodsUtils.requestApproval(
139
+ message,
140
+ method: pRequest.method,
141
+ chainId: pRequest.chainId,
142
+ address: address,
143
+ transportType: pRequest.transportType.name,
144
+ verifyContext: pRequest.verifyContext,
145
+ );
146
+
147
+ if (isApproved) {
148
+ try {
149
+ // Load the private key
150
+ final keys = wcKeyService.getKeysForChain(appStore.wallet!);
151
+ final credentials = EthPrivateKey.fromHex(keys[0].privateKey);
152
+
153
+ final signature = credentials.signPersonalMessageToUint8List(
154
+ utf8.encode(message),
155
+ );
156
+ final signedTx = bytesToHex(signature, include0x: true);
157
+
158
+ isValidSignature(signedTx, message, credentials.address.hex);
159
+
160
+ response = response.copyWith(result: signedTx);
161
+ } catch (e) {
162
+ debugPrint('ethSign error $e');
163
+ final error = Errors.getSdkError(Errors.MALFORMED_REQUEST_PARAMS);
164
+ response = response.copyWith(
165
+ error: JsonRpcError(code: error.code, message: error.message),
166
+ );
167
+ }
168
+ } else {
169
+ final error = Errors.getSdkError(Errors.USER_REJECTED).toSignError();
170
+ response = response.copyWith(
171
+ error: JsonRpcError(code: error.code, message: error.message),
172
+ );
173
+ }
174
+
175
+ _handleResponseForTopic(topic, response);
176
+ }
177
+
178
+ Future<void> ethSignTypedData(String topic, dynamic parameters) async {
179
+ debugPrint('ethSignTypedData request: $parameters');
180
+
181
+ final pRequest = walletKit.pendingRequests.getAll().last;
182
+ final address = EthUtils.getAddressFromSessionRequest(pRequest);
183
+ final data = EthUtils.getDataFromSessionRequest(pRequest) as String;
184
+ var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0');
185
+
186
+ final isApproved = await MethodsUtils.requestApproval(
187
+ data,
188
+ method: pRequest.method,
189
+ chainId: pRequest.chainId,
190
+ address: address,
191
+ transportType: pRequest.transportType.name,
192
+ verifyContext: pRequest.verifyContext,
193
+ );
194
+
195
+ if (isApproved) {
196
+ try {
197
+ final keys = wcKeyService.getKeysForChain(appStore.wallet!);
198
+
199
+ final signature = EthSigUtil.signTypedData(
200
+ privateKey: keys[0].privateKey,
201
+ jsonData: data,
202
+ version: TypedDataVersion.V4,
203
+ );
204
+
205
+ response = response.copyWith(result: signature);
206
+ } catch (e) {
207
+ debugPrint('ethSignTypedData error $e');
208
+ final error = Errors.getSdkError(Errors.MALFORMED_REQUEST_PARAMS);
209
+ response = response.copyWith(
210
+ error: JsonRpcError(code: error.code, message: error.message),
211
+ );
212
+ }
213
+ } else {
214
+ final error = Errors.getSdkError(Errors.USER_REJECTED).toSignError();
215
+ response = response.copyWith(
216
+ error: JsonRpcError(code: error.code, message: error.message),
217
+ );
218
+ }
219
+
220
+ _handleResponseForTopic(topic, response);
221
+ }
222
+
223
+ Future<void> ethSignTypedDataV4(String topic, dynamic parameters) async {
224
+ debugPrint('ethSignTypedDataV4 request: $parameters');
225
+
226
+ final permitRequestMessage = await extractPermitData(parameters);
227
+
228
+ final pRequest = walletKit.pendingRequests.getAll().last;
229
+ final address = EthUtils.getAddressFromSessionRequest(pRequest);
230
+ final data = EthUtils.getDataFromSessionRequest(pRequest) as String;
231
+ var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0');
232
+
233
+ final isApproved = await MethodsUtils.requestApproval(
234
+ permitRequestMessage,
235
+ method: pRequest.method,
236
+ chainId: pRequest.chainId,
237
+ address: address,
238
+ transportType: pRequest.transportType.name,
239
+ verifyContext: pRequest.verifyContext,
240
+ );
241
+
242
+ if (isApproved) {
243
+ try {
244
+ final keys = wcKeyService.getKeysForChain(appStore.wallet!);
245
+
246
+ final signature = EthSigUtil.signTypedData(
247
+ privateKey: keys[0].privateKey,
248
+ jsonData: data,
249
+ version: TypedDataVersion.V4,
250
+ );
251
+
252
+ response = response.copyWith(result: signature);
253
+ } catch (e) {
254
+ debugPrint('ethSignTypedDataV4 error $e');
255
+ final error = Errors.getSdkError(Errors.MALFORMED_REQUEST_PARAMS);
256
+ response = response.copyWith(
257
+ error: JsonRpcError(code: error.code, message: error.message),
258
+ );
259
+ }
260
+ } else {
261
+ response = response.copyWith(
262
+ error: JsonRpcError(code: 5002, message: S.current.user_rejected_method),
263
+ );
264
+ }
265
+
266
+ _handleResponseForTopic(topic, response);
267
+ }
268
+
269
+ Future<void> ethSignTransaction(String topic, dynamic parameters) async {
270
+ debugPrint('ethSignTransaction request: $parameters');
271
+
272
+ final SessionRequest pRequest = walletKit.pendingRequests.getAll().last;
273
+ final data = EthUtils.getTransactionFromSessionRequest(pRequest);
274
+
275
+ if (data == null) return;
276
+
277
+ final address = EthUtils.getAddressFromSessionRequest(pRequest);
278
+ var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0');
279
+
280
+ final transaction = await _approveTransaction(
281
+ data,
282
+ method: pRequest.method,
283
+ chainId: pRequest.chainId,
284
+ address: address,
285
+ transportType: pRequest.transportType.name,
286
+ verifyContext: pRequest.verifyContext,
287
+ );
288
+
289
+ if (transaction is Transaction) {
290
+ try {
291
+ // Load the private key
292
+ final keys = wcKeyService.getKeysForChain(appStore.wallet!);
293
+ final credentials = EthPrivateKey.fromHex(keys[0].privateKey);
294
+
295
+ final chainId = getChainId().split(':').last;
296
+
297
+ final signature = await ethClient.signTransaction(
298
+ credentials,
299
+ transaction,
300
+ chainId: int.parse(chainId),
301
+ );
302
+
303
+ // Sign the transaction
304
+ final signedTx = bytesToHex(signature, include0x: true);
305
+ response = response.copyWith(result: signedTx);
306
+ } on RPCError catch (e) {
307
+ debugPrint('ethSignTransaction error $e');
308
+ response = response.copyWith(
309
+ error: JsonRpcError(code: e.errorCode, message: e.message),
310
+ );
311
+ } catch (e) {
312
+ debugPrint('ethSignTransaction error $e');
313
+ final error = Errors.getSdkError(Errors.MALFORMED_REQUEST_PARAMS);
314
+ response = response.copyWith(
315
+ error: JsonRpcError(code: error.code, message: error.message),
316
+ );
317
+ }
318
+ } else {
319
+ response = response.copyWith(error: transaction as JsonRpcError);
320
+ }
321
+
322
+ _handleResponseForTopic(topic, response);
323
+ }
324
+
325
+ Future<void> ethSendTransaction(String topic, dynamic parameters) async {
326
+ debugPrint('ethSendTransaction request: $parameters');
327
+ final SessionRequest pRequest = walletKit.pendingRequests.getAll().last;
328
+
329
+ final data = EthUtils.getTransactionFromSessionRequest(pRequest);
330
+ if (data == null) return;
331
+
332
+ var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0');
333
+
334
+ final transaction = await _approveTransaction(
335
+ data,
336
+ method: pRequest.method,
337
+ chainId: pRequest.chainId,
338
+ transportType: pRequest.transportType.name,
339
+ verifyContext: pRequest.verifyContext,
340
+ );
341
+ if (transaction is Transaction) {
342
+ try {
343
+ // Load the private key
344
+ final keys = wcKeyService.getKeysForChain(appStore.wallet!);
345
+ final credentials = EthPrivateKey.fromHex(keys[0].privateKey);
346
+ final chainId = getChainId().split(':').last;
347
+
348
+ final signedTx = await ethClient.sendTransaction(
349
+ credentials,
350
+ transaction,
351
+ chainId: int.parse(chainId),
352
+ );
353
+
354
+ response = response.copyWith(result: signedTx);
355
+ } on RPCError catch (e) {
356
+ debugPrint('ethSendTransaction error $e');
357
+ response = response.copyWith(
358
+ error: JsonRpcError(code: e.errorCode, message: e.message),
359
+ );
360
+ } catch (e) {
361
+ debugPrint('ethSendTransaction error $e');
362
+ final error = Errors.getSdkError(Errors.MALFORMED_REQUEST_PARAMS);
363
+ response = response.copyWith(
364
+ error: JsonRpcError(code: error.code, message: error.message),
365
+ );
366
+ }
367
+ } else {
368
+ response = response.copyWith(error: transaction as JsonRpcError);
369
+ }
370
+
371
+ _handleResponseForTopic(topic, response);
372
+ }
373
+
374
+ void _handleResponseForTopic(String topic, JsonRpcResponse<dynamic> response) async {
375
+ final session = walletKit.sessions.get(topic);
376
+
377
+ try {
378
+ await walletKit.respondSessionRequest(
379
+ topic: topic,
380
+ response: response,
381
+ );
382
+ MethodsUtils.handleRedirect(
383
+ topic,
384
+ session!.peer.metadata.redirect,
385
+ response.error?.message,
386
+ response.error == null,
387
+ );
388
+ } on ReownSignError catch (error) {
389
+ MethodsUtils.handleRedirect(
390
+ topic,
391
+ session!.peer.metadata.redirect,
392
+ error.message,
393
+ );
394
+ }
395
+ }
396
+
397
+ Future<dynamic> _approveTransaction(
398
+ Map<String, dynamic> transactionJson, {
399
+ String? title,
400
+ String? method,
401
+ String? chainId,
402
+ String? address,
403
+ VerifyContext? verifyContext,
404
+ required String transportType,
405
+ }) async {
406
+ Transaction transaction = transactionJson.toTransaction();
407
+
408
+ final gasPrice = await ethClient.getGasPrice();
409
+ try {
410
+ final gasLimit = await ethClient.estimateGas(
411
+ sender: transaction.from,
412
+ to: transaction.to,
413
+ value: transaction.value,
414
+ data: transaction.data,
415
+ gasPrice: gasPrice,
416
+ );
417
+
418
+ transaction = transaction.copyWith(
419
+ gasPrice: gasPrice,
420
+ maxGas: gasLimit.toInt(),
421
+ );
422
+ } on RPCError catch (e) {
423
+ return JsonRpcError(code: e.errorCode, message: e.message);
424
+ }
425
+
426
+ final gweiGasPrice = (transaction.gasPrice?.getInWei ?? BigInt.zero) / BigInt.from(1000000000);
427
+
428
+ final amount = (transaction.value?.getInWei ?? BigInt.zero) / BigInt.from(1e18);
429
+
430
+ final txMessageText = '${S.current.value}: ${amount.toStringAsFixed(9)} ETH\n'
431
+ '${S.current.from}: ${transaction.from?.hex}\n'
432
+ '${S.current.to}: ${transaction.to?.hex}';
433
+
434
+ if (await MethodsUtils.requestApproval(
435
+ txMessageText,
436
+ title: title,
437
+ method: method,
438
+ chainId: chainId,
439
+ address: address,
440
+ transportType: transportType,
441
+ verifyContext: verifyContext,
442
+ extraModels: [
443
+ WCConnectionModel(
444
+ title: S.current.gas_price,
445
+ elements: ['${gweiGasPrice.toStringAsFixed(2)} GWEI'],
446
+ ),
447
+ ],
448
+ )) {
449
+ return transaction;
450
+ }
451
+
452
+ return JsonRpcError(code: 5002, message: S.current.user_rejected_method);
453
+ }
454
+
455
+ void _onSessionRequest(SessionRequestEvent? args) async {
456
+ if (args != null && args.chainId == getChainId()) {
457
+ debugPrint('_onSessionRequest ${args.toString()}');
458
+ final handler = sessionRequestHandlers[args.method];
459
+ if (handler != null) {
460
+ await handler(args.topic, args.params);
461
+ }
462
+ }
463
+ }
464
+
465
+ bool isValidSignature(String hexSignature, String message, String hexAddress) {
466
+ try {
467
+ debugPrint('isValidSignature: $hexSignature, $message, $hexAddress');
468
+ final recoveredAddress = EthSigUtil.recoverPersonalSignature(
469
+ signature: hexSignature,
470
+ message: utf8.encode(message),
471
+ );
472
+ debugPrint('recoveredAddress: $recoveredAddress');
473
+
474
+ final recoveredAddress2 = EthSigUtil.recoverSignature(
475
+ signature: hexSignature,
476
+ message: utf8.encode(message),
477
+ );
478
+ debugPrint('recoveredAddress2: $recoveredAddress2');
479
+
480
+ final isValid = recoveredAddress == hexAddress;
481
+ return isValid;
482
+ } catch (e) {
483
+ return false;
484
+ }
485
+ }
486
+
487
+ Future<String> extractPermitData(dynamic data) async {
488
+ if (data is List && data.length >= 2) {
489
+ final typedData = jsonDecode(data[1] as String) as Map<String, dynamic>;
490
+
491
+ // Extracting domain details.
492
+ final domain = typedData['domain'] ?? {} as Map<String, dynamic>;
493
+ final domainName = domain['name']?.toString() ?? '';
494
+ final verifyingContract = domain['verifyingContract']?.toString() ?? '';
495
+
496
+ final chainId = domain['chainId']?.toString() ?? '';
497
+ final chainName = getChainNameBasedOnWalletType(appStore.wallet!.type);
498
+
499
+ // Get the primary type.
500
+ final primaryType = typedData['primaryType']?.toString() ?? '';
501
+
502
+ // Extracting message details.
503
+ final message = typedData['message'] ?? {} as Map<String, dynamic>;
504
+ final details = message['details'] ?? {} as Map<String, dynamic>;
505
+ final amount = details['amount']?.toString() ?? '';
506
+ final expirationRaw = details['expiration']?.toString() ?? '';
507
+ final nonce = details['nonce']?.toString() ?? '';
508
+
509
+ final tokenAddress = details['token']?.toString() ?? '';
510
+ final token = await getTokenDetails(tokenAddress, chainName);
511
+
512
+ final spender = message['spender']?.toString() ?? '';
513
+ final sigDeadlineRaw = message['sigDeadline']?.toString() ?? '';
514
+
515
+ // Converting expiration and sigDeadline from Unix time (seconds) to DateTime.
516
+ DateTime? expirationDate;
517
+ DateTime? sigDeadlineDate;
518
+ try {
519
+ if (expirationRaw.isNotEmpty) {
520
+ final int expirationInt = int.parse(expirationRaw);
521
+ expirationDate = DateTime.fromMillisecondsSinceEpoch(expirationInt * 1000);
522
+ }
523
+ if (sigDeadlineRaw.isNotEmpty) {
524
+ final int sigDeadlineInt = int.parse(sigDeadlineRaw);
525
+ sigDeadlineDate = DateTime.fromMillisecondsSinceEpoch(sigDeadlineInt * 1000);
526
+ }
527
+ } catch (e) {
528
+ // Parsing failed; we leave dates as null.
529
+ }
530
+
531
+ final permitData = {
532
+ 'domainName': domainName,
533
+ 'chainId': chainId,
534
+ 'verifyingContract': verifyingContract,
535
+ 'primaryType': primaryType,
536
+ 'token': token,
537
+ 'amount': amount,
538
+ 'expiration': expirationDate,
539
+ 'nonce': nonce,
540
+ 'spender': spender,
541
+ 'sigDeadline': sigDeadlineDate,
542
+ };
543
+
544
+ return 'Domain: ${permitData['domainName']}'
545
+ 'Chain ID: ${permitData['chainId']}'
546
+ 'Verifying Contract: ${permitData['verifyingContract']}'
547
+ 'Primary Type: ${permitData['primaryType']}'
548
+ 'Token: ${permitData['token']}'
549
+ 'Expiration: ${permitData['expiration'] != null ? permitData['expiration'] : 'N/A'}'
550
+ 'Spender: ${permitData['spender']}'
551
+ 'Signature Deadline: ${permitData['sigDeadline'] != null ? permitData['sigDeadline'] : 'N/A'}';
552
+ }
553
+ return '';
554
+ }
555
+
556
+ Future<String> getTokenDetails(String contractAddress, String chainName) async {
557
+ final uri = Uri.https(
558
+ 'deep-index.moralis.io',
559
+ '/api/v2.2/erc20/metadata',
560
+ {
561
+ "chain": chainName,
562
+ "addresses": contractAddress,
563
+ },
564
+ );
565
+
566
+ final response = await http.get(
567
+ uri,
568
+ headers: {
569
+ "Accept": "application/json",
570
+ "X-API-Key": secrets.moralisApiKey,
571
+ },
572
+ );
573
+
574
+ final decodedResponse = jsonDecode(response.body)[0] as Map<String, dynamic>;
575
+
576
+ final symbol = (decodedResponse['symbol'] ?? '') as String;
577
+
578
+ final name = decodedResponse['name'] ?? '';
579
+ return '$name ($symbol)';
580
+ }
581
+}