mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-02-02 19:26:37 +00:00
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
This commit is contained in:
parent
d1cbc28059
commit
56b9e1f851
6 changed files with 198 additions and 104 deletions
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
Loading…
Reference in a new issue