feat: bip39 decred (#3348)

This commit is contained in:
cyan
2026-08-26 01:01:52 +08:00
committed by GitHub
parent b72c5c3232
commit 3d9b79f536
20 changed files with 208 additions and 43 deletions
+1 -1
View File
@@ -182,7 +182,7 @@ class Libwallet {
final cName = name.toCString();
final cSignReq = signReq.toCString();
res = executePayloadFn(
fn: () => dcrwalletApi.createSignedTransaction(cName, cSignReq),
fn: () => dcrwalletApi.createTransaction(cName, cSignReq),
ptrsToFree: [cName, cSignReq],
);
break;
+24
View File
@@ -0,0 +1,24 @@
import "package:bip39/bip39.dart" as bip39;
class DecredMnemonicIsIncorrectException implements Exception {
@override
String toString() =>
"Decred mnemonic has incorrect format. Mnemonic should be a valid BIP39 seed (12 or 24 words) or a native Decred seed (15 words).";
}
bool isValidDecredMnemonic(String mnemonic, {bool allowNativeSeed = true}) {
if (bip39.validateMnemonic(mnemonic)) {
return true;
}
if (!allowNativeSeed) {
return false;
}
final wordCount = mnemonic.trim().split(RegExp(r"\s+")).where((word) => word.isNotEmpty).length;
return wordCount == 15;
}
void validateDecredMnemonic(String mnemonic, {bool allowNativeSeed = true}) {
if (!isValidDecredMnemonic(mnemonic, allowNativeSeed: allowNativeSeed)) {
throw DecredMnemonicIsIncorrectException();
}
}
+32 -5
View File
@@ -22,8 +22,10 @@ import 'package:cw_decred/wallet_service.dart';
import 'package:cw_decred/balance.dart';
import 'package:cw_decred/transaction_info.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/sync_status.dart';
@@ -36,9 +38,11 @@ part 'wallet.g.dart';
class DecredWallet = DecredWalletBase with _$DecredWallet;
abstract class DecredWalletBase
extends WalletBase<DecredBalance, DecredTransactionHistory, DecredTransactionInfo> with Store {
extends WalletBase<DecredBalance, DecredTransactionHistory, DecredTransactionInfo>
with Store, WalletKeysFile {
DecredWalletBase(WalletInfo walletInfo, DerivationInfo derivationInfo, String password,
Box<UnspentCoinsInfo> unspentCoinsInfo, Libwallet libwallet, Function() closeLibwallet)
Box<UnspentCoinsInfo> unspentCoinsInfo, Libwallet libwallet, Function() closeLibwallet,
{this.passphrase, required this.encryptionFileUtils})
: _password = password,
_libwallet = libwallet,
_closeLibwallet = closeLibwallet,
@@ -113,6 +117,17 @@ abstract class DecredWalletBase
return _seed;
}
@override
final String? passphrase;
final EncryptionFileUtils encryptionFileUtils;
@override
WalletKeysData get walletKeysData => WalletKeysData(
mnemonic: seed,
passphrase: passphrase,
);
@override
Object get keys => {};
@@ -402,11 +417,11 @@ abstract class DecredWalletBase
"feerate": creds.feeRate ?? defaultFeeRate,
"password": _password,
"sendall": sendAll,
"sign": true,
};
final res = await _libwallet.createSignedTransaction(walletInfo.name, jsonEncode(signReq));
final decoded = json.decode(res);
final signedHex = decoded["signedhex"];
final signedHex = decoded["hex"];
final send = () async {
await _libwallet.sendRawTransaction(walletInfo.name, signedHex);
await updateBalance();
@@ -518,7 +533,12 @@ abstract class DecredWalletBase
String uniqueTxID(String id, int vout) => "$id:$vout";
@override
Future<void> save() async {}
Future<void> save() async {
if (watchingOnly) {
return;
}
await saveKeysFile(_password, encryptionFileUtils);
}
@override
bool get hasRescan => walletBirthdayBlockHeight() != -1;
@@ -620,6 +640,13 @@ abstract class DecredWalletBase
}
await sourceDir.delete(recursive: true);
for (final suffix in const ['.keys', '.keys.backup']) {
final file = File(p.join(newDirPath, '${walletInfo.name}$suffix'));
if (file.existsSync()) {
await file.rename(p.join(newDirPath, '$newWalletName$suffix'));
}
}
}
@override
+11 -3
View File
@@ -3,8 +3,15 @@ import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/hardware/hardware_account_data.dart';
class DecredNewWalletCredentials extends WalletCredentials {
DecredNewWalletCredentials({required String name, WalletInfo? walletInfo})
: super(name: name, walletInfo: walletInfo);
DecredNewWalletCredentials(
{required String name,
String? password,
String? passphrase,
this.mnemonic,
WalletInfo? walletInfo})
: super(name: name, password: password, passphrase: passphrase, walletInfo: walletInfo);
final String? mnemonic;
}
class DecredRestoreWalletFromSeedCredentials extends WalletCredentials {
@@ -12,8 +19,9 @@ class DecredRestoreWalletFromSeedCredentials extends WalletCredentials {
{required String name,
required String password,
required this.mnemonic,
String? passphrase,
WalletInfo? walletInfo})
: super(name: name, password: password, walletInfo: walletInfo);
: super(name: name, password: password, passphrase: passphrase, walletInfo: walletInfo);
final String mnemonic;
}
+89 -11
View File
@@ -1,6 +1,10 @@
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';
@@ -17,9 +21,10 @@ class DecredWalletService extends WalletService<
DecredRestoreWalletFromSeedCredentials,
DecredRestoreWalletFromPubkeyCredentials,
DecredRestoreWalletFromHardwareCredentials> {
DecredWalletService(this.unspentCoinsInfoSource);
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'";
@@ -55,6 +60,11 @@ class DecredWalletService extends WalletService<
@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;
@@ -62,12 +72,16 @@ class DecredWalletService extends WalletService<
"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;
@@ -78,9 +92,14 @@ class DecredWalletService extends WalletService<
// going forward.
credentials.walletInfo!.dirPath = "";
credentials.walletInfo!.path = "";
final wallet = DecredWallet(credentials.walletInfo!, di, credentials.password!,
this.unspentCoinsInfoSource, libwallet!, closeLibwallet);
final wallet = _createWalletInstance(
credentials.walletInfo!,
di,
credentials.password!,
passphrase: credentials.passphrase,
);
await wallet.init();
await wallet.save();
return wallet;
}
@@ -151,9 +170,10 @@ class DecredWalletService extends WalletService<
"unsyncedaddrs": true,
};
await libwallet!.loadWallet(jsonEncode(config));
final wallet = DecredWallet(
walletInfo, di, password, this.unspentCoinsInfoSource, libwallet!, closeLibwallet);
final passphrase = await _loadPassphrase(name, password);
final wallet = _createWalletInstance(walletInfo, di, password, passphrase: passphrase);
await wallet.init();
await _persistSeedDerivationType(wallet);
return wallet;
}
@@ -184,8 +204,7 @@ class DecredWalletService extends WalletService<
libwallet = await Libwallet.spawn();
libwallet!.initLibdcrwallet("", "err");
}
final currentWallet = DecredWallet(
currentWalletInfo, di, password, this.unspentCoinsInfoSource, libwallet!, closeLibwallet);
final currentWallet = _createWalletInstance(currentWalletInfo, di, password);
await currentWallet.renameWalletFiles(newName);
@@ -201,6 +220,7 @@ class DecredWalletService extends WalletService<
@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());
@@ -209,22 +229,81 @@ class DecredWalletService extends WalletService<
"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 = DecredWallet(credentials.walletInfo!, di, credentials.password!,
this.unspentCoinsInfoSource, libwallet!, closeLibwallet);
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
@@ -247,8 +326,7 @@ class DecredWalletService extends WalletService<
credentials.walletInfo!.network = network;
credentials.walletInfo!.dirPath = "";
credentials.walletInfo!.path = "";
final wallet = DecredWallet(credentials.walletInfo!, di, credentials.password!,
this.unspentCoinsInfoSource, libwallet!, closeLibwallet);
final wallet = _createWalletInstance(credentials.walletInfo!, di, credentials.password!);
await wallet.init();
return wallet;
}
+1
View File
@@ -12,6 +12,7 @@ environment:
dependencies:
flutter:
sdk: flutter
bip39: ^1.0.6
cw_core:
path: ../cw_core
+1 -1
View File
@@ -86,6 +86,7 @@ class WalletCreationService {
case WalletType.dogecoin:
case WalletType.nano:
case WalletType.zcash:
case WalletType.decred:
return true;
case WalletType.monero:
case WalletType.wownero:
@@ -93,7 +94,6 @@ class WalletCreationService {
case WalletType.haven:
case WalletType.banano:
case WalletType.zano:
case WalletType.decred:
return false;
}
}
+19 -6
View File
@@ -5,13 +5,26 @@ class CWDecred extends Decred {
@override
WalletCredentials createDecredNewWalletCredentials(
{required String name, WalletInfo? walletInfo}) =>
DecredNewWalletCredentials(name: name, walletInfo: walletInfo);
{required String name,
String? password,
String? passphrase,
String? mnemonic,
WalletInfo? walletInfo}) =>
DecredNewWalletCredentials(
name: name,
password: password,
passphrase: passphrase,
mnemonic: mnemonic,
walletInfo: walletInfo);
@override
WalletCredentials createDecredRestoreWalletFromSeedCredentials(
{required String name, required String mnemonic, required String password}) =>
DecredRestoreWalletFromSeedCredentials(name: name, mnemonic: mnemonic, password: password);
{required String name,
required String mnemonic,
required String password,
String? passphrase}) =>
DecredRestoreWalletFromSeedCredentials(
name: name, mnemonic: mnemonic, password: password, passphrase: passphrase);
@override
WalletCredentials createDecredRestoreWalletFromPubkeyCredentials(
@@ -19,8 +32,8 @@ class CWDecred extends Decred {
DecredRestoreWalletFromPubkeyCredentials(name: name, pubkey: pubkey, password: password);
@override
WalletService createDecredWalletService(Box<UnspentCoinsInfo> unspentCoinSource) =>
DecredWalletService(unspentCoinSource);
WalletService createDecredWalletService(Box<UnspentCoinsInfo> unspentCoinSource, bool isDirect) =>
DecredWalletService(unspentCoinSource, isDirect);
@override
List<TransactionPriority> getTransactionPriorities() => DecredTransactionPriority.all;
+2 -1
View File
@@ -1343,7 +1343,8 @@ Future<void> setup({
case WalletType.zano:
return zano!.createZanoWalletService();
case WalletType.decred:
return decred!.createDecredWalletService(_unspentCoinsInfoSource);
return decred!.createDecredWalletService(
_unspentCoinsInfoSource, SettingsStoreBase.walletPasswordDirectInput);
case WalletType.haven:
return HavenWalletService();
case WalletType.zcash:
+1 -1
View File
@@ -17,11 +17,11 @@ bool isBIP39Wallet(WalletType walletType) {
case WalletType.monero:
case WalletType.dogecoin:
case WalletType.zcash:
case WalletType.decred:
return true;
case WalletType.wownero:
case WalletType.haven:
case WalletType.zano:
case WalletType.decred:
case WalletType.none:
return false;
}
@@ -630,7 +630,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody>
}
if ((walletRestoreViewModel.type == WalletType.decred) &&
seedWords.length != WalletRestoreViewModelBase.decredSeedMnemonicLength) {
![12, 15, 24].contains(seedWords.length)) {
return false;
}
@@ -59,6 +59,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store {
case WalletType.solana:
case WalletType.tron:
case WalletType.zcash:
case WalletType.decred:
return true;
case WalletType.bitcoin:
@@ -74,7 +75,6 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store {
case WalletType.none:
case WalletType.haven:
case WalletType.zano:
case WalletType.decred:
return false;
}
}
@@ -107,7 +107,8 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store {
WalletType.zano,
WalletType.dogecoin,
WalletType.zcash,
].contains(type);
WalletType.decred,
].contains(type);
@computed
bool get addCustomNode => _addCustomNode;
+2
View File
@@ -179,6 +179,8 @@ abstract class WalletCreationVMBase with Store {
);
}
return bitcoin!.getElectrumDerivations()[DerivationType.electrum]!.first;
case WalletType.decred:
return DerivationInfo(derivationType: DerivationType.bip39);
default:
return DerivationInfo(derivationType: DerivationType.unknown);
}
@@ -131,6 +131,10 @@ abstract class WalletGroupsDisplayViewModelBase with Store {
bool isNotMoneroBip39Wallet =
wallet.type == WalletType.monero && di.derivationType != DerivationType.bip39;
bool isNotDecredBip39Wallet = wallet.type == WalletType.decred &&
di.derivationType != DerivationType.bip39 &&
di.derivationType != DerivationType.unknown;
// Exclude if any of these conditions are true
shouldExcludeGroup = shouldExcludeGroup ||
isNonBIP39Wallet ||
@@ -138,7 +142,8 @@ abstract class WalletGroupsDisplayViewModelBase with Store {
isElectrumDerivationType ||
isSameTypeAsSelectedWallet ||
isNonSeedWallet ||
isNotMoneroBip39Wallet;
isNotMoneroBip39Wallet ||
isNotDecredBip39Wallet;
}
if (shouldExcludeGroup) continue;
+5 -1
View File
@@ -149,7 +149,11 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
network: zcashNetwork,
);
case WalletType.decred:
return decred!.createDecredNewWalletCredentials(name: name);
return decred!.createDecredNewWalletCredentials(
name: name,
password: walletPassword,
passphrase: passphrase,
mnemonic: newWalletArguments!.mnemonic);
case WalletType.none:
case WalletType.haven:
throw Exception('Unexpected type: ${type.toString()}');
@@ -216,6 +216,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
name: name,
mnemonic: seed,
password: password,
passphrase: passphrase,
);
case WalletType.zcash:
return zcash!.createZcashRestoreWalletFromSeedCredentials(
+4 -4
View File
@@ -1871,8 +1871,8 @@ packages:
dependency: "direct overridden"
description:
path: "."
ref: 40af7c48cf34211ba3cc9e87dd05ed1af8a429a2
resolved-ref: 40af7c48cf34211ba3cc9e87dd05ed1af8a429a2
ref: "40af7c48cf34211ba3cc9e87dd05ed1af8a429a2"
resolved-ref: "40af7c48cf34211ba3cc9e87dd05ed1af8a429a2"
url: "https://github.com/cake-tech/ledger-usb-plus"
source: git
version: "1.0.8"
@@ -3161,8 +3161,8 @@ packages:
dependency: "direct overridden"
description:
path: "."
ref: 6735c1bdf7f42c501d51c20564eb957c3a12b981
resolved-ref: 6735c1bdf7f42c501d51c20564eb957c3a12b981
ref: "6735c1bdf7f42c501d51c20564eb957c3a12b981"
resolved-ref: "6735c1bdf7f42c501d51c20564eb957c3a12b981"
url: "https://github.com/cake-tech/universal_ble.git"
source: git
version: "2.0.0"
+1 -1
View File
@@ -7,7 +7,7 @@ cd "$(dirname "$0")"
CW_DECRED_DIR=$(realpath ../..)/cw_decred
LIBWALLET_PATH="${PWD}/decred/libwallet"
LIBWALLET_URL="https://github.com/decred/libwallet.git"
LIBWALLET_VERSION="05f8d7374999400fe4d525eb365c39b77d307b14"
LIBWALLET_VERSION="ecc4a5fb9594368777848de42d7e072d62406507"
if [[ -e $LIBWALLET_PATH ]]; then
rm -fr $LIBWALLET_PATH || true
+1 -1
View File
@@ -3,7 +3,7 @@ set -e
. ./config.sh
LIBWALLET_PATH="${EXTERNAL_IOS_SOURCE_DIR}/libwallet"
LIBWALLET_URL="https://github.com/decred/libwallet.git"
LIBWALLET_VERSION="05f8d7374999400fe4d525eb365c39b77d307b14"
LIBWALLET_VERSION="ecc4a5fb9594368777848de42d7e072d62406507"
if [[ -e $LIBWALLET_PATH ]]; then
rm -fr $LIBWALLET_PATH
+3 -3
View File
@@ -1259,12 +1259,12 @@ import 'package:cw_decred/mnemonic.dart';
abstract class Decred {
WalletCredentials createDecredNewWalletCredentials(
{required String name, WalletInfo? walletInfo});
{required String name, String? password, String? passphrase, String? mnemonic, WalletInfo? walletInfo});
WalletCredentials createDecredRestoreWalletFromSeedCredentials(
{required String name, required String mnemonic, required String password});
{required String name, required String mnemonic, required String password, String? passphrase});
WalletCredentials createDecredRestoreWalletFromPubkeyCredentials(
{required String name, required String pubkey, required String password});
WalletService createDecredWalletService(Box<UnspentCoinsInfo> unspentCoinSource);
WalletService createDecredWalletService(Box<UnspentCoinsInfo> unspentCoinSource, bool isDirect);
List<TransactionPriority> getTransactionPriorities();
TransactionPriority getDecredTransactionPriorityMedium();