From 56b9e1f85185b98a91b3d06617a6b99114261d93 Mon Sep 17 00:00:00 2001
From: julian <julian@cypherstack.com>
Date: Thu, 23 Nov 2023 12:32:08 -0600
Subject: [PATCH] Use different derivation path for new tezos wallets and scan
 tezos derivation path variations on recover and use first with history or
 default to the new standard path

---
 ...w_wallet_recovery_phrase_warning_view.dart | 12 ++-
 .../helpers/restore_create_backup.dart        | 16 +--
 lib/wallets/crypto_currency/coins/tezos.dart  | 92 ++++++++++++++++-
 lib/wallets/isar/models/wallet_info.dart      |  3 +
 lib/wallets/wallet/impl/tezos_wallet.dart     | 98 ++++++++++++++++---
 .../wallet/supporting/tezos_utils.dart        | 81 ---------------
 6 files changed, 198 insertions(+), 104 deletions(-)
 delete mode 100644 lib/wallets/wallet/supporting/tezos_utils.dart

diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart
index 538ce4ea7..8c019962d 100644
--- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart
+++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart
@@ -9,6 +9,7 @@
  */
 
 import 'dart:async';
+import 'dart:convert';
 
 import 'package:bip39/bip39.dart' as bip39;
 import 'package:flutter/material.dart';
@@ -30,9 +31,10 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
 import 'package:stackwallet/utilities/logger.dart';
 import 'package:stackwallet/utilities/text_styles.dart';
 import 'package:stackwallet/utilities/util.dart';
+import 'package:stackwallet/wallets/crypto_currency/coins/tezos.dart';
 import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
-import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
 import 'package:stackwallet/wallets/wallet/wallet.dart';
+import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
 import 'package:stackwallet/widgets/conditional_parent.dart';
 import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
 import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
@@ -459,6 +461,14 @@ class _NewWalletRecoveryPhraseWarningViewState
                                     final info = WalletInfo.createNew(
                                       coin: widget.coin,
                                       name: widget.walletName,
+                                      otherDataJsonString: coin == Coin.tezos
+                                          ? jsonEncode({
+                                              WalletInfoKeys
+                                                      .tezosDerivationPath:
+                                                  Tezos.standardDerivationPath
+                                                      .value,
+                                            })
+                                          : null,
                                     );
 
                                     var node = ref
diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart
index 21e9f53dd..0bbc1c891 100644
--- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart
+++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart
@@ -42,9 +42,9 @@ import 'package:stackwallet/utilities/logger.dart';
 import 'package:stackwallet/utilities/prefs.dart';
 import 'package:stackwallet/utilities/util.dart';
 import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
-import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
 import 'package:stackwallet/wallets/wallet/private_key_based_wallet.dart';
 import 'package:stackwallet/wallets/wallet/wallet.dart';
+import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
 import 'package:tuple/tuple.dart';
 import 'package:uuid/uuid.dart';
 import 'package:wakelock/wakelock.dart';
@@ -292,6 +292,7 @@ abstract class SWB {
         backupWallet['name'] = wallet.info.name;
         backupWallet['id'] = wallet.walletId;
         backupWallet['isFavorite'] = wallet.info.isFavourite;
+        backupWallet['otherDataJsonString'] = wallet.info.otherDataJsonString;
 
         if (wallet is MnemonicInterface) {
           backupWallet['mnemonic'] = await wallet.getMnemonic();
@@ -689,13 +690,14 @@ abstract class SWB {
       // TODO: use these for monero and possibly other coins later on?
       // final List<String> txidList = List<String>.from(walletbackup['txidList'] as List? ?? []);
 
-      final restoreHeight = walletbackup['restoreHeight'] as int? ?? 0;
-
-      final info = WalletInfo.createNew(
-        coin: coin,
+      final info = WalletInfo(
+        coinName: coin.name,
+        walletId: walletId,
         name: walletName,
-        walletIdOverride: walletId,
-        restoreHeight: restoreHeight,
+        mainAddressType: coin.primaryAddressType,
+        restoreHeight: walletbackup['restoreHeight'] as int? ?? 0,
+        otherDataJsonString: walletbackup["otherDataJsonString"] as String?,
+        cachedChainHeight: walletbackup['storedChainHeight'] as int? ?? 0,
       );
 
       var node = nodeService.getPrimaryNodeFor(coin: coin);
diff --git a/lib/wallets/crypto_currency/coins/tezos.dart b/lib/wallets/crypto_currency/coins/tezos.dart
index 52c49deac..88a7dadd0 100644
--- a/lib/wallets/crypto_currency/coins/tezos.dart
+++ b/lib/wallets/crypto_currency/coins/tezos.dart
@@ -1,9 +1,16 @@
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:bip39/bip39.dart' as bip39;
+import 'package:coinlib_flutter/coinlib_flutter.dart';
 import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
 import 'package:stackwallet/models/node_model.dart';
 import 'package:stackwallet/utilities/default_nodes.dart';
 import 'package:stackwallet/utilities/enums/coin_enum.dart';
 import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
 import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_currency.dart';
+import 'package:tezart/src/crypto/crypto.dart';
+import 'package:tezart/tezart.dart';
 
 class Tezos extends Bip39Currency {
   Tezos(super.network) {
@@ -15,16 +22,46 @@ class Tezos extends Bip39Currency {
     }
   }
 
-  DerivationPath get standardDerivationPath =>
+  // ===========================================================================
+  // =========== Public ========================================================
+
+  static DerivationPath get standardDerivationPath =>
       DerivationPath()..value = "m/44'/1729'/0'/0'";
 
-  List<DerivationPath> get possibleDerivationPaths => [
+  static List<DerivationPath> get possibleDerivationPaths => [
         standardDerivationPath,
         DerivationPath()..value = "",
         DerivationPath()..value = "m/44'/1729'/0'/0'/0'",
         DerivationPath()..value = "m/44'/1729'/0'/0/0",
       ];
 
+  static Keystore mnemonicToKeyStore({
+    required String mnemonic,
+    String mnemonicPassphrase = "",
+    String derivationPath = "",
+  }) {
+    if (derivationPath.isEmpty) {
+      return Keystore.fromMnemonic(mnemonic, password: mnemonicPassphrase);
+    }
+
+    final pathArray = _derivationPathToArray(derivationPath);
+    final seed = bip39.mnemonicToSeed(mnemonic, passphrase: mnemonicPassphrase);
+    ({Uint8List privateKey, Uint8List chainCode}) node = _deriveRootNode(seed);
+    for (final index in pathArray) {
+      node = _deriveChildNode(node, index);
+    }
+
+    final encoded = encodeWithPrefix(
+      prefix: Prefixes.edsk2,
+      bytes: node.privateKey,
+    );
+
+    return Keystore.fromSeed(encoded);
+  }
+
+  // ===========================================================================
+  // =========== Overrides =====================================================
+
   @override
   String get genesisHash => throw UnimplementedError(
         "Not used in tezos at the moment",
@@ -58,4 +95,55 @@ class Tezos extends Bip39Currency {
         throw UnimplementedError();
     }
   }
+
+  // ===========================================================================
+  // =========== Private =======================================================
+
+  static ({Uint8List privateKey, Uint8List chainCode}) _deriveRootNode(
+      Uint8List seed) {
+    return _deriveNode(seed, Uint8List.fromList(utf8.encode("ed25519 seed")));
+  }
+
+  static ({Uint8List privateKey, Uint8List chainCode}) _deriveNode(
+      Uint8List msg, Uint8List key) {
+    final hMac = hmacSha512(key, msg);
+    final privateKey = hMac.sublist(0, 32);
+    final chainCode = hMac.sublist(32);
+    return (privateKey: privateKey, chainCode: chainCode);
+  }
+
+  static ({Uint8List privateKey, Uint8List chainCode}) _deriveChildNode(
+      ({Uint8List privateKey, Uint8List chainCode}) node, int index) {
+    Uint8List indexBuf = Uint8List(4);
+    ByteData.view(indexBuf.buffer).setUint32(0, index, Endian.big);
+
+    Uint8List message = Uint8List.fromList([
+      Uint8List(1)[0],
+      ...node.privateKey,
+      ...indexBuf,
+    ]);
+
+    return _deriveNode(message, Uint8List.fromList(node.chainCode));
+  }
+
+  static List<int> _derivationPathToArray(String derivationPath) {
+    if (derivationPath.isEmpty) {
+      return [];
+    }
+
+    derivationPath = derivationPath.replaceAll('m/', '').replaceAll("'", 'h');
+
+    return derivationPath.split('/').map((level) {
+      if (level.endsWith("h")) {
+        level = level.substring(0, level.length - 1);
+      }
+      final int levelNumber = int.parse(level);
+      if (levelNumber >= 0x80000000) {
+        throw ArgumentError('Invalid derivation path. Out of bound.');
+      }
+      return levelNumber + 0x80000000;
+    }).toList();
+  }
+
+  // ===========================================================================
 }
diff --git a/lib/wallets/isar/models/wallet_info.dart b/lib/wallets/isar/models/wallet_info.dart
index 30e97d73c..3f2c62185 100644
--- a/lib/wallets/isar/models/wallet_info.dart
+++ b/lib/wallets/isar/models/wallet_info.dart
@@ -343,6 +343,7 @@ class WalletInfo implements IsarId {
     required String name,
     int restoreHeight = 0,
     String? walletIdOverride,
+    String? otherDataJsonString,
   }) {
     return WalletInfo(
       coinName: coin.name,
@@ -350,6 +351,7 @@ class WalletInfo implements IsarId {
       name: name,
       mainAddressType: coin.primaryAddressType,
       restoreHeight: restoreHeight,
+      otherDataJsonString: otherDataJsonString,
     );
   }
 
@@ -391,4 +393,5 @@ abstract class WalletInfoKeys {
   static const String tokenContractAddresses = "tokenContractAddressesKey";
   static const String epiccashData = "epiccashDataKey";
   static const String bananoMonkeyImageBytes = "monkeyImageBytesKey";
+  static const String tezosDerivationPath = "tezosDerivationPathKey";
 }
diff --git a/lib/wallets/wallet/impl/tezos_wallet.dart b/lib/wallets/wallet/impl/tezos_wallet.dart
index f4ae7f66f..64c178932 100644
--- a/lib/wallets/wallet/impl/tezos_wallet.dart
+++ b/lib/wallets/wallet/impl/tezos_wallet.dart
@@ -15,6 +15,7 @@ import 'package:stackwallet/wallets/api/tezos/tezos_api.dart';
 import 'package:stackwallet/wallets/api/tezos/tezos_rpc_api.dart';
 import 'package:stackwallet/wallets/crypto_currency/coins/tezos.dart';
 import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
+import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
 import 'package:stackwallet/wallets/models/tx_data.dart';
 import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart';
 import 'package:tezart/tezart.dart' as tezart;
@@ -27,15 +28,51 @@ import 'package:tuple/tuple.dart';
 // const kDefaultKeyRevealStorageLimit = 0;
 // const kDefaultKeyRevealGasLimit = 1100;
 
-class TezosWallet extends Bip39Wallet {
+class TezosWallet extends Bip39Wallet<Tezos> {
   TezosWallet(CryptoCurrencyNetwork network) : super(Tezos(network));
 
   NodeModel? _xtzNode;
 
-  Future<tezart.Keystore> _getKeyStore() async {
+  String get derivationPath =>
+      info.otherData[WalletInfoKeys.tezosDerivationPath] as String? ?? "";
+
+  Future<DerivationPath> _scanPossiblePaths({
+    required String mnemonic,
+    String passphrase = "",
+  }) async {
+    try {
+      for (final path in Tezos.possibleDerivationPaths) {
+        final ks = await _getKeyStore(path: path.value);
+
+        // TODO: some kind of better check to see if the address has been used
+
+        final hasHistory =
+            (await TezosAPI.getTransactions(ks.address)).isNotEmpty;
+
+        if (hasHistory) {
+          return path;
+        }
+      }
+
+      return Tezos.standardDerivationPath;
+    } catch (e, s) {
+      Logging.instance.log(
+        "Error in _scanPossiblePaths() in tezos_wallet.dart: $e\n$s",
+        level: LogLevel.Error,
+      );
+      rethrow;
+    }
+  }
+
+  Future<tezart.Keystore> _getKeyStore({String? path}) async {
     final mnemonic = await getMnemonic();
     final passphrase = await getMnemonicPassphrase();
-    return tezart.Keystore.fromMnemonic(mnemonic, password: passphrase);
+
+    return Tezos.mnemonicToKeyStore(
+      mnemonic: mnemonic,
+      mnemonicPassphrase: passphrase,
+      derivationPath: path ?? derivationPath,
+    );
   }
 
   Future<Address> _getAddressFromMnemonic() async {
@@ -45,7 +82,7 @@ class TezosWallet extends Bip39Wallet {
       value: keyStore.address,
       publicKey: keyStore.publicKey.toUint8ListFromBase58CheckEncoded,
       derivationIndex: 0,
-      derivationPath: null,
+      derivationPath: DerivationPath()..value = derivationPath,
       type: info.coin.primaryAddressType,
       subType: AddressSubType.receiving,
     );
@@ -98,7 +135,7 @@ class TezosWallet extends Bip39Wallet {
       return opList;
     } catch (e, s) {
       Logging.instance.log(
-        "Error in _buildSendTransaction() in tezos_wallet.dart: $e\n$s}",
+        "Error in _buildSendTransaction() in tezos_wallet.dart: $e\n$s",
         level: LogLevel.Error,
       );
       rethrow;
@@ -139,8 +176,10 @@ class TezosWallet extends Bip39Wallet {
       if (sendAmount > info.cachedBalance.spendable) {
         throw Exception("Insufficient available balance");
       }
+
+      final myAddress = (await getCurrentReceivingAddress())!;
       final account = await TezosAPI.getAccount(
-        (await getCurrentReceivingAddress())!.value,
+        myAddress.value,
       );
 
       // final bool isSendAll = sendAmount == info.cachedBalance.spendable;
@@ -216,7 +255,7 @@ class TezosWallet extends Bip39Wallet {
       );
     } catch (e, s) {
       Logging.instance.log(
-        "Error in prepareSend() in tezos_wallet.dart: $e\n$s}",
+        "Error in prepareSend() in tezos_wallet.dart: $e\n$s",
         level: LogLevel.Error,
       );
 
@@ -247,7 +286,9 @@ class TezosWallet extends Bip39Wallet {
   int _estCount = 0;
 
   Future<({int reveal, int transfer})> _estimate(
-      TezosAccount account, String recipientAddress) async {
+    TezosAccount account,
+    String recipientAddress,
+  ) async {
     try {
       final opList = await _buildSendTransaction(
         amount: Amount(
@@ -279,7 +320,7 @@ class TezosWallet extends Bip39Wallet {
       if (_estCount > 3) {
         _estCount = 0;
         Logging.instance.log(
-          " Error in _estimate in tezos_wallet.dart: $e\n$s}",
+          " Error in _estimate in tezos_wallet.dart: $e\n$s",
           level: LogLevel.Error,
         );
         rethrow;
@@ -310,8 +351,9 @@ class TezosWallet extends Bip39Wallet {
       );
     }
 
+    final myAddress = (await getCurrentReceivingAddress())!;
     final account = await TezosAPI.getAccount(
-      (await getCurrentReceivingAddress())!.value,
+      myAddress.value,
     );
 
     try {
@@ -325,7 +367,7 @@ class TezosWallet extends Bip39Wallet {
       return fee;
     } catch (e, s) {
       Logging.instance.log(
-        "  Error in estimateFeeFor() in tezos_wallet.dart: $e\n$s}",
+        "  Error in estimateFeeFor() in tezos_wallet.dart: $e\n$s",
         level: LogLevel.Error,
       );
       rethrow;
@@ -362,12 +404,42 @@ class TezosWallet extends Bip39Wallet {
     await refreshMutex.protect(() async {
       if (isRescan) {
         await mainDB.deleteWalletBlockchainData(walletId);
+      } else {
+        final derivationPath = await _scanPossiblePaths(
+          mnemonic: await getMnemonic(),
+          passphrase: await getMnemonicPassphrase(),
+        );
+
+        await info.updateOtherData(
+          newEntries: {
+            WalletInfoKeys.tezosDerivationPath: derivationPath.value,
+          },
+          isar: mainDB.isar,
+        );
       }
 
       final address = await _getAddressFromMnemonic();
 
       await mainDB.updateOrPutAddresses([address]);
 
+      // ensure we only have a single address
+      await mainDB.isar.writeTxn(() async {
+        await mainDB.isar.addresses
+            .where()
+            .walletIdEqualTo(walletId)
+            .filter()
+            .not()
+            .derivationPath((q) => q.valueEqualTo(derivationPath))
+            .deleteAll();
+      });
+
+      if (info.cachedReceivingAddress != address.value) {
+        await info.updateReceivingAddress(
+          newAddress: address.value,
+          isar: mainDB.isar,
+        );
+      }
+
       await Future.wait([
         updateBalance(),
         updateTransactions(),
@@ -405,7 +477,7 @@ class TezosWallet extends Bip39Wallet {
       await info.updateBalance(newBalance: newBalance, isar: mainDB.isar);
     } catch (e, s) {
       Logging.instance.log(
-        "Error getting balance in tezos_wallet.dart: $e\n$s}",
+        "Error getting balance in tezos_wallet.dart: $e\n$s",
         level: LogLevel.Error,
       );
     }
@@ -429,7 +501,7 @@ class TezosWallet extends Bip39Wallet {
     } catch (e, s) {
       Logging.instance.log(
         "Error occurred in tezos_wallet.dart while getting"
-        " chain height for tezos: $e\n$s}",
+        " chain height for tezos: $e\n$s",
         level: LogLevel.Error,
       );
     }
diff --git a/lib/wallets/wallet/supporting/tezos_utils.dart b/lib/wallets/wallet/supporting/tezos_utils.dart
deleted file mode 100644
index b52882507..000000000
--- a/lib/wallets/wallet/supporting/tezos_utils.dart
+++ /dev/null
@@ -1,81 +0,0 @@
-import 'dart:convert';
-import 'dart:typed_data';
-
-import 'package:bip39/bip39.dart' as bip39;
-import 'package:coinlib_flutter/coinlib_flutter.dart';
-import 'package:tezart/src/crypto/crypto.dart';
-import 'package:tezart/tezart.dart';
-
-class _Node {
-  final Uint8List privateKey;
-  final Uint8List chainCode;
-
-  _Node(this.privateKey, this.chainCode);
-}
-
-_Node _deriveRootNode(Uint8List seed) {
-  return _deriveNode(seed, Uint8List.fromList(utf8.encode("ed25519 seed")));
-}
-
-_Node _deriveNode(Uint8List msg, Uint8List key) {
-  final hMac = hmacSha512(key, msg);
-  final privateKey = hMac.sublist(0, 32);
-  final chainCode = hMac.sublist(32);
-  return _Node(privateKey, chainCode);
-}
-
-_Node _deriveChildNode(_Node node, int index) {
-  Uint8List indexBuf = Uint8List(4);
-  ByteData.view(indexBuf.buffer).setUint32(0, index, Endian.big);
-
-  Uint8List message = Uint8List.fromList([
-    Uint8List(1)[0],
-    ...node.privateKey,
-    ...indexBuf,
-  ]);
-
-  return _deriveNode(message, Uint8List.fromList(node.chainCode));
-}
-
-List<int> _derivationPathToArray(String derivationPath) {
-  if (derivationPath.isEmpty) {
-    return [];
-  }
-
-  derivationPath = derivationPath.replaceAll('m/', '').replaceAll("'", 'h');
-
-  return derivationPath.split('/').map((level) {
-    if (level.endsWith("h")) {
-      level = level.substring(0, level.length - 1);
-    }
-    final int levelNumber = int.parse(level);
-    if (levelNumber >= 0x80000000) {
-      throw ArgumentError('Invalid derivation path. Out of bound.');
-    }
-    return levelNumber + 0x80000000;
-  }).toList();
-}
-
-Keystore mnemonicToKeyStore({
-  required String mnemonic,
-  String mnemonicPassphrase = "",
-  String derivationPath = "",
-}) {
-  if (derivationPath.isEmpty) {
-    return Keystore.fromMnemonic(mnemonic, password: mnemonicPassphrase);
-  }
-
-  final pathArray = _derivationPathToArray(derivationPath);
-  final seed = bip39.mnemonicToSeed(mnemonic, passphrase: mnemonicPassphrase);
-  _Node node = _deriveRootNode(seed);
-  for (final index in pathArray) {
-    node = _deriveChildNode(node, index);
-  }
-
-  final encoded = encodeWithPrefix(
-    prefix: Prefixes.edsk2,
-    bytes: node.privateKey,
-  );
-
-  return Keystore.fromSeed(encoded);
-}