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