diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index 0f2dfc25b..af03c5e30 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -92,6 +92,7 @@ jobs: cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. + cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. flutter packages pub run build_runner build --delete-conflicting-outputs - name: Add secrets diff --git a/.gitignore b/.gitignore index 09583004b..e8fb0048c 100644 --- a/.gitignore +++ b/.gitignore @@ -124,6 +124,7 @@ lib/bitcoin/bitcoin.dart lib/monero/monero.dart lib/haven/haven.dart lib/ethereum/ethereum.dart +lib/nano/nano.dart ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png diff --git a/assets/nano_node_list.yml b/assets/nano_node_list.yml new file mode 100644 index 000000000..63b4baec1 --- /dev/null +++ b/assets/nano_node_list.yml @@ -0,0 +1,6 @@ +- + uri: rpc.nano.to + useSSL: true + is_default: true +- + uri: node.perish.co:9076 \ No newline at end of file diff --git a/assets/nano_pow_node_list.yml b/assets/nano_pow_node_list.yml new file mode 100644 index 000000000..b90845034 --- /dev/null +++ b/assets/nano_pow_node_list.yml @@ -0,0 +1,9 @@ +- + uri: rpc.nano.to + useSSL: true + is_default: true +- + uri: workers.perish.co +- + uri: worker.nanoriver.cc + useSSL: true \ No newline at end of file diff --git a/configure_cake_wallet_android.sh b/configure_cake_wallet_android.sh old mode 100644 new mode 100755 index b80ebc46e..792159f29 --- a/configure_cake_wallet_android.sh +++ b/configure_cake_wallet_android.sh @@ -7,4 +7,5 @@ cd cw_monero && flutter pub get && flutter packages pub run build_runner build - cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. flutter packages pub run build_runner build --delete-conflicting-outputs diff --git a/cw_bitcoin/lib/bitcoin_mnemonic.dart b/cw_bitcoin/lib/bitcoin_mnemonic.dart index f4ebd7e5d..9163fcb11 100644 --- a/cw_bitcoin/lib/bitcoin_mnemonic.dart +++ b/cw_bitcoin/lib/bitcoin_mnemonic.dart @@ -2297,4 +2297,4 @@ final englishWordlist = [ 'zero', 'zone', 'zoo' -]; +]; \ No newline at end of file diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index c4675df1c..2c66d02fe 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -89,4 +89,4 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialRegularAddressIndex: snp.regularAddressIndex, initialChangeAddressIndex: snp.changeAddressIndex); } -} +} \ No newline at end of file diff --git a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart index 82173b2d2..37b272a1b 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart @@ -20,4 +20,4 @@ class BitcoinRestoreWalletFromWIFCredentials extends WalletCredentials { : super(name: name, password: password, walletInfo: walletInfo); final String wif; -} +} \ No newline at end of file diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index bfadaf2a3..3a97e0682 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -100,4 +100,4 @@ class BitcoinWalletService extends WalletService< await wallet.init(); return wallet; } -} +} \ No newline at end of file diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 6db0c23f2..def991ebe 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -56,4 +56,4 @@ class ElectrumWallletSnapshot { regularAddressIndex: regularAddressIndex, changeAddressIndex: changeAddressIndex); } -} +} \ No newline at end of file diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 86ea3f214..f6ffcdc8b 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -90,6 +90,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.zrx, CryptoCurrency.dydx, CryptoCurrency.steth, + CryptoCurrency.banano, ]; static const havenCurrencies = [ @@ -119,7 +120,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const eos = CryptoCurrency(title: 'EOS', fullName: 'EOS', raw: 7, name: 'eos', iconPath: 'assets/images/eos_icon.png'); static const eth = CryptoCurrency(title: 'ETH', fullName: 'Ethereum', raw: 8, name: 'eth', iconPath: 'assets/images/eth_icon.png'); static const ltc = CryptoCurrency(title: 'LTC', fullName: 'Litecoin', raw: 9, name: 'ltc', iconPath: 'assets/images/litecoin-ltc_icon.png'); - static const nano = CryptoCurrency(title: 'NANO', raw: 10, name: 'nano', iconPath: 'assets/images/nano.png'); + static const nano = CryptoCurrency(title: 'XNO', raw: 10, fullName: 'Nano', name: 'xno', iconPath: 'assets/images/nano_icon.png'); static const trx = CryptoCurrency(title: 'TRX', fullName: 'TRON', raw: 11, name: 'trx', iconPath: 'assets/images/trx_icon.png'); static const usdt = CryptoCurrency(title: 'USDT', tag: 'OMNI', fullName: 'USDT Tether', raw: 12, name: 'usdt', iconPath: 'assets/images/usdt_icon.png'); static const usdterc20 = CryptoCurrency(title: 'USDT', tag: 'ETH', fullName: 'USDT Tether', raw: 13, name: 'usdterc20', iconPath: 'assets/images/usdterc20_icon.png'); @@ -198,6 +199,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const zrx = CryptoCurrency(title: 'ZRX', tag: 'ETH', fullName: '0x Protocol', raw: 83, name: 'zrx', iconPath: 'assets/images/zrx_icon.png'); static const dydx = CryptoCurrency(title: 'DYDX', tag: 'ETH', fullName: 'dYdX', raw: 84, name: 'dydx', iconPath: 'assets/images/dydx_icon.png'); static const steth = CryptoCurrency(title: 'STETH', tag: 'ETH', fullName: 'Lido Staked Ethereum', raw: 85, name: 'steth', iconPath: 'assets/images/steth_icon.png'); + static const banano = CryptoCurrency(title: 'BAN', raw: 86, name: 'banano', iconPath: 'assets/images/nano_icon.png'); static final Map _rawCurrencyMap = diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index 8ac8c1fc6..2db858b30 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -13,6 +13,10 @@ CryptoCurrency currencyForWalletType(WalletType type) { return CryptoCurrency.xhv; case WalletType.ethereum: return CryptoCurrency.eth; + case WalletType.nano: + return CryptoCurrency.nano; + case WalletType.banano: + return CryptoCurrency.banano; default: throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); } diff --git a/cw_core/lib/hive_type_ids.dart b/cw_core/lib/hive_type_ids.dart index 950f39e1f..4d4d1a6a8 100644 --- a/cw_core/lib/hive_type_ids.dart +++ b/cw_core/lib/hive_type_ids.dart @@ -1,4 +1,4 @@ -const CONTACT_TYPE_ID = 0; +const CONTACT_TYPE_ID = 0; const NODE_TYPE_ID = 1; const TRANSACTION_TYPE_ID = 2; const TRADE_TYPE_ID = 3; @@ -11,3 +11,6 @@ const UNSPENT_COINS_INFO_TYPE_ID = 9; const ANONPAY_INVOICE_INFO_TYPE_ID = 10; const ADDRESS_INFO_TYPE_ID = 11; const ERC20_TOKEN_TYPE_ID = 12; +const NANO_ACCOUNT_TYPE_ID = 13; +const POW_NODE_TYPE_ID = 14; +const DERIVATION_TYPE_TYPE_ID = 15; \ No newline at end of file diff --git a/cw_core/lib/nano_account.dart b/cw_core/lib/nano_account.dart new file mode 100644 index 000000000..91df62e75 --- /dev/null +++ b/cw_core/lib/nano_account.dart @@ -0,0 +1,23 @@ +import 'package:cw_core/hive_type_ids.dart'; +import 'package:hive/hive.dart'; + +part 'nano_account.g.dart'; + +@HiveType(typeId: NanoAccount.typeId) +class NanoAccount extends HiveObject { + NanoAccount({required this.label, required this.id, this.balance, this.isSelected = false}); + + static const typeId = NANO_ACCOUNT_TYPE_ID; + + @HiveField(0) + String label; + + @HiveField(1) + final int id; + + @HiveField(2) + bool isSelected; + + @HiveField(3) + String? balance; +} diff --git a/cw_core/lib/nano_account_info_response.dart b/cw_core/lib/nano_account_info_response.dart new file mode 100644 index 000000000..319bbb861 --- /dev/null +++ b/cw_core/lib/nano_account_info_response.dart @@ -0,0 +1,23 @@ +class AccountInfoResponse { + String frontier; + int confirmationHeight; + String balance; + String representative; + String? address; + + AccountInfoResponse({ + required this.frontier, + required this.balance, + required this.representative, + required this.confirmationHeight, + }); + + factory AccountInfoResponse.fromJson(Map json) { + return AccountInfoResponse( + frontier: json['frontier'] as String, + representative: json['representative'] as String, + balance: json['balance'] as String, + confirmationHeight: int.parse(json['confirmation_height'] as String), + ); + } +} diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 59a1450f6..a07030d64 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -9,19 +9,19 @@ import 'package:http/io_client.dart' as ioc; part 'node.g.dart'; -Uri createUriFromElectrumAddress(String address) => - Uri.tryParse('tcp://$address')!; +Uri createUriFromElectrumAddress(String address) => Uri.tryParse('tcp://$address')!; @HiveType(typeId: Node.typeId) class Node extends HiveObject with Keyable { - Node( - {this.login, - this.password, - this.useSSL, - this.trusted = false, - this.socksProxyAddress, - String? uri, - WalletType? type,}) { + Node({ + this.login, + this.password, + this.useSSL, + this.trusted = false, + this.socksProxyAddress, + String? uri, + WalletType? type, + }) { if (uri != null) { uriRaw = uri; } @@ -78,6 +78,13 @@ class Node extends HiveObject with Keyable { return Uri.http(uriRaw, ''); case WalletType.ethereum: return Uri.https(uriRaw, ''); + case WalletType.nano: + case WalletType.banano: + if (isSSL) { + return Uri.https(uriRaw, ''); + } else { + return Uri.http(uriRaw, ''); + } default: throw Exception('Unexpected type ${type.toString()} for Node uri'); } @@ -86,13 +93,13 @@ class Node extends HiveObject with Keyable { @override bool operator ==(other) => other is Node && - (other.uriRaw == uriRaw && - other.login == login && - other.password == password && - other.typeRaw == typeRaw && - other.useSSL == useSSL && - other.trusted == trusted && - other.socksProxyAddress == socksProxyAddress); + (other.uriRaw == uriRaw && + other.login == login && + other.password == password && + other.typeRaw == typeRaw && + other.useSSL == useSSL && + other.trusted == trusted && + other.socksProxyAddress == socksProxyAddress); @override int get hashCode => @@ -120,7 +127,9 @@ class Node extends HiveObject with Keyable { try { switch (type) { case WalletType.monero: - return useSocksProxy ? requestNodeWithProxy(socksProxyAddress ?? '') : requestMoneroNode(); + return useSocksProxy + ? requestNodeWithProxy(socksProxyAddress ?? '') + : requestMoneroNode(); case WalletType.bitcoin: return requestElectrumServer(); case WalletType.litecoin: @@ -129,6 +138,9 @@ class Node extends HiveObject with Keyable { return requestMoneroNode(); case WalletType.ethereum: return requestElectrumServer(); + case WalletType.nano: + case WalletType.banano: + return requestNanoNode(); default: return false; } @@ -141,27 +153,23 @@ class Node extends HiveObject with Keyable { final path = '/json_rpc'; final rpcUri = isSSL ? Uri.https(uri.authority, path) : Uri.http(uri.authority, path); final realm = 'monero-rpc'; - final body = { - 'jsonrpc': '2.0', - 'id': '0', - 'method': 'get_info' - }; + final body = {'jsonrpc': '2.0', 'id': '0', 'method': 'get_info'}; try { final authenticatingClient = HttpClient(); authenticatingClient.addCredentials( - rpcUri, - realm, - HttpClientDigestCredentials(login ?? '', password ?? ''), + rpcUri, + realm, + HttpClientDigestCredentials(login ?? '', password ?? ''), ); final http.Client client = ioc.IOClient(authenticatingClient); final response = await client.post( - rpcUri, - headers: {'Content-Type': 'application/json'}, - body: json.encode(body), + rpcUri, + headers: {'Content-Type': 'application/json'}, + body: json.encode(body), ); client.close(); @@ -173,8 +181,24 @@ class Node extends HiveObject with Keyable { } } - Future requestNodeWithProxy(String proxy) async { + Future requestNanoNode() async { + http.Response response = await http.post( + uri, + headers: {'Content-type': 'application/json'}, + body: json.encode( + { + "action": "block_count", + }, + ), + ); + if (response.statusCode == 200) { + return true; + } else { + return false; + } + } + Future requestNodeWithProxy(String proxy) async { if (proxy.isEmpty || !proxy.contains(':')) { return false; } diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index f8db67b24..d15ea42cd 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -13,9 +13,7 @@ import 'package:cw_core/sync_status.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/wallet_type.dart'; -abstract class WalletBase< - BalanceType extends Balance, - HistoryType extends TransactionHistoryBase, +abstract class WalletBase { WalletBase(this.walletInfo); @@ -58,6 +56,9 @@ abstract class WalletBase< Future connectToNode({required Node node}); + // there is a default definition here because only coins with a pow node (nano based) need to override this + Future connectToPowNode({required Node node}) async {} + Future startSync(); Future createTransaction(Object credentials); diff --git a/cw_core/lib/wallet_credentials.dart b/cw_core/lib/wallet_credentials.dart index e028232e8..0cdf892bd 100644 --- a/cw_core/lib/wallet_credentials.dart +++ b/cw_core/lib/wallet_credentials.dart @@ -5,10 +5,15 @@ abstract class WalletCredentials { required this.name, this.height, this.walletInfo, - this.password}); + this.password, + this.derivationType, + this.derivationPath, + }); final String name; final int? height; String? password; + DerivationType? derivationType; + String? derivationPath; WalletInfo? walletInfo; } diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index 210adb9a4..c4ccea00a 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -6,29 +6,92 @@ import 'package:hive/hive.dart'; part 'wallet_info.g.dart'; +@HiveType(typeId: DERIVATION_TYPE_TYPE_ID) +enum DerivationType { + @HiveField(0) + unknown, + @HiveField(1) + def, // default is a reserved word + @HiveField(2) + nano, + @HiveField(3) + bip39, + @HiveField(4) + electrum1, + @HiveField(5) + electrum2, +} + +class DerivationInfo { + DerivationInfo({ + required this.derivationType, + this.derivationPath, + this.balance = "", + this.address = "", + this.height = 0, + this.script_type, + this.description, + }); + + String balance; + String address; + int height; + final DerivationType derivationType; + final String? derivationPath; + final String? script_type; + final String? description; +} + @HiveType(typeId: WalletInfo.typeId) class WalletInfo extends HiveObject { - WalletInfo(this.id, this.name, this.type, this.isRecovery, this.restoreHeight, - this.timestamp, this.dirPath, this.path, this.address, this.yatEid, - this.yatLastUsedAddressRaw, this.showIntroCakePayCard) + WalletInfo( + this.id, + this.name, + this.type, + this.isRecovery, + this.restoreHeight, + this.timestamp, + this.dirPath, + this.path, + this.address, + this.yatEid, + this.yatLastUsedAddressRaw, + this.showIntroCakePayCard, + this.derivationType, + this.derivationPath) : _yatLastUsedAddressController = StreamController.broadcast(); - factory WalletInfo.external( - {required String id, - required String name, - required WalletType type, - required bool isRecovery, - required int restoreHeight, - required DateTime date, - required String dirPath, - required String path, - required String address, - bool? showIntroCakePayCard, - String yatEid ='', - String yatLastUsedAddressRaw = ''}) { - return WalletInfo(id, name, type, isRecovery, restoreHeight, - date.millisecondsSinceEpoch, dirPath, path, address, - yatEid, yatLastUsedAddressRaw, showIntroCakePayCard); + factory WalletInfo.external({ + required String id, + required String name, + required WalletType type, + required bool isRecovery, + required int restoreHeight, + required DateTime date, + required String dirPath, + required String path, + required String address, + bool? showIntroCakePayCard, + String yatEid = '', + String yatLastUsedAddressRaw = '', + DerivationType? derivationType, + String? derivationPath, + }) { + return WalletInfo( + id, + name, + type, + isRecovery, + restoreHeight, + date.millisecondsSinceEpoch, + dirPath, + path, + address, + yatEid, + yatLastUsedAddressRaw, + showIntroCakePayCard, + derivationType, + derivationPath); } static const typeId = WALLET_INFO_TYPE_ID; @@ -79,6 +142,12 @@ class WalletInfo extends HiveObject { @HiveField(15) List? usedAddresses; + @HiveField(16) + DerivationType? derivationType; + + @HiveField(17) + String? derivationPath; + String get yatLastUsedAddress => yatLastUsedAddressRaw ?? ''; set yatLastUsedAddress(String address) { @@ -89,7 +158,7 @@ class WalletInfo extends HiveObject { String get yatEmojiId => yatEid ?? ''; bool get isShowIntroCakePayCard { - if(showIntroCakePayCard == null) { + if (showIntroCakePayCard == null) { return type != WalletType.haven; } return showIntroCakePayCard!; diff --git a/cw_core/lib/wallet_service.dart b/cw_core/lib/wallet_service.dart index f95bc1a44..f6d0ca192 100644 --- a/cw_core/lib/wallet_service.dart +++ b/cw_core/lib/wallet_service.dart @@ -1,9 +1,11 @@ +import 'package:cw_core/node.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; -abstract class WalletService { +abstract class WalletService { WalletType getType(); Future create(N credentials); diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index 62c2ad410..0125facaf 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -10,6 +10,8 @@ const walletTypes = [ WalletType.litecoin, WalletType.haven, WalletType.ethereum, + WalletType.nano, + WalletType.banano, ]; @HiveType(typeId: WALLET_TYPE_TYPE_ID) @@ -31,6 +33,12 @@ enum WalletType { @HiveField(5) ethereum, + + @HiveField(6) + nano, + + @HiveField(7) + banano, } int serializeToInt(WalletType type) { @@ -45,6 +53,10 @@ int serializeToInt(WalletType type) { return 3; case WalletType.ethereum: return 4; + case WalletType.nano: + return 5; + case WalletType.banano: + return 6; default: return -1; } @@ -62,6 +74,10 @@ WalletType deserializeFromInt(int raw) { return WalletType.haven; case 4: return WalletType.ethereum; + case 5: + return WalletType.nano; + case 6: + return WalletType.banano; default: throw Exception('Unexpected token: $raw for WalletType deserializeFromInt'); } @@ -79,6 +95,10 @@ String walletTypeToString(WalletType type) { return 'Haven'; case WalletType.ethereum: return 'Ethereum'; + case WalletType.nano: + return 'Nano'; + case WalletType.banano: + return 'Banano'; default: return ''; } @@ -96,6 +116,10 @@ String walletTypeToDisplayName(WalletType type) { return 'Haven (XHV)'; case WalletType.ethereum: return 'Ethereum (ETH)'; + case WalletType.nano: + return 'Nano (XNO)'; + case WalletType.banano: + return 'Banano (BAN)'; default: return ''; } @@ -113,6 +137,10 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) { return CryptoCurrency.xhv; case WalletType.ethereum: return CryptoCurrency.eth; + case WalletType.nano: + return CryptoCurrency.nano; + case WalletType.banano: + return CryptoCurrency.banano; default: throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); } diff --git a/cw_monero/ios/Classes/monero_api.cpp b/cw_monero/ios/Classes/monero_api.cpp index 1cb01fac7..e04282fe8 100644 --- a/cw_monero/ios/Classes/monero_api.cpp +++ b/cw_monero/ios/Classes/monero_api.cpp @@ -234,7 +234,6 @@ extern "C" } void setUnlocked(bool unlocked); - }; Monero::Coins *m_coins; @@ -568,7 +567,7 @@ extern "C" _preferred_inputs.insert(std::string(*preferred_inputs)); preferred_inputs++; } - + auto priority = static_cast(priority_raw); std::string _payment_id; Monero::PendingTransaction *transaction; diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index 9d1a0b3e4..9ee4cf374 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -57,7 +57,7 @@ class MoneroWalletService extends WalletService< final Box walletInfoSource; final Box unspentCoinsInfoSource; - + static bool walletFilesExist(String path) => !File(path).existsSync() && !File('$path.keys').existsSync(); diff --git a/cw_nano/lib/banano_balance.dart b/cw_nano/lib/banano_balance.dart new file mode 100644 index 000000000..b85609b60 --- /dev/null +++ b/cw_nano/lib/banano_balance.dart @@ -0,0 +1,20 @@ +import 'package:cw_core/balance.dart'; +import 'package:cw_nano/nano_util.dart'; + +class BananoBalance extends Balance { + final BigInt currentBalance; + final BigInt receivableBalance; + + BananoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0) { + } + + @override + String get formattedAvailableBalance { + return NanoUtil.getRawAsUsableString(currentBalance.toString(), NanoUtil.rawPerBanano); + } + + @override + String get formattedAdditionalBalance { + return NanoUtil.getRawAsUsableString(receivableBalance.toString(), NanoUtil.rawPerBanano); + } +} diff --git a/cw_nano/lib/cw_nano.dart b/cw_nano/lib/cw_nano.dart new file mode 100644 index 000000000..08e23a232 --- /dev/null +++ b/cw_nano/lib/cw_nano.dart @@ -0,0 +1,7 @@ +library cw_nano; + +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} 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_account_list.dart b/cw_nano/lib/nano_account_list.dart new file mode 100644 index 000000000..7207eafe1 --- /dev/null +++ b/cw_nano/lib/nano_account_list.dart @@ -0,0 +1,69 @@ +import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/nano_account.dart'; +import 'package:mobx/mobx.dart'; +import 'package:hive/hive.dart'; + +part 'nano_account_list.g.dart'; + +class NanoAccountList = NanoAccountListBase with _$NanoAccountList; + +abstract class NanoAccountListBase with Store { + NanoAccountListBase(this.address) + : accounts = ObservableList(), + _isRefreshing = false, + _isUpdating = false { + refresh(); + } + + @observable + ObservableList accounts; + bool _isRefreshing; + bool _isUpdating; + + String address; + + Future update(String? address) async { + if (_isUpdating) { + return; + } + + try { + _isUpdating = true; + + final accounts = await getAll(address: address ?? this.address); + + if (accounts.isNotEmpty) { + this.accounts.clear(); + this.accounts.addAll(accounts); + } + + _isUpdating = false; + } catch (e) { + _isUpdating = false; + rethrow; + } + } + + Future> getAll({String? address}) async { + final box = await CakeHive.openBox(address ?? this.address); + + // get all accounts in box: + return box.values.toList(); + } + + Future addAccount({required String label}) async { + final box = await CakeHive.openBox(address); + final account = NanoAccount(id: box.length, label: label, balance: "0.00", isSelected: false); + await box.add(account); + await account.save(); + } + + Future setLabelAccount({required int accountIndex, required String label}) async { + final box = await CakeHive.openBox(address); + final account = box.getAt(accountIndex); + account!.label = label; + await account.save(); + } + + void refresh() {} +} diff --git a/cw_nano/lib/nano_balance.dart b/cw_nano/lib/nano_balance.dart new file mode 100644 index 000000000..dbb39d2a3 --- /dev/null +++ b/cw_nano/lib/nano_balance.dart @@ -0,0 +1,34 @@ +import 'package:cw_core/balance.dart'; +import 'package:cw_nano/nano_util.dart'; + +BigInt stringAmountToBigInt(String amount) { + return BigInt.parse(NanoUtil.getAmountAsRaw(amount, NanoUtil.rawPerNano)); +} + +class NanoBalance extends Balance { + final BigInt currentBalance; + final BigInt receivableBalance; + late String formattedCurrentBalance; + late String formattedReceivableBalance; + + NanoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0) { + this.formattedCurrentBalance = ""; + this.formattedReceivableBalance = ""; + } + + NanoBalance.fromString( + {required this.formattedCurrentBalance, required this.formattedReceivableBalance}) + : currentBalance = stringAmountToBigInt(formattedCurrentBalance), + receivableBalance = stringAmountToBigInt(formattedReceivableBalance), + super(0, 0); + + @override + String get formattedAvailableBalance { + return NanoUtil.getRawAsUsableString(currentBalance.toString(), NanoUtil.rawPerNano); + } + + @override + String get formattedAdditionalBalance { + return NanoUtil.getRawAsUsableString(receivableBalance.toString(), NanoUtil.rawPerNano); + } +} diff --git a/cw_nano/lib/nano_client.dart b/cw_nano/lib/nano_client.dart new file mode 100644 index 000000000..29f47cc2d --- /dev/null +++ b/cw_nano/lib/nano_client.dart @@ -0,0 +1,424 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:cw_core/nano_account_info_response.dart'; +import 'package:cw_nano/nano_balance.dart'; +import 'package:cw_nano/nano_transaction_model.dart'; +import 'package:cw_nano/nano_util.dart'; +import 'package:http/http.dart' as http; +import 'package:nanodart/nanodart.dart'; +import 'package:cw_core/node.dart'; + +class NanoClient { + static const String DEFAULT_REPRESENTATIVE = + "nano_38713x95zyjsqzx6nm1dsom1jmm668owkeb9913ax6nfgj15az3nu8xkx579"; + + Node? _node; + Node? _powNode; + + bool connect(Node node) { + try { + _node = node; + return true; + } catch (e) { + return false; + } + } + + bool connectPow(Node node) { + try { + _powNode = node; + return true; + } catch (e) { + return false; + } + } + + Future getBalance(String address) async { + final response = await http.post( + _node!.uri, + headers: {"Content-Type": "application/json"}, + body: jsonEncode( + { + "action": "account_balance", + "account": address, + }, + ), + ); + final data = await jsonDecode(response.body); + final String currentBalance = data["balance"] as String; + final String receivableBalance = data["receivable"] as String; + final BigInt cur = BigInt.parse(currentBalance); + final BigInt rec = BigInt.parse(receivableBalance); + return NanoBalance(currentBalance: cur, receivableBalance: rec); + } + + Future getAccountInfo(String address) async { + try { + final response = await http.post( + _node!.uri, + headers: {"Content-Type": "application/json"}, + body: jsonEncode( + { + "action": "account_info", + "representative": "true", + "account": address, + }, + ), + ); + final data = await jsonDecode(response.body); + return AccountInfoResponse.fromJson(data as Map); + } catch (e) { + print("error while getting account info"); + return null; + } + } + + Future changeRep({ + required String privateKey, + required String repAddress, + required String ourAddress, + }) async { + try { + AccountInfoResponse? accountInfo = await getAccountInfo(ourAddress); + + if (accountInfo == null) { + throw Exception("error while getting account info"); + } + + // construct the change block: + Map changeBlock = { + "type": "state", + "account": ourAddress, + "previous": accountInfo.frontier, + "representative": repAddress, + "balance": accountInfo.balance, + "link": "0000000000000000000000000000000000000000000000000000000000000000", + "link_as_account": "nano_1111111111111111111111111111111111111111111111111111hifc8npp", + }; + + // sign the change block: + final String hash = NanoBlocks.computeStateHash( + NanoAccountType.NANO, + changeBlock["account"]!, + changeBlock["previous"]!, + changeBlock["representative"]!, + BigInt.parse(changeBlock["balance"]!), + changeBlock["link"]!, + ); + final String signature = NanoSignatures.signBlock(hash, privateKey); + + // get PoW for the send block: + final String work = await requestWork(accountInfo.frontier); + + changeBlock["signature"] = signature; + changeBlock["work"] = work; + + return await processBlock(changeBlock, "change"); + } catch (e) { + throw Exception("error while changing representative"); + } + } + + Future requestWork(String hash) async { + final response = await http.post( + _powNode!.uri, + headers: {'Content-type': 'application/json'}, + body: json.encode( + { + "action": "work_generate", + "hash": hash, + }, + ), + ); + if (response.statusCode == 200) { + final Map decoded = json.decode(response.body) as Map; + if (decoded.containsKey("error")) { + throw Exception("Received error ${decoded["error"]}"); + } + return decoded["work"] as String; + } else { + throw Exception("Received work error ${response.body}"); + } + } + + Future send({ + required String privateKey, + required String amountRaw, + required String destinationAddress, + }) async { + final Map sendBlock = await constructSendBlock( + privateKey: privateKey, + amountRaw: amountRaw, + destinationAddress: destinationAddress, + ); + + return await processBlock(sendBlock, "send"); + } + + Future processBlock(Map block, String subtype) async { + final headers = {"Content-Type": "application/json"}; + final processBody = jsonEncode({ + "action": "process", + "json_block": "true", + "subtype": subtype, + "block": block, + }); + + final processResponse = await http.post( + _node!.uri, + headers: headers, + body: processBody, + ); + + final Map decoded = json.decode(processResponse.body) as Map; + if (decoded.containsKey("error")) { + throw Exception("Received error ${decoded["error"]}"); + } + + // return the hash of the transaction: + return decoded["hash"].toString(); + } + + Future> constructSendBlock({ + required String privateKey, + required String amountRaw, + required String destinationAddress, + BigInt? balanceAfterTx, + String? previousHash, + }) async { + try { + // our address: + final String publicAddress = NanoUtil.privateKeyToAddress(privateKey); + + // first get the current account balance: + if (balanceAfterTx == null) { + final BigInt currentBalance = (await getBalance(publicAddress)).currentBalance; + final BigInt txAmount = BigInt.parse(amountRaw); + balanceAfterTx = currentBalance - txAmount; + } + + // get the account info (we need the frontier and representative): + AccountInfoResponse? infoResponse = await getAccountInfo(publicAddress); + if (infoResponse == null) { + throw Exception( + "error while getting account info! (we probably don't have an open account yet)"); + } + + String frontier = infoResponse.frontier; + // override if provided: + if (previousHash != null) { + frontier = previousHash; + } + final String representative = infoResponse.representative; + // link = destination address: + final String link = NanoAccounts.extractPublicKey(destinationAddress); + final String linkAsAccount = destinationAddress; + + // construct the send block: + Map sendBlock = { + "type": "state", + "account": publicAddress, + "previous": frontier, + "representative": representative, + "balance": balanceAfterTx.toString(), + "link": link, + }; + + // sign the send block: + final String hash = NanoBlocks.computeStateHash( + NanoAccountType.NANO, + sendBlock["account"]!, + sendBlock["previous"]!, + sendBlock["representative"]!, + BigInt.parse(sendBlock["balance"]!), + sendBlock["link"]!, + ); + final String signature = NanoSignatures.signBlock(hash, privateKey); + + // get PoW for the send block: + final String work = await requestWork(frontier); + + sendBlock["link_as_account"] = linkAsAccount; + sendBlock["signature"] = signature; + sendBlock["work"] = work; + + // ready to post send block: + return sendBlock; + } catch (e) { + print(e); + rethrow; + } + } + + Future receiveBlock({ + required String blockHash, + required String source, + required String amountRaw, + required String destinationAddress, + required String privateKey, + }) async { + bool openBlock = false; + + final headers = { + "Content-Type": "application/json", + }; + + // first check if the account is open: + // get the account info (we need the frontier and representative): + AccountInfoResponse? infoData = await getAccountInfo(destinationAddress); + String? frontier; + String? representative; + + if (infoData == null) { + // account is not open yet, we need to create an open block: + openBlock = true; + // we don't have a representative set yet: + representative = DEFAULT_REPRESENTATIVE; + // we don't have a frontier yet: + frontier = "0000000000000000000000000000000000000000000000000000000000000000"; + } else { + frontier = infoData.frontier; + representative = infoData.representative; + } + + // first get the account balance: + final BigInt currentBalance = (await getBalance(destinationAddress)).currentBalance; + final BigInt txAmount = BigInt.parse(amountRaw); + final BigInt balanceAfterTx = currentBalance + txAmount; + + // link = send block hash: + final String link = blockHash; + // this "linkAsAccount" is meaningless: + final String linkAsAccount = NanoAccounts.createAccount(NanoAccountType.NANO, blockHash); + + // construct the receive block: + Map receiveBlock = { + "type": "state", + "account": destinationAddress, + "previous": frontier, + "representative": representative, + "balance": balanceAfterTx.toString(), + "link": link, + "link_as_account": linkAsAccount, + }; + + // sign the receive block: + final String hash = NanoBlocks.computeStateHash( + NanoAccountType.NANO, + receiveBlock["account"]!, + receiveBlock["previous"]!, + receiveBlock["representative"]!, + BigInt.parse(receiveBlock["balance"]!), + receiveBlock["link"]!, + ); + final String signature = NanoSignatures.signBlock(hash, privateKey); + + // get PoW for the receive block: + String? work; + if (openBlock) { + work = await requestWork(NanoAccounts.extractPublicKey(destinationAddress)); + } else { + work = await requestWork(frontier); + } + receiveBlock["link_as_account"] = linkAsAccount; + receiveBlock["signature"] = signature; + receiveBlock["work"] = work; + + // process the receive block: + + final processBody = jsonEncode({ + "action": "process", + "json_block": "true", + "subtype": "receive", + "block": receiveBlock, + }); + final processResponse = await http.post( + _node!.uri, + headers: headers, + body: processBody, + ); + + final Map decoded = json.decode(processResponse.body) as Map; + if (decoded.containsKey("error")) { + throw Exception("Received error ${decoded["error"]}"); + } + } + + // returns the number of blocks received: + Future confirmAllReceivable({ + required String destinationAddress, + required String privateKey, + }) async { + final receivableResponse = await http.post(_node!.uri, + headers: {"Content-Type": "application/json"}, + body: jsonEncode({ + "action": "receivable", + "account": destinationAddress, + "count": "-1", + "source": true, + })); + + final receivableData = await jsonDecode(receivableResponse.body); + if (receivableData["blocks"] == "" || receivableData["blocks"] == null) { + return 0; + } + + dynamic blocks; + if (receivableData["blocks"] is List) { + var listBlocks = receivableData["blocks"] as List; + if (listBlocks.isEmpty) { + return 0; + } + blocks = {for (var block in listBlocks) block['hash']: block}; + } else { + blocks = receivableData["blocks"] as Map; + } + + blocks = blocks as Map; + + // confirm all receivable blocks: + for (final blockHash in blocks.keys) { + final block = blocks[blockHash]; + final String amountRaw = block["amount"] as String; + final String source = block["source"] as String; + await receiveBlock( + blockHash: blockHash, + source: source, + amountRaw: amountRaw, + privateKey: privateKey, + destinationAddress: destinationAddress, + ); + // a bit of a hack: + await Future.delayed(const Duration(seconds: 2)); + } + + return blocks.keys.length; + } + + void stop() {} + + Future> fetchTransactions(String address) async { + try { + final response = await http.post(_node!.uri, + headers: {"Content-Type": "application/json"}, + body: jsonEncode({ + "action": "account_history", + "account": address, + "count": "250", // TODO: pick a number + // "raw": true, + })); + final data = await jsonDecode(response.body); + final transactions = data["history"] is List ? data["history"] as List : []; + + // Map the transactions list to NanoTransactionModel using the factory + // reversed so that the DateTime is correct when local_timestamp is absent + return transactions.reversed + .map((transaction) => NanoTransactionModel.fromJson(transaction)) + .toList(); + } catch (e) { + print(e); + return []; + } + } +} diff --git a/cw_nano/lib/nano_mnemonic.dart b/cw_nano/lib/nano_mnemonic.dart new file mode 100644 index 000000000..2a06fe515 --- /dev/null +++ b/cw_nano/lib/nano_mnemonic.dart @@ -0,0 +1,2088 @@ +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) { + if (!NanoSeeds.isValidSeed(seed)) { + throw Exception('Invalid Seed'); + } + String words = bip39.entropyToMnemonic(seed); + return words.split(' '); + } + + /// Convert a 24-word mnemonic word list to a nano seed + static String mnemonicListToSeed(List words) { + if (words.length != 24) { + throw Exception('Expected a 24-word list, got a ${words.length} list'); + } + return bip39.mnemonicToEntropy(words.join(' ')).toUpperCase(); + } + + /// Validate a mnemonic word list + static bool validateMnemonic(List words) { + return bip39.validateMnemonic(words.join(' ')); + } + + /// Validate a specific menmonic word + static bool isValidWord(String word) { + return WORDLIST.contains(word); + } + + static const WORDLIST = [ + "abandon", + "ability", + "able", + "about", + "above", + "absent", + "absorb", + "abstract", + "absurd", + "abuse", + "access", + "accident", + "account", + "accuse", + "achieve", + "acid", + "acoustic", + "acquire", + "across", + "act", + "action", + "actor", + "actress", + "actual", + "adapt", + "add", + "addict", + "address", + "adjust", + "admit", + "adult", + "advance", + "advice", + "aerobic", + "affair", + "afford", + "afraid", + "again", + "age", + "agent", + "agree", + "ahead", + "aim", + "air", + "airport", + "aisle", + "alarm", + "album", + "alcohol", + "alert", + "alien", + "all", + "alley", + "allow", + "almost", + "alone", + "alpha", + "already", + "also", + "alter", + "always", + "amateur", + "amazing", + "among", + "amount", + "amused", + "analyst", + "anchor", + "ancient", + "anger", + "angle", + "angry", + "animal", + "ankle", + "announce", + "annual", + "another", + "answer", + "antenna", + "antique", + "anxiety", + "any", + "apart", + "apology", + "appear", + "apple", + "approve", + "april", + "arch", + "arctic", + "area", + "arena", + "argue", + "arm", + "armed", + "armor", + "army", + "around", + "arrange", + "arrest", + "arrive", + "arrow", + "art", + "artefact", + "artist", + "artwork", + "ask", + "aspect", + "assault", + "asset", + "assist", + "assume", + "asthma", + "athlete", + "atom", + "attack", + "attend", + "attitude", + "attract", + "auction", + "audit", + "august", + "aunt", + "author", + "auto", + "autumn", + "average", + "avocado", + "avoid", + "awake", + "aware", + "away", + "awesome", + "awful", + "awkward", + "axis", + "baby", + "bachelor", + "bacon", + "badge", + "bag", + "balance", + "balcony", + "ball", + "bamboo", + "banana", + "banner", + "bar", + "barely", + "bargain", + "barrel", + "base", + "basic", + "basket", + "battle", + "beach", + "bean", + "beauty", + "because", + "become", + "beef", + "before", + "begin", + "behave", + "behind", + "believe", + "below", + "belt", + "bench", + "benefit", + "best", + "betray", + "better", + "between", + "beyond", + "bicycle", + "bid", + "bike", + "bind", + "biology", + "bird", + "birth", + "bitter", + "black", + "blade", + "blame", + "blanket", + "blast", + "bleak", + "bless", + "blind", + "blood", + "blossom", + "blouse", + "blue", + "blur", + "blush", + "board", + "boat", + "body", + "boil", + "bomb", + "bone", + "bonus", + "book", + "boost", + "border", + "boring", + "borrow", + "boss", + "bottom", + "bounce", + "box", + "boy", + "bracket", + "brain", + "brand", + "brass", + "brave", + "bread", + "breeze", + "brick", + "bridge", + "brief", + "bright", + "bring", + "brisk", + "broccoli", + "broken", + "bronze", + "broom", + "brother", + "brown", + "brush", + "bubble", + "buddy", + "budget", + "buffalo", + "build", + "bulb", + "bulk", + "bullet", + "bundle", + "bunker", + "burden", + "burger", + "burst", + "bus", + "business", + "busy", + "butter", + "buyer", + "buzz", + "cabbage", + "cabin", + "cable", + "cactus", + "cage", + "cake", + "call", + "calm", + "camera", + "camp", + "can", + "canal", + "cancel", + "candy", + "cannon", + "canoe", + "canvas", + "canyon", + "capable", + "capital", + "captain", + "car", + "carbon", + "card", + "cargo", + "carpet", + "carry", + "cart", + "case", + "cash", + "casino", + "castle", + "casual", + "cat", + "catalog", + "catch", + "category", + "cattle", + "caught", + "cause", + "caution", + "cave", + "ceiling", + "celery", + "cement", + "census", + "century", + "cereal", + "certain", + "chair", + "chalk", + "champion", + "change", + "chaos", + "chapter", + "charge", + "chase", + "chat", + "cheap", + "check", + "cheese", + "chef", + "cherry", + "chest", + "chicken", + "chief", + "child", + "chimney", + "choice", + "choose", + "chronic", + "chuckle", + "chunk", + "churn", + "cigar", + "cinnamon", + "circle", + "citizen", + "city", + "civil", + "claim", + "clap", + "clarify", + "claw", + "clay", + "clean", + "clerk", + "clever", + "click", + "client", + "cliff", + "climb", + "clinic", + "clip", + "clock", + "clog", + "close", + "cloth", + "cloud", + "clown", + "club", + "clump", + "cluster", + "clutch", + "coach", + "coast", + "coconut", + "code", + "coffee", + "coil", + "coin", + "collect", + "color", + "column", + "combine", + "come", + "comfort", + "comic", + "common", + "company", + "concert", + "conduct", + "confirm", + "congress", + "connect", + "consider", + "control", + "convince", + "cook", + "cool", + "copper", + "copy", + "coral", + "core", + "corn", + "correct", + "cost", + "cotton", + "couch", + "country", + "couple", + "course", + "cousin", + "cover", + "coyote", + "crack", + "cradle", + "craft", + "cram", + "crane", + "crash", + "crater", + "crawl", + "crazy", + "cream", + "credit", + "creek", + "crew", + "cricket", + "crime", + "crisp", + "critic", + "crop", + "cross", + "crouch", + "crowd", + "crucial", + "cruel", + "cruise", + "crumble", + "crunch", + "crush", + "cry", + "crystal", + "cube", + "culture", + "cup", + "cupboard", + "curious", + "current", + "curtain", + "curve", + "cushion", + "custom", + "cute", + "cycle", + "dad", + "damage", + "damp", + "dance", + "danger", + "daring", + "dash", + "daughter", + "dawn", + "day", + "deal", + "debate", + "debris", + "decade", + "december", + "decide", + "decline", + "decorate", + "decrease", + "deer", + "defense", + "define", + "defy", + "degree", + "delay", + "deliver", + "demand", + "demise", + "denial", + "dentist", + "deny", + "depart", + "depend", + "deposit", + "depth", + "deputy", + "derive", + "describe", + "desert", + "design", + "desk", + "despair", + "destroy", + "detail", + "detect", + "develop", + "device", + "devote", + "diagram", + "dial", + "diamond", + "diary", + "dice", + "diesel", + "diet", + "differ", + "digital", + "dignity", + "dilemma", + "dinner", + "dinosaur", + "direct", + "dirt", + "disagree", + "discover", + "disease", + "dish", + "dismiss", + "disorder", + "display", + "distance", + "divert", + "divide", + "divorce", + "dizzy", + "doctor", + "document", + "dog", + "doll", + "dolphin", + "domain", + "donate", + "donkey", + "donor", + "door", + "dose", + "double", + "dove", + "draft", + "dragon", + "drama", + "drastic", + "draw", + "dream", + "dress", + "drift", + "drill", + "drink", + "drip", + "drive", + "drop", + "drum", + "dry", + "duck", + "dumb", + "dune", + "during", + "dust", + "dutch", + "duty", + "dwarf", + "dynamic", + "eager", + "eagle", + "early", + "earn", + "earth", + "easily", + "east", + "easy", + "echo", + "ecology", + "economy", + "edge", + "edit", + "educate", + "effort", + "egg", + "eight", + "either", + "elbow", + "elder", + "electric", + "elegant", + "element", + "elephant", + "elevator", + "elite", + "else", + "embark", + "embody", + "embrace", + "emerge", + "emotion", + "employ", + "empower", + "empty", + "enable", + "enact", + "end", + "endless", + "endorse", + "enemy", + "energy", + "enforce", + "engage", + "engine", + "enhance", + "enjoy", + "enlist", + "enough", + "enrich", + "enroll", + "ensure", + "enter", + "entire", + "entry", + "envelope", + "episode", + "equal", + "equip", + "era", + "erase", + "erode", + "erosion", + "error", + "erupt", + "escape", + "essay", + "essence", + "estate", + "eternal", + "ethics", + "evidence", + "evil", + "evoke", + "evolve", + "exact", + "example", + "excess", + "exchange", + "excite", + "exclude", + "excuse", + "execute", + "exercise", + "exhaust", + "exhibit", + "exile", + "exist", + "exit", + "exotic", + "expand", + "expect", + "expire", + "explain", + "expose", + "express", + "extend", + "extra", + "eye", + "eyebrow", + "fabric", + "face", + "faculty", + "fade", + "faint", + "faith", + "fall", + "false", + "fame", + "family", + "famous", + "fan", + "fancy", + "fantasy", + "farm", + "fashion", + "fat", + "fatal", + "father", + "fatigue", + "fault", + "favorite", + "feature", + "february", + "federal", + "fee", + "feed", + "feel", + "female", + "fence", + "festival", + "fetch", + "fever", + "few", + "fiber", + "fiction", + "field", + "figure", + "file", + "film", + "filter", + "final", + "find", + "fine", + "finger", + "finish", + "fire", + "firm", + "first", + "fiscal", + "fish", + "fit", + "fitness", + "fix", + "flag", + "flame", + "flash", + "flat", + "flavor", + "flee", + "flight", + "flip", + "float", + "flock", + "floor", + "flower", + "fluid", + "flush", + "fly", + "foam", + "focus", + "fog", + "foil", + "fold", + "follow", + "food", + "foot", + "force", + "forest", + "forget", + "fork", + "fortune", + "forum", + "forward", + "fossil", + "foster", + "found", + "fox", + "fragile", + "frame", + "frequent", + "fresh", + "friend", + "fringe", + "frog", + "front", + "frost", + "frown", + "frozen", + "fruit", + "fuel", + "fun", + "funny", + "furnace", + "fury", + "future", + "gadget", + "gain", + "galaxy", + "gallery", + "game", + "gap", + "garage", + "garbage", + "garden", + "garlic", + "garment", + "gas", + "gasp", + "gate", + "gather", + "gauge", + "gaze", + "general", + "genius", + "genre", + "gentle", + "genuine", + "gesture", + "ghost", + "giant", + "gift", + "giggle", + "ginger", + "giraffe", + "girl", + "give", + "glad", + "glance", + "glare", + "glass", + "glide", + "glimpse", + "globe", + "gloom", + "glory", + "glove", + "glow", + "glue", + "goat", + "goddess", + "gold", + "good", + "goose", + "gorilla", + "gospel", + "gossip", + "govern", + "gown", + "grab", + "grace", + "grain", + "grant", + "grape", + "grass", + "gravity", + "great", + "green", + "grid", + "grief", + "grit", + "grocery", + "group", + "grow", + "grunt", + "guard", + "guess", + "guide", + "guilt", + "guitar", + "gun", + "gym", + "habit", + "hair", + "half", + "hammer", + "hamster", + "hand", + "happy", + "harbor", + "hard", + "harsh", + "harvest", + "hat", + "have", + "hawk", + "hazard", + "head", + "health", + "heart", + "heavy", + "hedgehog", + "height", + "hello", + "helmet", + "help", + "hen", + "hero", + "hidden", + "high", + "hill", + "hint", + "hip", + "hire", + "history", + "hobby", + "hockey", + "hold", + "hole", + "holiday", + "hollow", + "home", + "honey", + "hood", + "hope", + "horn", + "horror", + "horse", + "hospital", + "host", + "hotel", + "hour", + "hover", + "hub", + "huge", + "human", + "humble", + "humor", + "hundred", + "hungry", + "hunt", + "hurdle", + "hurry", + "hurt", + "husband", + "hybrid", + "ice", + "icon", + "idea", + "identify", + "idle", + "ignore", + "ill", + "illegal", + "illness", + "image", + "imitate", + "immense", + "immune", + "impact", + "impose", + "improve", + "impulse", + "inch", + "include", + "income", + "increase", + "index", + "indicate", + "indoor", + "industry", + "infant", + "inflict", + "inform", + "inhale", + "inherit", + "initial", + "inject", + "injury", + "inmate", + "inner", + "innocent", + "input", + "inquiry", + "insane", + "insect", + "inside", + "inspire", + "install", + "intact", + "interest", + "into", + "invest", + "invite", + "involve", + "iron", + "island", + "isolate", + "issue", + "item", + "ivory", + "jacket", + "jaguar", + "jar", + "jazz", + "jealous", + "jeans", + "jelly", + "jewel", + "job", + "join", + "joke", + "journey", + "joy", + "judge", + "juice", + "jump", + "jungle", + "junior", + "junk", + "just", + "kangaroo", + "keen", + "keep", + "ketchup", + "key", + "kick", + "kid", + "kidney", + "kind", + "kingdom", + "kiss", + "kit", + "kitchen", + "kite", + "kitten", + "kiwi", + "knee", + "knife", + "knock", + "know", + "lab", + "label", + "labor", + "ladder", + "lady", + "lake", + "lamp", + "language", + "laptop", + "large", + "later", + "latin", + "laugh", + "laundry", + "lava", + "law", + "lawn", + "lawsuit", + "layer", + "lazy", + "leader", + "leaf", + "learn", + "leave", + "lecture", + "left", + "leg", + "legal", + "legend", + "leisure", + "lemon", + "lend", + "length", + "lens", + "leopard", + "lesson", + "letter", + "level", + "liar", + "liberty", + "library", + "license", + "life", + "lift", + "light", + "like", + "limb", + "limit", + "link", + "lion", + "liquid", + "list", + "little", + "live", + "lizard", + "load", + "loan", + "lobster", + "local", + "lock", + "logic", + "lonely", + "long", + "loop", + "lottery", + "loud", + "lounge", + "love", + "loyal", + "lucky", + "luggage", + "lumber", + "lunar", + "lunch", + "luxury", + "lyrics", + "machine", + "mad", + "magic", + "magnet", + "maid", + "mail", + "main", + "major", + "make", + "mammal", + "man", + "manage", + "mandate", + "mango", + "mansion", + "manual", + "maple", + "marble", + "march", + "margin", + "marine", + "market", + "marriage", + "mask", + "mass", + "master", + "match", + "material", + "math", + "matrix", + "matter", + "maximum", + "maze", + "meadow", + "mean", + "measure", + "meat", + "mechanic", + "medal", + "media", + "melody", + "melt", + "member", + "memory", + "mention", + "menu", + "mercy", + "merge", + "merit", + "merry", + "mesh", + "message", + "metal", + "method", + "middle", + "midnight", + "milk", + "million", + "mimic", + "mind", + "minimum", + "minor", + "minute", + "miracle", + "mirror", + "misery", + "miss", + "mistake", + "mix", + "mixed", + "mixture", + "mobile", + "model", + "modify", + "mom", + "moment", + "monitor", + "monkey", + "monster", + "month", + "moon", + "moral", + "more", + "morning", + "mosquito", + "mother", + "motion", + "motor", + "mountain", + "mouse", + "move", + "movie", + "much", + "muffin", + "mule", + "multiply", + "muscle", + "museum", + "mushroom", + "music", + "must", + "mutual", + "myself", + "mystery", + "myth", + "naive", + "name", + "napkin", + "narrow", + "nasty", + "nation", + "nature", + "near", + "neck", + "need", + "negative", + "neglect", + "neither", + "nephew", + "nerve", + "nest", + "net", + "network", + "neutral", + "never", + "news", + "next", + "nice", + "night", + "noble", + "noise", + "nominee", + "noodle", + "normal", + "north", + "nose", + "notable", + "note", + "nothing", + "notice", + "novel", + "now", + "nuclear", + "number", + "nurse", + "nut", + "oak", + "obey", + "object", + "oblige", + "obscure", + "observe", + "obtain", + "obvious", + "occur", + "ocean", + "october", + "odor", + "off", + "offer", + "office", + "often", + "oil", + "okay", + "old", + "olive", + "olympic", + "omit", + "once", + "one", + "onion", + "online", + "only", + "open", + "opera", + "opinion", + "oppose", + "option", + "orange", + "orbit", + "orchard", + "order", + "ordinary", + "organ", + "orient", + "original", + "orphan", + "ostrich", + "other", + "outdoor", + "outer", + "output", + "outside", + "oval", + "oven", + "over", + "own", + "owner", + "oxygen", + "oyster", + "ozone", + "pact", + "paddle", + "page", + "pair", + "palace", + "palm", + "panda", + "panel", + "panic", + "panther", + "paper", + "parade", + "parent", + "park", + "parrot", + "party", + "pass", + "patch", + "path", + "patient", + "patrol", + "pattern", + "pause", + "pave", + "payment", + "peace", + "peanut", + "pear", + "peasant", + "pelican", + "pen", + "penalty", + "pencil", + "people", + "pepper", + "perfect", + "permit", + "person", + "pet", + "phone", + "photo", + "phrase", + "physical", + "piano", + "picnic", + "picture", + "piece", + "pig", + "pigeon", + "pill", + "pilot", + "pink", + "pioneer", + "pipe", + "pistol", + "pitch", + "pizza", + "place", + "planet", + "plastic", + "plate", + "play", + "please", + "pledge", + "pluck", + "plug", + "plunge", + "poem", + "poet", + "point", + "polar", + "pole", + "police", + "pond", + "pony", + "pool", + "popular", + "portion", + "position", + "possible", + "post", + "potato", + "pottery", + "poverty", + "powder", + "power", + "practice", + "praise", + "predict", + "prefer", + "prepare", + "present", + "pretty", + "prevent", + "price", + "pride", + "primary", + "print", + "priority", + "prison", + "private", + "prize", + "problem", + "process", + "produce", + "profit", + "program", + "project", + "promote", + "proof", + "property", + "prosper", + "protect", + "proud", + "provide", + "public", + "pudding", + "pull", + "pulp", + "pulse", + "pumpkin", + "punch", + "pupil", + "puppy", + "purchase", + "purity", + "purpose", + "purse", + "push", + "put", + "puzzle", + "pyramid", + "quality", + "quantum", + "quarter", + "question", + "quick", + "quit", + "quiz", + "quote", + "rabbit", + "raccoon", + "race", + "rack", + "radar", + "radio", + "rail", + "rain", + "raise", + "rally", + "ramp", + "ranch", + "random", + "range", + "rapid", + "rare", + "rate", + "rather", + "raven", + "raw", + "razor", + "ready", + "real", + "reason", + "rebel", + "rebuild", + "recall", + "receive", + "recipe", + "record", + "recycle", + "reduce", + "reflect", + "reform", + "refuse", + "region", + "regret", + "regular", + "reject", + "relax", + "release", + "relief", + "rely", + "remain", + "remember", + "remind", + "remove", + "render", + "renew", + "rent", + "reopen", + "repair", + "repeat", + "replace", + "report", + "require", + "rescue", + "resemble", + "resist", + "resource", + "response", + "result", + "retire", + "retreat", + "return", + "reunion", + "reveal", + "review", + "reward", + "rhythm", + "rib", + "ribbon", + "rice", + "rich", + "ride", + "ridge", + "rifle", + "right", + "rigid", + "ring", + "riot", + "ripple", + "risk", + "ritual", + "rival", + "river", + "road", + "roast", + "robot", + "robust", + "rocket", + "romance", + "roof", + "rookie", + "room", + "rose", + "rotate", + "rough", + "round", + "route", + "royal", + "rubber", + "rude", + "rug", + "rule", + "run", + "runway", + "rural", + "sad", + "saddle", + "sadness", + "safe", + "sail", + "salad", + "salmon", + "salon", + "salt", + "salute", + "same", + "sample", + "sand", + "satisfy", + "satoshi", + "sauce", + "sausage", + "save", + "say", + "scale", + "scan", + "scare", + "scatter", + "scene", + "scheme", + "school", + "science", + "scissors", + "scorpion", + "scout", + "scrap", + "screen", + "script", + "scrub", + "sea", + "search", + "season", + "seat", + "second", + "secret", + "section", + "security", + "seed", + "seek", + "segment", + "select", + "sell", + "seminar", + "senior", + "sense", + "sentence", + "series", + "service", + "session", + "settle", + "setup", + "seven", + "shadow", + "shaft", + "shallow", + "share", + "shed", + "shell", + "sheriff", + "shield", + "shift", + "shine", + "ship", + "shiver", + "shock", + "shoe", + "shoot", + "shop", + "short", + "shoulder", + "shove", + "shrimp", + "shrug", + "shuffle", + "shy", + "sibling", + "sick", + "side", + "siege", + "sight", + "sign", + "silent", + "silk", + "silly", + "silver", + "similar", + "simple", + "since", + "sing", + "siren", + "sister", + "situate", + "six", + "size", + "skate", + "sketch", + "ski", + "skill", + "skin", + "skirt", + "skull", + "slab", + "slam", + "sleep", + "slender", + "slice", + "slide", + "slight", + "slim", + "slogan", + "slot", + "slow", + "slush", + "small", + "smart", + "smile", + "smoke", + "smooth", + "snack", + "snake", + "snap", + "sniff", + "snow", + "soap", + "soccer", + "social", + "sock", + "soda", + "soft", + "solar", + "soldier", + "solid", + "solution", + "solve", + "someone", + "song", + "soon", + "sorry", + "sort", + "soul", + "sound", + "soup", + "source", + "south", + "space", + "spare", + "spatial", + "spawn", + "speak", + "special", + "speed", + "spell", + "spend", + "sphere", + "spice", + "spider", + "spike", + "spin", + "spirit", + "split", + "spoil", + "sponsor", + "spoon", + "sport", + "spot", + "spray", + "spread", + "spring", + "spy", + "square", + "squeeze", + "squirrel", + "stable", + "stadium", + "staff", + "stage", + "stairs", + "stamp", + "stand", + "start", + "state", + "stay", + "steak", + "steel", + "stem", + "step", + "stereo", + "stick", + "still", + "sting", + "stock", + "stomach", + "stone", + "stool", + "story", + "stove", + "strategy", + "street", + "strike", + "strong", + "struggle", + "student", + "stuff", + "stumble", + "style", + "subject", + "submit", + "subway", + "success", + "such", + "sudden", + "suffer", + "sugar", + "suggest", + "suit", + "summer", + "sun", + "sunny", + "sunset", + "super", + "supply", + "supreme", + "sure", + "surface", + "surge", + "surprise", + "surround", + "survey", + "suspect", + "sustain", + "swallow", + "swamp", + "swap", + "swarm", + "swear", + "sweet", + "swift", + "swim", + "swing", + "switch", + "sword", + "symbol", + "symptom", + "syrup", + "system", + "table", + "tackle", + "tag", + "tail", + "talent", + "talk", + "tank", + "tape", + "target", + "task", + "taste", + "tattoo", + "taxi", + "teach", + "team", + "tell", + "ten", + "tenant", + "tennis", + "tent", + "term", + "test", + "text", + "thank", + "that", + "theme", + "then", + "theory", + "there", + "they", + "thing", + "this", + "thought", + "three", + "thrive", + "throw", + "thumb", + "thunder", + "ticket", + "tide", + "tiger", + "tilt", + "timber", + "time", + "tiny", + "tip", + "tired", + "tissue", + "title", + "toast", + "tobacco", + "today", + "toddler", + "toe", + "together", + "toilet", + "token", + "tomato", + "tomorrow", + "tone", + "tongue", + "tonight", + "tool", + "tooth", + "top", + "topic", + "topple", + "torch", + "tornado", + "tortoise", + "toss", + "total", + "tourist", + "toward", + "tower", + "town", + "toy", + "track", + "trade", + "traffic", + "tragic", + "train", + "transfer", + "trap", + "trash", + "travel", + "tray", + "treat", + "tree", + "trend", + "trial", + "tribe", + "trick", + "trigger", + "trim", + "trip", + "trophy", + "trouble", + "truck", + "true", + "truly", + "trumpet", + "trust", + "truth", + "try", + "tube", + "tuition", + "tumble", + "tuna", + "tunnel", + "turkey", + "turn", + "turtle", + "twelve", + "twenty", + "twice", + "twin", + "twist", + "two", + "type", + "typical", + "ugly", + "umbrella", + "unable", + "unaware", + "uncle", + "uncover", + "under", + "undo", + "unfair", + "unfold", + "unhappy", + "uniform", + "unique", + "unit", + "universe", + "unknown", + "unlock", + "until", + "unusual", + "unveil", + "update", + "upgrade", + "uphold", + "upon", + "upper", + "upset", + "urban", + "urge", + "usage", + "use", + "used", + "useful", + "useless", + "usual", + "utility", + "vacant", + "vacuum", + "vague", + "valid", + "valley", + "valve", + "van", + "vanish", + "vapor", + "various", + "vast", + "vault", + "vehicle", + "velvet", + "vendor", + "venture", + "venue", + "verb", + "verify", + "version", + "very", + "vessel", + "veteran", + "viable", + "vibrant", + "vicious", + "victory", + "video", + "view", + "village", + "vintage", + "violin", + "virtual", + "virus", + "visa", + "visit", + "visual", + "vital", + "vivid", + "vocal", + "voice", + "void", + "volcano", + "volume", + "vote", + "voyage", + "wage", + "wagon", + "wait", + "walk", + "wall", + "walnut", + "want", + "warfare", + "warm", + "warrior", + "wash", + "wasp", + "waste", + "water", + "wave", + "way", + "wealth", + "weapon", + "wear", + "weasel", + "weather", + "web", + "wedding", + "weekend", + "weird", + "welcome", + "west", + "wet", + "whale", + "what", + "wheat", + "wheel", + "when", + "where", + "whip", + "whisper", + "wide", + "width", + "wife", + "wild", + "will", + "win", + "window", + "wine", + "wing", + "wink", + "winner", + "winter", + "wire", + "wisdom", + "wise", + "wish", + "witness", + "wolf", + "woman", + "wonder", + "wood", + "wool", + "word", + "work", + "world", + "worry", + "worth", + "wrap", + "wreck", + "wrestle", + "wrist", + "write", + "wrong", + "yard", + "year", + "yellow", + "you", + "young", + "youth", + "zebra", + "zero", + "zone", + "zoo" + ]; +} diff --git a/cw_nano/lib/nano_transaction_credentials.dart b/cw_nano/lib/nano_transaction_credentials.dart new file mode 100644 index 000000000..6ede488a1 --- /dev/null +++ b/cw_nano/lib/nano_transaction_credentials.dart @@ -0,0 +1,7 @@ +import 'package:cw_core/output_info.dart'; + +class NanoTransactionCredentials { + NanoTransactionCredentials(this.outputs); + + final List outputs; +} diff --git a/cw_nano/lib/nano_transaction_history.dart b/cw_nano/lib/nano_transaction_history.dart new file mode 100644 index 000000000..dadd353c4 --- /dev/null +++ b/cw_nano/lib/nano_transaction_history.dart @@ -0,0 +1,72 @@ +import 'dart:convert'; +import 'dart:core'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_nano/file.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_nano/nano_transaction_info.dart'; + +part 'nano_transaction_history.g.dart'; +const transactionsHistoryFileName = 'transactions.json'; + +class NanoTransactionHistory = NanoTransactionHistoryBase with _$NanoTransactionHistory; + +abstract class NanoTransactionHistoryBase + extends TransactionHistoryBase with Store { + NanoTransactionHistoryBase({required this.walletInfo, required String password}) + : _password = password { + transactions = ObservableMap(); + } + + final WalletInfo walletInfo; + String _password; + + Future init() async => await _load(); + + @override + Future save() async { + try { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$transactionsHistoryFileName'; + final data = json.encode({'transactions': transactions}); + await writeData(path: path, password: _password, data: data); + } catch (e) { + print('Error while save nano transaction history: ${e.toString()}'); + } + } + + @override + void addOne(NanoTransactionInfo transaction) => transactions[transaction.id] = transaction; + + @override + void addMany(Map transactions) => + this.transactions.addAll(transactions); + + Future> _read() async { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$transactionsHistoryFileName'; + final content = await read(path: path, password: _password); + return json.decode(content) as Map; + } + + Future _load() async { + try { + final content = await _read(); + final txs = content['transactions'] as Map? ?? {}; + + txs.entries.forEach((entry) { + final val = entry.value; + + if (val is Map) { + final tx = NanoTransactionInfo.fromJson(val); + _update(tx); + } + }); + } catch (e) { + print(e); + } + } + + void _update(NanoTransactionInfo transaction) => transactions[transaction.id] = transaction; +} diff --git a/cw_nano/lib/nano_transaction_info.dart b/cw_nano/lib/nano_transaction_info.dart new file mode 100644 index 000000000..8958086dd --- /dev/null +++ b/cw_nano/lib/nano_transaction_info.dart @@ -0,0 +1,70 @@ +import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_nano/nano_util.dart'; + +class NanoTransactionInfo extends TransactionInfo { + NanoTransactionInfo({ + required this.id, + required this.height, + required this.amountRaw, + this.tokenSymbol = "XNO", + required this.direction, + required this.confirmed, + required this.date, + required this.confirmations, + }) : this.amount = amountRaw.toInt(); + + final String id; + final int height; + final int amount; + final BigInt amountRaw; + final TransactionDirection direction; + final DateTime date; + final bool confirmed; + final int confirmations; + final String tokenSymbol; + String? _fiatAmount; + + bool get isPending => !this.confirmed; + + @override + String amountFormatted() { + final String amt = NanoUtil.getRawAsUsableString(amountRaw.toString(), NanoUtil.rawPerNano); + final String acc = NanoUtil.getRawAccuracy(amountRaw.toString(), NanoUtil.rawPerNano); + return "$acc$amt $tokenSymbol"; + } + + @override + String fiatAmount() => _fiatAmount ?? ''; + + @override + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); + + @override + String feeFormatted() => "0 XNO"; + + factory NanoTransactionInfo.fromJson(Map data) { + return NanoTransactionInfo( + id: data['id'] as String, + height: data['height'] as int, + amountRaw: BigInt.parse(data['amountRaw'] as String), + direction: parseTransactionDirectionFromInt(data['direction'] as int), + date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), + confirmed: data['confirmed'] as bool, + confirmations: data['confirmations'] as int, + tokenSymbol: data['tokenSymbol'] as String, + ); + } + + Map toJson() => { + 'id': id, + 'height': height, + 'amountRaw': amountRaw.toString(), + 'direction': direction.index, + 'date': date.millisecondsSinceEpoch, + 'confirmed': confirmed, + 'confirmations': confirmations, + 'tokenSymbol': tokenSymbol, + }; +} diff --git a/cw_nano/lib/nano_transaction_model.dart b/cw_nano/lib/nano_transaction_model.dart new file mode 100644 index 000000000..e9c59da5a --- /dev/null +++ b/cw_nano/lib/nano_transaction_model.dart @@ -0,0 +1,39 @@ +class NanoTransactionModel { + final DateTime? date; + final String hash; + final bool confirmed; + final String account; + final BigInt amount; + final int height; + final String type; + + NanoTransactionModel({ + this.date, + required this.hash, + required this.height, + required this.amount, + required this.confirmed, + required this.type, + required this.account, + }); + + factory NanoTransactionModel.fromJson(dynamic json) { + DateTime? localTimestamp; + try { + localTimestamp = DateTime.fromMillisecondsSinceEpoch( + int.parse(json["local_timestamp"] as String) * 1000); + } catch (e) { + localTimestamp = DateTime.now(); + } + + return NanoTransactionModel( + date: localTimestamp, + hash: json["hash"] as String, + height: int.parse(json["height"] as String), + type: json["type"] as String, + amount: BigInt.parse(json["amount"] as String), + account: json["account"] as String, + confirmed: (json["confirmed"] as String) == "true", + ); + } +} diff --git a/cw_nano/lib/nano_util.dart b/cw_nano/lib/nano_util.dart new file mode 100644 index 000000000..13d6f5649 --- /dev/null +++ b/cw_nano/lib/nano_util.dart @@ -0,0 +1,193 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:convert/convert.dart'; +import "package:ed25519_hd_key/ed25519_hd_key.dart"; +import 'package:libcrypto/libcrypto.dart'; +import 'package:nanodart/nanodart.dart'; +import 'package:decimal/decimal.dart'; + +class NanoUtil { + // standard: + static String seedToPrivate(String seed, int index) { + return NanoKeys.seedToPrivate(seed, index); + } + + static String seedToAddress(String seed, int index) { + return NanoAccounts.createAccount( + NanoAccountType.NANO, privateKeyToPublic(seedToPrivate(seed, index))); + } + + static String seedToMnemonic(String seed) { + return NanoMnemomics.seedToMnemonic(seed).join(" "); + } + + static Future mnemonicToSeed(String mnemonic) async { + return NanoMnemomics.mnemonicListToSeed(mnemonic.split(' ')); + } + + static String privateKeyToPublic(String privateKey) { + // return NanoHelpers.byteToHex(Ed25519Blake2b.getPubkey(NanoHelpers.hexToBytes(privateKey))!); + return NanoKeys.createPublicKey(privateKey); + } + + static String addressToPublicKey(String publicAddress) { + return NanoAccounts.extractPublicKey(publicAddress); + } + + // universal: + static String privateKeyToAddress(String privateKey) { + return NanoAccounts.createAccount(NanoAccountType.NANO, privateKeyToPublic(privateKey)); + } + + static String publicKeyToAddress(String publicKey) { + return NanoAccounts.createAccount(NanoAccountType.NANO, publicKey); + } + + // standard + hd: + static bool isValidSeed(String seed) { + // Ensure seed is 64 or 128 characters long + if (seed == null || (seed.length != 64 && seed.length != 128)) { + return false; + } + // Ensure seed only contains hex characters, 0-9;A-F + return NanoHelpers.isHexString(seed); + } + + // // hd: + static 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; + } + + static 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); + } + + static Future hdSeedToAddress(String seed, int index) async { + return NanoAccounts.createAccount( + NanoAccountType.NANO, privateKeyToPublic(await hdSeedToPrivate(seed, index))); + } + + static 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'); + } + } + + static 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'); + } + } + + static 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 NanoHelpers.isHexString(seed); + } + + // number util: + + static const int maxDecimalDigits = 6; // Max digits after decimal + static BigInt rawPerNano = BigInt.parse("1000000000000000000000000000000"); + static BigInt rawPerNyano = BigInt.parse("1000000000000000000000000"); + static BigInt rawPerBanano = BigInt.parse("100000000000000000000000000000"); + static BigInt rawPerXMR = BigInt.parse("1000000000000"); + static 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 + /// + static 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; + } + + static 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 + /// + static 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"; + } + + static 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 + /// + static String getAmountAsRaw(String amount, BigInt rawPerCur) { + final Decimal asDecimal = Decimal.parse(amount); + final Decimal rawDecimal = Decimal.parse(rawPerCur.toString()); + return (asDecimal * rawDecimal).toString(); + } +} diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart new file mode 100644 index 000000000..da50f4ebb --- /dev/null +++ b/cw_nano/lib/nano_wallet.dart @@ -0,0 +1,437 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/nano_account_info_response.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_nano/file.dart'; +import 'package:cw_core/nano_account.dart'; +import 'package:cw_nano/nano_balance.dart'; +import 'package:cw_nano/nano_client.dart'; +import 'package:cw_nano/nano_transaction_credentials.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:cw_nano/nano_wallet_keys.dart'; +import 'package:cw_nano/pending_nano_transaction.dart'; +import 'package:mobx/mobx.dart'; +import 'dart:async'; +import 'package:cw_nano/nano_wallet_addresses.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:nanodart/nanodart.dart'; +import 'package:bip39/bip39.dart' as bip39; + +part 'nano_wallet.g.dart'; + +class NanoWallet = NanoWalletBase with _$NanoWallet; + +abstract class NanoWalletBase + extends WalletBase with Store { + NanoWalletBase({ + required WalletInfo walletInfo, + required String mnemonic, + required String password, + NanoBalance? initialBalance, + }) : syncStatus = NotConnectedSyncStatus(), + _password = password, + _mnemonic = mnemonic, + _derivationType = walletInfo.derivationType!, + _isTransactionUpdating = false, + _client = NanoClient(), + walletAddresses = NanoWalletAddresses(walletInfo), + balance = ObservableMap.of({ + CryptoCurrency.nano: initialBalance ?? + NanoBalance(currentBalance: BigInt.zero, receivableBalance: BigInt.zero) + }), + super(walletInfo) { + this.walletInfo = walletInfo; + transactionHistory = NanoTransactionHistory(walletInfo: walletInfo, password: password); + if (!CakeHive.isAdapterRegistered(NanoAccount.typeId)) { + CakeHive.registerAdapter(NanoAccountAdapter()); + } + } + + final String _mnemonic; + final String _password; + final DerivationType _derivationType; + + String? _privateKey; + String? _publicAddress; + String? _seedKey; + + String? _representativeAddress; + Timer? _receiveTimer; + + late final NanoClient _client; + bool _isTransactionUpdating; + + @override + NanoWalletAddresses walletAddresses; + + @override + @observable + SyncStatus syncStatus; + + @override + @observable + late ObservableMap balance; + + // initialize the different forms of private / public key we'll need: + Future init() async { + final String type = (_derivationType == DerivationType.nano) ? "standard" : "hd"; + + // our "mnemonic" is actually a seedkey: + if (!_mnemonic.contains(' ')) { + _seedKey = _mnemonic; + } + + if (_seedKey == null) { + if (_derivationType == DerivationType.nano) { + _seedKey = bip39.mnemonicToEntropy(_mnemonic).toUpperCase(); + } else { + _seedKey = await NanoUtil.hdMnemonicListToSeed(_mnemonic.split(' ')); + } + } + _privateKey = await NanoUtil.uniSeedToPrivate(_seedKey!, 0, type); + _publicAddress = await NanoUtil.uniSeedToAddress(_seedKey!, 0, type); + this.walletInfo.address = _publicAddress!; + + await walletAddresses.init(); + await transactionHistory.init(); + await save(); + } + + @override + int calculateEstimatedFee(TransactionPriority priority, int? amount) { + return 0; // always 0 :) + } + + @override + Future changePassword(String password) { + throw UnimplementedError("changePassword"); + } + + @override + void close() { + _client.stop(); + } + + @action + @override + Future connectToNode({required Node node}) async { + try { + syncStatus = ConnectingSyncStatus(); + final isConnected = _client.connect(node); + if (!isConnected) { + throw Exception("Nano Node connection failed"); + } + + try { + await _updateBalance(); + await _updateRep(); + await _receiveAll(); + } catch (e) { + print(e); + } + + syncStatus = ConnectedSyncStatus(); + } catch (e) { + print(e); + syncStatus = FailedSyncStatus(); + } + } + + @override + Future connectToPowNode({required Node node}) async { + _client.connectPow(node); + } + + @override + Future createTransaction(Object credentials) async { + credentials = credentials as NanoTransactionCredentials; + + BigInt runningAmount = BigInt.zero; + await _updateBalance(); + BigInt runningBalance = balance[currency]?.currentBalance ?? BigInt.zero; + + final List> blocks = []; + String? previousHash; + + for (var txOut in credentials.outputs) { + late BigInt amt; + if (txOut.sendAll) { + amt = balance[currency]?.currentBalance ?? BigInt.zero; + } else { + amt = BigInt.tryParse( + NanoUtil.getAmountAsRaw(txOut.cryptoAmount ?? "0", NanoUtil.rawPerNano)) ?? + BigInt.zero; + } + + if (balance[currency]?.currentBalance != null && amt > balance[currency]!.currentBalance) { + throw Exception("Trying to send more than entire balance!"); + } + + runningBalance = runningBalance - amt; + + final block = await _client.constructSendBlock( + amountRaw: amt.toString(), + destinationAddress: txOut.extractedAddress ?? txOut.address, + privateKey: _privateKey!, + balanceAfterTx: runningBalance, + previousHash: previousHash, + ); + previousHash = NanoBlocks.computeStateHash( + NanoAccountType.NANO, + block["account"]!, + block["previous"]!, + block["representative"]!, + BigInt.parse(block["balance"]!), + block["link"]!, + ); + + blocks.add(block); + runningAmount += amt; + } + + try { + if (runningAmount > balance[currency]!.currentBalance || runningBalance < BigInt.zero) { + throw Exception(("Trying to send more than entire balance!")); + } + } catch (e) { + rethrow; + } + + return PendingNanoTransaction( + amount: runningAmount, + id: "", + nanoClient: _client, + blocks: blocks, + ); + } + + Future _receiveAll() async { + await _updateBalance(); + int blocksReceived = await this._client.confirmAllReceivable( + destinationAddress: _publicAddress!, + privateKey: _privateKey!, + ); + + if (blocksReceived > 0) { + await Future.delayed(Duration(seconds: 3)); + _updateBalance(); + updateTransactions(); + } + } + + Future updateTransactions() async { + try { + if (_isTransactionUpdating) { + return; + } + + _isTransactionUpdating = true; + final transactions = await fetchTransactions(); + transactionHistory.addMany(transactions); + await transactionHistory.save(); + _isTransactionUpdating = false; + } catch (_) { + _isTransactionUpdating = false; + } + } + + @override + Future> fetchTransactions() async { + String address = _publicAddress!; + + final transactions = await _client.fetchTransactions(address); + + final Map result = {}; + + for (var transactionModel in transactions) { + result[transactionModel.hash] = NanoTransactionInfo( + id: transactionModel.hash, + amountRaw: transactionModel.amount, + height: transactionModel.height, + direction: transactionModel.type == "send" + ? TransactionDirection.outgoing + : TransactionDirection.incoming, + confirmed: transactionModel.confirmed, + date: transactionModel.date ?? DateTime.now(), + confirmations: transactionModel.confirmed ? 1 : 0, + ); + } + + return result; + } + + @override + NanoWalletKeys get keys { + return NanoWalletKeys(seedKey: _seedKey!); + } + + @override + String? get privateKey => _seedKey!; + + @override + Future rescan({required int height}) async { + updateTransactions(); + _updateBalance(); + return; + } + + @override + Future save() async { + await walletAddresses.updateAddressesInBox(); + final path = await makePath(); + await write(path: path, password: _password, data: toJSON()); + await transactionHistory.save(); + } + + @override + String get seed => _mnemonic; + + String get representative => _representativeAddress ?? ""; + + @action + @override + Future startSync() async { + try { + syncStatus = AttemptingSyncStatus(); + await _updateBalance(); + await updateTransactions(); + + _receiveTimer?.cancel(); + _receiveTimer = Timer.periodic(const Duration(seconds: 15), (timer) async { + // get our balance: + await _updateBalance(); + // if we have anything to receive, process it: + if (balance[currency]!.receivableBalance > BigInt.zero) { + await _receiveAll(); + } + }); + + syncStatus = SyncedSyncStatus(); + } catch (e) { + print(e); + syncStatus = FailedSyncStatus(); + rethrow; + } + } + + Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); + + String toJSON() => json.encode({ + 'seedKey': _seedKey, + 'mnemonic': _mnemonic, + 'currentBalance': balance[currency]?.currentBalance.toString() ?? "0", + 'receivableBalance': balance[currency]?.receivableBalance.toString() ?? "0", + 'derivationType': _derivationType.toString() + }); + + static Future open({ + required String name, + required String password, + required WalletInfo walletInfo, + }) async { + final path = await pathForWallet(name: name, type: walletInfo.type); + final jsonSource = await read(path: path, password: password); + + final data = json.decode(jsonSource) as Map; + final mnemonic = data['mnemonic'] as String; + final balance = NanoBalance.fromString( + formattedCurrentBalance: data['currentBalance'] as String? ?? "0", + formattedReceivableBalance: data['receivableBalance'] as String? ?? "0"); + + DerivationType derivationType = DerivationType.bip39; + if (data['derivationType'] == "DerivationType.nano") { + derivationType = DerivationType.nano; + } + + walletInfo.derivationType = derivationType; + + return NanoWallet( + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + initialBalance: balance, + ); + // init() should always be run after this! + } + + Future _updateBalance() async { + try { + balance[currency] = await _client.getBalance(_publicAddress!); + } catch (e) { + print("Failed to get balance $e"); + } + await save(); + } + + Future _updateRep() async { + try { + AccountInfoResponse accountInfo = (await _client.getAccountInfo(_publicAddress!))!; + _representativeAddress = accountInfo.representative; + } catch (e) { + // account not found: + _representativeAddress = NanoClient.DEFAULT_REPRESENTATIVE; + throw Exception("Failed to get representative address $e"); + } + } + + Future regenerateAddress() async { + final String type = (_derivationType == DerivationType.nano) ? "standard" : "hd"; + _privateKey = + await NanoUtil.uniSeedToPrivate(_seedKey!, this.walletAddresses.account!.id, type); + _publicAddress = + await NanoUtil.uniSeedToAddress(_seedKey!, this.walletAddresses.account!.id, type); + + this.walletInfo.address = _publicAddress!; + this.walletAddresses.address = _publicAddress!; + } + + Future changeRep(String address) async { + try { + final String hash = await _client.changeRep( + privateKey: _privateKey!, + repAddress: address, + ourAddress: _publicAddress!, + ); + if (hash.isNotEmpty) { + _representativeAddress = address; + } + } catch (e) { + throw Exception("Failed to change representative address $e"); + } + } + + Future? updateBalance() async => await _updateBalance(); + + @override + Future renameWalletFiles(String newWalletName) async { + final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type); + final currentWalletFile = File(currentWalletPath); + + final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type); + final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName'); + + // Copies current wallet files into new wallet name's dir and files + if (currentWalletFile.existsSync()) { + final newWalletPath = await pathForWallet(name: newWalletName, type: type); + await currentWalletFile.copy(newWalletPath); + } + if (currentTransactionsFile.existsSync()) { + final newDirPath = await pathForWalletDir(name: newWalletName, type: type); + await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName'); + } + + // Delete old name's dir and files + await Directory(currentDirPath).delete(recursive: true); + } +} diff --git a/cw_nano/lib/nano_wallet_addresses.dart b/cw_nano/lib/nano_wallet_addresses.dart new file mode 100644 index 000000000..cc532d2c7 --- /dev/null +++ b/cw_nano/lib/nano_wallet_addresses.dart @@ -0,0 +1,50 @@ +import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/nano_account.dart'; +import 'package:cw_nano/nano_account_list.dart'; +import 'package:mobx/mobx.dart'; + +part 'nano_wallet_addresses.g.dart'; + +class NanoWalletAddresses = NanoWalletAddressesBase with _$NanoWalletAddresses; + +abstract class NanoWalletAddressesBase extends WalletAddresses with Store { + NanoWalletAddressesBase(WalletInfo walletInfo) + : accountList = NanoAccountList(walletInfo.address), + address = '', + super(walletInfo); + @override + @observable + String address; + + @observable + NanoAccount? account; + + NanoAccountList accountList; + + @override + Future init() async { + var box = await CakeHive.openBox(walletInfo.address); + try { + box.getAt(0); + } catch (e) { + box.add(NanoAccount(id: 0, label: "Primary Account", balance: "0.00")); + } + + await accountList.update(walletInfo.address); + account = accountList.accounts.first; + address = walletInfo.address; + } + + @override + Future updateAddressesInBox() async { + try { + addressesMap.clear(); + addressesMap[address] = ''; + await saveAddressesInBox(); + } catch (e) { + print(e.toString()); + } + } +} diff --git a/cw_nano/lib/nano_wallet_creation_credentials.dart b/cw_nano/lib/nano_wallet_creation_credentials.dart new file mode 100644 index 000000000..84531e24a --- /dev/null +++ b/cw_nano/lib/nano_wallet_creation_credentials.dart @@ -0,0 +1,41 @@ +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; + +class NanoNewWalletCredentials extends WalletCredentials { + NanoNewWalletCredentials({required String name, String? password}) + : super(name: name, password: password); +} + +class NanoRestoreWalletFromSeedCredentials extends WalletCredentials { + NanoRestoreWalletFromSeedCredentials({ + required String name, + required this.mnemonic, + int height = 0, + String? password, + DerivationType? derivationType, + }) : super( + name: name, + password: password, + height: height, + derivationType: derivationType, + ); + + final String mnemonic; +} + +class NanoWalletLoadingException implements Exception { + @override + String toString() => 'Failure to load the wallet.'; +} + +class NanoRestoreWalletFromKeysCredentials extends WalletCredentials { + NanoRestoreWalletFromKeysCredentials({ + required String name, + required String password, + required this.seedKey, + this.derivationType, + }) : super(name: name, password: password); + + final String seedKey; + final DerivationType? derivationType; +} \ No newline at end of file diff --git a/cw_nano/lib/nano_wallet_keys.dart b/cw_nano/lib/nano_wallet_keys.dart new file mode 100644 index 000000000..80a845e64 --- /dev/null +++ b/cw_nano/lib/nano_wallet_keys.dart @@ -0,0 +1,5 @@ +class NanoWalletKeys { + const NanoWalletKeys({required this.seedKey}); + + final String seedKey; +} diff --git a/cw_nano/lib/nano_wallet_service.dart b/cw_nano/lib/nano_wallet_service.dart new file mode 100644 index 000000000..2f183d1cc --- /dev/null +++ b/cw_nano/lib/nano_wallet_service.dart @@ -0,0 +1,163 @@ +import 'dart:io'; + +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_base.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' as nm; +import 'package:cw_nano/nano_util.dart'; +import 'package:cw_nano/nano_wallet.dart'; +import 'package:cw_nano/nano_wallet_creation_credentials.dart'; +import 'package:hive/hive.dart'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:nanodart/nanodart.dart'; + +class NanoWalletService extends WalletService { + NanoWalletService(this.walletInfoSource); + + final Box walletInfoSource; + + static bool walletFilesExist(String path) => + !File(path).existsSync() && !File('$path.keys').existsSync(); + + @override + WalletType getType() => WalletType.nano; + + @override + Future create(NanoNewWalletCredentials credentials) async { + // nano standard: + DerivationType derivationType = DerivationType.nano; + String seedKey = NanoSeeds.generateSeed(); + String mnemonic = NanoUtil.seedToMnemonic(seedKey); + + credentials.walletInfo!.derivationType = derivationType; + + final wallet = NanoWallet( + walletInfo: credentials.walletInfo!, + mnemonic: mnemonic, + password: credentials.password!, + ); + wallet.init(); + return wallet; + } + + @override + Future remove(String wallet) async { + final path = await pathForWalletDir(name: wallet, type: getType()); + final file = Directory(path); + final isExist = file.existsSync(); + + if (isExist) { + await file.delete(recursive: true); + } + + final walletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(wallet, getType())); + await walletInfoSource.delete(walletInfo.key); + } + + @override + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); + + String randomWords = + (List.from(nm.NanoMnemomics.WORDLIST)..shuffle()).take(24).join(' '); + final currentWallet = + NanoWallet(walletInfo: currentWalletInfo, password: password, mnemonic: randomWords); + + await currentWallet.renameWalletFiles(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } + + @override + Future restoreFromKeys(NanoRestoreWalletFromKeysCredentials credentials) async { + if (credentials.seedKey.contains(' ')) { + throw Exception("Invalid key!"); + } else { + if (credentials.seedKey.length != 64 && credentials.seedKey.length != 128) { + throw Exception("Invalid key length!"); + } + } + + DerivationType derivationType = credentials.derivationType ?? DerivationType.nano; + credentials.walletInfo!.derivationType = derivationType; + + String? mnemonic; + + // we can't derive the mnemonic from the key in all cases, only if it's a "nano" seed + if (credentials.seedKey.length == 64) { + try { + mnemonic = NanoUtil.seedToMnemonic(credentials.seedKey); + } catch (e) { + throw Exception("Wasn't a valid nano style seed!"); + } + } + + final wallet = await NanoWallet( + password: credentials.password!, + mnemonic: mnemonic ?? credentials.seedKey, + walletInfo: credentials.walletInfo!, + ); + await wallet.init(); + await wallet.save(); + return wallet; + } + + @override + Future restoreFromSeed(NanoRestoreWalletFromSeedCredentials credentials) async { + if (credentials.mnemonic.contains(' ')) { + if (!bip39.validateMnemonic(credentials.mnemonic)) { + throw nm.NanoMnemonicIsIncorrectException(); + } + + if (!NanoMnemomics.validateMnemonic(credentials.mnemonic.split(' '))) { + throw nm.NanoMnemonicIsIncorrectException(); + } + } else { + if (credentials.mnemonic.length != 64 && credentials.mnemonic.length != 128) { + throw Exception("Invalid seed length"); + } + } + + DerivationType derivationType = credentials.derivationType ?? DerivationType.nano; + + credentials.walletInfo!.derivationType = derivationType; + + final wallet = await NanoWallet( + password: credentials.password!, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + ); + + await wallet.init(); + await wallet.save(); + return wallet; + } + + @override + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); + + @override + 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/cw_nano/lib/pending_nano_transaction.dart b/cw_nano/lib/pending_nano_transaction.dart new file mode 100644 index 000000000..727f02534 --- /dev/null +++ b/cw_nano/lib/pending_nano_transaction.dart @@ -0,0 +1,40 @@ +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_nano/nano_client.dart'; +import 'package:cw_nano/nano_util.dart'; + +class PendingNanoTransaction with PendingTransaction { + PendingNanoTransaction({ + required this.nanoClient, + required this.amount, + required this.id, + required this.blocks, + }); + + final NanoClient nanoClient; + final BigInt amount; + final String id; + final List> blocks; + String hex = "unused"; + + @override + String get amountFormatted { + final String amt = NanoUtil.getRawAsUsableString(amount.toString(), NanoUtil.rawPerNano); + return amt; + } + + String get accurateAmountFormatted { + final String amt = NanoUtil.getRawAsUsableString(amount.toString(), NanoUtil.rawPerNano); + final String acc = NanoUtil.getRawAccuracy(amount.toString(), NanoUtil.rawPerNano); + return "$acc$amt"; + } + + @override + String get feeFormatted => "0"; + + @override + Future commit() async { + for (var block in blocks) { + await nanoClient.processBlock(block, "send"); + } + } +} diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock new file mode 100644 index 000000000..ea932baee --- /dev/null +++ b/cw_nano/pubspec.lock @@ -0,0 +1,756 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" + url: "https://pub.dev" + source: hosted + version: "47.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" + url: "https://pub.dev" + source: hosted + version: "4.7.0" + args: + dependency: transitive + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: b74e3842a52c61f8819a1ec8444b4de5419b41a7465e69d4aa681445377398b0 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + bip32: + dependency: "direct main" + description: + name: bip32 + sha256: "54787cd7a111e9d37394aabbf53d1fc5e2e0e0af2cd01c459147a97c0e3f8a97" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + bip39: + dependency: "direct main" + description: + name: bip39 + sha256: de1ee27ebe7d96b84bb3a04a4132a0a3007dcdd5ad27dd14aa87a29d97c45edc + url: "https://pub.dev" + source: hosted + version: "1.0.6" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + bs58check: + dependency: transitive + description: + name: bs58check + sha256: c4a164d42b25c2f6bc88a8beccb9fc7d01440f3c60ba23663a20a70faf484ea9 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + build: + dependency: transitive + description: + name: build + sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" + url: "https://pub.dev" + source: hosted + version: "2.0.10" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + url: "https://pub.dev" + source: hosted + version: "2.3.3" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "0671ad4162ed510b70d0eb4ad6354c249f8429cab4ae7a4cec86bbc2886eb76e" + url: "https://pub.dev" + source: hosted + version: "7.2.7+1" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" + url: "https://pub.dev" + source: hosted + version: "8.6.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + url: "https://pub.dev" + source: hosted + version: "4.5.0" + collection: + dependency: transitive + description: + name: collection + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + url: "https://pub.dev" + source: hosted + version: "1.17.1" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + cw_core: + dependency: "direct main" + description: + path: "../cw_core" + relative: true + source: path + version: "0.0.1" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + url: "https://pub.dev" + source: hosted + version: "2.2.4" + decimal: + dependency: "direct main" + description: + name: decimal + sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + ed25519_hd_key: + dependency: "direct main" + description: + name: ed25519_hd_key + sha256: "326608234e986ea826a5db4cf4cd6826058d860875a3fff7926c0725fe1a604d" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + encrypt: + dependency: transitive + description: + name: encrypt + sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb" + url: "https://pub.dev" + source: hosted + version: "5.0.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + fixnum_nanodart: + dependency: transitive + description: + name: fixnum_nanodart + sha256: "4b0132d11ecddc0d2ca64b6d7dee6726db432ed02cac1349d7532a08be5c54fc" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_mobx: + dependency: transitive + description: + name: flutter_mobx + sha256: "0da4add0016387a7bf309a0d0c41d36c6b3ae25ed7a176409267f166509e723e" + url: "https://pub.dev" + source: hosted + version: "2.0.6+5" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + hex: + dependency: "direct main" + description: + name: hex + sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + hive: + dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + sha256: "81fd20125cb2ce8fd23623d7744ffbaf653aae93706c9bd3bf7019ea0ace3938" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + http: + dependency: "direct main" + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + intl: + dependency: transitive + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + libcrypto: + dependency: "direct main" + description: + name: libcrypto + sha256: "18a97db8d88147b0b60d2755f29b5e4944181c4c1a9f52bd1ecbea1b0a5aab03" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + url: "https://pub.dev" + source: hosted + version: "0.12.15" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + meta: + dependency: transitive + description: + name: meta + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + mobx: + dependency: "direct main" + description: + name: mobx + sha256: "0afcf88b3ee9d6819890bf16c11a727fc8c62cf736fda8e5d3b9b4eace4e62ea" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + mobx_codegen: + dependency: "direct dev" + description: + name: mobx_codegen + sha256: d4beb9cea4b7b014321235f8fdc7c2193ee0fe1d1198e9da7403f8bc85c4407c + url: "https://pub.dev" + source: hosted + version: "2.3.0" + nanodart: + dependency: "direct main" + description: + name: nanodart + sha256: "4b2f42d60307b54e8cf384d6193a567d07f8efd773858c0d5948246153c13282" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" + url: "https://pub.dev" + source: hosted + version: "2.0.15" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" + url: "https://pub.dev" + source: hosted + version: "2.0.27" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "916731ccbdce44d545414dd9961f26ba5fbaa74bcbb55237d8e65a623a8c7297" + url: "https://pub.dev" + source: hosted + version: "2.2.4" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 + url: "https://pub.dev" + source: hosted + version: "2.1.11" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" + url: "https://pub.dev" + source: hosted + version: "2.0.6" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" + url: "https://pub.dev" + source: hosted + version: "2.1.7" + pinenacl: + dependency: transitive + description: + name: pinenacl + sha256: e5fb0bce1717b7f136f35ee98b5c02b3e6383211f8a77ca882fa7812232a07b9 + url: "https://pub.dev" + source: hosted + version: "0.3.4" + platform: + dependency: transitive + description: + name: platform + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.dev" + source: hosted + version: "3.7.3" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + rational: + dependency: transitive + description: + name: rational + sha256: ba58e9e18df9abde280e8b10051e4bce85091e41e8e7e411b6cde2e738d357cf + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + url: "https://pub.dev" + source: hosted + version: "1.2.6" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + source_span: + dependency: transitive + description: + name: source_span + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" + source: hosted + version: "1.9.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + url: "https://pub.dev" + source: hosted + version: "0.5.1" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + watcher: + dependency: transitive + description: + name: watcher + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + win32: + dependency: transitive + description: + name: win32 + sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" + url: "https://pub.dev" + source: hosted + version: "4.1.4" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: e0b1147eec179d3911f1f19b59206448f78195ca1d20514134e10641b7d7fbff + url: "https://pub.dev" + source: hosted + version: "1.0.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.0.0 <4.0.0" + flutter: ">=3.3.0" diff --git a/cw_nano/pubspec.yaml b/cw_nano/pubspec.yaml new file mode 100644 index 000000000..054dd5df4 --- /dev/null +++ b/cw_nano/pubspec.yaml @@ -0,0 +1,69 @@ +name: cw_nano +description: A new Flutter package project. +version: 0.0.1 +publish_to: none +author: Cake Wallet +homepage: https://cakewallet.com + +environment: + sdk: '>=2.18.2 <3.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + mobx: ^2.0.7+4 + bip39: ^1.0.6 + bip32: ^2.0.0 + nanodart: ^2.0.0 + decimal: ^2.3.3 + libcrypto: ^0.2.2 + ed25519_hd_key: ^2.2.0 + hex: ^0.2.0 + http: ^1.1.0 + cw_core: + path: ../cw_core + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.1.11 + mobx_codegen: ^2.0.7 + hive_generator: ^1.1.3 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/cw_nano/test/cw_nano_test.dart b/cw_nano/test/cw_nano_test.dart new file mode 100644 index 000000000..fbabc7b54 --- /dev/null +++ b/cw_nano/test/cw_nano_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cw_nano/cw_nano.dart'; + +void main() { + test('adds one to input values', () { + final calculator = Calculator(); + expect(calculator.addOne(2), 3); + expect(calculator.addOne(-7), -6); + expect(calculator.addOne(0), 1); + }); +} diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 3164aacaa..353458937 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -161,4 +161,4 @@ class CWBitcoin extends Bitcoin { @override TransactionPriority getLitecoinTransactionPrioritySlow() => LitecoinTransactionPriority.slow; -} +} \ No newline at end of file diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart index 2f0e47f02..91309a2ca 100644 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -27,6 +27,8 @@ class OnRamperBuyProvider { return "LTC_LITECOIN"; case CryptoCurrency.xmr: return "XMR_MONERO"; + case CryptoCurrency.nano: + return "XNO_NANO"; default: return _wallet.currency.title; } diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index abafea0d5..5f5b004ba 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -28,6 +28,8 @@ class AddressValidator extends TextValidator { return '^3[0-9a-zA-Z]{32}\$|^3[0-9a-zA-Z]{33}\$|^bc1[0-9a-zA-Z]{59}\$'; case CryptoCurrency.nano: return '[0-9a-zA-Z_]'; + case CryptoCurrency.banano: + return '[0-9a-zA-Z_]'; case CryptoCurrency.usdc: case CryptoCurrency.usdcpoly: case CryptoCurrency.ape: @@ -177,6 +179,8 @@ class AddressValidator extends TextValidator { return [34, 43, 63]; case CryptoCurrency.nano: return [64, 65]; + case CryptoCurrency.banano: + return [64, 65]; case CryptoCurrency.sc: return [76]; case CryptoCurrency.sol: diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index eba1bbda4..1c6e7cd20 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/core/validator.dart'; import 'package:cake_wallet/entities/mnemonic_item.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/utils/language_list.dart'; class SeedValidator extends Validator { @@ -28,6 +29,9 @@ class SeedValidator extends Validator { return haven!.getMoneroWordList(language); case WalletType.ethereum: return ethereum!.getEthereumWordList(language); + case WalletType.nano: + case WalletType.banano: + return nano!.getNanoWordList(language); default: return []; } diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index 3b28f36c3..dda591115 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -39,15 +39,11 @@ class WalletCreationService { bool exists(String name) { final walletName = name.toLowerCase(); - return walletInfoSource - .values - .any((walletInfo) => walletInfo.name.toLowerCase() == walletName); + return walletInfoSource.values.any((walletInfo) => walletInfo.name.toLowerCase() == walletName); } bool typeExists(WalletType type) { - return walletInfoSource - .values - .any((walletInfo) => walletInfo.type == type); + return walletInfoSource.values.any((walletInfo) => walletInfo.type == type); } void checkIfExists(String name) { @@ -60,15 +56,12 @@ class WalletCreationService { checkIfExists(credentials.name); final password = generateWalletPassword(); credentials.password = password; - await keyService.saveWalletPassword( - password: password, walletName: credentials.name); - final wallet = await _service!.create(credentials); + await keyService.saveWalletPassword(password: password, walletName: credentials.name); + final wallet = await _service!.create(credentials); if (wallet.type == WalletType.monero) { - await sharedPreferences - .setBool( - PreferencesKey.moneroWalletUpdateV1Key(wallet.name), - _isNewMoneroWalletPasswordUpdated); + await sharedPreferences.setBool( + PreferencesKey.moneroWalletUpdateV1Key(wallet.name), _isNewMoneroWalletPasswordUpdated); } return wallet; @@ -78,15 +71,12 @@ class WalletCreationService { checkIfExists(credentials.name); final password = generateWalletPassword(); credentials.password = password; - await keyService.saveWalletPassword( - password: password, walletName: credentials.name); + await keyService.saveWalletPassword(password: password, walletName: credentials.name); final wallet = await _service!.restoreFromKeys(credentials); if (wallet.type == WalletType.monero) { - await sharedPreferences - .setBool( - PreferencesKey.moneroWalletUpdateV1Key(wallet.name), - _isNewMoneroWalletPasswordUpdated); + await sharedPreferences.setBool( + PreferencesKey.moneroWalletUpdateV1Key(wallet.name), _isNewMoneroWalletPasswordUpdated); } return wallet; @@ -96,15 +86,12 @@ class WalletCreationService { checkIfExists(credentials.name); final password = generateWalletPassword(); credentials.password = password; - await keyService.saveWalletPassword( - password: password, walletName: credentials.name); + await keyService.saveWalletPassword(password: password, walletName: credentials.name); final wallet = await _service!.restoreFromSeed(credentials); if (wallet.type == WalletType.monero) { - await sharedPreferences - .setBool( - PreferencesKey.moneroWalletUpdateV1Key(wallet.name), - _isNewMoneroWalletPasswordUpdated); + await sharedPreferences.setBool( + PreferencesKey.moneroWalletUpdateV1Key(wallet.name), _isNewMoneroWalletPasswordUpdated); } return wallet; diff --git a/lib/di.dart b/lib/di.dart index 678a8a325..96c7505ef 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -13,6 +13,7 @@ import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/receive_page_option.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/ionia/ionia_anypay.dart'; import 'package:cake_wallet/ionia/ionia_gift_card.dart'; import 'package:cake_wallet/ionia/ionia_tip.dart'; @@ -26,8 +27,13 @@ import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet import 'package:cake_wallet/src/screens/dashboard/edit_token_page.dart'; import 'package:cake_wallet/src/screens/dashboard/home_settings_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart'; +import 'package:cake_wallet/src/screens/nano/nano_change_rep_page.dart'; +import 'package:cake_wallet/src/screens/nano_accounts/nano_account_edit_or_create_page.dart'; +import 'package:cake_wallet/src/screens/nano_accounts/nano_account_list_page.dart'; +import 'package:cake_wallet/src/screens/nodes/pow_node_create_or_edit_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; +import 'package:cake_wallet/src/screens/restore/wallet_restore_choose_derivation.dart'; import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; @@ -73,6 +79,9 @@ import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart'; import 'package:cake_wallet/view_model/ionia/ionia_account_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart'; +import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart'; +import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart'; +import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart'; import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart'; import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart'; @@ -83,7 +92,9 @@ import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; +import 'package:cake_wallet/view_model/wallet_restore_choose_derivation_view_model.dart'; import 'package:cw_core/erc20_token.dart'; +import 'package:cw_core/nano_account.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cake_wallet/core/backup_service.dart'; import 'package:cw_core/wallet_service.dart'; @@ -206,6 +217,7 @@ import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; +import 'package:cake_wallet/nano/nano.dart' as nanoNano; import 'core/totp_request_details.dart'; @@ -214,6 +226,7 @@ final getIt = GetIt.instance; var _isSetupFinished = false; late Box _walletInfoSource; late Box _nodeSource; +late Box _powNodeSource; late Box _contactSource; late Box _tradesSource; late Box