mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-19 05:58:50 +02:00
* feat: initial hive -> SQLite Includes migration script and initial work done to remove dependency on hive generators. Also contains some refactoring to make sync code async. Features new dev screen that lists all SQLite tables Aspects that need testing: - backups (from this version, and from previous versions as well) - wallet loading / wallet list - almost everything else - renaming - deletion - sorting in wallet list - wallet groups - wallet seed/keys - wallet switching - wallet creation / restore - all methods - wallet address page - hardware wallet connection (and prompts from mall screens) - contacts - advanced settings in wallet creation - derivation paths - some minor wownero fixes - base, hardware wallets * fix: build errors * fix: Bad state: No element in backup restore * address comments from review * fix: HardwareWalletType
262 lines
9.6 KiB
Dart
262 lines
9.6 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'package:cw_decred/api/libdcrwallet.dart';
|
|
import 'package:cw_decred/wallet_creation_credentials.dart';
|
|
import 'package:cw_decred/wallet.dart';
|
|
import 'package:cw_core/wallet_base.dart';
|
|
import 'package:cw_core/wallet_service.dart';
|
|
import 'package:cw_core/pathForWallet.dart';
|
|
import 'package:cw_core/wallet_info.dart';
|
|
import 'package:cw_core/wallet_type.dart';
|
|
import 'package:path/path.dart';
|
|
import 'package:hive/hive.dart';
|
|
import 'package:collection/collection.dart';
|
|
import 'package:cw_core/unspent_coins_info.dart';
|
|
|
|
class DecredWalletService extends WalletService<
|
|
DecredNewWalletCredentials,
|
|
DecredRestoreWalletFromSeedCredentials,
|
|
DecredRestoreWalletFromPubkeyCredentials,
|
|
DecredRestoreWalletFromHardwareCredentials> {
|
|
DecredWalletService(this.unspentCoinsInfoSource);
|
|
|
|
final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
|
|
final seedRestorePath = "m/44'/42'";
|
|
static final seedRestorePathTestnet = "m/44'/1'";
|
|
static final pubkeyRestorePath = "m/44'/42'/0'";
|
|
static final pubkeyRestorePathTestnet = "m/44'/1'/0'";
|
|
final mainnet = "mainnet";
|
|
final testnet = "testnet";
|
|
static Libwallet? libwallet;
|
|
|
|
Future<void> init() async {
|
|
if (libwallet != null) {
|
|
return;
|
|
}
|
|
libwallet = await Libwallet.spawn();
|
|
// Init logging with no directory to force printing to stdout and only
|
|
// print ERROR level logs.
|
|
libwallet!.initLibdcrwallet("", "err");
|
|
}
|
|
|
|
void closeLibwallet() {
|
|
if (libwallet == null) {
|
|
return;
|
|
}
|
|
libwallet!.close();
|
|
libwallet = null;
|
|
}
|
|
|
|
@override
|
|
WalletType getType() => WalletType.decred;
|
|
|
|
@override
|
|
Future<bool> isWalletExit(String name) async =>
|
|
File(await pathForWallet(name: name, type: getType())).existsSync();
|
|
|
|
@override
|
|
Future<DecredWallet> create(DecredNewWalletCredentials credentials, {bool? isTestnet}) async {
|
|
await this.init();
|
|
final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType());
|
|
final network = isTestnet == true ? testnet : mainnet;
|
|
final config = {
|
|
"name": credentials.walletInfo!.name,
|
|
"datadir": dirPath,
|
|
"pass": credentials.password!,
|
|
"net": network,
|
|
"unsyncedaddrs": true,
|
|
};
|
|
await libwallet!.createWallet(jsonEncode(config));
|
|
final di = await credentials.walletInfo!.getDerivationInfo();
|
|
di.derivationPath = isTestnet == true ? seedRestorePathTestnet : seedRestorePath;
|
|
await di.save();
|
|
credentials.walletInfo!.save();
|
|
credentials.walletInfo!.network = network;
|
|
// ios will move our wallet directory when updating. Since we must
|
|
// recalculate the new path every time we open the wallet, ensure this path
|
|
// is not used. An older wallet will have a directory here which is a
|
|
// condition for moving the wallet when opening, so this must be kept blank
|
|
// going forward.
|
|
credentials.walletInfo!.dirPath = "";
|
|
credentials.walletInfo!.path = "";
|
|
final wallet = DecredWallet(credentials.walletInfo!, di, credentials.password!,
|
|
this.unspentCoinsInfoSource, libwallet!, closeLibwallet);
|
|
await wallet.init();
|
|
return wallet;
|
|
}
|
|
|
|
void copyDirectorySync(Directory source, Directory destination) {
|
|
/// create destination folder if not exist
|
|
if (!destination.existsSync()) {
|
|
destination.createSync(recursive: true);
|
|
}
|
|
|
|
/// get all files from source (recursive: false is important here)
|
|
source.listSync(recursive: false).forEach((entity) {
|
|
final newPath = destination.path + Platform.pathSeparator + basename(entity.path);
|
|
if (entity is File) {
|
|
entity.rename(newPath);
|
|
} else if (entity is Directory) {
|
|
copyDirectorySync(entity, Directory(newPath));
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> moveWallet(String fromPath, String toPath) async {
|
|
final oldWalletDir = new Directory(fromPath);
|
|
final newWalletDir = new Directory(toPath);
|
|
copyDirectorySync(oldWalletDir, newWalletDir);
|
|
// It would be ideal to delete the old directory here, but ios will error
|
|
// sometimes with "OS Error: No such file or directory, errno = 2" even
|
|
// after checking if it exists.
|
|
}
|
|
|
|
@override
|
|
Future<DecredWallet> openWallet(String name, String password) async {
|
|
final walletInfo = await WalletInfo.get(name, getType());
|
|
if (walletInfo == null) {
|
|
throw Exception('Wallet not found');
|
|
}
|
|
final di = await walletInfo.getDerivationInfo();
|
|
if (walletInfo.network == null || walletInfo.network == "") {
|
|
walletInfo.network = di.derivationPath == seedRestorePathTestnet ||
|
|
di.derivationPath == pubkeyRestorePathTestnet
|
|
? testnet
|
|
: mainnet;
|
|
walletInfo.save();
|
|
}
|
|
|
|
await this.init();
|
|
|
|
// Cake wallet version 4.27.0 and earlier gave a wallet dir that did not
|
|
// match the name. Move those to the correct place.
|
|
final dirPath = await pathForWalletDir(name: name, type: getType());
|
|
if (walletInfo.path != "") {
|
|
// On ios the stored dir no longer exists. We can only trust the basename.
|
|
// dirPath may already be updated and lost the basename, so look at path.
|
|
final randomBasename = basename(walletInfo.path);
|
|
final oldDir = await pathForWalletDir(name: randomBasename, type: getType());
|
|
if (oldDir != dirPath) {
|
|
await this.moveWallet(oldDir, dirPath);
|
|
}
|
|
// Clear the path so this does not trigger again.
|
|
walletInfo.dirPath = "";
|
|
walletInfo.path = "";
|
|
await walletInfo.save();
|
|
}
|
|
|
|
final config = {
|
|
"name": name,
|
|
"datadir": dirPath,
|
|
"net": walletInfo.network,
|
|
"unsyncedaddrs": true,
|
|
};
|
|
await libwallet!.loadWallet(jsonEncode(config));
|
|
final wallet =
|
|
DecredWallet(walletInfo, di, password, this.unspentCoinsInfoSource, libwallet!, closeLibwallet);
|
|
await wallet.init();
|
|
return wallet;
|
|
}
|
|
|
|
@override
|
|
Future<void> remove(String wallet) async {
|
|
File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
|
|
final walletInfo = await WalletInfo.get(wallet, getType());
|
|
if (walletInfo == null) {
|
|
throw Exception('Wallet not found');
|
|
}
|
|
await WalletInfo.delete(walletInfo);
|
|
}
|
|
|
|
@override
|
|
Future<void> rename(String currentName, String password, String newName) async {
|
|
final currentWalletInfo = await WalletInfo.get(currentName, getType());
|
|
if (currentWalletInfo == null) {
|
|
throw Exception('Wallet not found');
|
|
}
|
|
final di = await currentWalletInfo.getDerivationInfo();
|
|
final network = di.derivationPath == seedRestorePathTestnet ||
|
|
di.derivationPath == pubkeyRestorePathTestnet
|
|
? testnet
|
|
: mainnet;
|
|
currentWalletInfo.network = network;
|
|
currentWalletInfo.save();
|
|
if (libwallet == null) {
|
|
libwallet = await Libwallet.spawn();
|
|
libwallet!.initLibdcrwallet("", "err");
|
|
}
|
|
final currentWallet = DecredWallet(
|
|
currentWalletInfo, di, password, this.unspentCoinsInfoSource, libwallet!, closeLibwallet);
|
|
|
|
await currentWallet.renameWalletFiles(newName);
|
|
|
|
final newWalletInfo = currentWalletInfo;
|
|
newWalletInfo.id = WalletBase.idFor(newName, getType());
|
|
newWalletInfo.name = newName;
|
|
newWalletInfo.dirPath = "";
|
|
newWalletInfo.path = "";
|
|
|
|
await newWalletInfo.save();
|
|
}
|
|
|
|
@override
|
|
Future<DecredWallet> restoreFromSeed(DecredRestoreWalletFromSeedCredentials credentials,
|
|
{bool? isTestnet}) async {
|
|
await this.init();
|
|
final network = isTestnet == true ? testnet : mainnet;
|
|
final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType());
|
|
final config = {
|
|
"name": credentials.walletInfo!.name,
|
|
"datadir": dirPath,
|
|
"pass": credentials.password!,
|
|
"mnemonic": credentials.mnemonic,
|
|
"net": network,
|
|
"unsyncedaddrs": true,
|
|
};
|
|
await libwallet!.createWallet(jsonEncode(config));
|
|
final di = await credentials.walletInfo!.getDerivationInfo();
|
|
di.derivationPath = isTestnet == true ? seedRestorePathTestnet : seedRestorePath;
|
|
await di.save();
|
|
credentials.walletInfo!.network = network;
|
|
credentials.walletInfo!.dirPath = "";
|
|
credentials.walletInfo!.path = "";
|
|
final wallet = DecredWallet(credentials.walletInfo!, di, credentials.password!,
|
|
this.unspentCoinsInfoSource, libwallet!, closeLibwallet);
|
|
await wallet.init();
|
|
return wallet;
|
|
}
|
|
|
|
// restoreFromKeys only supports restoring a watch only wallet from an account
|
|
// pubkey.
|
|
@override
|
|
Future<DecredWallet> restoreFromKeys(DecredRestoreWalletFromPubkeyCredentials credentials,
|
|
{bool? isTestnet}) async {
|
|
await this.init();
|
|
final network = isTestnet == true ? testnet : mainnet;
|
|
final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType());
|
|
final config = {
|
|
"name": credentials.walletInfo!.name,
|
|
"datadir": dirPath,
|
|
"pubkey": credentials.pubkey,
|
|
"net": network,
|
|
"unsyncedaddrs": true,
|
|
};
|
|
await libwallet!.createWatchOnlyWallet(jsonEncode(config));
|
|
final di = await credentials.walletInfo!.getDerivationInfo();
|
|
di.derivationPath = isTestnet == true ? pubkeyRestorePathTestnet : pubkeyRestorePath;
|
|
await di.save();
|
|
credentials.walletInfo!.network = network;
|
|
credentials.walletInfo!.dirPath = "";
|
|
credentials.walletInfo!.path = "";
|
|
final wallet = DecredWallet(credentials.walletInfo!, di, credentials.password!,
|
|
this.unspentCoinsInfoSource, libwallet!, closeLibwallet);
|
|
await wallet.init();
|
|
return wallet;
|
|
}
|
|
|
|
@override
|
|
Future<DecredWallet> restoreFromHardwareWallet(
|
|
DecredRestoreWalletFromHardwareCredentials credentials) async =>
|
|
throw UnimplementedError();
|
|
}
|