mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
339 lines
12 KiB
Dart
339 lines
12 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'package:bip39/bip39.dart' as bip39;
|
|
import 'package:cw_core/encryption_file_utils.dart';
|
|
import 'package:cw_core/wallet_keys_file.dart';
|
|
import 'package:cw_decred/api/libdcrwallet.dart';
|
|
import 'package:cw_decred/mnemonic_validation.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:cw_core/unspent_coins_info.dart';
|
|
|
|
class DecredWalletService extends WalletService<
|
|
DecredNewWalletCredentials,
|
|
DecredRestoreWalletFromSeedCredentials,
|
|
DecredRestoreWalletFromPubkeyCredentials,
|
|
DecredRestoreWalletFromHardwareCredentials> {
|
|
DecredWalletService(this.unspentCoinsInfoSource, this.isDirect);
|
|
|
|
final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
|
|
final bool isDirect;
|
|
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 {
|
|
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
|
|
final mnemonic = (credentials.mnemonic?.isNotEmpty == true)
|
|
? credentials.mnemonic!
|
|
: bip39.generateMnemonic(strength: strength);
|
|
validateDecredMnemonic(mnemonic, allowNativeSeed: false);
|
|
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!,
|
|
"mnemonic": mnemonic,
|
|
"seedpass": credentials.passphrase ?? "",
|
|
"birthday": DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
|
"net": network,
|
|
"unsyncedaddrs": true,
|
|
};
|
|
await libwallet!.createWallet(jsonEncode(config));
|
|
final di = await credentials.walletInfo!.getDerivationInfo();
|
|
di.derivationPath = isTestnet == true ? seedRestorePathTestnet : seedRestorePath;
|
|
di.derivationType = DerivationType.bip39;
|
|
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 = _createWalletInstance(
|
|
credentials.walletInfo!,
|
|
di,
|
|
credentials.password!,
|
|
passphrase: credentials.passphrase,
|
|
);
|
|
await wallet.init();
|
|
await wallet.save();
|
|
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 passphrase = await _loadPassphrase(name, password);
|
|
final wallet = _createWalletInstance(walletInfo, di, password, passphrase: passphrase);
|
|
await wallet.init();
|
|
await _persistSeedDerivationType(wallet);
|
|
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 = _createWalletInstance(currentWalletInfo, di, password);
|
|
|
|
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 {
|
|
validateDecredMnemonic(credentials.mnemonic);
|
|
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,
|
|
"seedpass": credentials.passphrase ?? "",
|
|
"net": network,
|
|
"unsyncedaddrs": true,
|
|
};
|
|
await libwallet!.createWallet(jsonEncode(config));
|
|
final di = await credentials.walletInfo!.getDerivationInfo();
|
|
di.derivationPath = isTestnet == true ? seedRestorePathTestnet : seedRestorePath;
|
|
di.derivationType =
|
|
bip39.validateMnemonic(credentials.mnemonic) ? DerivationType.bip39 : DerivationType.def;
|
|
await di.save();
|
|
credentials.walletInfo!.network = network;
|
|
credentials.walletInfo!.dirPath = "";
|
|
credentials.walletInfo!.path = "";
|
|
final wallet = _createWalletInstance(
|
|
credentials.walletInfo!,
|
|
di,
|
|
credentials.password!,
|
|
passphrase: credentials.passphrase,
|
|
);
|
|
await wallet.init();
|
|
await wallet.save();
|
|
return wallet;
|
|
}
|
|
|
|
EncryptionFileUtils get _encryptionFileUtils => encryptionFileUtilsFor(isDirect);
|
|
|
|
DecredWallet _createWalletInstance(
|
|
WalletInfo walletInfo,
|
|
DerivationInfo di,
|
|
String password, {
|
|
String? passphrase,
|
|
}) {
|
|
return DecredWallet(
|
|
walletInfo,
|
|
di,
|
|
password,
|
|
unspentCoinsInfoSource,
|
|
libwallet!,
|
|
closeLibwallet,
|
|
passphrase: passphrase,
|
|
encryptionFileUtils: _encryptionFileUtils,
|
|
);
|
|
}
|
|
|
|
Future<String?> _loadPassphrase(String name, String password) async {
|
|
if (!await WalletKeysFile.hasKeysFile(name, getType())) {
|
|
return null;
|
|
}
|
|
try {
|
|
final keys = await WalletKeysFile.readKeysFile(
|
|
name,
|
|
getType(),
|
|
password,
|
|
_encryptionFileUtils,
|
|
);
|
|
return keys.passphrase;
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<void> _persistSeedDerivationType(DecredWallet wallet) async {
|
|
final seed = wallet.seed;
|
|
if (seed == null || seed.isEmpty) {
|
|
return;
|
|
}
|
|
final expected =
|
|
bip39.validateMnemonic(seed) ? DerivationType.bip39 : DerivationType.def;
|
|
if (wallet.derivationInfo.derivationType == expected) {
|
|
return;
|
|
}
|
|
wallet.derivationInfo.derivationType = expected;
|
|
await wallet.derivationInfo.save();
|
|
}
|
|
|
|
// 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 = _createWalletInstance(credentials.walletInfo!, di, credentials.password!);
|
|
await wallet.init();
|
|
return wallet;
|
|
}
|
|
|
|
@override
|
|
Future<DecredWallet> restoreFromHardwareWallet(
|
|
DecredRestoreWalletFromHardwareCredentials credentials) async =>
|
|
throw UnimplementedError();
|
|
}
|