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