From fa8881f4388b84e85e3d69e145cf69a52d14d043 Mon Sep 17 00:00:00 2001 From: JoeGruff <joegruffins@gmail.com> Date: Thu, 14 Mar 2024 17:36:51 +0900 Subject: [PATCH] decred: Add watching only wallets. --- cw_decred/lib/api/libdcrwallet.dart | 20 ++++++++++++++ cw_decred/lib/wallet.dart | 26 +++++++++++++++++-- .../lib/wallet_creation_credentials.dart | 12 ++++----- cw_decred/lib/wallet_service.dart | 6 +++++ lib/decred/cw_decred.dart | 19 ++++++++++++-- .../wallet_restore_from_keys_form.dart | 19 ++++++++++++++ .../screens/restore/wallet_restore_page.dart | 19 +++++++++----- lib/view_model/wallet_keys_view_model.dart | 12 ++++++++- lib/view_model/wallet_restore_view_model.dart | 7 +++++ scripts/android/build_decred.sh | 4 +-- scripts/ios/build_decred.sh | 4 +-- tool/configure.dart | 6 +++++ 12 files changed, 132 insertions(+), 22 deletions(-) diff --git a/cw_decred/lib/api/libdcrwallet.dart b/cw_decred/lib/api/libdcrwallet.dart index 4d0d7824c..1942dafae 100644 --- a/cw_decred/lib/api/libdcrwallet.dart +++ b/cw_decred/lib/api/libdcrwallet.dart @@ -57,6 +57,17 @@ void createWalletSync(Map<String, String> args) { ); } +void createWatchOnlyWallet(String walletName, String datadir, String pubkey) { + final cName = walletName.toCString(); + final cDataDir = datadir.toCString(); + final cPub = pubkey.toCString(); + final cNet = "testnet".toCString(); + executePayloadFn( + fn: () => dcrwalletApi.createWatchOnlyWallet(cName, cDataDir, cNet, cPub), + ptrsToFree: [cName, cDataDir, cNet, cPub], + ); +} + /// loadWalletAsync calls the libdcrwallet's loadWallet function asynchronously. Future<void> loadWalletAsync({required String name, required String dataDir}) { final args = <String, String>{ @@ -269,6 +280,15 @@ String? newExternalAddress(String walletName) { return res.payload; } +String defaultPubkey(String walletName) { + final cName = walletName.toCString(); + final res = executePayloadFn( + fn: () => dcrwalletApi.defaultPubkey(cName), + ptrsToFree: [cName], + ); + return res.payload; +} + String addresses(String walletName) { final cName = walletName.toCString(); final res = executePayloadFn( diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart index 950f80ab0..01c025390 100644 --- a/cw_decred/lib/wallet.dart +++ b/cw_decred/lib/wallet.dart @@ -13,6 +13,7 @@ import 'package:cw_decred/api/libdcrwallet.dart' as libdcrwallet; import 'package:cw_decred/transaction_history.dart'; import 'package:cw_decred/wallet_addresses.dart'; import 'package:cw_decred/transaction_priority.dart'; +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'; @@ -37,6 +38,8 @@ abstract class DecredWalletBase extends WalletBase<DecredBalance, : _password = password, this.syncStatus = NotConnectedSyncStatus(), this.unspentCoinsInfo = unspentCoinsInfo, + this.watchingOnly = + walletInfo.derivationPath == DecredWalletService.pubkeyRestorePath, this.balance = ObservableMap.of({CryptoCurrency.dcr: DecredBalance.zero()}), super(walletInfo) { @@ -50,6 +53,7 @@ abstract class DecredWalletBase extends WalletBase<DecredBalance, static final defaultFeeRate = 10000; final String _password; final idPrefix = "decred_"; + bool watchingOnly; bool connecting = false; int bestHeight = 0; String bestHash = ""; @@ -73,12 +77,17 @@ abstract class DecredWalletBase extends WalletBase<DecredBalance, @override String? get seed { + if (watchingOnly) { + return null; + } return libdcrwallet.walletSeed(walletInfo.name, _password); } @override - Object get keys { - return {}; + Object get keys => {}; + + String get pubkey { + return libdcrwallet.defaultPubkey(walletInfo.name); } Future<void> init() async { @@ -240,6 +249,16 @@ abstract class DecredWalletBase extends WalletBase<DecredBalance, @override Future<PendingTransaction> createTransaction(Object credentials) async { + if (watchingOnly) { + return DecredPendingTransaction( + txid: "", + amount: 0, + fee: 0, + rawHex: "", + send: () async { + throw "unable to send with watching only wallet"; + }); + } final inputs = []; this.unspentCoinsInfo.values.forEach((unspent) { if (unspent.isSending) { @@ -417,6 +436,9 @@ abstract class DecredWalletBase extends WalletBase<DecredBalance, @override Future<void> changePassword(String password) async { + if (watchingOnly) { + return; + } return () async { libdcrwallet.changeWalletPassword(walletInfo.name, _password, password); }(); diff --git a/cw_decred/lib/wallet_creation_credentials.dart b/cw_decred/lib/wallet_creation_credentials.dart index ef63bbc37..b0c68bb56 100644 --- a/cw_decred/lib/wallet_creation_credentials.dart +++ b/cw_decred/lib/wallet_creation_credentials.dart @@ -18,17 +18,15 @@ class DecredRestoreWalletFromSeedCredentials extends WalletCredentials { final String mnemonic; } -class DecredRestoreWalletFromWIFCredentials extends WalletCredentials { - DecredRestoreWalletFromWIFCredentials( +class DecredRestoreWalletFromPubkeyCredentials extends WalletCredentials { + DecredRestoreWalletFromPubkeyCredentials( {required String name, required String password, - required this.wif, + required String this.pubkey, WalletInfo? walletInfo}) - : t = throw UnimplementedError(), // TODO: Maybe can be used to create watching only wallets? - super(name: name, password: password, walletInfo: walletInfo); + : super(name: name, password: password, walletInfo: walletInfo); - final String wif; - final void t; + final String pubkey; } class DecredRestoreWalletFromHardwareCredentials extends WalletCredentials { diff --git a/cw_decred/lib/wallet_service.dart b/cw_decred/lib/wallet_service.dart index 2e64b8495..8edd86928 100644 --- a/cw_decred/lib/wallet_service.dart +++ b/cw_decred/lib/wallet_service.dart @@ -20,6 +20,8 @@ class DecredWalletService extends WalletService< final Box<WalletInfo> walletInfoSource; final Box<UnspentCoinsInfo> unspentCoinsInfoSource; + final seedRestorePath = "m/44'/42'"; + static final pubkeyRestorePath = "m/44'/42'/0'"; static void init() async { // Use the general path for all dcr wallets as the general log directory. @@ -43,6 +45,7 @@ class DecredWalletService extends WalletService< dataDir: credentials.walletInfo!.dirPath, password: credentials.password!, ); + credentials.walletInfo!.derivationPath = seedRestorePath; final wallet = DecredWallet(credentials.walletInfo!, credentials.password!, this.unspentCoinsInfoSource); await wallet.init(); @@ -101,12 +104,15 @@ class DecredWalletService extends WalletService< password: credentials.password!, mnemonic: credentials.mnemonic, ); + credentials.walletInfo!.derivationPath = seedRestorePath; final wallet = DecredWallet(credentials.walletInfo!, credentials.password!, this.unspentCoinsInfoSource); await wallet.init(); return wallet; } + // restoreFromKeys only supports restoring a watch only wallet from an account + // pubkey. @override Future<DecredWallet> restoreFromKeys( DecredRestoreWalletFromPubkeyCredentials credentials, diff --git a/lib/decred/cw_decred.dart b/lib/decred/cw_decred.dart index a544b9aaf..43d3a4e23 100644 --- a/lib/decred/cw_decred.dart +++ b/lib/decred/cw_decred.dart @@ -19,6 +19,15 @@ class CWDecred extends Decred { DecredRestoreWalletFromSeedCredentials( name: name, mnemonic: mnemonic, password: password); + @override + WalletCredentials createDecredRestoreWalletFromPubkeyCredentials( + {required String name, + required String pubkey, + required String password}) => + DecredRestoreWalletFromPubkeyCredentials( + name: name, pubkey: pubkey, password: password); + + @override WalletService createDecredWalletService(Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource) { return DecredWalletService(walletInfoSource, unspentCoinSource); @@ -117,6 +126,12 @@ class CWDecred extends Decred { return (minutesDiff / 5).toInt(); } - @override - List<String> getDecredWordList() => wordlist; + @override + List<String> getDecredWordList() => wordlist; + + @override + String pubkey(Object wallet) { + final decredWallet = wallet as DecredWallet; + return decredWallet.pubkey; + } } diff --git a/lib/src/screens/restore/wallet_restore_from_keys_form.dart b/lib/src/screens/restore/wallet_restore_from_keys_form.dart index 83772f866..aa99c29db 100644 --- a/lib/src/screens/restore/wallet_restore_from_keys_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_keys_form.dart @@ -14,6 +14,7 @@ class WalletRestoreFromKeysFrom extends StatefulWidget { WalletRestoreFromKeysFrom({ required this.walletRestoreViewModel, required this.onPrivateKeyChange, + required this.onViewKeyEntered, required this.displayPrivateKeyField, required this.onHeightOrDateEntered, required this.displayWalletPassword, @@ -25,6 +26,7 @@ class WalletRestoreFromKeysFrom extends StatefulWidget { final Function(bool) onHeightOrDateEntered; final WalletRestoreViewModel walletRestoreViewModel; final void Function(String)? onPrivateKeyChange; + final void Function(bool)? onViewKeyEntered; final bool displayPrivateKeyField; final bool displayWalletPassword; final void Function(String)? onPasswordChange; @@ -80,6 +82,10 @@ class WalletRestoreFromKeysFromState extends State<WalletRestoreFromKeysFrom> { } widget.onPrivateKeyChange?.call(privateKeyController.text); }); + + viewKeyController.addListener(() { + widget.onViewKeyEntered?.call(viewKeyController.text.isNotEmpty); + }); } @override @@ -168,6 +174,19 @@ class WalletRestoreFromKeysFromState extends State<WalletRestoreFromKeysFrom> { } Widget _restoreFromKeysFormFields() { + // Decred can only restore a view only wallet with an account pubkey. Other + // fields are not used. + if (widget.walletRestoreViewModel.type == WalletType.decred) { + return Column( + children: [ + BaseTextFormField( + controller: viewKeyController, + hintText: S.of(context).view_key_public, + maxLines: null, + )], + ); + } + if (widget.displayPrivateKeyField) { // the term "private key" isn't actually what we're accepting here, and it's confusing to // users of the nano community, what this form actually accepts (when importing for nano) is a nano seed in it's hex form, referred to in code as a "seed key" diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index a86e97fd6..8e15f3a65 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -67,6 +67,11 @@ class WalletRestorePage extends BasePage { walletRestoreViewModel.isButtonEnabled = _isValidSeedKey(); } }, + onViewKeyEntered: (bool entered) { + if (walletRestoreViewModel.type == WalletType.decred) { + walletRestoreViewModel.isButtonEnabled = entered; + } + }, displayPrivateKeyField: walletRestoreViewModel.hasRestoreFromPrivateKey, displayWalletPassword: walletRestoreViewModel.hasWalletPassword, onPasswordChange: (String password) => walletRestoreViewModel.walletPassword = password, @@ -336,14 +341,16 @@ class WalletRestorePage extends BasePage { credentials['name'] = walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text; } else { - credentials['address'] = walletRestoreFromKeysFormKey.currentState!.addressController.text; - credentials['viewKey'] = walletRestoreFromKeysFormKey.currentState!.viewKeyController.text; - credentials['spendKey'] = - walletRestoreFromKeysFormKey.currentState!.spendKeyController.text; - credentials['height'] = - walletRestoreFromKeysFormKey.currentState!.blockchainHeightKey.currentState!.height; credentials['name'] = walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text; + credentials['viewKey'] = walletRestoreFromKeysFormKey.currentState!.viewKeyController.text; + if (walletRestoreViewModel.type != WalletType.decred) { + credentials['address'] = walletRestoreFromKeysFormKey.currentState!.addressController.text; + credentials['spendKey'] = + walletRestoreFromKeysFormKey.currentState!.spendKeyController.text; + credentials['height'] = + walletRestoreFromKeysFormKey.currentState!.blockchainHeightKey.currentState!.height; + } } } diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index 4457b6f04..46b0dc6b8 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -12,6 +12,7 @@ import 'package:cw_core/wallet_type.dart'; import 'package:cw_monero/monero_wallet.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:polyseed/polyseed.dart'; part 'wallet_keys_view_model.g.dart'; @@ -132,6 +133,16 @@ abstract class WalletKeysViewModelBase with Store { } } + if (_appStore.wallet!.type == WalletType.decred) { + final seed = _appStore.wallet!.seed; + final pubkey = decred!.pubkey(_appStore.wallet!); + items.addAll([ + if (seed != null) + StandartListItem(title: S.current.wallet_seed, value: seed), + StandartListItem(title: S.current.view_key_public, value: pubkey), + ]); + } + if (_appStore.wallet!.type == WalletType.haven) { final keys = haven!.getKeys(_appStore.wallet!); @@ -227,7 +238,6 @@ abstract class WalletKeysViewModelBase with Store { if (_appStore.wallet!.type == WalletType.bitcoin || _appStore.wallet!.type == WalletType.litecoin || - _appStore.wallet!.type == WalletType.decred || _appStore.wallet!.type == WalletType.bitcoinCash) { // final keys = bitcoin!.getWalletKeys(_appStore.wallet!); diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 04db8ca1c..1d8165174 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -57,6 +57,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.haven: case WalletType.ethereum: case WalletType.polygon: + case WalletType.decred: availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys]; break; case WalletType.bitcoin: @@ -245,6 +246,12 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { password: password, language: 'English', ); + case WalletType.decred: + return decred!.createDecredRestoreWalletFromPubkeyCredentials( + name: name, + password: password, + pubkey: viewKey!, + ); default: break; } diff --git a/scripts/android/build_decred.sh b/scripts/android/build_decred.sh index 942dc5b58..4bc4fb199 100755 --- a/scripts/android/build_decred.sh +++ b/scripts/android/build_decred.sh @@ -4,7 +4,7 @@ CW_DECRED_DIR=${WORKDIR}/cake_wallet/cw_decred LIBWALLET_PATH="${WORKDIR}/decred/libwallet" LIBWALLET_URL="https://github.com/decred/libwallet.git" -LIBWALLET_COMMIT="9f39f38b460e2dece5704cbc4aee293c741ee710" +LIBWALLET_VERSION="v1.0.0" if [ -e $LIBWALLET_PATH ]; then rm -fr $LIBWALLET_PATH @@ -12,7 +12,7 @@ fi mkdir -p $LIBWALLET_PATH git clone $LIBWALLET_URL $LIBWALLET_PATH cd $LIBWALLET_PATH -git checkout $LIBWALLET_COMMIT +git checkout $LIBWALLET_VERSION export CPATH="$(clang -v 2>&1 | grep "Selected GCC installation" | rev | cut -d' ' -f1 | rev)/include" diff --git a/scripts/ios/build_decred.sh b/scripts/ios/build_decred.sh index 4c541033c..d5a743750 100755 --- a/scripts/ios/build_decred.sh +++ b/scripts/ios/build_decred.sh @@ -3,7 +3,7 @@ . ./config.sh LIBWALLET_PATH="${EXTERNAL_IOS_SOURCE_DIR}/libwallet" LIBWALLET_URL="https://github.com/decred/libwallet.git" -LIBWALLET_COMMIT="9f39f38b460e2dece5704cbc4aee293c741ee710" +LIBWALLET_VERSION="v1.0.0" if [ -e $LIBWALLET_PATH ]; then rm -fr $LIBWALLET_PATH @@ -11,7 +11,7 @@ fi mkdir -p $LIBWALLET_PATH git clone $LIBWALLET_URL $LIBWALLET_PATH cd $LIBWALLET_PATH -git checkout $LIBWALLET_COMMIT +git checkout $LIBWALLET_VERSION SYSROOT=`xcrun --sdk iphoneos --show-sdk-path` CLANG="clang -isysroot ${SYSROOT}" diff --git a/tool/configure.dart b/tool/configure.dart index 68b4976c5..c4d07ccd0 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -1439,6 +1439,10 @@ abstract class Decred { {required String name, required String mnemonic, required String password}); + WalletCredentials createDecredRestoreWalletFromPubkeyCredentials( + {required String name, + required String pubkey, + required String password}); WalletService createDecredWalletService(Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource); @@ -1466,6 +1470,8 @@ abstract class Decred { int heightByDate(DateTime date); List<String> getDecredWordList(); + + String pubkey(Object wallet); } """;