diff --git a/assets/nano_node_list.yml b/assets/nano_node_list.yml new file mode 100644 index 000000000..c7eb79aa2 --- /dev/null +++ b/assets/nano_node_list.yml @@ -0,0 +1,4 @@ +- + uri: rpc.nano.to +- + uri: eth.llamarpc.com \ No newline at end of file diff --git a/cw_nano/lib/file.dart b/cw_nano/lib/file.dart new file mode 100644 index 000000000..8fd236ec3 --- /dev/null +++ b/cw_nano/lib/file.dart @@ -0,0 +1,39 @@ +import 'dart:io'; +import 'package:cw_core/key.dart'; +import 'package:encrypt/encrypt.dart' as encrypt; + +Future write( + {required String path, + required String password, + required String data}) async { + final keys = extractKeys(password); + final key = encrypt.Key.fromBase64(keys.first); + final iv = encrypt.IV.fromBase64(keys.last); + final encrypted = await encode(key: key, iv: iv, data: data); + final f = File(path); + f.writeAsStringSync(encrypted); +} + +Future writeData( + {required String path, + required String password, + required String data}) async { + final keys = extractKeys(password); + final key = encrypt.Key.fromBase64(keys.first); + final iv = encrypt.IV.fromBase64(keys.last); + final encrypted = await encode(key: key, iv: iv, data: data); + final f = File(path); + f.writeAsStringSync(encrypted); +} + +Future read({required String path, required String password}) async { + final file = File(path); + + if (!file.existsSync()) { + file.createSync(); + } + + final encrypted = file.readAsStringSync(); + + return decode(password: password, data: encrypted); +} diff --git a/cw_nano/lib/nano_mnemonic.dart b/cw_nano/lib/nano_mnemonic.dart index bc6deb4e4..2a06fe515 100644 --- a/cw_nano/lib/nano_mnemonic.dart +++ b/cw_nano/lib/nano_mnemonic.dart @@ -1,6 +1,12 @@ import 'package:bip39/bip39.dart' as bip39; import 'package:nanodart/nanodart.dart'; +class NanoMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Nano mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; +} + class NanoMnemomics { /// Converts a nano seed to a 24-word mnemonic word list static List seedToMnemonic(String seed) { diff --git a/cw_nano/lib/nano_util.dart b/cw_nano/lib/nano_util.dart index 2a0e1b493..7f6b09ea7 100644 --- a/cw_nano/lib/nano_util.dart +++ b/cw_nano/lib/nano_util.dart @@ -6,15 +6,20 @@ import "package:ed25519_hd_key/ed25519_hd_key.dart"; import 'package:flutter/material.dart'; import 'package:libcrypto/libcrypto.dart'; import 'package:nanodart/nanodart.dart'; +import 'package:ed25519_hd_key/ed25519_hd_key.dart'; +import 'package:nanodart/nanodart.dart'; class NanoUtil { // standard: static String seedToPrivate(String seed, int index) { - return NanoHelpers.byteToHex(Ed25519Blake2b.derivePrivkey(NanoHelpers.hexToBytes(seed), index)!).toUpperCase(); + // return NanoHelpers.byteToHex(Ed25519Blake2b.derivePrivkey(NanoHelpers.hexToBytes(seed), index)!) + // .toUpperCase(); + return NanoKeys.seedToPrivate(seed, index); } static String seedToAddress(String seed, int index) { - return NanoAccounts.createAccount(NanoAccountType.NANO, privateKeyToPublic(seedToPrivate(seed, index))); + return NanoAccounts.createAccount( + NanoAccountType.NANO, privateKeyToPublic(seedToPrivate(seed, index))); } // static String createPublicKey(String privateKey) { @@ -22,7 +27,8 @@ class NanoUtil { // } static String privateKeyToPublic(String privateKey) { - return NanoHelpers.byteToHex(Ed25519Blake2b.getPubkey(NanoHelpers.hexToBytes(privateKey))!); + // return NanoHelpers.byteToHex(Ed25519Blake2b.getPubkey(NanoHelpers.hexToBytes(privateKey))!); + return NanoKeys.createPublicKey(privateKey); } static String addressToPublicKey(String publicAddress) { @@ -114,4 +120,4 @@ class NanoUtil { // Ensure seed only contains hex characters, 0-9;A-F return NanoHelpers.isHexString(seed); } -} \ No newline at end of file +} diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart index a5a646852..bd490ad8a 100644 --- a/cw_nano/lib/nano_wallet.dart +++ b/cw_nano/lib/nano_wallet.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -9,32 +8,37 @@ import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_nano/file.dart'; import 'package:cw_nano/nano_balance.dart'; import 'package:cw_nano/nano_transaction_history.dart'; import 'package:cw_nano/nano_transaction_info.dart'; +import 'package:cw_nano/nano_util.dart'; import 'package:mobx/mobx.dart'; -import 'package:web3dart/credentials.dart'; import 'dart:async'; -import 'dart:io'; import 'package:cw_nano/nano_wallet_addresses.dart'; -import 'package:cw_nano/nano_wallet_keys.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:web3dart/web3dart.dart'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:bip32/bip32.dart' as bip32; part 'nano_wallet.g.dart'; class NanoWallet = NanoWalletBase with _$NanoWallet; +enum DerivationType { bip39, nano } + abstract class NanoWalletBase extends WalletBase with Store { NanoWalletBase({ required WalletInfo walletInfo, required String mnemonic, required String password, + required DerivationType derivationType, NanoBalance? initialBalance, }) : syncStatus = NotConnectedSyncStatus(), _password = password, _mnemonic = mnemonic, + _derivationType = derivationType, _isTransactionUpdating = false, _priorityFees = [], walletAddresses = NanoWalletAddresses(walletInfo), @@ -43,13 +47,17 @@ abstract class NanoWalletBase NanoBalance(currentBalance: BigInt.zero, receivableBalance: BigInt.zero) }), super(walletInfo) { - print("@@@@@ initializing nano wallet @@@@@"); this.walletInfo = walletInfo; transactionHistory = NanoTransactionHistory(); } final String _mnemonic; final String _password; + final DerivationType _derivationType; + + late final String _privateKey; + late final String _publicAddress; + late final String _seed; List _priorityFees; int? _gasPrice; @@ -66,11 +74,25 @@ abstract class NanoWalletBase @observable late ObservableMap balance; - Future init() async {} + + // initialize the different forms of private / public key we'll need: + Future init() async { + final String type = (_derivationType == DerivationType.nano) ? "standard" : "hd"; + + _seed = bip39.mnemonicToEntropy(_mnemonic).toUpperCase(); + _privateKey = await NanoUtil.uniSeedToPrivate(_mnemonic, 0, type); + _publicAddress = await NanoUtil.uniSeedToAddress(_mnemonic, 0, type); + + await walletAddresses.init(); + // await transactionHistory.init(); + + // walletAddresses.address = _privateKey.address.toString(); + await save(); + } @override int calculateEstimatedFee(TransactionPriority priority, int? amount) { - return 0; + return 0; // always 0 :) } @override @@ -122,8 +144,10 @@ abstract class NanoWalletBase @override Future save() async { - print("l"); - throw UnimplementedError(); + await walletAddresses.updateAddressesInBox(); + final path = await makePath(); + await write(path: path, password: _password, data: toJSON()); + await transactionHistory.save(); } @override @@ -139,6 +163,13 @@ abstract class NanoWalletBase throw UnimplementedError(); } + Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); + + String toJSON() => json.encode({ + 'mnemonic': _mnemonic, + // 'balance': balance[currency]!.toJSON(), + }); + static Future open({ required String name, required String password, @@ -151,10 +182,6 @@ abstract class NanoWalletBase await save(); } - Future _fetchErc20Balances() async { - throw UnimplementedError(); - } - Future getPrivateKey(String mnemonic, String password) async { print("o"); throw UnimplementedError(); @@ -162,14 +189,6 @@ abstract class NanoWalletBase Future? updateBalance() async => await _updateBalance(); - Future addErc20Token(Erc20Token token) async { - throw UnimplementedError(); - } - - Future deleteErc20Token(Erc20Token token) async { - throw UnimplementedError(); - } - void _onNewTransaction(FilterEvent event) { throw UnimplementedError(); } diff --git a/cw_nano/lib/nano_wallet_service.dart b/cw_nano/lib/nano_wallet_service.dart index b95e8b6ed..524f5d0c2 100644 --- a/cw_nano/lib/nano_wallet_service.dart +++ b/cw_nano/lib/nano_wallet_service.dart @@ -6,6 +6,7 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cw_nano/nano_mnemonic.dart'; // import 'package:cw_nano/nano_mnemonics.dart'; import 'package:cw_nano/nano_wallet.dart'; import 'package:cw_nano/nano_wallet_creation_credentials.dart'; @@ -19,10 +20,15 @@ class NanoNewWalletCredentials extends WalletCredentials { class NanoRestoreWalletFromSeedCredentials extends WalletCredentials { NanoRestoreWalletFromSeedCredentials( - {required String name, required this.mnemonic, int height = 0, String? password}) + {required String name, + required this.mnemonic, + required this.derivationType, + int height = 0, + String? password}) : super(name: name, password: password, height: height); final String mnemonic; + final DerivationType derivationType; } class NanoWalletLoadingException implements Exception { @@ -67,6 +73,7 @@ class NanoWalletService extends WalletService compareDerivationMethods({String? mnemonic, String? seedKey}) async { + return DerivationType.nano; + } + @override Future restoreFromSeed(NanoRestoreWalletFromSeedCredentials credentials) async { - print("b"); - throw UnimplementedError(); - // try { - // final path = await pathForWallet(name: credentials.name, type: getType()); - // await monero_wallet_manager.restoreFromSeed( - // path: path, - // password: credentials.password!, - // seed: credentials.mnemonic, - // restoreHeight: credentials.height!); - // final wallet = NanoWallet(walletInfo: credentials.walletInfo!); - // await wallet.init(); + if (!bip39.validateMnemonic(credentials.mnemonic)) { + throw NanoMnemonicIsIncorrectException(); + } - // return wallet; - // } catch (e) { - // // TODO: Implement Exception for wallet list service. - // print('NanoWalletsManager Error: $e'); - // rethrow; - // } + if (!NanoMnemomics.validateMnemonic(credentials.mnemonic.split(' '))) { + throw NanoMnemonicIsIncorrectException(); + } + + DerivationType derivationType = DerivationType.bip39; + + if (credentials.mnemonic.split(' ').length == 12) { + derivationType = DerivationType.bip39; + } else { + // we don't know for sure, but probably the nano standard: + derivationType = await compareDerivationMethods(mnemonic: credentials.mnemonic); + } + + final wallet = await NanoWallet( + password: credentials.password!, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + derivationType: derivationType, + ); + + try { + await wallet.init(); + } catch (e) { + print("test"); + print(e); + rethrow; + } + await wallet.save(); + return wallet; } @override - Future isWalletExit(String s) async { - print("c"); - throw UnimplementedError(); - } + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); @override - Future openWallet(String s, String s2) async { - print("d"); - throw UnimplementedError(); + Future openWallet(String name, String password) async { + final walletInfo = + walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType())); + final wallet = await NanoWalletBase.open( + name: name, + password: password, + walletInfo: walletInfo, + ); + + await wallet.init(); + await wallet.save(); + return wallet; } } diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index b4cb23131..12d49244a 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -27,6 +27,7 @@ const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002'; const cakeWalletLitecoinElectrumUri = 'ltc-electrum.cakewallet.com:50002'; const havenDefaultNodeUri = 'nodes.havenprotocol.org:443'; const ethereumDefaultNodeUri = 'ethereum.publicnode.com'; +const nanoDefaultNodeUri = 'rpc.nano.to'; Future defaultSettingsMigration( {required int version, @@ -255,6 +256,12 @@ Node? getEthereumDefaultNode({required Box nodes}) { ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.ethereum); } +Node? getNanoDefaultNode({required Box nodes}) { + return nodes.values.firstWhereOrNull( + (Node node) => node.uriRaw == nanoDefaultNodeUri) + ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.ethereum); +} + Node getMoneroDefaultNode({required Box nodes}) { final timeZone = DateTime.now().timeZoneOffset.inHours; var nodeUri = ''; diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index b06351a79..959fb3112 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -86,16 +86,34 @@ Future> loadDefaultEthereumNodes() async { return nodes; } +Future> loadDefaultNanoNodes() async { + final nodesRaw = await rootBundle.loadString('assets/nano_node_list.yml'); + final loadedNodes = loadYaml(nodesRaw) as YamlList; + final nodes = []; + + for (final raw in loadedNodes) { + if (raw is Map) { + final node = Node.fromMap(Map.from(raw)); + node.type = WalletType.ethereum; + nodes.add(node); + } + } + + return nodes; +} + Future resetToDefault(Box nodeSource) async { final moneroNodes = await loadDefaultNodes(); final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); final litecoinElectrumServerList = await loadLitecoinElectrumServerList(); final havenNodes = await loadDefaultHavenNodes(); + final nanoNodes = await loadDefaultNanoNodes(); final nodes = moneroNodes + bitcoinElectrumServerList + litecoinElectrumServerList + - havenNodes; + havenNodes + + nanoNodes; await nodeSource.clear(); await nodeSource.addAll(nodes); diff --git a/lib/nano/cw_nano.dart b/lib/nano/cw_nano.dart index 04db6e96c..f03ea262f 100644 --- a/lib/nano/cw_nano.dart +++ b/lib/nano/cw_nano.dart @@ -40,12 +40,14 @@ class CWNano extends Nano { WalletCredentials createNanoRestoreWalletFromSeedCredentials({ required String name, required String mnemonic, + required DerivationType derivationType, required String password, }) => NanoRestoreWalletFromSeedCredentials( name: name, password: password, mnemonic: mnemonic, + derivationType: derivationType, ); @override diff --git a/lib/nano/nano.dart b/lib/nano/nano.dart index c5732ca54..2d265a0ba 100644 --- a/lib/nano/nano.dart +++ b/lib/nano/nano.dart @@ -1,3 +1,4 @@ +import 'package:cw_nano/nano_wallet.dart'; import 'package:cw_nano/nano_wallet_service.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/wallet_credentials.dart'; @@ -52,6 +53,7 @@ abstract class Nano { required String name, required String mnemonic, required String password, + required DerivationType derivationType, }); String getTransactionAddress(Object wallet, int accountIndex, int addressIndex); diff --git a/lib/view_model/node_list/node_list_view_model.dart b/lib/view_model/node_list/node_list_view_model.dart index 3663d48ac..5d7c178ad 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -63,6 +63,9 @@ abstract class NodeListViewModelBase with Store { case WalletType.haven: node = getHavenDefaultNode(nodes: _nodeSource)!; break; + case WalletType.nano: + node = getNanoDefaultNode(nodes: _nodeSource)!; + break; default: throw Exception('Unexpected wallet type: ${_appStore.wallet!.type}'); } diff --git a/lib/view_model/wallet_restoration_from_seed_vm.dart b/lib/view_model/wallet_restoration_from_seed_vm.dart index 12012f579..8cc73237b 100644 --- a/lib/view_model/wallet_restoration_from_seed_vm.dart +++ b/lib/view_model/wallet_restoration_from_seed_vm.dart @@ -47,9 +47,9 @@ abstract class WalletRestorationFromSeedVMBase extends WalletCreationVM with Sto case WalletType.bitcoin: return bitcoin!.createBitcoinRestoreWalletFromSeedCredentials( name: name, mnemonic: seed, password: password); - case WalletType.nano: - return nano!.createNanoRestoreWalletFromSeedCredentials( - name: name, mnemonic: seed, password: password); + // case WalletType.nano: + // return nano!.createNanoRestoreWalletFromSeedCredentials( + // name: name, mnemonic: seed, password: password, derivationType: ); default: throw Exception('Unexpected type: ${type.toString()}'); } diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 01ee01a94..31c524495 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cw_nano/nano_wallet.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/store/app_store.dart'; @@ -75,8 +76,14 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { return ethereum!.createEthereumRestoreWalletFromSeedCredentials( name: name, mnemonic: seed, password: password); case WalletType.nano: + // default to bip39 for now: + final DerivationType derivationType = DerivationType.bip39; return nano!.createNanoRestoreWalletFromSeedCredentials( - name: name, mnemonic: seed, password: password); + name: name, + mnemonic: seed, + password: password, + derivationType: derivationType, + ); default: break; } diff --git a/tool/configure.dart b/tool/configure.dart index 6e11fe28f..81aa339f7 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -609,6 +609,7 @@ abstract class Nano { required String name, required String mnemonic, required String password, + required DerivationType derivationType, }); String getTransactionAddress(Object wallet, int accountIndex, int addressIndex);