dev
dart 402 lines 13 KB
Raw
1 import "dart:io";
2
3 import 'package:bip39/bip39.dart' as bip39;
4 import 'package:cw_core/encryption_file_utils.dart';
5 import "package:cw_core/erc20_token.dart";
6 import 'package:cw_core/pathForWallet.dart';
7 import 'package:cw_core/utils/print_verbose.dart';
8 import 'package:cw_core/wallet_base.dart';
9 import 'package:cw_core/wallet_info.dart';
10 import 'package:cw_core/wallet_service.dart';
11 import 'package:cw_core/wallet_type.dart';
12 import 'package:path/path.dart' as p;
13 import 'package:cw_evm/clients/evm_chain_client.dart';
14 import 'package:cw_evm/evm_chain_client_factory.dart';
15 import 'package:cw_evm/evm_chain_exceptions.dart';
16 import 'package:cw_evm/evm_chain_registry.dart';
17 import 'package:cw_evm/evm_chain_wallet.dart';
18 import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart';
19
20 /// Unified service for all EVM chains (Ethereum, Polygon, Base, Arbitrum, etc.)
21 ///
22 /// This service dynamically determines which chain to use based on WalletType
23 /// from credentials or walletInfo, eliminating the need for separate service
24 /// classes per chain.
25 class EVMChainWalletService extends WalletService<
26 EVMChainNewWalletCredentials,
27 EVMChainRestoreWalletFromSeedCredentials,
28 EVMChainRestoreWalletFromPrivateKey,
29 EVMChainRestoreWalletFromHardware> {
30 EVMChainWalletService(this.isDirect);
31
32 final bool isDirect;
33 final EvmChainRegistry _registry = EvmChainRegistry();
34
35 List<WalletType> get _evmWalletTypes {
36 return _registry.getRegisteredWalletTypes();
37 }
38
39 Future<WalletInfo?> _findWalletByName(String name) async {
40 for (final type in _evmWalletTypes) {
41 final walletInfo = await WalletInfo.get(name, type);
42 if (walletInfo != null) {
43 return walletInfo;
44 }
45 }
46 return null;
47 }
48
49 /// getType() is not meaningful for this unified service, it throws to prevent misuse
50 @override
51 WalletType getType() {
52 throw UnsupportedError(
53 "EVMChainWalletService is unified and does not have a single type. "
54 "Use walletInfo.type instead.",
55 );
56 }
57
58 /// Override saveBackup to look up walletType from wallet name
59 /// Optionally accepts walletInfo to avoid lookup (useful during rename)
60 @override
61 Future<void> saveBackup(String name, {WalletInfo? walletInfo}) async {
62 final info = walletInfo ?? await _findWalletByName(name);
63 if (info == null) {
64 throw Exception("Wallet not found: $name");
65 }
66
67 final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: info.type);
68 final walletDirPath = await pathForWalletDir(name: name, type: info.type);
69
70 if (File(walletDirPath).existsSync()) {
71 await File(walletDirPath).copy(backupWalletDirPath);
72 }
73 }
74
75 /// Override restoreWalletFilesFromBackup to look up walletType from wallet name
76 @override
77 Future<void> restoreWalletFilesFromBackup(String name) async {
78 final walletInfo = await _findWalletByName(name);
79 if (walletInfo == null) {
80 throw Exception("Wallet not found: $name");
81 }
82
83 final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: walletInfo.type);
84 final walletDirPath = await pathForWalletDir(name: name, type: walletInfo.type);
85
86 if (File(backupWalletDirPath).existsSync()) {
87 await File(backupWalletDirPath).copy(walletDirPath);
88 }
89 }
90
91 @override
92 Future<EVMChainWallet> create(
93 EVMChainNewWalletCredentials credentials, {
94 bool? isTestnet,
95 }) async {
96 final walletInfo = credentials.walletInfo!;
97
98 // Get chainId from wallet type
99 final chainConfig = _registry.getChainConfigByWalletType(walletInfo.type);
100 if (chainConfig == null) {
101 throw Exception("Chain config not found for wallet type: ${walletInfo.type}");
102 }
103 final initialChainId = chainConfig.chainId;
104
105 final client = EVMChainClientFactory.createClient(initialChainId);
106 final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
107 final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength);
108
109 final derivationInfo = await walletInfo.getDerivationInfo();
110 if (derivationInfo.derivationPath == null || derivationInfo.derivationPath!.isEmpty) {
111 derivationInfo.derivationPath = "m/44'/60'/0'/0";
112 derivationInfo.derivationType = DerivationType.bip39;
113 await derivationInfo.save();
114 }
115
116 final wallet = _createWalletInstance(
117 walletType: walletInfo.type,
118 walletInfo: walletInfo,
119 derivationInfo: derivationInfo,
120 mnemonic: mnemonic,
121 password: credentials.password!,
122 passphrase: credentials.passphrase,
123 client: client,
124 encryptionFileUtils: encryptionFileUtilsFor(isDirect),
125 initialChainId: initialChainId,
126 );
127
128 await wallet.init();
129 await wallet.addInitialTokens();
130 await wallet.save();
131 return wallet;
132 }
133
134 @override
135 Future<EVMChainWallet> openWallet(String name, String password) async {
136 final walletInfo = await _findWalletByName(name);
137 if (walletInfo == null) {
138 throw Exception("Wallet not found");
139 }
140
141 try {
142 final wallet = await _openWalletInstance(
143 name: name,
144 password: password,
145 walletInfo: walletInfo,
146 encryptionFileUtils: encryptionFileUtilsFor(isDirect),
147 );
148
149 await wallet.init();
150 await wallet.addInitialTokens();
151 await wallet.save();
152 await saveBackup(name);
153 return wallet;
154 } catch (_) {
155 await restoreWalletFilesFromBackup(name);
156
157 final wallet = await _openWalletInstance(
158 name: name,
159 password: password,
160 walletInfo: walletInfo,
161 encryptionFileUtils: encryptionFileUtilsFor(isDirect),
162 );
163
164 await wallet.init();
165 await wallet.addInitialTokens();
166 await wallet.save();
167 return wallet;
168 }
169 }
170
171 @override
172 Future<void> rename(String currentName, String password, String newName) async {
173 if (currentName == newName) return;
174
175 final currentWalletInfo = await _findWalletByName(currentName);
176 if (currentWalletInfo == null) {
177 throw Exception("Wallet not found");
178 }
179
180 final type = currentWalletInfo.type;
181
182 await copyWalletFilesTo(fromName: currentName, toName: newName, type: type);
183 await saveBackup(newName, walletInfo: currentWalletInfo);
184
185 currentWalletInfo.id = WalletBase.idFor(newName, type);
186 currentWalletInfo.name = newName;
187 await currentWalletInfo.save();
188
189 final oldNameStillUsed = (await _findWalletByName(currentName)) != null;
190 if (oldNameStillUsed) {
191 for (final token in await Erc20Token.selectList("walletName = ?", [currentName])) {
192 final copiedToken = Erc20Token.copyWith(token, walletName: newName);
193 await copiedToken.save();
194 }
195 } else {
196 await Erc20Token.renameWallet(currentName, newName);
197 }
198
199 final oldDir = Directory(p.join(await pathForWalletTypeDir(type: type), currentName));
200 if (oldDir.existsSync()) {
201 try {
202 await oldDir.delete(recursive: true);
203 } catch (e) {
204 printV('rename: failed to delete old wallet dir "$currentName": $e');
205 }
206 }
207 }
208
209 @override
210 Future<EVMChainWallet> restoreFromSeed(
211 EVMChainRestoreWalletFromSeedCredentials credentials, {
212 bool? isTestnet,
213 }) async {
214 if (!bip39.validateMnemonic(credentials.mnemonic)) {
215 throw EVMChainMnemonicIsIncorrectException();
216 }
217
218 final walletInfo = credentials.walletInfo!;
219
220 // Get chainId from wallet type
221 final chainConfig = _registry.getChainConfigByWalletType(walletInfo.type);
222 if (chainConfig == null) {
223 throw Exception("Chain config not found for wallet type: ${walletInfo.type}");
224 }
225 final initialChainId = chainConfig.chainId;
226
227 final client = EVMChainClientFactory.createClient(initialChainId);
228
229 final derivationInfo = await walletInfo.getDerivationInfo();
230 if (derivationInfo.derivationPath == null || derivationInfo.derivationPath!.isEmpty) {
231 derivationInfo.derivationPath = "m/44'/60'/0'/0";
232 derivationInfo.derivationType = DerivationType.bip39;
233 await derivationInfo.save();
234 }
235
236 final wallet = _createWalletInstance(
237 walletType: walletInfo.type,
238 walletInfo: walletInfo,
239 derivationInfo: derivationInfo,
240 mnemonic: credentials.mnemonic,
241 password: credentials.password!,
242 passphrase: credentials.passphrase,
243 client: client,
244 encryptionFileUtils: encryptionFileUtilsFor(isDirect),
245 initialChainId: initialChainId,
246 );
247
248 await wallet.init();
249 await wallet.addInitialTokens();
250 await wallet.save();
251 return wallet;
252 }
253
254 @override
255 Future<EVMChainWallet> restoreFromKeys(
256 EVMChainRestoreWalletFromPrivateKey credentials, {
257 bool? isTestnet,
258 }) async {
259 final walletInfo = credentials.walletInfo!;
260
261 // Get chainId from wallet type
262 final chainConfig = _registry.getChainConfigByWalletType(walletInfo.type);
263 if (chainConfig == null) {
264 throw Exception("Chain config not found for wallet type: ${walletInfo.type}");
265 }
266 final initialChainId = chainConfig.chainId;
267
268 final client = EVMChainClientFactory.createClient(initialChainId);
269
270 final derivationInfo = await walletInfo.getDerivationInfo();
271 if (derivationInfo.derivationPath == null || derivationInfo.derivationPath!.isEmpty) {
272 derivationInfo.derivationPath = "m/44'/60'/0'/0";
273 derivationInfo.derivationType = DerivationType.bip39;
274 await derivationInfo.save();
275 }
276
277 final wallet = _createWalletInstance(
278 walletType: walletInfo.type,
279 walletInfo: walletInfo,
280 derivationInfo: derivationInfo,
281 privateKey: credentials.privateKey,
282 password: credentials.password!,
283 client: client,
284 encryptionFileUtils: encryptionFileUtilsFor(isDirect),
285 initialChainId: initialChainId,
286 );
287
288 await wallet.init();
289 await wallet.addInitialTokens();
290 await wallet.save();
291 return wallet;
292 }
293
294 @override
295 Future<EVMChainWallet> restoreFromHardwareWallet(
296 EVMChainRestoreWalletFromHardware credentials,
297 ) async {
298 final walletInfo = credentials.walletInfo!;
299
300 // Get chainId from wallet type
301 final chainConfig = _registry.getChainConfigByWalletType(walletInfo.type);
302 if (chainConfig == null) {
303 throw Exception("Chain config not found for wallet type: ${walletInfo.type}");
304 }
305 final initialChainId = chainConfig.chainId;
306
307 final client = EVMChainClientFactory.createClient(initialChainId);
308 final derivationInfo = await walletInfo.getDerivationInfo();
309 derivationInfo.derivationType = DerivationType.bip39;
310 derivationInfo.derivationPath = "m/44'/60'/${credentials.hwAccountData.accountIndex}'/0/0";
311 await derivationInfo.save();
312 walletInfo.hardwareWalletType = credentials.hardwareWalletType;
313 walletInfo.address = credentials.hwAccountData.address;
314 await walletInfo.save();
315
316 final wallet = _createWalletInstance(
317 walletType: walletInfo.type,
318 walletInfo: walletInfo,
319 derivationInfo: derivationInfo,
320 password: credentials.password!,
321 client: client,
322 encryptionFileUtils: encryptionFileUtilsFor(isDirect),
323 initialChainId: initialChainId,
324 );
325
326 await wallet.init();
327 await wallet.addInitialTokens();
328 await wallet.save();
329 return wallet;
330 }
331
332 @override
333 Future<bool> isWalletExit(String name) async {
334 for (final type in _evmWalletTypes) {
335 if (File(await pathForWallet(name: name, type: type)).existsSync()) {
336 return true;
337 }
338 }
339 return false;
340 }
341
342 @override
343 Future<void> remove(String wallet) async {
344 final walletInfo = await _findWalletByName(wallet);
345 if (walletInfo == null) {
346 throw Exception("Wallet not found");
347 }
348
349 File(await pathForWalletDir(name: wallet, type: walletInfo.type)).delete(recursive: true);
350 await WalletInfo.delete(walletInfo);
351 final nameStillUsed = (await _findWalletByName(wallet)) != null;
352 if (!nameStillUsed) {
353 await Erc20Token.deleteAllForWallet(wallet);
354 }
355 }
356
357 EVMChainWallet _createWalletInstance({
358 required WalletType walletType,
359 required WalletInfo walletInfo,
360 required DerivationInfo derivationInfo,
361 String? mnemonic,
362 String? privateKey,
363 required String password,
364 required EVMChainClient client,
365 required EncryptionFileUtils encryptionFileUtils,
366 String? passphrase,
367 int? initialChainId,
368 }) {
369 final chainConfig = _registry.getChainConfigByWalletType(walletType);
370
371 if (chainConfig == null) {
372 throw Exception("Chain config not found for wallet type: $walletType");
373 }
374
375 return EVMChainWallet(
376 walletInfo: walletInfo,
377 derivationInfo: derivationInfo,
378 mnemonic: mnemonic,
379 privateKey: privateKey,
380 password: password,
381 passphrase: passphrase,
382 client: client,
383 nativeCurrency: chainConfig.nativeCurrency,
384 encryptionFileUtils: encryptionFileUtils,
385 initialChainId: initialChainId,
386 );
387 }
388
389 Future<EVMChainWallet> _openWalletInstance({
390 required String name,
391 required String password,
392 required WalletInfo walletInfo,
393 required EncryptionFileUtils encryptionFileUtils,
394 }) {
395 return EVMChainWalletBase.open(
396 name: name,
397 password: password,
398 walletInfo: walletInfo,
399 encryptionFileUtils: encryptionFileUtils,
400 );
401 }
402 }