From fb33a6f23dce32172d9b80f343972dc4e07289c3 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 9 Aug 2024 23:15:30 +0300 Subject: [PATCH] Cw 688 avoid wallet file corruption (#1582) * CW-688 Store Seed and keys in .keys file * CW-688 Open wallet from keys in .keys file and migrate wallets using the old file * CW-688 Open wallet from keys in .keys file and migrate wallets using the old file * CW-688 Restore .keys file from .keys.backup * CW-688 Restore .keys file from .keys.backup * CW-688 Move saving .keys files into the save function instead of the service * CW-688 Handle corrupt wallets * CW-688 Handle corrupt wallets * CW-688 Remove code duplication * CW-688 Reduce cache dependency * wrap any file reading/writing function with try/catch [skip ci] --------- Co-authored-by: Konstantin Ullrich --- cw_bitcoin/lib/bitcoin_wallet.dart | 66 +++++++---- cw_bitcoin/lib/bitcoin_wallet_service.dart | 2 + cw_bitcoin/lib/electrum_wallet.dart | 14 ++- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 8 +- cw_bitcoin/lib/litecoin_wallet.dart | 55 ++++++--- cw_bitcoin/lib/litecoin_wallet_service.dart | 1 + .../lib/src/bitcoin_cash_wallet.dart | 35 ++++-- .../lib/src/bitcoin_cash_wallet_service.dart | 2 + cw_core/lib/wallet_keys_file.dart | 110 ++++++++++++++++++ cw_ethereum/lib/ethereum_wallet.dart | 33 ++++-- cw_evm/lib/evm_chain_wallet.dart | 11 +- cw_nano/lib/nano_wallet.dart | 81 ++++++++----- cw_nano/lib/nano_wallet_service.dart | 2 +- cw_polygon/lib/polygon_wallet.dart | 37 ++++-- cw_polygon/lib/polygon_wallet_service.dart | 2 - cw_solana/lib/solana_wallet.dart | 47 ++++++-- cw_tron/lib/tron_wallet.dart | 66 +++++++---- cw_tron/lib/tron_wallet_service.dart | 5 +- 18 files changed, 433 insertions(+), 144 deletions(-) create mode 100644 cw_core/lib/wallet_keys_file.dart diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index d061480ed..ce3e2caa8 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -6,15 +6,16 @@ import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:convert/convert.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; -import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; +import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:cw_bitcoin/psbt_transaction_builder.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_keys_file.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:ledger_bitcoin/ledger_bitcoin.dart'; @@ -143,49 +144,66 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final network = walletInfo.network != null ? BasedUtxoNetwork.fromName(walletInfo.network!) : BitcoinNetwork.mainnet; - final snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password, network); - walletInfo.derivationInfo ??= DerivationInfo( - derivationType: snp.derivationType ?? DerivationType.electrum, - derivationPath: snp.derivationPath, - ); + final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); + + ElectrumWalletSnapshot? snp = null; + + try { + snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password, network); + } catch (e) { + if (!hasKeysFile) rethrow; + } + + final WalletKeysData keysData; + // Migrate wallet from the old scheme to then new .keys file scheme + if (!hasKeysFile) { + keysData = + WalletKeysData(mnemonic: snp!.mnemonic, xPub: snp.xpub, passphrase: snp.passphrase); + } else { + keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + } + + walletInfo.derivationInfo ??= DerivationInfo(); // set the default if not present: - walletInfo.derivationInfo!.derivationPath = snp.derivationPath ?? electrum_path; - walletInfo.derivationInfo!.derivationType = snp.derivationType ?? DerivationType.electrum; + walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path; + walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum; Uint8List? seedBytes = null; + final mnemonic = keysData.mnemonic; + final passphrase = keysData.passphrase; - if (snp.mnemonic != null) { + if (mnemonic != null) { switch (walletInfo.derivationInfo!.derivationType) { case DerivationType.electrum: - seedBytes = await mnemonicToSeedBytes(snp.mnemonic!); + seedBytes = await mnemonicToSeedBytes(mnemonic); break; case DerivationType.bip39: default: seedBytes = await bip39.mnemonicToSeed( - snp.mnemonic!, - passphrase: snp.passphrase ?? '', + mnemonic, + passphrase: passphrase ?? '', ); break; } } return BitcoinWallet( - mnemonic: snp.mnemonic, - xpub: snp.xpub, + mnemonic: mnemonic, + xpub: keysData.xPub, password: password, - passphrase: snp.passphrase, + passphrase: passphrase, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: snp.addresses, - initialSilentAddresses: snp.silentAddresses, - initialSilentAddressIndex: snp.silentAddressIndex, - initialBalance: snp.balance, + initialAddresses: snp?.addresses, + initialSilentAddresses: snp?.silentAddresses, + initialSilentAddressIndex: snp?.silentAddressIndex ?? 0, + initialBalance: snp?.balance, seedBytes: seedBytes, - initialRegularAddressIndex: snp.regularAddressIndex, - initialChangeAddressIndex: snp.changeAddressIndex, - addressPageType: snp.addressPageType, + initialRegularAddressIndex: snp?.regularAddressIndex, + initialChangeAddressIndex: snp?.changeAddressIndex, + addressPageType: snp?.addressPageType, networkParam: network, alwaysScan: alwaysScan, ); @@ -249,8 +267,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final accountPath = walletInfo.derivationInfo?.derivationPath; final derivationPath = accountPath != null ? "$accountPath/$isChange/$index" : null; - final signature = await _bitcoinLedgerApp! - .signMessage(_ledgerDevice!, message: ascii.encode(message), signDerivationPath: derivationPath); + final signature = await _bitcoinLedgerApp!.signMessage(_ledgerDevice!, + message: ascii.encode(message), signDerivationPath: derivationPath); return base64Encode(signature); } diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index a9a6d96db..cf93aa29d 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -41,8 +41,10 @@ class BitcoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, network: network, ); + await wallet.save(); await wallet.init(); + return wallet; } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 39cf95009..e55e5ed0e 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -37,6 +37,7 @@ import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/utils/file.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/get_height_by_date.dart'; import 'package:flutter/foundation.dart'; @@ -54,7 +55,7 @@ const int TWEAKS_COUNT = 25; abstract class ElectrumWalletBase extends WalletBase - with Store { + with Store, WalletKeysFile { ElectrumWalletBase({ required String password, required WalletInfo walletInfo, @@ -169,6 +170,10 @@ abstract class ElectrumWalletBase @override String? get seed => _mnemonic; + @override + WalletKeysData get walletKeysData => + WalletKeysData(mnemonic: _mnemonic, xPub: xpub, passphrase: passphrase); + bitcoin.NetworkType networkType; BasedUtxoNetwork network; @@ -1076,6 +1081,11 @@ abstract class ElectrumWalletBase @override Future save() async { + if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) { + await saveKeysFile(_password); + saveKeysFile(_password, true); + } + final path = await makePath(); await write(path: path, password: _password, data: toJSON()); await transactionHistory.save(); @@ -1131,8 +1141,6 @@ abstract class ElectrumWalletBase _autoSaveTimer?.cancel(); } - Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); - @action Future updateAllUnspents() async { List updatedUnspentCoins = []; diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 15ad1cf63..082460f72 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -32,15 +32,21 @@ class ElectrumWalletSnapshot { final WalletType type; final String? addressPageType; + @deprecated String? mnemonic; + + @deprecated String? xpub; + + @deprecated + String? passphrase; + List addresses; List silentAddresses; ElectrumBalance balance; Map regularAddressIndex; Map changeAddressIndex; int silentAddressIndex; - String? passphrase; DerivationType? derivationType; String? derivationPath; diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 209ddc774..bfb9a1b16 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,20 +1,21 @@ +import 'package:bip39/bip39.dart' as bip39; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_bitcoin/electrum_balance.dart'; +import 'package:cw_bitcoin/electrum_wallet.dart'; +import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; +import 'package:cw_bitcoin/litecoin_network.dart'; import 'package:cw_bitcoin/litecoin_wallet_addresses.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_keys_file.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; -import 'package:cw_bitcoin/electrum_wallet.dart'; -import 'package:cw_bitcoin/bitcoin_address_record.dart'; -import 'package:cw_bitcoin/electrum_balance.dart'; -import 'package:cw_bitcoin/litecoin_network.dart'; -import 'package:bip39/bip39.dart' as bip39; part 'litecoin_wallet.g.dart'; @@ -101,19 +102,37 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { required Box unspentCoinsInfo, required String password, }) async { - final snp = - await ElectrumWalletSnapshot.load(name, walletInfo.type, password, LitecoinNetwork.mainnet); + final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); + + ElectrumWalletSnapshot? snp = null; + + try { + snp = await ElectrumWalletSnapshot.load( + name, walletInfo.type, password, LitecoinNetwork.mainnet); + } catch (e) { + if (!hasKeysFile) rethrow; + } + + final WalletKeysData keysData; + // Migrate wallet from the old scheme to then new .keys file scheme + if (!hasKeysFile) { + keysData = + WalletKeysData(mnemonic: snp!.mnemonic, xPub: snp.xpub, passphrase: snp.passphrase); + } else { + keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + } + return LitecoinWallet( - mnemonic: snp.mnemonic!, + mnemonic: keysData.mnemonic!, password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: snp.addresses, - initialBalance: snp.balance, - seedBytes: await mnemonicToSeedBytes(snp.mnemonic!), - initialRegularAddressIndex: snp.regularAddressIndex, - initialChangeAddressIndex: snp.changeAddressIndex, - addressPageType: snp.addressPageType, + initialAddresses: snp?.addresses, + initialBalance: snp?.balance, + seedBytes: await mnemonicToSeedBytes(keysData.mnemonic!), + initialRegularAddressIndex: snp?.regularAddressIndex, + initialChangeAddressIndex: snp?.changeAddressIndex, + addressPageType: snp?.addressPageType, ); } diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index bb51a4eaa..7025b72e5 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -33,6 +33,7 @@ class LitecoinWalletService extends WalletService< passphrase: credentials.passphrase, walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); + await wallet.save(); await wallet.init(); diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 51bd3612d..f15eed10d 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -12,6 +12,7 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_keys_file.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -89,14 +90,32 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { required Box unspentCoinsInfo, required String password, }) async { - final snp = await ElectrumWalletSnapshot.load( - name, walletInfo.type, password, BitcoinCashNetwork.mainnet); + final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); + + ElectrumWalletSnapshot? snp = null; + + try { + snp = await ElectrumWalletSnapshot.load( + name, walletInfo.type, password, BitcoinCashNetwork.mainnet); + } catch (e) { + if (!hasKeysFile) rethrow; + } + + final WalletKeysData keysData; + // Migrate wallet from the old scheme to then new .keys file scheme + if (!hasKeysFile) { + keysData = + WalletKeysData(mnemonic: snp!.mnemonic, xPub: snp.xpub, passphrase: snp.passphrase); + } else { + keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + } + return BitcoinCashWallet( - mnemonic: snp.mnemonic!, + mnemonic: keysData.mnemonic!, password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: snp.addresses.map((addr) { + initialAddresses: snp?.addresses.map((addr) { try { BitcoinCashAddress(addr.address); return BitcoinAddressRecord( @@ -116,10 +135,10 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { ); } }).toList(), - initialBalance: snp.balance, - seedBytes: await Mnemonic.toSeed(snp.mnemonic!), - initialRegularAddressIndex: snp.regularAddressIndex, - initialChangeAddressIndex: snp.changeAddressIndex, + initialBalance: snp?.balance, + seedBytes: await Mnemonic.toSeed(keysData.mnemonic!), + initialRegularAddressIndex: snp?.regularAddressIndex, + initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, ); } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart index e6c0cad07..01ae8ace3 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart @@ -34,8 +34,10 @@ class BitcoinCashWalletService extends WalletService + on WalletBase { + Future makePath() => pathForWallet(name: walletInfo.name, type: walletInfo.type); + + // this needs to be overridden + WalletKeysData get walletKeysData; + + Future makeKeysFilePath() async => "${await makePath()}.keys"; + + Future saveKeysFile(String password, [bool isBackup = false]) async { + try { + final rootPath = await makeKeysFilePath(); + final path = "$rootPath${isBackup ? ".backup" : ""}"; + dev.log("Saving .keys file '$path'"); + await write(path: path, password: password, data: walletKeysData.toJSON()); + } catch (_) {} + } + + static Future createKeysFile( + String name, WalletType type, String password, WalletKeysData walletKeysData, + [bool withBackup = true]) async { + try { + final rootPath = await pathForWallet(name: name, type: type); + final path = "$rootPath.keys"; + + dev.log("Saving .keys file '$path'"); + await write(path: path, password: password, data: walletKeysData.toJSON()); + + if (withBackup) { + dev.log("Saving .keys.backup file '$path.backup'"); + await write(path: "$path.backup", password: password, data: walletKeysData.toJSON()); + } + } catch (_) {} + } + + static Future hasKeysFile(String name, WalletType type) async { + try { + final path = await pathForWallet(name: name, type: type); + return File("$path.keys").existsSync() || File("$path.keys.backup").existsSync(); + } catch (_) { + return false; + } + } + + static Future readKeysFile(String name, WalletType type, String password) async { + final path = await pathForWallet(name: name, type: type); + + var readPath = "$path.keys"; + try { + if (!File(readPath).existsSync()) throw Exception("No .keys file found for $name $type"); + + final jsonSource = await read(path: readPath, password: password); + final data = json.decode(jsonSource) as Map; + return WalletKeysData.fromJSON(data); + } catch (e) { + dev.log("Failed to read .keys file. Trying .keys.backup file..."); + + readPath = "$readPath.backup"; + if (!File(readPath).existsSync()) + throw Exception("No .keys nor a .keys.backup file found for $name $type"); + + final jsonSource = await read(path: readPath, password: password); + final data = json.decode(jsonSource) as Map; + final keysData = WalletKeysData.fromJSON(data); + + dev.log("Restoring .keys from .keys.backup"); + createKeysFile(name, type, password, keysData, false); + return keysData; + } + } +} + +class WalletKeysData { + final String? privateKey; + final String? mnemonic; + final String? altMnemonic; + final String? passphrase; + final String? xPub; + + WalletKeysData({this.privateKey, this.mnemonic, this.altMnemonic, this.passphrase, this.xPub}); + + String toJSON() => jsonEncode({ + "privateKey": privateKey, + "mnemonic": mnemonic, + if (altMnemonic != null) "altMnemonic": altMnemonic, + if (passphrase != null) "passphrase": passphrase, + if (xPub != null) "xPub": xPub + }); + + static WalletKeysData fromJSON(Map json) => WalletKeysData( + privateKey: json["privateKey"] as String?, + mnemonic: json["mnemonic"] as String?, + altMnemonic: json["altMnemonic"] as String?, + passphrase: json["passphrase"] as String?, + xPub: json["xPub"] as String?, + ); +} diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart index 2c58cd31d..7bcd55cf4 100644 --- a/cw_ethereum/lib/ethereum_wallet.dart +++ b/cw_ethereum/lib/ethereum_wallet.dart @@ -6,6 +6,7 @@ import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_ethereum/default_ethereum_erc20_tokens.dart'; import 'package:cw_ethereum/ethereum_client.dart'; import 'package:cw_ethereum/ethereum_transaction_history.dart'; @@ -122,19 +123,37 @@ class EthereumWallet extends EVMChainWallet { static Future open( {required String name, required String password, required WalletInfo walletInfo}) async { + final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); final path = await pathForWallet(name: name, type: walletInfo.type); - final jsonSource = await read(path: path, password: password); - final data = json.decode(jsonSource) as Map; - final mnemonic = data['mnemonic'] as String?; - final privateKey = data['private_key'] as String?; - final balance = EVMChainERC20Balance.fromJSON(data['balance'] as String) ?? + + Map? data; + try { + final jsonSource = await read(path: path, password: password); + + data = json.decode(jsonSource) as Map; + } catch (e) { + if (!hasKeysFile) rethrow; + } + + final balance = EVMChainERC20Balance.fromJSON(data?['balance'] as String) ?? EVMChainERC20Balance(BigInt.zero); + final WalletKeysData keysData; + // Migrate wallet from the old scheme to then new .keys file scheme + if (!hasKeysFile) { + final mnemonic = data!['mnemonic'] as String?; + final privateKey = data['private_key'] as String?; + + keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey); + } else { + keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + } + return EthereumWallet( walletInfo: walletInfo, password: password, - mnemonic: mnemonic, - privateKey: privateKey, + mnemonic: keysData.mnemonic, + privateKey: keysData.privateKey, initialBalance: balance, client: EthereumClient(), ); diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 760c50a04..55dcea959 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -16,6 +16,7 @@ import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_evm/evm_chain_client.dart'; import 'package:cw_evm/evm_chain_exceptions.dart'; @@ -58,7 +59,7 @@ abstract class EVMChainWallet = EVMChainWalletBase with _$EVMChainWallet; abstract class EVMChainWalletBase extends WalletBase - with Store { + with Store, WalletKeysFile { EVMChainWalletBase({ required WalletInfo walletInfo, required EVMChainClient client, @@ -508,6 +509,11 @@ abstract class EVMChainWalletBase @override Future save() async { + if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) { + await saveKeysFile(_password); + saveKeysFile(_password, true); + } + await walletAddresses.updateAddressesInBox(); final path = await makePath(); await write(path: path, password: _password, data: toJSON()); @@ -522,7 +528,8 @@ abstract class EVMChainWalletBase ? HEX.encode((evmChainPrivateKey as EthPrivateKey).privateKey) : null; - Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); + @override + WalletKeysData get walletKeysData => WalletKeysData(mnemonic: _mnemonic, privateKey: privateKey); String toJSON() => json.encode({ 'mnemonic': _mnemonic, diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart index 5efe3006d..55e01d10b 100644 --- a/cw_nano/lib/nano_wallet.dart +++ b/cw_nano/lib/nano_wallet.dart @@ -1,8 +1,12 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:bip39/bip39.dart' as bip39; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/n2_node.dart'; +import 'package:cw_core/nano_account.dart'; import 'package:cw_core/nano_account_info_response.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/pathForWallet.dart'; @@ -10,23 +14,20 @@ import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_nano/file.dart'; -import 'package:cw_core/nano_account.dart'; -import 'package:cw_core/n2_node.dart'; import 'package:cw_nano/nano_balance.dart'; import 'package:cw_nano/nano_client.dart'; import 'package:cw_nano/nano_transaction_credentials.dart'; import 'package:cw_nano/nano_transaction_history.dart'; import 'package:cw_nano/nano_transaction_info.dart'; +import 'package:cw_nano/nano_wallet_addresses.dart'; import 'package:cw_nano/nano_wallet_keys.dart'; import 'package:cw_nano/pending_nano_transaction.dart'; import 'package:mobx/mobx.dart'; -import 'dart:async'; -import 'package:cw_nano/nano_wallet_addresses.dart'; -import 'package:cw_core/wallet_base.dart'; import 'package:nanodart/nanodart.dart'; -import 'package:bip39/bip39.dart' as bip39; import 'package:nanoutil/nanoutil.dart'; part 'nano_wallet.g.dart'; @@ -34,7 +35,8 @@ part 'nano_wallet.g.dart'; class NanoWallet = NanoWalletBase with _$NanoWallet; abstract class NanoWalletBase - extends WalletBase with Store { + extends WalletBase + with Store, WalletKeysFile { NanoWalletBase({ required WalletInfo walletInfo, required String mnemonic, @@ -70,6 +72,7 @@ abstract class NanoWalletBase String? _representativeAddress; int repScore = 100; + bool get isRepOk => repScore >= 90; late final NanoClient _client; @@ -128,14 +131,10 @@ abstract class NanoWalletBase } @override - int calculateEstimatedFee(TransactionPriority priority, int? amount) { - return 0; // always 0 :) - } + int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0; // always 0 :) @override - Future changePassword(String password) { - throw UnimplementedError("changePassword"); - } + Future changePassword(String password) => throw UnimplementedError("changePassword"); @override void close() { @@ -170,9 +169,7 @@ abstract class NanoWalletBase } @override - Future connectToPowNode({required Node node}) async { - _client.connectPow(node); - } + Future connectToPowNode({required Node node}) async => _client.connectPow(node); @override Future createTransaction(Object credentials) async { @@ -296,9 +293,7 @@ abstract class NanoWalletBase } @override - NanoWalletKeys get keys { - return NanoWalletKeys(seedKey: _hexSeed!); - } + NanoWalletKeys get keys => NanoWalletKeys(seedKey: _hexSeed!); @override String? get privateKey => _privateKey!; @@ -312,6 +307,11 @@ abstract class NanoWalletBase @override Future save() async { + if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) { + await saveKeysFile(_password); + saveKeysFile(_password, true); + } + await walletAddresses.updateAddressesInBox(); final path = await makePath(); await write(path: path, password: _password, data: toJSON()); @@ -323,6 +323,9 @@ abstract class NanoWalletBase String get hexSeed => _hexSeed!; + @override + WalletKeysData get walletKeysData => WalletKeysData(mnemonic: _mnemonic, altMnemonic: hexSeed); + String get representative => _representativeAddress ?? ""; @action @@ -358,8 +361,6 @@ abstract class NanoWalletBase } } - Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); - String toJSON() => json.encode({ 'seedKey': _hexSeed, 'mnemonic': _mnemonic, @@ -373,31 +374,47 @@ abstract class NanoWalletBase required String password, required WalletInfo walletInfo, }) async { + final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); final path = await pathForWallet(name: name, type: walletInfo.type); - final jsonSource = await read(path: path, password: password); - final data = json.decode(jsonSource) as Map; - final mnemonic = data['mnemonic'] as String; + Map? data = null; + try { + final jsonSource = await read(path: path, password: password); + + data = json.decode(jsonSource) as Map; + } catch (e) { + if (!hasKeysFile) rethrow; + } final balance = NanoBalance.fromRawString( - currentBalance: data['currentBalance'] as String? ?? "0", - receivableBalance: data['receivableBalance'] as String? ?? "0", + currentBalance: data?['currentBalance'] as String? ?? "0", + receivableBalance: data?['receivableBalance'] as String? ?? "0", ); + final WalletKeysData keysData; + // Migrate wallet from the old scheme to then new .keys file scheme + if (!hasKeysFile) { + final mnemonic = data!['mnemonic'] as String; + final isHexSeed = !mnemonic.contains(' '); + + keysData = WalletKeysData( + mnemonic: isHexSeed ? null : mnemonic, altMnemonic: isHexSeed ? mnemonic : null); + } else { + keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + } + DerivationType derivationType = DerivationType.nano; - if (data['derivationType'] == "DerivationType.bip39") { + if (data?['derivationType'] == "DerivationType.bip39") { derivationType = DerivationType.bip39; } walletInfo.derivationInfo ??= DerivationInfo(derivationType: derivationType); - if (walletInfo.derivationInfo!.derivationType == null) { - walletInfo.derivationInfo!.derivationType = derivationType; - } + walletInfo.derivationInfo!.derivationType ??= derivationType; return NanoWallet( walletInfo: walletInfo, password: password, - mnemonic: mnemonic, + mnemonic: keysData.mnemonic!, initialBalance: balance, ); // init() should always be run after this! @@ -435,7 +452,7 @@ abstract class NanoWalletBase _representativeAddress = await _client.getRepFromPrefs(); throw Exception("Failed to get representative address $e"); } - + repScore = await _client.getRepScore(_representativeAddress!); } diff --git a/cw_nano/lib/nano_wallet_service.dart b/cw_nano/lib/nano_wallet_service.dart index a1af3c872..755598705 100644 --- a/cw_nano/lib/nano_wallet_service.dart +++ b/cw_nano/lib/nano_wallet_service.dart @@ -39,7 +39,7 @@ class NanoWalletService extends WalletService open( {required String name, required String password, required WalletInfo walletInfo}) async { + final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); final path = await pathForWallet(name: name, type: walletInfo.type); - final jsonSource = await read(path: path, password: password); - final data = json.decode(jsonSource) as Map; - final mnemonic = data['mnemonic'] as String?; - final privateKey = data['private_key'] as String?; - final balance = EVMChainERC20Balance.fromJSON(data['balance'] as String) ?? + + Map? data; + try { + final jsonSource = await read(path: path, password: password); + + data = json.decode(jsonSource) as Map; + } catch (e) { + if (!hasKeysFile) rethrow; + } + + final balance = EVMChainERC20Balance.fromJSON(data?['balance'] as String) ?? EVMChainERC20Balance(BigInt.zero); + final WalletKeysData keysData; + // Migrate wallet from the old scheme to then new .keys file scheme + if (!hasKeysFile) { + final mnemonic = data!['mnemonic'] as String?; + final privateKey = data['private_key'] as String?; + + keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey); + } else { + keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + } + return PolygonWallet( walletInfo: walletInfo, password: password, - mnemonic: mnemonic, - privateKey: privateKey, + mnemonic: keysData.mnemonic, + privateKey: keysData.privateKey, initialBalance: balance, client: PolygonClient(), ); diff --git a/cw_polygon/lib/polygon_wallet_service.dart b/cw_polygon/lib/polygon_wallet_service.dart index ee84a014e..14baffc44 100644 --- a/cw_polygon/lib/polygon_wallet_service.dart +++ b/cw_polygon/lib/polygon_wallet_service.dart @@ -35,7 +35,6 @@ class PolygonWalletService extends EVMChainWalletService { await wallet.init(); wallet.addInitialTokens(); await wallet.save(); - return wallet; } @@ -83,7 +82,6 @@ class PolygonWalletService extends EVMChainWalletService { await wallet.init(); wallet.addInitialTokens(); await wallet.save(); - return wallet; } diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index 401968698..2b30a204c 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; + import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/node.dart'; @@ -12,6 +13,7 @@ import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_solana/default_spl_tokens.dart'; import 'package:cw_solana/file.dart'; import 'package:cw_solana/solana_balance.dart'; @@ -36,7 +38,8 @@ part 'solana_wallet.g.dart'; class SolanaWallet = SolanaWalletBase with _$SolanaWallet; abstract class SolanaWalletBase - extends WalletBase with Store { + extends WalletBase + with Store, WalletKeysFile { SolanaWalletBase({ required WalletInfo walletInfo, String? mnemonic, @@ -121,6 +124,9 @@ abstract class SolanaWalletBase return privateKey; } + @override + WalletKeysData get walletKeysData => WalletKeysData(mnemonic: _mnemonic, privateKey: privateKey); + Future init() async { final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${SPLToken.boxName}"; @@ -336,6 +342,11 @@ abstract class SolanaWalletBase @override Future save() async { + if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) { + await saveKeysFile(_password); + saveKeysFile(_password, true); + } + await walletAddresses.updateAddressesInBox(); final path = await makePath(); await write(path: path, password: _password, data: toJSON()); @@ -361,8 +372,6 @@ abstract class SolanaWalletBase } } - Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); - String toJSON() => json.encode({ 'mnemonic': _mnemonic, 'private_key': _hexPrivateKey, @@ -374,18 +383,36 @@ abstract class SolanaWalletBase required String password, required WalletInfo walletInfo, }) async { + final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); final path = await pathForWallet(name: name, type: walletInfo.type); - final jsonSource = await read(path: path, password: password); - final data = json.decode(jsonSource) as Map; - final mnemonic = data['mnemonic'] as String?; - final privateKey = data['private_key'] as String?; - final balance = SolanaBalance.fromJSON(data['balance'] as String) ?? SolanaBalance(0.0); + + Map? data; + try { + final jsonSource = await read(path: path, password: password); + + data = json.decode(jsonSource) as Map; + } catch (e) { + if (!hasKeysFile) rethrow; + } + + final balance = SolanaBalance.fromJSON(data?['balance'] as String) ?? SolanaBalance(0.0); + + final WalletKeysData keysData; + // Migrate wallet from the old scheme to then new .keys file scheme + if (!hasKeysFile) { + final mnemonic = data!['mnemonic'] as String?; + final privateKey = data['private_key'] as String?; + + keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey); + } else { + keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + } return SolanaWallet( walletInfo: walletInfo, password: password, - mnemonic: mnemonic, - privateKey: privateKey, + mnemonic: keysData.mnemonic, + privateKey: keysData.privateKey, initialBalance: balance, ); } diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart index 96f92e450..cb4c9c024 100644 --- a/cw_tron/lib/tron_wallet.dart +++ b/cw_tron/lib/tron_wallet.dart @@ -16,6 +16,7 @@ import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_tron/default_tron_tokens.dart'; import 'package:cw_tron/file.dart'; @@ -37,7 +38,8 @@ part 'tron_wallet.g.dart'; class TronWallet = TronWalletBase with _$TronWallet; abstract class TronWalletBase - extends WalletBase with Store { + extends WalletBase + with Store, WalletKeysFile { TronWalletBase({ required WalletInfo walletInfo, String? mnemonic, @@ -124,18 +126,36 @@ abstract class TronWalletBase required String password, required WalletInfo walletInfo, }) async { + final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); final path = await pathForWallet(name: name, type: walletInfo.type); - final jsonSource = await read(path: path, password: password); - final data = json.decode(jsonSource) as Map; - final mnemonic = data['mnemonic'] as String?; - final privateKey = data['private_key'] as String?; - final balance = TronBalance.fromJSON(data['balance'] as String) ?? TronBalance(BigInt.zero); + + Map? data; + try { + final jsonSource = await read(path: path, password: password); + + data = json.decode(jsonSource) as Map; + } catch (e) { + if (!hasKeysFile) rethrow; + } + + final balance = TronBalance.fromJSON(data?['balance'] as String) ?? TronBalance(BigInt.zero); + + final WalletKeysData keysData; + // Migrate wallet from the old scheme to then new .keys file scheme + if (!hasKeysFile) { + final mnemonic = data!['mnemonic'] as String?; + final privateKey = data['private_key'] as String?; + + keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey); + } else { + keysData = await WalletKeysFile.readKeysFile(name, walletInfo.type, password); + } return TronWallet( walletInfo: walletInfo, password: password, - mnemonic: mnemonic, - privateKey: privateKey, + mnemonic: keysData.mnemonic, + privateKey: keysData.privateKey, initialBalance: balance, ); } @@ -163,9 +183,7 @@ abstract class TronWalletBase }) async { assert(mnemonic != null || privateKey != null); - if (privateKey != null) { - return TronPrivateKey(privateKey); - } + if (privateKey != null) return TronPrivateKey(privateKey); final seed = bip39.mnemonicToSeed(mnemonic!); @@ -181,14 +199,10 @@ abstract class TronWalletBase int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0; @override - Future changePassword(String password) { - throw UnimplementedError("changePassword"); - } + Future changePassword(String password) => throw UnimplementedError("changePassword"); @override - void close() { - _transactionsUpdateTimer?.cancel(); - } + void close() => _transactionsUpdateTimer?.cancel(); @action @override @@ -406,12 +420,15 @@ abstract class TronWalletBase Object get keys => throw UnimplementedError("keys"); @override - Future rescan({required int height}) { - throw UnimplementedError("rescan"); - } + Future rescan({required int height}) => throw UnimplementedError("rescan"); @override Future save() async { + if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) { + await saveKeysFile(_password); + saveKeysFile(_password, true); + } + await walletAddresses.updateAddressesInBox(); final path = await makePath(); await write(path: path, password: _password, data: toJSON()); @@ -424,7 +441,8 @@ abstract class TronWalletBase @override String get privateKey => _tronPrivateKey.toHex(); - Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); + @override + WalletKeysData get walletKeysData => WalletKeysData(mnemonic: _mnemonic, privateKey: privateKey); String toJSON() => json.encode({ 'mnemonic': _mnemonic, @@ -512,7 +530,7 @@ abstract class TronWalletBase @override Future renameWalletFiles(String newWalletName) async { - String transactionHistoryFileNameForWallet = 'tron_transactions.json'; + const transactionHistoryFileNameForWallet = 'tron_transactions.json'; final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type); final currentWalletFile = File(currentWalletPath); @@ -550,9 +568,7 @@ abstract class TronWalletBase Future signMessage(String message, {String? address}) async => _tronPrivateKey.signPersonalMessage(ascii.encode(message)); - String getTronBase58AddressFromHex(String hexAddress) { - return TronAddress(hexAddress).toAddress(); - } + String getTronBase58AddressFromHex(String hexAddress) => TronAddress(hexAddress).toAddress(); void updateScanProviderUsageState(bool isEnabled) { if (isEnabled) { diff --git a/cw_tron/lib/tron_wallet_service.dart b/cw_tron/lib/tron_wallet_service.dart index c8344d5f4..ba217a265 100644 --- a/cw_tron/lib/tron_wallet_service.dart +++ b/cw_tron/lib/tron_wallet_service.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:bip39/bip39.dart' as bip39; +import 'package:collection/collection.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/transaction_history.dart'; @@ -14,7 +15,6 @@ import 'package:cw_tron/tron_exception.dart'; import 'package:cw_tron/tron_wallet.dart'; import 'package:cw_tron/tron_wallet_creation_credentials.dart'; import 'package:hive/hive.dart'; -import 'package:collection/collection.dart'; class TronWalletService extends WalletService< TronNewWalletCredentials, @@ -153,7 +153,8 @@ class TronWalletService extends WalletService< } @override - Future, TransactionInfo>> restoreFromHardwareWallet(TronNewWalletCredentials credentials) { + Future, TransactionInfo>> + restoreFromHardwareWallet(TronNewWalletCredentials credentials) { // TODO: implement restoreFromHardwareWallet throw UnimplementedError(); }