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 <konstantinullrich12@gmail.com>
This commit is contained in:
Omar Hatem 2024-08-09 23:15:30 +03:00 committed by GitHub
parent 8e4082d680
commit fb33a6f23d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 433 additions and 144 deletions

View file

@ -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);
}

View file

@ -41,8 +41,10 @@ class BitcoinWalletService extends WalletService<
unspentCoinsInfo: unspentCoinsInfoSource,
network: network,
);
await wallet.save();
await wallet.init();
return wallet;
}

View file

@ -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<ElectrumBalance, ElectrumTransactionHistory, ElectrumTransactionInfo>
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<void> 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<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
@action
Future<void> updateAllUnspents() async {
List<BitcoinUnspent> updatedUnspentCoins = [];

View file

@ -32,15 +32,21 @@ class ElectrumWalletSnapshot {
final WalletType type;
final String? addressPageType;
@deprecated
String? mnemonic;
@deprecated
String? xpub;
@deprecated
String? passphrase;
List<BitcoinAddressRecord> addresses;
List<BitcoinSilentPaymentAddressRecord> silentAddresses;
ElectrumBalance balance;
Map<String, int> regularAddressIndex;
Map<String, int> changeAddressIndex;
int silentAddressIndex;
String? passphrase;
DerivationType? derivationType;
String? derivationPath;

View file

@ -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> 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,
);
}

View file

@ -33,6 +33,7 @@ class LitecoinWalletService extends WalletService<
passphrase: credentials.passphrase,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource);
await wallet.save();
await wallet.init();

View file

@ -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> 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,
);
}

View file

@ -34,8 +34,10 @@ class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredent
password: credentials.password!,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource);
await wallet.save();
await wallet.init();
return wallet;
}

View file

@ -0,0 +1,110 @@
import 'dart:convert';
import 'dart:developer' as dev;
import 'dart:io';
import 'package:cw_core/balance.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/utils/file.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
mixin WalletKeysFile<BalanceType extends Balance, HistoryType extends TransactionHistoryBase,
TransactionType extends TransactionInfo>
on WalletBase<BalanceType, HistoryType, TransactionType> {
Future<String> makePath() => pathForWallet(name: walletInfo.name, type: walletInfo.type);
// this needs to be overridden
WalletKeysData get walletKeysData;
Future<String> makeKeysFilePath() async => "${await makePath()}.keys";
Future<void> 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<void> 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<bool> 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<WalletKeysData> 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<String, dynamic>;
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<String, dynamic>;
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<String, dynamic> 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?,
);
}

View file

@ -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<EthereumWallet> 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<String, dynamic>? data;
try {
final jsonSource = await read(path: path, password: password);
data = json.decode(jsonSource) as Map<String, dynamic>;
} 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(),
);

View file

@ -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<EVMChainERC20Balance, EVMChainTransactionHistory, EVMChainTransactionInfo>
with Store {
with Store, WalletKeysFile {
EVMChainWalletBase({
required WalletInfo walletInfo,
required EVMChainClient client,
@ -508,6 +509,11 @@ abstract class EVMChainWalletBase
@override
Future<void> 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<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
@override
WalletKeysData get walletKeysData => WalletKeysData(mnemonic: _mnemonic, privateKey: privateKey);
String toJSON() => json.encode({
'mnemonic': _mnemonic,

View file

@ -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<NanoBalance, NanoTransactionHistory, NanoTransactionInfo> with Store {
extends WalletBase<NanoBalance, NanoTransactionHistory, NanoTransactionInfo>
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<void> changePassword(String password) {
throw UnimplementedError("changePassword");
}
Future<void> changePassword(String password) => throw UnimplementedError("changePassword");
@override
void close() {
@ -170,9 +169,7 @@ abstract class NanoWalletBase
}
@override
Future<void> connectToPowNode({required Node node}) async {
_client.connectPow(node);
}
Future<void> connectToPowNode({required Node node}) async => _client.connectPow(node);
@override
Future<PendingTransaction> 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<void> 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<String> 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<String, dynamic>? data = null;
try {
final jsonSource = await read(path: path, password: password);
data = json.decode(jsonSource) as Map<String, dynamic>;
} 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!);
}

View file

@ -39,7 +39,7 @@ class NanoWalletService extends WalletService<NanoNewWalletCredentials,
mnemonic: mnemonic,
password: credentials.password!,
);
wallet.init();
await wallet.init();
return wallet;
}

View file

@ -1,11 +1,12 @@
import 'dart:convert';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/crypto_currency.dart';
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_evm/evm_chain_transaction_history.dart';
import 'package:cw_evm/evm_chain_transaction_info.dart';
import 'package:cw_evm/evm_chain_transaction_model.dart';
@ -13,9 +14,9 @@ import 'package:cw_evm/evm_chain_wallet.dart';
import 'package:cw_evm/evm_erc20_balance.dart';
import 'package:cw_evm/file.dart';
import 'package:cw_polygon/default_polygon_erc20_tokens.dart';
import 'package:cw_polygon/polygon_transaction_info.dart';
import 'package:cw_polygon/polygon_client.dart';
import 'package:cw_polygon/polygon_transaction_history.dart';
import 'package:cw_polygon/polygon_transaction_info.dart';
class PolygonWallet extends EVMChainWallet {
PolygonWallet({
@ -97,19 +98,37 @@ class PolygonWallet extends EVMChainWallet {
static Future<PolygonWallet> 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<String, dynamic>? data;
try {
final jsonSource = await read(path: path, password: password);
data = json.decode(jsonSource) as Map<String, dynamic>;
} 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(),
);

View file

@ -35,7 +35,6 @@ class PolygonWalletService extends EVMChainWalletService<PolygonWallet> {
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
return wallet;
}
@ -83,7 +82,6 @@ class PolygonWalletService extends EVMChainWalletService<PolygonWallet> {
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
return wallet;
}

View file

@ -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<SolanaBalance, SolanaTransactionHistory, SolanaTransactionInfo> with Store {
extends WalletBase<SolanaBalance, SolanaTransactionHistory, SolanaTransactionInfo>
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<void> init() async {
final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${SPLToken.boxName}";
@ -336,6 +342,11 @@ abstract class SolanaWalletBase
@override
Future<void> 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<String> 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<String, dynamic>? data;
try {
final jsonSource = await read(path: path, password: password);
data = json.decode(jsonSource) as Map<String, dynamic>;
} 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,
);
}

View file

@ -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<TronBalance, TronTransactionHistory, TronTransactionInfo> with Store {
extends WalletBase<TronBalance, TronTransactionHistory, TronTransactionInfo>
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<String, dynamic>? data;
try {
final jsonSource = await read(path: path, password: password);
data = json.decode(jsonSource) as Map<String, dynamic>;
} 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<void> changePassword(String password) {
throw UnimplementedError("changePassword");
}
Future<void> 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<void> rescan({required int height}) {
throw UnimplementedError("rescan");
}
Future<void> rescan({required int height}) => throw UnimplementedError("rescan");
@override
Future<void> 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<String> 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<void> 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<String> 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) {

View file

@ -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<WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>> restoreFromHardwareWallet(TronNewWalletCredentials credentials) {
Future<WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>>
restoreFromHardwareWallet(TronNewWalletCredentials credentials) {
// TODO: implement restoreFromHardwareWallet
throw UnimplementedError();
}