part of 'nano.dart'; class CWNanoAccountList extends NanoAccountList { CWNanoAccountList(this._wallet); final Object _wallet; @override @computed ObservableList get accounts { final nanoWallet = _wallet as NanoWallet; final accounts = nanoWallet.walletAddresses.accountList.accounts .map((acc) => NanoAccount(id: acc.id, label: acc.label, balance: acc.balance)) .toList(); return ObservableList.of(accounts); } @override void update(Object wallet) { final nanoWallet = wallet as NanoWallet; nanoWallet.walletAddresses.accountList.update(null); } @override void refresh(Object wallet) { final nanoWallet = wallet as NanoWallet; nanoWallet.walletAddresses.accountList.refresh(); } @override Future> getAll(Object wallet) async { final nanoWallet = wallet as NanoWallet; return (await nanoWallet.walletAddresses.accountList.getAll()) .map((acc) => NanoAccount(id: acc.id, label: acc.label, balance: acc.balance)) .toList(); } @override Future addAccount(Object wallet, {required String label}) async { final nanoWallet = wallet as NanoWallet; await nanoWallet.walletAddresses.accountList.addAccount(label: label); } @override Future setLabelAccount(Object wallet, {required int accountIndex, required String label}) async { final nanoWallet = wallet as NanoWallet; await nanoWallet.walletAddresses.accountList .setLabelAccount(accountIndex: accountIndex, label: label); } } class CWNano extends Nano { @override NanoAccountList getAccountList(Object wallet) { return CWNanoAccountList(wallet); } @override Account getCurrentAccount(Object wallet) { final nanoWallet = wallet as NanoWallet; final acc = nanoWallet.walletAddresses.account; return Account(id: acc!.id, label: acc.label, balance: acc.balance); } @override void setCurrentAccount(Object wallet, int id, String label, String? balance) { final nanoWallet = wallet as NanoWallet; nanoWallet.walletAddresses.account = NanoAccount(id: id, label: label, balance: balance); nanoWallet.regenerateAddress(); } @override List getNanoWordList(String language) { return NanoMnemomics.WORDLIST; } @override WalletService createNanoWalletService(Box walletInfoSource) { return NanoWalletService(walletInfoSource); } @override Map getKeys(Object wallet) { final nanoWallet = wallet as NanoWallet; final keys = nanoWallet.keys; return { "seedKey": keys.seedKey, }; } @override WalletCredentials createNanoNewWalletCredentials({ required String name, String? password, }) => NanoNewWalletCredentials( name: name, password: password, ); @override WalletCredentials createNanoRestoreWalletFromSeedCredentials({ required String name, required String password, required String mnemonic, DerivationType? derivationType, }) { if (derivationType == null) { // figure out the derivation type as best we can, otherwise set it to "unknown" if (mnemonic.split(" ").length == 12) { derivationType = DerivationType.bip39; } else { derivationType = DerivationType.unknown; } } return NanoRestoreWalletFromSeedCredentials( name: name, password: password, mnemonic: mnemonic, derivationType: derivationType, ); } @override WalletCredentials createNanoRestoreWalletFromKeysCredentials({ required String name, required String password, required String seedKey, DerivationType? derivationType, }) { if (derivationType == null) { // figure out the derivation type as best we can, otherwise set it to "unknown" if (seedKey.length == 64) { derivationType = DerivationType.nano; } else { derivationType = DerivationType.unknown; } } return NanoRestoreWalletFromKeysCredentials( name: name, password: password, seedKey: seedKey, derivationType: derivationType, ); } @override Object createNanoTransactionCredentials(List outputs) { return NanoTransactionCredentials( outputs .map((out) => OutputInfo( fiatAmount: out.fiatAmount, cryptoAmount: out.cryptoAmount, address: out.address, note: out.note, sendAll: out.sendAll, extractedAddress: out.extractedAddress, isParsedAddress: out.isParsedAddress, formattedCryptoAmount: out.formattedCryptoAmount, )) .toList(), ); } @override Future changeRep(Object wallet, String address) async { if ((wallet as NanoWallet).transactionHistory.transactions.isEmpty) { throw Exception("Can't change representative without an existing transaction history"); } return wallet.changeRep(address); } @override Future updateTransactions(Object wallet) async { return (wallet as NanoWallet).updateTransactions(); } @override BigInt getTransactionAmountRaw(TransactionInfo transactionInfo) { return (transactionInfo as NanoTransactionInfo).amountRaw; } @override String getRepresentative(Object wallet) { return (wallet as NanoWallet).representative; } } class CWNanoUtil extends NanoUtil { // standard: @override String seedToPrivate(String seed, int index) { return ND.NanoKeys.seedToPrivate(seed, index); } @override String seedToAddress(String seed, int index) { return ND.NanoAccounts.createAccount( ND.NanoAccountType.NANO, privateKeyToPublic(seedToPrivate(seed, index))); } @override String seedToMnemonic(String seed) { return NanoMnemomics.seedToMnemonic(seed).join(" "); } @override Future mnemonicToSeed(String mnemonic) async { return NanoMnemomics.mnemonicListToSeed(mnemonic.split(' ')); } @override String privateKeyToPublic(String privateKey) { // return NanoHelpers.byteToHex(Ed25519Blake2b.getPubkey(NanoHelpers.hexToBytes(privateKey))!); return ND.NanoKeys.createPublicKey(privateKey); } @override String addressToPublicKey(String publicAddress) { return ND.NanoAccounts.extractPublicKey(publicAddress); } // universal: @override String privateKeyToAddress(String privateKey) { return ND.NanoAccounts.createAccount(ND.NanoAccountType.NANO, privateKeyToPublic(privateKey)); } @override String publicKeyToAddress(String publicKey) { return ND.NanoAccounts.createAccount(ND.NanoAccountType.NANO, publicKey); } // standard + hd: @override bool isValidSeed(String seed) { // Ensure seed is 64 or 128 characters long if (seed.length != 64 && seed.length != 128) { return false; } // Ensure seed only contains hex characters, 0-9;A-F return ND.NanoHelpers.isHexString(seed); } // hd: @override Future hdMnemonicListToSeed(List words) async { // if (words.length != 24) { // throw Exception('Expected a 24-word list, got a ${words.length} list'); // } final Uint8List salt = Uint8List.fromList(utf8.encode('mnemonic')); final Pbkdf2 hasher = Pbkdf2(iterations: 2048); final String seed = await hasher.sha512(words.join(' '), salt); return seed; } @override Future hdSeedToPrivate(String seed, int index) async { List seedBytes = hex.decode(seed); KeyData data = await ED25519_HD_KEY.derivePath("m/44'/165'/$index'", seedBytes); return hex.encode(data.key); } @override Future hdSeedToAddress(String seed, int index) async { return ND.NanoAccounts.createAccount( ND.NanoAccountType.NANO, privateKeyToPublic(await hdSeedToPrivate(seed, index))); } @override Future uniSeedToAddress(String seed, int index, String type) { if (type == "standard") { return Future.value(seedToAddress(seed, index)); } else if (type == "hd") { return hdSeedToAddress(seed, index); } else { throw Exception('Unknown seed type'); } } @override Future uniSeedToPrivate(String seed, int index, String type) { if (type == "standard") { return Future.value(seedToPrivate(seed, index)); } else if (type == "hd") { return hdSeedToPrivate(seed, index); } else { throw Exception('Unknown seed type'); } } @override bool isValidBip39Seed(String seed) { // Ensure seed is 128 characters long if (seed.length != 128) { return false; } // Ensure seed only contains hex characters, 0-9;A-F return ND.NanoHelpers.isHexString(seed); } // number util: static const int maxDecimalDigits = 6; // Max digits after decimal BigInt rawPerNano = BigInt.parse("1000000000000000000000000000000"); BigInt rawPerNyano = BigInt.parse("1000000000000000000000000"); BigInt rawPerBanano = BigInt.parse("100000000000000000000000000000"); BigInt rawPerXMR = BigInt.parse("1000000000000"); BigInt convertXMRtoNano = BigInt.parse("1000000000000000000"); // static BigInt convertXMRtoNano = BigInt.parse("1000000000000000000000000000"); /// Convert raw to ban and return as BigDecimal /// /// @param raw 100000000000000000000000000000 /// @return Decimal value 1.000000000000000000000000000000 /// Decimal _getRawAsDecimal(String? raw, BigInt? rawPerCur) { rawPerCur ??= rawPerNano; final Decimal amount = Decimal.parse(raw.toString()); final Decimal result = (amount / Decimal.parse(rawPerCur.toString())).toDecimal(); return result; } @override String getRawAsDecimalString(String? raw, BigInt? rawPerCur) { final Decimal result = _getRawAsDecimal(raw, rawPerCur); return result.toString(); } @override String truncateDecimal(Decimal input, {int digits = maxDecimalDigits}) { Decimal bigger = input.shift(digits); bigger = bigger.floor(); // chop off the decimal: 1.059 -> 1.05 bigger = bigger.shift(-digits); return bigger.toString(); } /// Return raw as a NANO amount. /// /// @param raw 100000000000000000000000000000 /// @returns 1 /// @override String getRawAsUsableString(String? raw, BigInt rawPerCur) { final String res = truncateDecimal(_getRawAsDecimal(raw, rawPerCur), digits: maxDecimalDigits + 9); if (raw == null || raw == "0" || raw == "00000000000000000000000000000000") { return "0"; } if (!res.contains(".")) { return res; } final String numAmount = res.split(".")[0]; String decAmount = res.split(".")[1]; // truncate: if (decAmount.length > maxDecimalDigits) { decAmount = decAmount.substring(0, maxDecimalDigits); // remove trailing zeros: decAmount = decAmount.replaceAllMapped(RegExp(r'0+$'), (Match match) => ''); if (decAmount.isEmpty) { return numAmount; } } return "$numAmount.$decAmount"; } @override String getRawAccuracy(String? raw, BigInt rawPerCur) { final String rawString = getRawAsUsableString(raw, rawPerCur); final String rawDecimalString = _getRawAsDecimal(raw, rawPerCur).toString(); if (raw == null || raw.isEmpty || raw == "0") { return ""; } if (rawString != rawDecimalString) { return "~"; } return ""; } /// Return readable string amount as raw string /// @param amount 1.01 /// @returns 101000000000000000000000000000 /// @override String getAmountAsRaw(String amount, BigInt rawPerCur) { final Decimal asDecimal = Decimal.parse(amount); final Decimal rawDecimal = Decimal.parse(rawPerCur.toString()); return (asDecimal * rawDecimal).toString(); } @override Future getInfoFromSeedOrMnemonic( DerivationType derivationType, { String? seedKey, String? mnemonic, required Node node, }) async { NanoClient nanoClient = NanoClient(); nanoClient.connect(node); late String publicAddress; if (seedKey != null) { if (seedKey.length == 64) { try { mnemonic = nanoUtil!.seedToMnemonic(seedKey); } catch (e) { print("not a valid 'nano' seed key"); } } if (derivationType == DerivationType.bip39) { publicAddress = await hdSeedToAddress(seedKey, 0); } else if (derivationType == DerivationType.nano) { publicAddress = await seedToAddress(seedKey, 0); } } if (derivationType == DerivationType.bip39) { if (mnemonic != null) { seedKey = await hdMnemonicListToSeed(mnemonic.split(' ')); publicAddress = await hdSeedToAddress(seedKey, 0); } } if (derivationType == DerivationType.nano) { if (mnemonic != null) { seedKey = await mnemonicToSeed(mnemonic); publicAddress = await seedToAddress(seedKey, 0); } } AccountInfoResponse? accountInfo = await nanoClient.getAccountInfo(publicAddress); if (accountInfo == null) { accountInfo = AccountInfoResponse( frontier: "", balance: "0", representative: "", confirmationHeight: 0); } accountInfo.address = publicAddress; return accountInfo; } @override Future> compareDerivationMethods({ String? mnemonic, String? privateKey, required Node node, }) async { String? seedKey = privateKey; if (mnemonic?.split(' ').length == 12) { return [DerivationType.bip39]; } if (seedKey?.length == 128) { return [DerivationType.bip39]; } else if (seedKey?.length == 64) { try { mnemonic = nanoUtil!.seedToMnemonic(seedKey!); } catch (e) { print("not a valid 'nano' seed key"); } } late String publicAddressStandard; late String publicAddressBip39; try { NanoClient nanoClient = NanoClient(); nanoClient.connect(node); if (mnemonic != null) { seedKey = await hdMnemonicListToSeed(mnemonic.split(' ')); publicAddressBip39 = await hdSeedToAddress(seedKey, 0); seedKey = await mnemonicToSeed(mnemonic); publicAddressStandard = await seedToAddress(seedKey, 0); } else if (seedKey != null) { try { publicAddressBip39 = await hdSeedToAddress(seedKey, 0); } catch (e) { return [DerivationType.nano]; } try { publicAddressStandard = await seedToAddress(seedKey, 0); } catch (e) { return [DerivationType.bip39]; } } // check if account has a history: AccountInfoResponse? bip39Info; AccountInfoResponse? standardInfo; try { bip39Info = await nanoClient.getAccountInfo(publicAddressBip39); } catch (e) { bip39Info = null; } try { standardInfo = await nanoClient.getAccountInfo(publicAddressStandard); } catch (e) { standardInfo = null; } // one of these is *probably* null: if (bip39Info == null && standardInfo != null) { return [DerivationType.nano]; } else if (standardInfo == null && bip39Info != null) { return [DerivationType.bip39]; } // we don't know for sure: return [DerivationType.nano, DerivationType.bip39]; } catch (e) { return [DerivationType.nano, DerivationType.bip39]; } } }