mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-28 06:28:41 +01:00
* fix android CI * feat: Migrate EVM, Sol and Tron tokens to SQL * handle rename edgecase * refactor: enhance token migration logic and improve database handling
403 lines
13 KiB
Dart
403 lines
13 KiB
Dart
import "dart:io";
|
|
|
|
import 'package:bip39/bip39.dart' as bip39;
|
|
import 'package:cw_core/encryption_file_utils.dart';
|
|
import "package:cw_core/erc20_token.dart";
|
|
import 'package:cw_core/pathForWallet.dart';
|
|
import 'package:cw_core/utils/print_verbose.dart';
|
|
import 'package:cw_core/wallet_base.dart';
|
|
import 'package:cw_core/wallet_info.dart';
|
|
import 'package:cw_core/wallet_service.dart';
|
|
import 'package:cw_core/wallet_type.dart';
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:cw_evm/clients/evm_chain_client.dart';
|
|
import 'package:cw_evm/evm_chain_client_factory.dart';
|
|
import 'package:cw_evm/evm_chain_exceptions.dart';
|
|
import 'package:cw_evm/evm_chain_registry.dart';
|
|
import 'package:cw_evm/evm_chain_wallet.dart';
|
|
import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart';
|
|
|
|
/// Unified service for all EVM chains (Ethereum, Polygon, Base, Arbitrum, etc.)
|
|
///
|
|
/// This service dynamically determines which chain to use based on WalletType
|
|
/// from credentials or walletInfo, eliminating the need for separate service
|
|
/// classes per chain.
|
|
class EVMChainWalletService extends WalletService<
|
|
EVMChainNewWalletCredentials,
|
|
EVMChainRestoreWalletFromSeedCredentials,
|
|
EVMChainRestoreWalletFromPrivateKey,
|
|
EVMChainRestoreWalletFromHardware> {
|
|
EVMChainWalletService(this.isDirect);
|
|
|
|
final bool isDirect;
|
|
final EvmChainRegistry _registry = EvmChainRegistry();
|
|
|
|
List<WalletType> get _evmWalletTypes {
|
|
return _registry.getRegisteredWalletTypes();
|
|
}
|
|
|
|
Future<WalletInfo?> _findWalletByName(String name) async {
|
|
for (final type in _evmWalletTypes) {
|
|
final walletInfo = await WalletInfo.get(name, type);
|
|
if (walletInfo != null) {
|
|
return walletInfo;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// getType() is not meaningful for this unified service, it throws to prevent misuse
|
|
@override
|
|
WalletType getType() {
|
|
throw UnsupportedError(
|
|
"EVMChainWalletService is unified and does not have a single type. "
|
|
"Use walletInfo.type instead.",
|
|
);
|
|
}
|
|
|
|
/// Override saveBackup to look up walletType from wallet name
|
|
/// Optionally accepts walletInfo to avoid lookup (useful during rename)
|
|
@override
|
|
Future<void> saveBackup(String name, {WalletInfo? walletInfo}) async {
|
|
final info = walletInfo ?? await _findWalletByName(name);
|
|
if (info == null) {
|
|
throw Exception("Wallet not found: $name");
|
|
}
|
|
|
|
final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: info.type);
|
|
final walletDirPath = await pathForWalletDir(name: name, type: info.type);
|
|
|
|
if (File(walletDirPath).existsSync()) {
|
|
await File(walletDirPath).copy(backupWalletDirPath);
|
|
}
|
|
}
|
|
|
|
/// Override restoreWalletFilesFromBackup to look up walletType from wallet name
|
|
@override
|
|
Future<void> restoreWalletFilesFromBackup(String name) async {
|
|
final walletInfo = await _findWalletByName(name);
|
|
if (walletInfo == null) {
|
|
throw Exception("Wallet not found: $name");
|
|
}
|
|
|
|
final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: walletInfo.type);
|
|
final walletDirPath = await pathForWalletDir(name: name, type: walletInfo.type);
|
|
|
|
if (File(backupWalletDirPath).existsSync()) {
|
|
await File(backupWalletDirPath).copy(walletDirPath);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<EVMChainWallet> create(
|
|
EVMChainNewWalletCredentials credentials, {
|
|
bool? isTestnet,
|
|
}) async {
|
|
final walletInfo = credentials.walletInfo!;
|
|
|
|
// Get chainId from wallet type
|
|
final chainConfig = _registry.getChainConfigByWalletType(walletInfo.type);
|
|
if (chainConfig == null) {
|
|
throw Exception("Chain config not found for wallet type: ${walletInfo.type}");
|
|
}
|
|
final initialChainId = chainConfig.chainId;
|
|
|
|
final client = EVMChainClientFactory.createClient(initialChainId);
|
|
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
|
|
final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength);
|
|
|
|
final derivationInfo = await walletInfo.getDerivationInfo();
|
|
if (derivationInfo.derivationPath == null || derivationInfo.derivationPath!.isEmpty) {
|
|
derivationInfo.derivationPath = "m/44'/60'/0'/0";
|
|
derivationInfo.derivationType = DerivationType.bip39;
|
|
await derivationInfo.save();
|
|
}
|
|
|
|
final wallet = _createWalletInstance(
|
|
walletType: walletInfo.type,
|
|
walletInfo: walletInfo,
|
|
derivationInfo: derivationInfo,
|
|
mnemonic: mnemonic,
|
|
password: credentials.password!,
|
|
passphrase: credentials.passphrase,
|
|
client: client,
|
|
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
|
initialChainId: initialChainId,
|
|
);
|
|
|
|
await wallet.init();
|
|
await wallet.addInitialTokens();
|
|
await wallet.save();
|
|
return wallet;
|
|
}
|
|
|
|
@override
|
|
Future<EVMChainWallet> openWallet(String name, String password) async {
|
|
final walletInfo = await _findWalletByName(name);
|
|
if (walletInfo == null) {
|
|
throw Exception("Wallet not found");
|
|
}
|
|
|
|
try {
|
|
final wallet = await _openWalletInstance(
|
|
name: name,
|
|
password: password,
|
|
walletInfo: walletInfo,
|
|
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
|
);
|
|
|
|
await wallet.init();
|
|
await wallet.addInitialTokens();
|
|
await wallet.save();
|
|
await saveBackup(name);
|
|
return wallet;
|
|
} catch (_) {
|
|
await restoreWalletFilesFromBackup(name);
|
|
|
|
final wallet = await _openWalletInstance(
|
|
name: name,
|
|
password: password,
|
|
walletInfo: walletInfo,
|
|
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
|
);
|
|
|
|
await wallet.init();
|
|
await wallet.addInitialTokens();
|
|
await wallet.save();
|
|
return wallet;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> rename(String currentName, String password, String newName) async {
|
|
if (currentName == newName) return;
|
|
|
|
final currentWalletInfo = await _findWalletByName(currentName);
|
|
if (currentWalletInfo == null) {
|
|
throw Exception("Wallet not found");
|
|
}
|
|
|
|
final type = currentWalletInfo.type;
|
|
|
|
await copyWalletFilesTo(fromName: currentName, toName: newName, type: type);
|
|
await saveBackup(newName, walletInfo: currentWalletInfo);
|
|
|
|
currentWalletInfo.id = WalletBase.idFor(newName, type);
|
|
currentWalletInfo.name = newName;
|
|
await currentWalletInfo.save();
|
|
|
|
final oldNameStillUsed = (await _findWalletByName(currentName)) != null;
|
|
if (oldNameStillUsed) {
|
|
for (final token in await Erc20Token.selectList("walletName = ?", [currentName])) {
|
|
final copiedToken = Erc20Token.copyWith(token, walletName: newName);
|
|
await copiedToken.save();
|
|
}
|
|
} else {
|
|
await Erc20Token.renameWallet(currentName, newName);
|
|
}
|
|
|
|
final oldDir = Directory(p.join(await pathForWalletTypeDir(type: type), currentName));
|
|
if (oldDir.existsSync()) {
|
|
try {
|
|
await oldDir.delete(recursive: true);
|
|
} catch (e) {
|
|
printV('rename: failed to delete old wallet dir "$currentName": $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<EVMChainWallet> restoreFromSeed(
|
|
EVMChainRestoreWalletFromSeedCredentials credentials, {
|
|
bool? isTestnet,
|
|
}) async {
|
|
if (!bip39.validateMnemonic(credentials.mnemonic)) {
|
|
throw EVMChainMnemonicIsIncorrectException();
|
|
}
|
|
|
|
final walletInfo = credentials.walletInfo!;
|
|
|
|
// Get chainId from wallet type
|
|
final chainConfig = _registry.getChainConfigByWalletType(walletInfo.type);
|
|
if (chainConfig == null) {
|
|
throw Exception("Chain config not found for wallet type: ${walletInfo.type}");
|
|
}
|
|
final initialChainId = chainConfig.chainId;
|
|
|
|
final client = EVMChainClientFactory.createClient(initialChainId);
|
|
|
|
final derivationInfo = await walletInfo.getDerivationInfo();
|
|
if (derivationInfo.derivationPath == null || derivationInfo.derivationPath!.isEmpty) {
|
|
derivationInfo.derivationPath = "m/44'/60'/0'/0";
|
|
derivationInfo.derivationType = DerivationType.bip39;
|
|
await derivationInfo.save();
|
|
}
|
|
|
|
final wallet = _createWalletInstance(
|
|
walletType: walletInfo.type,
|
|
walletInfo: walletInfo,
|
|
derivationInfo: derivationInfo,
|
|
mnemonic: credentials.mnemonic,
|
|
password: credentials.password!,
|
|
passphrase: credentials.passphrase,
|
|
client: client,
|
|
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
|
initialChainId: initialChainId,
|
|
);
|
|
|
|
await wallet.init();
|
|
await wallet.addInitialTokens();
|
|
await wallet.save();
|
|
return wallet;
|
|
}
|
|
|
|
@override
|
|
Future<EVMChainWallet> restoreFromKeys(
|
|
EVMChainRestoreWalletFromPrivateKey credentials, {
|
|
bool? isTestnet,
|
|
}) async {
|
|
final walletInfo = credentials.walletInfo!;
|
|
|
|
// Get chainId from wallet type
|
|
final chainConfig = _registry.getChainConfigByWalletType(walletInfo.type);
|
|
if (chainConfig == null) {
|
|
throw Exception("Chain config not found for wallet type: ${walletInfo.type}");
|
|
}
|
|
final initialChainId = chainConfig.chainId;
|
|
|
|
final client = EVMChainClientFactory.createClient(initialChainId);
|
|
|
|
final derivationInfo = await walletInfo.getDerivationInfo();
|
|
if (derivationInfo.derivationPath == null || derivationInfo.derivationPath!.isEmpty) {
|
|
derivationInfo.derivationPath = "m/44'/60'/0'/0";
|
|
derivationInfo.derivationType = DerivationType.bip39;
|
|
await derivationInfo.save();
|
|
}
|
|
|
|
final wallet = _createWalletInstance(
|
|
walletType: walletInfo.type,
|
|
walletInfo: walletInfo,
|
|
derivationInfo: derivationInfo,
|
|
privateKey: credentials.privateKey,
|
|
password: credentials.password!,
|
|
client: client,
|
|
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
|
initialChainId: initialChainId,
|
|
);
|
|
|
|
await wallet.init();
|
|
await wallet.addInitialTokens();
|
|
await wallet.save();
|
|
return wallet;
|
|
}
|
|
|
|
@override
|
|
Future<EVMChainWallet> restoreFromHardwareWallet(
|
|
EVMChainRestoreWalletFromHardware credentials,
|
|
) async {
|
|
final walletInfo = credentials.walletInfo!;
|
|
|
|
// Get chainId from wallet type
|
|
final chainConfig = _registry.getChainConfigByWalletType(walletInfo.type);
|
|
if (chainConfig == null) {
|
|
throw Exception("Chain config not found for wallet type: ${walletInfo.type}");
|
|
}
|
|
final initialChainId = chainConfig.chainId;
|
|
|
|
final client = EVMChainClientFactory.createClient(initialChainId);
|
|
final derivationInfo = await walletInfo.getDerivationInfo();
|
|
derivationInfo.derivationType = DerivationType.bip39;
|
|
derivationInfo.derivationPath = "m/44'/60'/${credentials.hwAccountData.accountIndex}'/0/0";
|
|
await derivationInfo.save();
|
|
walletInfo.hardwareWalletType = credentials.hardwareWalletType;
|
|
walletInfo.address = credentials.hwAccountData.address;
|
|
await walletInfo.save();
|
|
|
|
final wallet = _createWalletInstance(
|
|
walletType: walletInfo.type,
|
|
walletInfo: walletInfo,
|
|
derivationInfo: derivationInfo,
|
|
password: credentials.password!,
|
|
client: client,
|
|
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
|
initialChainId: initialChainId,
|
|
);
|
|
|
|
await wallet.init();
|
|
await wallet.addInitialTokens();
|
|
await wallet.save();
|
|
return wallet;
|
|
}
|
|
|
|
@override
|
|
Future<bool> isWalletExit(String name) async {
|
|
for (final type in _evmWalletTypes) {
|
|
if (File(await pathForWallet(name: name, type: type)).existsSync()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@override
|
|
Future<void> remove(String wallet) async {
|
|
final walletInfo = await _findWalletByName(wallet);
|
|
if (walletInfo == null) {
|
|
throw Exception("Wallet not found");
|
|
}
|
|
|
|
File(await pathForWalletDir(name: wallet, type: walletInfo.type)).delete(recursive: true);
|
|
await WalletInfo.delete(walletInfo);
|
|
final nameStillUsed = (await _findWalletByName(wallet)) != null;
|
|
if (!nameStillUsed) {
|
|
await Erc20Token.deleteAllForWallet(wallet);
|
|
}
|
|
}
|
|
|
|
EVMChainWallet _createWalletInstance({
|
|
required WalletType walletType,
|
|
required WalletInfo walletInfo,
|
|
required DerivationInfo derivationInfo,
|
|
String? mnemonic,
|
|
String? privateKey,
|
|
required String password,
|
|
required EVMChainClient client,
|
|
required EncryptionFileUtils encryptionFileUtils,
|
|
String? passphrase,
|
|
int? initialChainId,
|
|
}) {
|
|
final chainConfig = _registry.getChainConfigByWalletType(walletType);
|
|
|
|
if (chainConfig == null) {
|
|
throw Exception("Chain config not found for wallet type: $walletType");
|
|
}
|
|
|
|
return EVMChainWallet(
|
|
walletInfo: walletInfo,
|
|
derivationInfo: derivationInfo,
|
|
mnemonic: mnemonic,
|
|
privateKey: privateKey,
|
|
password: password,
|
|
passphrase: passphrase,
|
|
client: client,
|
|
nativeCurrency: chainConfig.nativeCurrency,
|
|
encryptionFileUtils: encryptionFileUtils,
|
|
initialChainId: initialChainId,
|
|
);
|
|
}
|
|
|
|
Future<EVMChainWallet> _openWalletInstance({
|
|
required String name,
|
|
required String password,
|
|
required WalletInfo walletInfo,
|
|
required EncryptionFileUtils encryptionFileUtils,
|
|
}) {
|
|
return EVMChainWalletBase.open(
|
|
name: name,
|
|
password: password,
|
|
walletInfo: walletInfo,
|
|
encryptionFileUtils: encryptionFileUtils,
|
|
);
|
|
}
|
|
}
|