From c381326dd52f099bf2587fba8089b9ccbc101651 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 15 Nov 2023 15:59:01 -0600 Subject: [PATCH] refactored ba/nano wallets --- ...w_wallet_recovery_phrase_warning_view.dart | 35 +- lib/pages/monkey/monkey_view.dart | 2 +- .../change_representative_view.dart | 27 +- .../wallet_view/desktop_wallet_view.dart | 6 +- lib/services/coins/banano/banano_wallet.dart | 1026 ----------------- lib/services/coins/coin_service.dart | 16 +- lib/services/coins/nano/nano_wallet.dart | 1022 ---------------- lib/utilities/constants.dart | 3 + lib/wallets/crypto_currency/coins/banano.dart | 25 + lib/wallets/crypto_currency/coins/nano.dart | 25 + .../intermediate/nano_currency.dart | 21 + lib/wallets/isar/models/wallet_info.dart | 21 + lib/wallets/wallet/impl/banano_wallet.dart | 27 + lib/wallets/wallet/impl/nano_wallet.dart | 9 + lib/wallets/wallet/mixins/nano_based.dart | 675 +++++++++++ lib/wallets/wallet/wallet.dart | 8 + 16 files changed, 851 insertions(+), 2097 deletions(-) delete mode 100644 lib/services/coins/banano/banano_wallet.dart delete mode 100644 lib/services/coins/nano/nano_wallet.dart create mode 100644 lib/wallets/crypto_currency/coins/banano.dart create mode 100644 lib/wallets/crypto_currency/coins/nano.dart create mode 100644 lib/wallets/crypto_currency/intermediate/nano_currency.dart create mode 100644 lib/wallets/wallet/impl/banano_wallet.dart create mode 100644 lib/wallets/wallet/impl/nano_wallet.dart create mode 100644 lib/wallets/wallet/mixins/nano_based.dart diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart index 6734e98bb..2851eb701 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart @@ -486,34 +486,41 @@ class _NewWalletRecoveryPhraseWarningViewState String? mnemonic; String? privateKey; - // TODO: [prio=high] finish fleshing this out - if (coin.hasMnemonicPassphraseSupport) { + wordCount = + Constants.defaultSeedPhraseLengthFor( + coin: info.coin, + ); + if (wordCount > 0) { if (ref .read(pNewWalletOptions.state) .state != null) { - mnemonicPassphrase = ref - .read(pNewWalletOptions.state) - .state! - .mnemonicPassphrase; + if (coin.hasMnemonicPassphraseSupport) { + mnemonicPassphrase = ref + .read(pNewWalletOptions.state) + .state! + .mnemonicPassphrase; + } else {} + wordCount = ref .read(pNewWalletOptions.state) .state! .mnemonicWordsCount; } else { - wordCount = 12; mnemonicPassphrase = ""; } - final int strength; - if (wordCount == 12) { - strength = 128; - } else if (wordCount == 24) { - strength = 256; - } else { + + if (wordCount < 12 || + 24 < wordCount || + wordCount % 3 != 0) { throw Exception("Invalid word count"); } + + final strength = (wordCount ~/ 3) * 32; + mnemonic = bip39.generateMnemonic( - strength: strength); + strength: strength, + ); } final wallet = await Wallet.create( diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index 093fab50c..a747011ee 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -8,7 +8,6 @@ import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; -import 'package:stackwallet/services/coins/banano/banano_wallet.dart'; import 'package:stackwallet/services/monkey_service.dart'; import 'package:stackwallet/themes/coin_icon_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; @@ -17,6 +16,7 @@ import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; +import 'package:stackwallet/wallets/wallet/impl/banano_wallet.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart index 7e471d6e7..e94e061ff 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart @@ -16,16 +16,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; -import 'package:stackwallet/services/coins/banano/banano_wallet.dart'; -import 'package:stackwallet/services/coins/nano/nano_wallet.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/wallet/mixins/nano_based.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; @@ -51,10 +49,12 @@ class ChangeRepresentativeView extends ConsumerStatefulWidget { static const String routeName = "/changeRepresentative"; @override - ConsumerState createState() => _XPubViewState(); + ConsumerState createState() => + _ChangeRepresentativeViewState(); } -class _XPubViewState extends ConsumerState { +class _ChangeRepresentativeViewState + extends ConsumerState { final _textController = TextEditingController(); final _textFocusNode = FocusNode(); final bool isDesktop = Util.isDesktop; @@ -65,23 +65,18 @@ class _XPubViewState extends ConsumerState { Future loadRepresentative() async { final wallet = ref.read(pWallets).getWallet(widget.walletId); - final coin = wallet.info.coin; - if (coin == Coin.nano) { - return (wallet as NanoWallet).getCurrentRepresentative(); - } else if (coin == Coin.banano) { - return (wallet as BananoWallet).getCurrentRepresentative(); + if (wallet is NanoBased) { + return wallet.getCurrentRepresentative(); + } else { + throw Exception("Unsupported wallet attempted to show representative!"); } - throw Exception("Unsupported wallet attempted to show representative!"); } Future _save() async { - final wallet = ref.read(pWallets).getWallet(widget.walletId); - final coin = wallet.info.coin; + final wallet = ref.read(pWallets).getWallet(widget.walletId) as NanoBased; - final changeFuture = coin == Coin.nano - ? (wallet as NanoWallet).changeRepresentative - : (wallet as BananoWallet).changeRepresentative; + final changeFuture = wallet.changeRepresentative; final result = await showLoading( whileFuture: changeFuture(_textController.text), diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart index dae79310e..b3ddb5eb1 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -33,7 +33,6 @@ import 'package:stackwallet/providers/global/active_wallet_provider.dart'; import 'package:stackwallet/providers/global/auto_swb_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; -import 'package:stackwallet/services/coins/banano/banano_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; @@ -43,6 +42,7 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/wallets/wallet/impl/banano_wallet.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; @@ -193,9 +193,7 @@ class _DesktopWalletViewState extends ConsumerState { final wallet = ref.watch(pWallets).getWallet(widget.walletId); final walletInfo = wallet.info; - final monke = wallet.info.coin == Coin.banano - ? (wallet as BananoWallet).getMonkeyImageBytes() - : null; + final monke = wallet is BananoWallet ? wallet.getMonkeyImageBytes() : null; return ConditionalParent( condition: _rescanningOnOpen, diff --git a/lib/services/coins/banano/banano_wallet.dart b/lib/services/coins/banano/banano_wallet.dart deleted file mode 100644 index 735336824..000000000 --- a/lib/services/coins/banano/banano_wallet.dart +++ /dev/null @@ -1,1026 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:isar/isar.dart'; -import 'package:nanodart/nanodart.dart'; -import 'package:stackwallet/db/hive/db.dart'; -import 'package:stackwallet/db/isar/main_db.dart'; -import 'package:stackwallet/models/balance.dart'; -import 'package:stackwallet/models/isar/models/isar_models.dart'; -import 'package:stackwallet/models/node_model.dart'; -import 'package:stackwallet/models/paymint/fee_object_model.dart'; -import 'package:stackwallet/networking/http.dart'; -import 'package:stackwallet/services/coins/coin_service.dart'; -import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; -import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; -import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; -import 'package:stackwallet/services/event_bus/global_event_bus.dart'; -import 'package:stackwallet/services/mixins/wallet_cache.dart'; -import 'package:stackwallet/services/mixins/wallet_db.dart'; -import 'package:stackwallet/services/nano_api.dart'; -import 'package:stackwallet/services/node_service.dart'; -import 'package:stackwallet/services/tor_service.dart'; -import 'package:stackwallet/services/transaction_notification_tracker.dart'; -import 'package:stackwallet/utilities/amount/amount.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/default_nodes.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/prefs.dart'; -import 'package:tuple/tuple.dart'; - -const int MINIMUM_CONFIRMATIONS = 1; -const String DEFAULT_REPRESENTATIVE = - "ban_1ka1ium4pfue3uxtntqsrib8mumxgazsjf58gidh1xeo5te3whsq8z476goo"; - -class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB { - BananoWallet({ - required String walletId, - required String walletName, - required Coin coin, - required TransactionNotificationTracker tracker, - required SecureStorageInterface secureStore, - MainDB? mockableOverride, - }) { - txTracker = tracker; - _walletId = walletId; - _walletName = walletName; - _coin = coin; - _secureStore = secureStore; - initCache(walletId, coin); - initWalletDB(mockableOverride: mockableOverride); - } - - NodeModel? _xnoNode; - - @override - Future get mnemonicPassphrase => _secureStore.read( - key: '${_walletId}_mnemonicPassphrase', - ); - - @override - Future get mnemonicString => - _secureStore.read(key: '${_walletId}_mnemonic'); - - Future getSeedFromMnemonic() async { - var mnemonic = await mnemonicString; - return NanoMnemomics.mnemonicListToSeed(mnemonic!.split(" ")); - } - - Future getPrivateKeyFromMnemonic() async { - var mnemonic = await mnemonicString; - var seed = NanoMnemomics.mnemonicListToSeed(mnemonic!.split(" ")); - return NanoKeys.seedToPrivate(seed, 0); - } - - Future getAddressFromMnemonic() async { - var mnemonic = await mnemonicString; - var seed = NanoMnemomics.mnemonicListToSeed(mnemonic!.split(' ')); - var address = NanoAccounts.createAccount(NanoAccountType.BANANO, - NanoKeys.createPublicKey(NanoKeys.seedToPrivate(seed, 0))); - return address; - } - - Future getPublicKeyFromMnemonic() async { - var mnemonic = await mnemonicString; - if (mnemonic == null) { - return ""; - } else { - var seed = NanoMnemomics.mnemonicListToSeed(mnemonic.split(" ")); - return NanoKeys.createPublicKey(NanoKeys.seedToPrivate(seed, 0)); - } - } - - @override - String get walletId => _walletId; - late String _walletId; - - @override - String get walletName => _walletName; - late String _walletName; - - @override - set walletName(String name) => _walletName = name; - - @override - set isFavorite(bool markFavorite) { - _isFavorite = markFavorite; - updateCachedIsFavorite(markFavorite); - } - - @override - bool get isFavorite => _isFavorite ??= getCachedIsFavorite(); - bool? _isFavorite; - - @override - Coin get coin => _coin; - late Coin _coin; - - late SecureStorageInterface _secureStore; - late final TransactionNotificationTracker txTracker; - final _prefs = Prefs.instance; - - Timer? timer; - bool _shouldAutoSync = false; - - @override - bool get shouldAutoSync => _shouldAutoSync; - - @override - set shouldAutoSync(bool shouldAutoSync) { - if (_shouldAutoSync != shouldAutoSync) { - _shouldAutoSync = shouldAutoSync; - if (!shouldAutoSync) { - timer?.cancel(); - timer = null; - stopNetworkAlivePinging(); - } else { - startNetworkAlivePinging(); - refresh(); - } - } - } - - @override - Balance get balance => _balance ??= getCachedBalance(); - Balance? _balance; - - HTTP client = HTTP(); - - Future requestWork(String hash) async { - return client - .post( - url: Uri.parse("https://rpc.nano.to"), // this should be a - headers: {'Content-type': 'application/json'}, - body: json.encode( - { - "action": "work_generate", - "hash": hash, - }, - ), - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ) - .then((client) { - if (client.code == 200) { - final Map decoded = - json.decode(client.body) as Map; - if (decoded.containsKey("error")) { - throw Exception("Received error ${decoded["error"]}"); - } - return decoded["work"] as String?; - } else { - throw Exception("Received error ${client.code}"); - } - }); - } - - @override - Future confirmSend({required Map txData}) async { - try { - // our address: - final String publicAddress = await currentReceivingAddress; - - // first get the account balance: - final balanceBody = jsonEncode({ - "action": "account_balance", - "account": publicAddress, - }); - final headers = { - "Content-Type": "application/json", - }; - final balanceResponse = await client.post( - url: Uri.parse(getCurrentNode().host), - headers: headers, - body: balanceBody, - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - final balanceData = jsonDecode(balanceResponse.body); - - final BigInt currentBalance = - BigInt.parse(balanceData["balance"].toString()); - final BigInt txAmount = txData["recipientAmt"].raw as BigInt; - final BigInt balanceAfterTx = currentBalance - txAmount; - - // get the account info (we need the frontier and representative): - final infoBody = jsonEncode({ - "action": "account_info", - "representative": "true", - "account": publicAddress, - }); - final infoResponse = await client.post( - url: Uri.parse(getCurrentNode().host), - headers: headers, - body: infoBody, - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - - final String frontier = - jsonDecode(infoResponse.body)["frontier"].toString(); - final String representative = - jsonDecode(infoResponse.body)["representative"].toString(); - // link = destination address: - final String link = - NanoAccounts.extractPublicKey(txData["address"].toString()); - final String linkAsAccount = txData["address"].toString(); - - // 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.BANANO, - sendBlock["account"]!, - sendBlock["previous"]!, - sendBlock["representative"]!, - BigInt.parse(sendBlock["balance"]!), - sendBlock["link"]!, - ); - final String privateKey = await getPrivateKeyFromMnemonic(); - final String signature = NanoSignatures.signBlock(hash, privateKey); - - // get PoW for the send block: - final String? work = await requestWork(frontier); - if (work == null) { - throw Exception("Failed to get PoW for send block"); - } - - sendBlock["link_as_account"] = linkAsAccount; - sendBlock["signature"] = signature; - sendBlock["work"] = work; - - final processBody = jsonEncode({ - "action": "process", - "json_block": "true", - "subtype": "send", - "block": sendBlock, - }); - final processResponse = await client.post( - url: Uri.parse(getCurrentNode().host), - headers: headers, - body: processBody, - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - - 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(); - } catch (e, s) { - Logging.instance - .log("Error sending transaction $e - $s", level: LogLevel.Error); - rethrow; - } - } - - Future get _currentReceivingAddress => db - .getAddresses(walletId) - .filter() - .typeEqualTo(AddressType.banano) - .and() - .subTypeEqualTo(AddressSubType.receiving) - .sortByDerivationIndexDesc() - .findFirst(); - - @override - Future get currentReceivingAddress async => - (await _currentReceivingAddress)?.value ?? await getAddressFromMnemonic(); - - @override - Future estimateFeeFor(Amount amount, int feeRate) { - // fees are always 0 :) - return Future.value( - Amount( - rawValue: BigInt.from(0), - fractionDigits: coin.decimals, - ), - ); - } - - @override - Future exit() async { - _hasCalledExit = true; - timer?.cancel(); - timer = null; - stopNetworkAlivePinging(); - } - - @override - // Banano has no fees - Future get fees async => FeeObject( - numberOfBlocksFast: 1, - numberOfBlocksAverage: 1, - numberOfBlocksSlow: 1, - fast: 0, - medium: 0, - slow: 0, - ); - - Future updateBalance() async { - final body = jsonEncode({ - "action": "account_balance", - "account": await currentReceivingAddress, - }); - final headers = { - "Content-Type": "application/json", - }; - final response = await client.post( - url: Uri.parse(getCurrentNode().host), - headers: headers, - body: body, - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - final data = jsonDecode(response.body); - _balance = Balance( - total: Amount( - rawValue: (BigInt.parse(data["balance"].toString()) + - BigInt.parse(data["receivable"].toString())), - fractionDigits: coin.decimals), - spendable: Amount( - rawValue: BigInt.parse(data["balance"].toString()), - fractionDigits: coin.decimals), - blockedTotal: - Amount(rawValue: BigInt.parse("0"), fractionDigits: coin.decimals), - pendingSpendable: Amount( - rawValue: BigInt.parse(data["receivable"].toString()), - fractionDigits: coin.decimals), - ); - await updateCachedBalance(_balance!); - } - - Future receiveBlock( - String blockHash, String source, String amountRaw) async { - // TODO: the opening block of an account is a special case - bool openBlock = false; - - final headers = { - "Content-Type": "application/json", - }; - - // our address: - final String publicAddress = await currentReceivingAddress; - - // first check if the account is open: - // get the account info (we need the frontier and representative): - final infoBody = jsonEncode({ - "action": "account_info", - "representative": "true", - "account": publicAddress, - }); - final infoResponse = await client.post( - url: Uri.parse(getCurrentNode().host), - headers: headers, - body: infoBody, - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - final infoData = jsonDecode(infoResponse.body); - - if (infoData["error"] != null) { - // account is not open yet, we need to create an open block: - openBlock = true; - } - - // first get the account balance: - final balanceBody = jsonEncode({ - "action": "account_balance", - "account": publicAddress, - }); - - final balanceResponse = await client.post( - url: Uri.parse(getCurrentNode().host), - headers: headers, - body: balanceBody, - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - - final balanceData = jsonDecode(balanceResponse.body); - final BigInt currentBalance = - BigInt.parse(balanceData["balance"].toString()); - final BigInt txAmount = BigInt.parse(amountRaw); - final BigInt balanceAfterTx = currentBalance + txAmount; - - String frontier = infoData["frontier"].toString(); - String representative = infoData["representative"].toString(); - - if (openBlock) { - // we don't have a representative set yet: - representative = DEFAULT_REPRESENTATIVE; - } - - // link = send block hash: - final String link = blockHash; - // this "linkAsAccount" is meaningless: - final String linkAsAccount = - NanoAccounts.createAccount(NanoAccountType.BANANO, blockHash); - - // construct the receive block: - Map receiveBlock = { - "type": "state", - "account": publicAddress, - "previous": openBlock - ? "0000000000000000000000000000000000000000000000000000000000000000" - : frontier, - "representative": representative, - "balance": balanceAfterTx.toString(), - "link": link, - "link_as_account": linkAsAccount, - }; - - // sign the receive block: - final String hash = NanoBlocks.computeStateHash( - NanoAccountType.BANANO, - receiveBlock["account"]!, - receiveBlock["previous"]!, - receiveBlock["representative"]!, - BigInt.parse(receiveBlock["balance"]!), - receiveBlock["link"]!, - ); - final String privateKey = await getPrivateKeyFromMnemonic(); - final String signature = NanoSignatures.signBlock(hash, privateKey); - - // get PoW for the receive block: - String? work; - if (openBlock) { - work = await requestWork(NanoAccounts.extractPublicKey(publicAddress)); - } else { - work = await requestWork(frontier); - } - if (work == null) { - throw Exception("Failed to get PoW for receive block"); - } - 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 client.post( - url: Uri.parse(getCurrentNode().host), - headers: headers, - body: processBody, - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - - final Map decoded = - json.decode(processResponse.body) as Map; - if (decoded.containsKey("error")) { - throw Exception("Received error ${decoded["error"]}"); - } - } - - Future confirmAllReceivable() async { - final receivableResponse = await client.post( - url: Uri.parse(getCurrentNode().host), - headers: {"Content-Type": "application/json"}, - body: jsonEncode({ - "action": "receivable", - "source": "true", - "account": await currentReceivingAddress, - "count": "-1", - }), - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - - final receivableData = await jsonDecode(receivableResponse.body); - if (receivableData["blocks"] == "") { - return; - } - final blocks = receivableData["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, source, amountRaw); - // a bit of a hack: - await Future.delayed(const Duration(seconds: 1)); - } - } - - Future updateTransactions() async { - await confirmAllReceivable(); - final receivingAddress = (await _currentReceivingAddress)!; - final String publicAddress = receivingAddress.value; - final response = await client.post( - url: Uri.parse(getCurrentNode().host), - headers: {"Content-Type": "application/json"}, - body: jsonEncode({ - "action": "account_history", - "account": publicAddress, - "count": "-1", - }), - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - final data = await jsonDecode(response.body); - final transactions = - data["history"] is List ? data["history"] as List : []; - if (transactions.isEmpty) { - return; - } else { - List> transactionList = []; - for (var tx in transactions) { - var typeString = tx["type"].toString(); - TransactionType transactionType = TransactionType.unknown; - if (typeString == "send") { - transactionType = TransactionType.outgoing; - } else if (typeString == "receive") { - transactionType = TransactionType.incoming; - } - final amount = Amount( - rawValue: BigInt.parse(tx["amount"].toString()), - fractionDigits: coin.decimals, - ); - - var transaction = Transaction( - walletId: walletId, - txid: tx["hash"].toString(), - timestamp: int.parse(tx["local_timestamp"].toString()), - type: transactionType, - subType: TransactionSubType.none, - amount: 0, - amountString: amount.toJsonString(), - fee: 0, - height: int.parse(tx["height"].toString()), - isCancelled: false, - isLelantus: false, - slateId: "", - otherData: "", - inputs: [], - outputs: [], - nonce: 0, - numberOfMessages: null, - ); - - Address address = transactionType == TransactionType.incoming - ? receivingAddress - : Address( - walletId: walletId, - publicKey: [], - value: tx["account"].toString(), - derivationIndex: 0, - derivationPath: null, - type: AddressType.banano, - subType: AddressSubType.nonWallet, - ); - Tuple2 tuple = Tuple2(transaction, address); - transactionList.add(tuple); - } - - await db.addNewTransactionData(transactionList, walletId); - - if (transactionList.isNotEmpty) { - GlobalEventBus.instance.fire( - UpdatedInBackgroundEvent( - "Transactions updated/added for: $walletId $walletName ", - walletId, - ), - ); - } - } - } - - @override - Future fullRescan( - int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) async { - await _prefs.init(); - await updateTransactions(); - await updateBalance(); - } - - @override - Future generateNewAddress() { - // TODO: implement generateNewAddress - throw UnimplementedError(); - } - - @override - bool get hasCalledExit => _hasCalledExit; - bool _hasCalledExit = false; - - @override - Future initializeExisting() async { - await _prefs.init(); - } - - @override - Future initializeNew( - ({String mnemonicPassphrase, int wordCount})? data, - ) async { - if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) { - throw Exception( - "Attempted to overwrite mnemonic on generate new wallet!"); - } - - await _prefs.init(); - - String seed = NanoSeeds.generateSeed(); - final mnemonic = NanoMnemomics.seedToMnemonic(seed); - await _secureStore.write( - key: '${_walletId}_mnemonic', - value: mnemonic.join(' '), - ); - await _secureStore.write( - key: '${_walletId}_mnemonicPassphrase', - value: "", - ); - String privateKey = NanoKeys.seedToPrivate(seed, 0); - String publicKey = NanoKeys.createPublicKey(privateKey); - String publicAddress = - NanoAccounts.createAccount(NanoAccountType.BANANO, publicKey); - - final address = Address( - walletId: walletId, - value: publicAddress, - publicKey: [], // TODO: add public key - derivationIndex: 0, - derivationPath: null, - type: AddressType.banano, - subType: AddressSubType.receiving, - ); - - await db.putAddress(address); - - await Future.wait( - [updateCachedId(walletId), updateCachedIsFavorite(false)]); - } - - @override - bool get isConnected => _isConnected; - - bool _isConnected = false; - - @override - bool get isRefreshing => refreshMutex; - - bool refreshMutex = false; - - @override - Future get maxFee => Future.value(0); - - @override - Future> get mnemonic => _getMnemonicList(); - - Future> _getMnemonicList() async { - final _mnemonicString = await mnemonicString; - if (_mnemonicString == null) { - return []; - } - final List data = _mnemonicString.split(' '); - return data; - } - - @override - Future> prepareSend({ - required String address, - required Amount amount, - Map? args, - }) async { - try { - if (amount.decimals != coin.decimals) { - throw ArgumentError("Banano prepareSend attempted with invalid Amount"); - } - - Map txData = { - "fee": 0, - "addresss": address, - "recipientAmt": amount, - }; - - Logging.instance.log("prepare send: $txData", level: LogLevel.Info); - return txData; - } catch (e, s) { - Logging.instance.log("Error getting fees $e - $s", level: LogLevel.Error); - rethrow; - } - } - - @override - Future recoverFromMnemonic( - {required String mnemonic, - String? mnemonicPassphrase, - required int maxUnusedAddressGap, - required int maxNumberOfIndexesToCheck, - required int height}) async { - try { - if ((await mnemonicString) != null || - (await this.mnemonicPassphrase) != null) { - throw Exception("Attempted to overwrite mnemonic on restore!"); - } - - await _secureStore.write( - key: '${_walletId}_mnemonic', value: mnemonic.trim()); - await _secureStore.write( - key: '${_walletId}_mnemonicPassphrase', - value: mnemonicPassphrase ?? "", - ); - - String seed = NanoMnemomics.mnemonicListToSeed(mnemonic.split(" ")); - String privateKey = NanoKeys.seedToPrivate(seed, 0); - String publicKey = NanoKeys.createPublicKey(privateKey); - String publicAddress = - NanoAccounts.createAccount(NanoAccountType.BANANO, publicKey); - - final address = Address( - walletId: walletId, - value: publicAddress, - publicKey: [], - derivationIndex: 0, - derivationPath: null, - type: AddressType.banano, - subType: AddressSubType.receiving, - ); - - await db.putAddress(address); - - await Future.wait( - [updateCachedId(walletId), updateCachedIsFavorite(false)]); - } catch (e) { - rethrow; - } - } - - @override - Future refresh() async { - if (refreshMutex) { - Logging.instance.log( - "$walletId $walletName refreshMutex denied", - level: LogLevel.Info, - ); - return; - } else { - refreshMutex = true; - } - - await _prefs.init(); - - try { - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - coin, - ), - ); - - await _prefs.init(); - - await updateChainHeight(); - await updateTransactions(); - await updateBalance(); - - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - coin, - ), - ); - - if (shouldAutoSync) { - timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { - Logging.instance.log( - "Periodic refresh check for $walletId $walletName in object instance: $hashCode", - level: LogLevel.Info); - - await refresh(); - GlobalEventBus.instance.fire( - UpdatedInBackgroundEvent( - "New data found in $walletId $walletName in background!", - walletId, - ), - ); - }); - } - } catch (e, s) { - Logging.instance.log( - "Failed to refresh banano wallet $walletId: '$walletName': $e\n$s", - level: LogLevel.Warning, - ); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.unableToSync, - walletId, - coin, - ), - ); - } - - refreshMutex = false; - } - - @override - int get storedChainHeight => getCachedChainHeight(); - - NodeModel getCurrentNode() { - return _xnoNode ?? - NodeService(secureStorageInterface: _secureStore) - .getPrimaryNodeFor(coin: coin) ?? - DefaultNodes.getNodeFor(coin); - } - - @override - Future testNetworkConnection() async { - final uri = Uri.parse(getCurrentNode().host); - - final response = await client.post( - url: uri, - headers: {"Content-Type": "application/json"}, - body: jsonEncode( - { - "action": "version", - }, - ), - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - - return response.code == 200; - } - - Timer? _networkAliveTimer; - - void startNetworkAlivePinging() { - // call once on start right away - _periodicPingCheck(); - - // then periodically check - _networkAliveTimer = Timer.periodic( - Constants.networkAliveTimerDuration, - (_) async { - _periodicPingCheck(); - }, - ); - } - - void _periodicPingCheck() async { - bool hasNetwork = await testNetworkConnection(); - - if (_isConnected != hasNetwork) { - NodeConnectionStatus status = hasNetwork - ? NodeConnectionStatus.connected - : NodeConnectionStatus.disconnected; - - GlobalEventBus.instance.fire( - NodeConnectionStatusChangedEvent( - status, - walletId, - coin, - ), - ); - - _isConnected = hasNetwork; - if (hasNetwork) { - unawaited(refresh()); - } - } - } - - void stopNetworkAlivePinging() { - _networkAliveTimer?.cancel(); - _networkAliveTimer = null; - } - - @override - Future> get transactions => - db.getTransactions(walletId).findAll(); - - @override - Future updateNode(bool shouldRefresh) async { - _xnoNode = NodeService(secureStorageInterface: _secureStore) - .getPrimaryNodeFor(coin: coin) ?? - DefaultNodes.getNodeFor(coin); - - if (shouldRefresh) { - unawaited(refresh()); - } - } - - @override - Future updateSentCachedTxData(Map txData) async { - // not currently used for nano - return; - } - - @override - // TODO: implement utxos - Future> get utxos => throw UnimplementedError(); - - @override - bool validateAddress(String address) { - return NanoAccounts.isValid(NanoAccountType.BANANO, address); - } - - Future updateChainHeight() async { - final String publicAddress = await currentReceivingAddress; - // first get the account balance: - final infoBody = jsonEncode({ - "action": "account_info", - "account": publicAddress, - }); - final headers = { - "Content-Type": "application/json", - }; - final infoResponse = await client.post( - url: Uri.parse(getCurrentNode().host), - headers: headers, - body: infoBody, - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - final infoData = jsonDecode(infoResponse.body); - - final int? height = int.tryParse( - infoData["confirmation_height"].toString(), - ); - await updateCachedChainHeight(height ?? 0); - } - - Future updateMonkeyImageBytes(List bytes) async { - await DB.instance.put( - boxName: _walletId, - key: "monkeyImageBytesKey", - value: bytes, - ); - } - - List? getMonkeyImageBytes() { - return DB.instance.get( - boxName: _walletId, - key: "monkeyImageBytesKey", - ) as List?; - } - - Future getCurrentRepresentative() async { - final serverURI = Uri.parse(getCurrentNode().host); - final address = await currentReceivingAddress; - - final response = await NanoAPI.getAccountInfo( - server: serverURI, - representative: true, - account: address, - ); - - return response.accountInfo?.representative ?? DEFAULT_REPRESENTATIVE; - } - - Future changeRepresentative(String newRepresentative) async { - try { - final serverURI = Uri.parse(getCurrentNode().host); - final balance = this.balance.spendable.raw.toString(); - final String privateKey = await getPrivateKeyFromMnemonic(); - final address = await currentReceivingAddress; - - final response = await NanoAPI.getAccountInfo( - server: serverURI, - representative: true, - account: address, - ); - - if (response.accountInfo == null) { - throw response.exception ?? Exception("Failed to get account info"); - } - - final work = await requestWork(response.accountInfo!.frontier); - - return await NanoAPI.changeRepresentative( - server: serverURI, - accountType: NanoAccountType.BANANO, - account: address, - newRepresentative: newRepresentative, - previousBlock: response.accountInfo!.frontier, - balance: balance, - privateKey: privateKey, - work: work!, - ); - } catch (_) { - rethrow; - } - } -} diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 07543fc68..56f6ed648 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -14,14 +14,12 @@ import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; -import 'package:stackwallet/services/coins/banano/banano_wallet.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/litecoin/litecoin_wallet.dart'; import 'package:stackwallet/services/coins/monero/monero_wallet.dart'; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; -import 'package:stackwallet/services/coins/nano/nano_wallet.dart'; import 'package:stackwallet/services/coins/particl/particl_wallet.dart'; import 'package:stackwallet/services/coins/stellar/stellar_wallet.dart'; import 'package:stackwallet/services/coins/tezos/tezos_wallet.dart'; @@ -217,20 +215,10 @@ abstract class CoinServiceAPI { ); case Coin.nano: - return NanoWallet( - walletId: walletId, - walletName: walletName, - coin: coin, - tracker: tracker, - secureStore: secureStorageInterface); + throw UnimplementedError("moved"); case Coin.banano: - return BananoWallet( - walletId: walletId, - walletName: walletName, - coin: coin, - tracker: tracker, - secureStore: secureStorageInterface); + throw UnimplementedError("moved"); case Coin.dogecoinTestNet: throw UnimplementedError("moved"); diff --git a/lib/services/coins/nano/nano_wallet.dart b/lib/services/coins/nano/nano_wallet.dart deleted file mode 100644 index bf324a95d..000000000 --- a/lib/services/coins/nano/nano_wallet.dart +++ /dev/null @@ -1,1022 +0,0 @@ -/* - * This file is part of Stack Wallet. - * - * Copyright (c) 2023 Cypher Stack - * All Rights Reserved. - * The code is distributed under GPLv3 license, see LICENSE file for details. - * Generated by Cypher Stack on 2023-05-26 - * - */ - -import 'dart:async'; -import 'dart:convert'; - -import 'package:isar/isar.dart'; -import 'package:nanodart/nanodart.dart'; -import 'package:stackwallet/db/isar/main_db.dart'; -import 'package:stackwallet/models/balance.dart'; -import 'package:stackwallet/models/isar/models/isar_models.dart'; -import 'package:stackwallet/models/node_model.dart'; -import 'package:stackwallet/models/paymint/fee_object_model.dart'; -import 'package:stackwallet/networking/http.dart'; -import 'package:stackwallet/services/coins/coin_service.dart'; -import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; -import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; -import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; -import 'package:stackwallet/services/event_bus/global_event_bus.dart'; -import 'package:stackwallet/services/mixins/wallet_cache.dart'; -import 'package:stackwallet/services/mixins/wallet_db.dart'; -import 'package:stackwallet/services/nano_api.dart'; -import 'package:stackwallet/services/node_service.dart'; -import 'package:stackwallet/services/tor_service.dart'; -import 'package:stackwallet/services/transaction_notification_tracker.dart'; -import 'package:stackwallet/utilities/amount/amount.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/default_nodes.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/prefs.dart'; -import 'package:tuple/tuple.dart'; - -const int MINIMUM_CONFIRMATIONS = 1; -const String DEFAULT_REPRESENTATIVE = - "nano_38713x95zyjsqzx6nm1dsom1jmm668owkeb9913ax6nfgj15az3nu8xkx579"; - -class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB { - NanoWallet({ - required String walletId, - required String walletName, - required Coin coin, - required TransactionNotificationTracker tracker, - required SecureStorageInterface secureStore, - MainDB? mockableOverride, - }) { - txTracker = tracker; - _walletId = walletId; - _walletName = walletName; - _coin = coin; - _secureStore = secureStore; - initCache(walletId, coin); - initWalletDB(mockableOverride: mockableOverride); - } - - NodeModel? _xnoNode; - - @override - Future get mnemonicPassphrase => _secureStore.read( - key: '${_walletId}_mnemonicPassphrase', - ); - - @override - Future get mnemonicString => - _secureStore.read(key: '${_walletId}_mnemonic'); - - Future getSeedFromMnemonic() async { - var mnemonic = await mnemonicString; - return NanoMnemomics.mnemonicListToSeed(mnemonic!.split(" ")); - } - - Future getPrivateKeyFromMnemonic() async { - var mnemonic = await mnemonicString; - var seed = NanoMnemomics.mnemonicListToSeed(mnemonic!.split(" ")); - return NanoKeys.seedToPrivate(seed, 0); - } - - Future getAddressFromMnemonic() async { - var mnemonic = await mnemonicString; - var seed = NanoMnemomics.mnemonicListToSeed(mnemonic!.split(' ')); - var address = NanoAccounts.createAccount(NanoAccountType.NANO, - NanoKeys.createPublicKey(NanoKeys.seedToPrivate(seed, 0))); - return address; - } - - Future getPublicKeyFromMnemonic() async { - var mnemonic = await mnemonicString; - if (mnemonic == null) { - return ""; - } else { - var seed = NanoMnemomics.mnemonicListToSeed(mnemonic.split(" ")); - return NanoKeys.createPublicKey(NanoKeys.seedToPrivate(seed, 0)); - } - } - - @override - String get walletId => _walletId; - late String _walletId; - - @override - String get walletName => _walletName; - late String _walletName; - - @override - set walletName(String name) => _walletName = name; - - @override - set isFavorite(bool markFavorite) { - _isFavorite = markFavorite; - updateCachedIsFavorite(markFavorite); - } - - @override - bool get isFavorite => _isFavorite ??= getCachedIsFavorite(); - bool? _isFavorite; - - @override - Coin get coin => _coin; - late Coin _coin; - - late SecureStorageInterface _secureStore; - late final TransactionNotificationTracker txTracker; - final _prefs = Prefs.instance; - - Timer? timer; - bool _shouldAutoSync = false; - - @override - bool get shouldAutoSync => _shouldAutoSync; - - @override - set shouldAutoSync(bool shouldAutoSync) { - if (_shouldAutoSync != shouldAutoSync) { - _shouldAutoSync = shouldAutoSync; - if (!shouldAutoSync) { - timer?.cancel(); - timer = null; - stopNetworkAlivePinging(); - } else { - startNetworkAlivePinging(); - refresh(); - } - } - } - - @override - Balance get balance => _balance ??= getCachedBalance(); - Balance? _balance; - - HTTP client = HTTP(); - - Future requestWork(String hash) async { - return client - .post( - url: Uri.parse("https://rpc.nano.to"), // this should be a - headers: {'Content-type': 'application/json'}, - body: json.encode( - { - "action": "work_generate", - "hash": hash, - }, - ), - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ) - .then((Response response) { - if (response.code == 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 error ${response.body}"); - } - }); - } - - @override - Future confirmSend({required Map txData}) async { - try { - // our address: - final String publicAddress = await currentReceivingAddress; - - // first get the account balance: - final balanceBody = jsonEncode({ - "action": "account_balance", - "account": publicAddress, - }); - final headers = { - "Content-Type": "application/json", - }; - final balanceResponse = await client.post( - url: Uri.parse(getCurrentNode().host), - headers: headers, - body: balanceBody, - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - final balanceData = jsonDecode(balanceResponse.body); - - final BigInt currentBalance = - BigInt.parse(balanceData["balance"].toString()); - final BigInt txAmount = txData["recipientAmt"].raw as BigInt; - final BigInt balanceAfterTx = currentBalance - txAmount; - - // get the account info (we need the frontier and representative): - final infoBody = jsonEncode({ - "action": "account_info", - "representative": "true", - "account": publicAddress, - }); - final infoResponse = await client.post( - url: Uri.parse(getCurrentNode().host), - headers: headers, - body: infoBody, - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - - final String frontier = - jsonDecode(infoResponse.body)["frontier"].toString(); - final String representative = - jsonDecode(infoResponse.body)["representative"].toString(); - // link = destination address: - final String link = - NanoAccounts.extractPublicKey(txData["address"].toString()); - final String linkAsAccount = txData["address"].toString(); - - // 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 privateKey = await getPrivateKeyFromMnemonic(); - final String signature = NanoSignatures.signBlock(hash, privateKey); - - // get PoW for the send block: - final String? work = await requestWork(frontier); - if (work == null) { - throw Exception("Failed to get PoW for send block"); - } - - sendBlock["link_as_account"] = linkAsAccount; - sendBlock["signature"] = signature; - sendBlock["work"] = work; - - final processBody = jsonEncode({ - "action": "process", - "json_block": "true", - "subtype": "send", - "block": sendBlock, - }); - final processResponse = await client.post( - url: Uri.parse(getCurrentNode().host), - headers: headers, - body: processBody, - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - - 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(); - } catch (e, s) { - Logging.instance - .log("Error sending transaction $e - $s", level: LogLevel.Error); - rethrow; - } - } - - Future get _currentReceivingAddress => db - .getAddresses(walletId) - .filter() - .typeEqualTo(AddressType.nano) - .and() - .subTypeEqualTo(AddressSubType.receiving) - .sortByDerivationIndexDesc() - .findFirst(); - - @override - Future get currentReceivingAddress async => - (await _currentReceivingAddress)?.value ?? await getAddressFromMnemonic(); - - @override - Future estimateFeeFor(Amount amount, int feeRate) { - // fees are always 0 :) - return Future.value( - Amount(rawValue: BigInt.from(0), fractionDigits: coin.decimals)); - } - - @override - Future exit() async { - _hasCalledExit = true; - timer?.cancel(); - timer = null; - stopNetworkAlivePinging(); - } - - @override - // Nano has no fees - Future get fees async => FeeObject( - numberOfBlocksFast: 1, - numberOfBlocksAverage: 1, - numberOfBlocksSlow: 1, - fast: 0, - medium: 0, - slow: 0, - ); - - Future updateBalance() async { - final body = jsonEncode({ - "action": "account_balance", - "account": await currentReceivingAddress, - }); - final headers = { - "Content-Type": "application/json", - }; - final response = await client.post( - url: Uri.parse(getCurrentNode().host), - headers: headers, - body: body, - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - final data = jsonDecode(response.body); - _balance = Balance( - total: Amount( - rawValue: (BigInt.parse(data["balance"].toString()) + - BigInt.parse(data["receivable"].toString())), - fractionDigits: coin.decimals), - spendable: Amount( - rawValue: BigInt.parse(data["balance"].toString()), - fractionDigits: coin.decimals), - blockedTotal: - Amount(rawValue: BigInt.parse("0"), fractionDigits: coin.decimals), - pendingSpendable: Amount( - rawValue: BigInt.parse(data["receivable"].toString()), - fractionDigits: coin.decimals), - ); - await updateCachedBalance(_balance!); - } - - Future receiveBlock( - String blockHash, String source, String amountRaw) async { - // TODO: the opening block of an account is a special case - bool openBlock = false; - - final headers = { - "Content-Type": "application/json", - }; - - // our address: - final String publicAddress = await currentReceivingAddress; - - // first check if the account is open: - // get the account info (we need the frontier and representative): - final infoBody = jsonEncode({ - "action": "account_info", - "representative": "true", - "account": publicAddress, - }); - final infoResponse = await client.post( - url: Uri.parse(getCurrentNode().host), - headers: headers, - body: infoBody, - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - final infoData = jsonDecode(infoResponse.body); - - if (infoData["error"] != null) { - // account is not open yet, we need to create an open block: - openBlock = true; - } - - // first get the account balance: - final balanceBody = jsonEncode({ - "action": "account_balance", - "account": publicAddress, - }); - - final balanceResponse = await client.post( - url: Uri.parse(getCurrentNode().host), - headers: headers, - body: balanceBody, - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - - final balanceData = jsonDecode(balanceResponse.body); - final BigInt currentBalance = - BigInt.parse(balanceData["balance"].toString()); - final BigInt txAmount = BigInt.parse(amountRaw); - final BigInt balanceAfterTx = currentBalance + txAmount; - - String frontier = infoData["frontier"].toString(); - String representative = infoData["representative"].toString(); - - if (openBlock) { - // we don't have a representative set yet: - representative = DEFAULT_REPRESENTATIVE; - } - - // 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": publicAddress, - "previous": openBlock - ? "0000000000000000000000000000000000000000000000000000000000000000" - : 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 privateKey = await getPrivateKeyFromMnemonic(); - final String signature = NanoSignatures.signBlock(hash, privateKey); - - // get PoW for the receive block: - String? work; - if (openBlock) { - work = await requestWork(NanoAccounts.extractPublicKey(publicAddress)); - } else { - work = await requestWork(frontier); - } - if (work == null) { - throw Exception("Failed to get PoW for receive block"); - } - 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 client.post( - url: Uri.parse(getCurrentNode().host), - headers: headers, - body: processBody, - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - - final Map decoded = - json.decode(processResponse.body) as Map; - if (decoded.containsKey("error")) { - throw Exception("Received error ${decoded["error"]}"); - } - } - - Future confirmAllReceivable() async { - final receivableResponse = await client.post( - url: Uri.parse(getCurrentNode().host), - headers: {"Content-Type": "application/json"}, - body: jsonEncode({ - "action": "receivable", - "source": "true", - "account": await currentReceivingAddress, - "count": "-1", - }), - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - - final receivableData = await jsonDecode(receivableResponse.body); - if (receivableData["blocks"] == "") { - return; - } - final blocks = receivableData["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, source, amountRaw); - // a bit of a hack: - await Future.delayed(const Duration(seconds: 1)); - } - } - - Future updateTransactions() async { - await confirmAllReceivable(); - final receivingAddress = (await _currentReceivingAddress)!; - final String publicAddress = receivingAddress.value; - final response = await client.post( - url: Uri.parse(getCurrentNode().host), - headers: {"Content-Type": "application/json"}, - body: jsonEncode({ - "action": "account_history", - "account": publicAddress, - "count": "-1", - }), - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - final data = await jsonDecode(response.body); - final transactions = - data["history"] is List ? data["history"] as List : []; - if (transactions.isEmpty) { - return; - } else { - List> transactionList = []; - for (var tx in transactions) { - var typeString = tx["type"].toString(); - TransactionType transactionType = TransactionType.unknown; - if (typeString == "send") { - transactionType = TransactionType.outgoing; - } else if (typeString == "receive") { - transactionType = TransactionType.incoming; - } - final amount = Amount( - rawValue: BigInt.parse(tx["amount"].toString()), - fractionDigits: coin.decimals, - ); - - var transaction = Transaction( - walletId: walletId, - txid: tx["hash"].toString(), - timestamp: int.parse(tx["local_timestamp"].toString()), - type: transactionType, - subType: TransactionSubType.none, - amount: 0, - amountString: amount.toJsonString(), - fee: 0, - height: int.parse(tx["height"].toString()), - isCancelled: false, - isLelantus: false, - slateId: "", - otherData: "", - inputs: [], - outputs: [], - nonce: 0, - numberOfMessages: null, - ); - - Address address = transactionType == TransactionType.incoming - ? receivingAddress - : Address( - walletId: walletId, - publicKey: [], - value: tx["account"].toString(), - derivationIndex: 0, - derivationPath: null, - type: AddressType.nano, - subType: AddressSubType.nonWallet, - ); - Tuple2 tuple = Tuple2(transaction, address); - transactionList.add(tuple); - } - - await db.addNewTransactionData(transactionList, walletId); - - if (transactionList.isNotEmpty) { - GlobalEventBus.instance.fire( - UpdatedInBackgroundEvent( - "Transactions updated/added for: $walletId $walletName ", - walletId, - ), - ); - } - } - } - - @override - Future fullRescan( - int maxUnusedAddressGap, - int maxNumberOfIndexesToCheck, - ) async { - await _prefs.init(); - await updateTransactions(); - await updateBalance(); - } - - @override - Future generateNewAddress() { - // TODO: implement generateNewAddress - throw UnimplementedError(); - } - - @override - bool get hasCalledExit => _hasCalledExit; - bool _hasCalledExit = false; - - @override - Future initializeExisting() async { - await _prefs.init(); - } - - @override - Future initializeNew( - ({String mnemonicPassphrase, int wordCount})? data, - ) async { - if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) { - throw Exception( - "Attempted to overwrite mnemonic on generate new wallet!"); - } - - await _prefs.init(); - - String seed = NanoSeeds.generateSeed(); - final mnemonic = NanoMnemomics.seedToMnemonic(seed); - await _secureStore.write( - key: '${_walletId}_mnemonic', - value: mnemonic.join(' '), - ); - await _secureStore.write( - key: '${_walletId}_mnemonicPassphrase', - value: "", - ); - String privateKey = NanoKeys.seedToPrivate(seed, 0); - String publicKey = NanoKeys.createPublicKey(privateKey); - String publicAddress = NanoAccounts.createAccount( - NanoAccountType.NANO, - publicKey, - ); - - final address = Address( - walletId: walletId, - value: publicAddress, - publicKey: [], - derivationIndex: 0, - derivationPath: null, - type: AddressType.nano, - subType: AddressSubType.receiving, - ); - - await db.putAddress(address); - - await Future.wait([ - updateCachedId(walletId), - updateCachedIsFavorite(false), - ]); - } - - @override - bool get isConnected => _isConnected; - - bool _isConnected = false; - - @override - bool get isRefreshing => refreshMutex; - - bool refreshMutex = false; - - @override - Future get maxFee => Future.value(0); - - @override - Future> get mnemonic => _getMnemonicList(); - - Future> _getMnemonicList() async { - final _mnemonicString = await mnemonicString; - if (_mnemonicString == null) { - return []; - } - final List data = _mnemonicString.split(' '); - return data; - } - - @override - Future> prepareSend({ - required String address, - required Amount amount, - Map? args, - }) async { - try { - if (amount.decimals != coin.decimals) { - throw ArgumentError("Nano prepareSend attempted with invalid Amount"); - } - - Map txData = { - "fee": 0, - "addresss": address, - "recipientAmt": amount, - }; - - Logging.instance.log("prepare send: $txData", level: LogLevel.Info); - return txData; - } catch (e, s) { - Logging.instance.log("Error getting fees $e - $s", level: LogLevel.Error); - rethrow; - } - } - - @override - Future recoverFromMnemonic( - {required String mnemonic, - String? mnemonicPassphrase, - required int maxUnusedAddressGap, - required int maxNumberOfIndexesToCheck, - required int height}) async { - try { - if ((await mnemonicString) != null || - (await this.mnemonicPassphrase) != null) { - throw Exception("Attempted to overwrite mnemonic on restore!"); - } - - await _secureStore.write( - key: '${_walletId}_mnemonic', value: mnemonic.trim()); - await _secureStore.write( - key: '${_walletId}_mnemonicPassphrase', - value: mnemonicPassphrase ?? "", - ); - - String seed = NanoMnemomics.mnemonicListToSeed(mnemonic.split(" ")); - String privateKey = NanoKeys.seedToPrivate(seed, 0); - String publicKey = NanoKeys.createPublicKey(privateKey); - String publicAddress = - NanoAccounts.createAccount(NanoAccountType.NANO, publicKey); - - final address = Address( - walletId: walletId, - value: publicAddress, - publicKey: [], - derivationIndex: 0, - derivationPath: null, - type: AddressType.nano, - subType: AddressSubType.receiving, - ); - - await db.putAddress(address); - - await Future.wait([ - updateCachedId(walletId), - updateCachedIsFavorite(false), - ]); - } catch (e) { - rethrow; - } - } - - @override - Future refresh() async { - if (refreshMutex) { - Logging.instance.log( - "$walletId $walletName refreshMutex denied", - level: LogLevel.Info, - ); - return; - } else { - refreshMutex = true; - } - - try { - await _prefs.init(); - - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - coin, - ), - ); - - await updateChainHeight(); - await updateTransactions(); - await updateBalance(); - - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - coin, - ), - ); - - if (shouldAutoSync) { - timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { - Logging.instance.log( - "Periodic refresh check for $walletId $walletName in object instance: $hashCode", - level: LogLevel.Info); - - await refresh(); - GlobalEventBus.instance.fire( - UpdatedInBackgroundEvent( - "New data found in $walletId $walletName in background!", - walletId, - ), - ); - }); - } - } catch (e, s) { - Logging.instance.log( - "Failed to refresh nano wallet $walletId: '$walletName': $e\n$s", - level: LogLevel.Warning, - ); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.unableToSync, - walletId, - coin, - ), - ); - } - - refreshMutex = false; - } - - @override - int get storedChainHeight => getCachedChainHeight(); - - NodeModel getCurrentNode() { - return _xnoNode ?? - NodeService(secureStorageInterface: _secureStore) - .getPrimaryNodeFor(coin: coin) ?? - DefaultNodes.getNodeFor(coin); - } - - @override - Future testNetworkConnection() async { - final uri = Uri.parse(getCurrentNode().host); - - final response = await client.post( - url: uri, - headers: {"Content-Type": "application/json"}, - body: jsonEncode( - { - "action": "version", - }, - ), - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - - return response.code == 200; - } - - Timer? _networkAliveTimer; - - void startNetworkAlivePinging() { - // call once on start right away - _periodicPingCheck(); - - // then periodically check - _networkAliveTimer = Timer.periodic( - Constants.networkAliveTimerDuration, - (_) async { - _periodicPingCheck(); - }, - ); - } - - void _periodicPingCheck() async { - bool hasNetwork = await testNetworkConnection(); - - if (_isConnected != hasNetwork) { - NodeConnectionStatus status = hasNetwork - ? NodeConnectionStatus.connected - : NodeConnectionStatus.disconnected; - - GlobalEventBus.instance.fire( - NodeConnectionStatusChangedEvent( - status, - walletId, - coin, - ), - ); - - _isConnected = hasNetwork; - if (hasNetwork) { - unawaited(refresh()); - } - } - } - - void stopNetworkAlivePinging() { - _networkAliveTimer?.cancel(); - _networkAliveTimer = null; - } - - @override - Future> get transactions => - db.getTransactions(walletId).findAll(); - - @override - Future updateNode(bool shouldRefresh) async { - _xnoNode = NodeService(secureStorageInterface: _secureStore) - .getPrimaryNodeFor(coin: coin) ?? - DefaultNodes.getNodeFor(coin); - - if (shouldRefresh) { - unawaited(refresh()); - } - } - - @override - Future updateSentCachedTxData(Map txData) async { - // not currently used for nano - return; - } - - @override - // TODO: implement utxos - Future> get utxos => throw UnimplementedError(); - - @override - bool validateAddress(String address) { - return NanoAccounts.isValid(NanoAccountType.NANO, address); - } - - Future updateChainHeight() async { - final String publicAddress = await currentReceivingAddress; - // first get the account balance: - final infoBody = jsonEncode({ - "action": "account_info", - "account": publicAddress, - }); - final headers = { - "Content-Type": "application/json", - }; - final infoResponse = await client.post( - url: Uri.parse(getCurrentNode().host), - headers: headers, - body: infoBody, - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - final infoData = jsonDecode(infoResponse.body); - - final int? height = int.tryParse( - infoData["confirmation_height"].toString(), - ); - await updateCachedChainHeight(height ?? 0); - } - - Future getCurrentRepresentative() async { - final serverURI = Uri.parse(getCurrentNode().host); - final address = await currentReceivingAddress; - - final response = await NanoAPI.getAccountInfo( - server: serverURI, - representative: true, - account: address, - ); - - return response.accountInfo?.representative ?? DEFAULT_REPRESENTATIVE; - } - - Future changeRepresentative(String newRepresentative) async { - try { - final serverURI = Uri.parse(getCurrentNode().host); - final balance = this.balance.spendable.raw.toString(); - final String privateKey = await getPrivateKeyFromMnemonic(); - final address = await currentReceivingAddress; - - final response = await NanoAPI.getAccountInfo( - server: serverURI, - representative: true, - account: address, - ); - - if (response.accountInfo == null) { - throw response.exception ?? Exception("Failed to get account info"); - } - - final work = await requestWork(response.accountInfo!.frontier); - - return await NanoAPI.changeRepresentative( - server: serverURI, - accountType: NanoAccountType.NANO, - account: address, - newRepresentative: newRepresentative, - previousBlock: response.accountInfo!.frontier, - balance: balance, - privateKey: privateKey, - work: work!, - ); - } catch (_) { - rethrow; - } - } -} diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 2551b41dc..b8b74a5d3 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -277,6 +277,9 @@ abstract class Constants { case Coin.monero: return 25; + // + // default: + // -1; } } diff --git a/lib/wallets/crypto_currency/coins/banano.dart b/lib/wallets/crypto_currency/coins/banano.dart new file mode 100644 index 000000000..9b6060980 --- /dev/null +++ b/lib/wallets/crypto_currency/coins/banano.dart @@ -0,0 +1,25 @@ +import 'package:nanodart/nanodart.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/nano_currency.dart'; + +class Banano extends NanoCurrency { + Banano(super.network) { + switch (network) { + case CryptoCurrencyNetwork.main: + coin = Coin.banano; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + int get minConfirms => 1; + + @override + String get defaultRepresentative => + "ban_1ka1ium4pfue3uxtntqsrib8mumxgazsjf58gidh1xeo5te3whsq8z476goo"; + + @override + int get nanoAccountType => NanoAccountType.BANANO; +} diff --git a/lib/wallets/crypto_currency/coins/nano.dart b/lib/wallets/crypto_currency/coins/nano.dart new file mode 100644 index 000000000..448f17188 --- /dev/null +++ b/lib/wallets/crypto_currency/coins/nano.dart @@ -0,0 +1,25 @@ +import 'package:nanodart/nanodart.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/nano_currency.dart'; + +class Nano extends NanoCurrency { + Nano(super.network) { + switch (network) { + case CryptoCurrencyNetwork.main: + coin = Coin.nano; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + int get minConfirms => 1; + + @override + String get defaultRepresentative => + "nano_38713x95zyjsqzx6nm1dsom1jmm668owkeb9913ax6nfgj15az3nu8xkx579"; + + @override + int get nanoAccountType => NanoAccountType.NANO; +} diff --git a/lib/wallets/crypto_currency/intermediate/nano_currency.dart b/lib/wallets/crypto_currency/intermediate/nano_currency.dart new file mode 100644 index 000000000..617f8d952 --- /dev/null +++ b/lib/wallets/crypto_currency/intermediate/nano_currency.dart @@ -0,0 +1,21 @@ +import 'package:nanodart/nanodart.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_currency.dart'; + +abstract class NanoCurrency extends Bip39Currency { + NanoCurrency(super.network); + + String get defaultRepresentative; + + int get nanoAccountType; + + @override + bool validateAddress(String address) => NanoAccounts.isValid( + nanoAccountType, + address, + ); + + @override + String get genesisHash => throw UnimplementedError( + "Not used in nano based coins", + ); +} diff --git a/lib/wallets/isar/models/wallet_info.dart b/lib/wallets/isar/models/wallet_info.dart index a4c44814c..7f88f5e8d 100644 --- a/lib/wallets/isar/models/wallet_info.dart +++ b/lib/wallets/isar/models/wallet_info.dart @@ -215,6 +215,26 @@ class WalletInfo implements IsarId { } } + /// update [otherData] with the map entries in [newEntries] + Future updateOtherData({ + required Map newEntries, + required Isar isar, + }) async { + final Map newMap = {}; + newMap.addAll(otherData); + newMap.addAll(newEntries); + final encodedNew = jsonEncode(newMap); + + // only update if there were changes + if (_otherDataJsonString != encodedNew) { + _otherDataJsonString = encodedNew; + await isar.writeTxn(() async { + await isar.walletInfo.deleteByWalletId(walletId); + await isar.walletInfo.put(this); + }); + } + } + /// copies this with a new name and updates the db Future setMnemonicVerified({ required Isar isar, @@ -317,4 +337,5 @@ abstract class WalletInfoKeys { static const String tokenContractAddresses = "tokenContractAddressesKey"; static const String cachedSecondaryBalance = "cachedSecondaryBalanceKey"; static const String epiccashData = "epiccashDataKey"; + static const String bananoMonkeyImageBytes = "monkeyImageBytesKey"; } diff --git a/lib/wallets/wallet/impl/banano_wallet.dart b/lib/wallets/wallet/impl/banano_wallet.dart new file mode 100644 index 000000000..828a77471 --- /dev/null +++ b/lib/wallets/wallet/impl/banano_wallet.dart @@ -0,0 +1,27 @@ +import 'package:stackwallet/wallets/crypto_currency/coins/banano.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/nano_currency.dart'; +import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; +import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart'; +import 'package:stackwallet/wallets/wallet/mixins/nano_based.dart'; + +class BananoWallet extends Bip39Wallet with NanoBased { + BananoWallet(CryptoCurrencyNetwork network) : super(Banano(network)); + + Future updateMonkeyImageBytes(List bytes) async { + await info.updateOtherData( + newEntries: { + WalletInfoKeys.bananoMonkeyImageBytes: bytes, + }, + isar: mainDB.isar, + ); + } + + List? getMonkeyImageBytes() { + final list = info.otherData[WalletInfoKeys.bananoMonkeyImageBytes] as List?; + if (list == null) { + return null; + } + return List.from(list); + } +} diff --git a/lib/wallets/wallet/impl/nano_wallet.dart b/lib/wallets/wallet/impl/nano_wallet.dart new file mode 100644 index 000000000..3ec718615 --- /dev/null +++ b/lib/wallets/wallet/impl/nano_wallet.dart @@ -0,0 +1,9 @@ +import 'package:stackwallet/wallets/crypto_currency/coins/nano.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/nano_currency.dart'; +import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart'; +import 'package:stackwallet/wallets/wallet/mixins/nano_based.dart'; + +class NanoWallet extends Bip39Wallet with NanoBased { + NanoWallet(CryptoCurrencyNetwork network) : super(Nano(network)); +} diff --git a/lib/wallets/wallet/mixins/nano_based.dart b/lib/wallets/wallet/mixins/nano_based.dart new file mode 100644 index 000000000..a58c8e7b7 --- /dev/null +++ b/lib/wallets/wallet/mixins/nano_based.dart @@ -0,0 +1,675 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:isar/isar.dart'; +import 'package:nanodart/nanodart.dart'; +import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/networking/http.dart'; +import 'package:stackwallet/services/nano_api.dart'; +import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/services/tor_service.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/extensions/impl/string.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/nano_currency.dart'; +import 'package:stackwallet/wallets/models/tx_data.dart'; +import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart'; +import 'package:tuple/tuple.dart'; + +const _kWorkServer = "https://rpc.nano.to"; + +mixin NanoBased on Bip39Wallet { + // since nano based coins only have a single address/account we can cache + // the address instead of fetching from db every time we need it in certain + // cases + Address? _cachedAddress; + + NodeModel? _cachedNode; + + final _httpClient = HTTP(); + + Future _requestWork(String hash) async { + return _httpClient + .post( + url: Uri.parse(_kWorkServer), // this should be a + headers: {'Content-type': 'application/json'}, + body: json.encode( + { + "action": "work_generate", + "hash": hash, + }, + ), + proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, + ) + .then((_httpClient) { + if (_httpClient.code == 200) { + final Map decoded = + json.decode(_httpClient.body) as Map; + if (decoded.containsKey("error")) { + throw Exception("Received error ${decoded["error"]}"); + } + return decoded["work"] as String?; + } else { + throw Exception("Received error ${_httpClient.code}"); + } + }); + } + + Future _getPrivateKeyFromMnemonic() async { + final mnemonicList = await getMnemonicAsWords(); + final seed = NanoMnemomics.mnemonicListToSeed(mnemonicList); + return NanoKeys.seedToPrivate(seed, 0); + } + + Future
_getAddressFromMnemonic() async { + final publicKey = NanoKeys.createPublicKey( + await _getPrivateKeyFromMnemonic(), + ); + + final addressString = + NanoAccounts.createAccount(cryptoCurrency.nanoAccountType, publicKey); + + return Address( + walletId: walletId, + value: addressString, + publicKey: publicKey.toUint8ListFromHex, + derivationIndex: 0, + derivationPath: null, + type: cryptoCurrency.coin.primaryAddressType, + subType: AddressSubType.receiving, + ); + } + + Future _receiveBlock( + String blockHash, + String source, + String amountRaw, + String publicAddress, + ) async { + // TODO: the opening block of an account is a special case + 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): + final infoBody = jsonEncode({ + "action": "account_info", + "representative": "true", + "account": publicAddress, + }); + final infoResponse = await _httpClient.post( + url: Uri.parse(getCurrentNode().host), + headers: headers, + body: infoBody, + proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, + ); + final infoData = jsonDecode(infoResponse.body); + + if (infoData["error"] != null) { + // account is not open yet, we need to create an open block: + openBlock = true; + } + + // first get the account balance: + final balanceBody = jsonEncode({ + "action": "account_balance", + "account": publicAddress, + }); + + final balanceResponse = await _httpClient.post( + url: Uri.parse(getCurrentNode().host), + headers: headers, + body: balanceBody, + proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, + ); + + final balanceData = jsonDecode(balanceResponse.body); + final BigInt currentBalance = + BigInt.parse(balanceData["balance"].toString()); + final BigInt txAmount = BigInt.parse(amountRaw); + final BigInt balanceAfterTx = currentBalance + txAmount; + + String frontier = infoData["frontier"].toString(); + String representative = infoData["representative"].toString(); + + if (openBlock) { + // we don't have a representative set yet: + representative = cryptoCurrency.defaultRepresentative; + } + + // link = send block hash: + final String link = blockHash; + // this "linkAsAccount" is meaningless: + final String linkAsAccount = + NanoAccounts.createAccount(NanoAccountType.BANANO, blockHash); + + // construct the receive block: + Map receiveBlock = { + "type": "state", + "account": publicAddress, + "previous": openBlock + ? "0000000000000000000000000000000000000000000000000000000000000000" + : frontier, + "representative": representative, + "balance": balanceAfterTx.toString(), + "link": link, + "link_as_account": linkAsAccount, + }; + + // sign the receive block: + final String hash = NanoBlocks.computeStateHash( + NanoAccountType.BANANO, + receiveBlock["account"]!, + receiveBlock["previous"]!, + receiveBlock["representative"]!, + BigInt.parse(receiveBlock["balance"]!), + receiveBlock["link"]!, + ); + final String privateKey = await _getPrivateKeyFromMnemonic(); + final String signature = NanoSignatures.signBlock(hash, privateKey); + + // get PoW for the receive block: + String? work; + if (openBlock) { + work = await _requestWork(NanoAccounts.extractPublicKey(publicAddress)); + } else { + work = await _requestWork(frontier); + } + if (work == null) { + throw Exception("Failed to get PoW for receive block"); + } + 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 _httpClient.post( + url: Uri.parse(getCurrentNode().host), + headers: headers, + body: processBody, + proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, + ); + + final Map decoded = + json.decode(processResponse.body) as Map; + if (decoded.containsKey("error")) { + throw Exception("Received error ${decoded["error"]}"); + } + } + + Future _confirmAllReceivable(String accountAddress) async { + final receivableResponse = await _httpClient.post( + url: Uri.parse(getCurrentNode().host), + headers: {"Content-Type": "application/json"}, + body: jsonEncode({ + "action": "receivable", + "source": "true", + "account": accountAddress, + "count": "-1", + }), + proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, + ); + + final receivableData = await jsonDecode(receivableResponse.body); + if (receivableData["blocks"] == "") { + return; + } + final blocks = receivableData["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, source, amountRaw, accountAddress); + // a bit of a hack: + await Future.delayed(const Duration(seconds: 1)); + } + } + + //========= public =========================================================== + + Future getCurrentRepresentative() async { + final serverURI = Uri.parse(getCurrentNode().host); + final address = + (_cachedAddress ?? await getCurrentReceivingAddress())!.value; + + final response = await NanoAPI.getAccountInfo( + server: serverURI, + representative: true, + account: address, + ); + + return response.accountInfo?.representative ?? + cryptoCurrency.defaultRepresentative; + } + + Future changeRepresentative(String newRepresentative) async { + try { + final serverURI = Uri.parse(getCurrentNode().host); + await updateBalance(); + final balance = info.cachedBalance.spendable.raw.toString(); + final String privateKey = await _getPrivateKeyFromMnemonic(); + final address = + (_cachedAddress ?? await getCurrentReceivingAddress())!.value; + + final response = await NanoAPI.getAccountInfo( + server: serverURI, + representative: true, + account: address, + ); + + if (response.accountInfo == null) { + throw response.exception ?? Exception("Failed to get account info"); + } + + final work = await _requestWork(response.accountInfo!.frontier); + + return await NanoAPI.changeRepresentative( + server: serverURI, + accountType: NanoAccountType.BANANO, + account: address, + newRepresentative: newRepresentative, + previousBlock: response.accountInfo!.frontier, + balance: balance, + privateKey: privateKey, + work: work!, + ); + } catch (_) { + rethrow; + } + } + + //========= overrides ======================================================== + + @override + Future updateNode() async { + _cachedNode = NodeService(secureStorageInterface: secureStorageInterface) + .getPrimaryNodeFor(coin: info.coin) ?? + DefaultNodes.getNodeFor(info.coin); + + unawaited(refresh()); + } + + @override + NodeModel getCurrentNode() { + return _cachedNode ?? + NodeService(secureStorageInterface: secureStorageInterface) + .getPrimaryNodeFor(coin: info.coin) ?? + DefaultNodes.getNodeFor(info.coin); + } + + @override + Future init() async { + _cachedAddress = await getCurrentReceivingAddress(); + if (_cachedAddress == null) { + _cachedAddress = await _getAddressFromMnemonic(); + await mainDB.putAddress(_cachedAddress!); + } + + return super.init(); + } + + @override + Future pingCheck() async { + final uri = Uri.parse(getCurrentNode().host); + + final response = await _httpClient.post( + url: uri, + headers: {"Content-Type": "application/json"}, + body: jsonEncode( + { + "action": "version", + }, + ), + proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, + ); + + return response.code == 200; + } + + @override + Future prepareSend({required TxData txData}) async { + if (txData.recipients!.length != 1) { + throw ArgumentError( + "${cryptoCurrency.runtimeType} currently only " + "supports one recipient per transaction", + ); + } + + return txData.copyWith( + fee: Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ), + ); + } + + @override + Future confirmSend({required TxData txData}) async { + try { + // our address: + final String publicAddress = + (_cachedAddress ?? await getCurrentReceivingAddress())!.value; + + // first update to get latest account balance: + + final currentBalance = info.cachedBalance.spendable; + final txAmount = txData.amount!; + final BigInt balanceAfterTx = (currentBalance - txAmount).raw; + + // get the account info (we need the frontier and representative): + final infoBody = jsonEncode({ + "action": "account_info", + "representative": "true", + "account": publicAddress, + }); + + final headers = { + "Content-Type": "application/json", + }; + + final infoResponse = await _httpClient.post( + url: Uri.parse(getCurrentNode().host), + headers: headers, + body: infoBody, + proxyInfo: + prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, + ); + + final String frontier = + jsonDecode(infoResponse.body)["frontier"].toString(); + final String representative = + jsonDecode(infoResponse.body)["representative"].toString(); + // link = destination address: + final String linkAsAccount = txData.recipients!.first.address; + final String link = NanoAccounts.extractPublicKey(linkAsAccount); + + // 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.BANANO, + sendBlock["account"]!, + sendBlock["previous"]!, + sendBlock["representative"]!, + BigInt.parse(sendBlock["balance"]!), + sendBlock["link"]!, + ); + final String privateKey = await _getPrivateKeyFromMnemonic(); + final String signature = NanoSignatures.signBlock(hash, privateKey); + + // get PoW for the send block: + final String? work = await _requestWork(frontier); + if (work == null) { + throw Exception("Failed to get PoW for send block"); + } + + sendBlock["link_as_account"] = linkAsAccount; + sendBlock["signature"] = signature; + sendBlock["work"] = work; + + final processBody = jsonEncode({ + "action": "process", + "json_block": "true", + "subtype": "send", + "block": sendBlock, + }); + final processResponse = await _httpClient.post( + url: Uri.parse(getCurrentNode().host), + headers: headers, + body: processBody, + proxyInfo: + prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, + ); + + 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 txData.copyWith( + txid: decoded["hash"].toString(), + ); + } catch (e, s) { + Logging.instance + .log("Error sending transaction $e - $s", level: LogLevel.Error); + rethrow; + } + } + + @override + Future recover({required bool isRescan}) async { + try { + await refreshMutex.protect(() async { + if (isRescan) { + await mainDB.deleteWalletBlockchainData(walletId); + } + _cachedAddress = await _getAddressFromMnemonic(); + + await mainDB.putAddress(_cachedAddress!); + }); + + await refresh(); + } catch (e) { + rethrow; + } + } + + @override + Future updateTransactions() async { + final receivingAddress = + (_cachedAddress ?? await getCurrentReceivingAddress())!; + final String publicAddress = receivingAddress.value; + await _confirmAllReceivable(publicAddress); + final response = await _httpClient.post( + url: Uri.parse(getCurrentNode().host), + headers: {"Content-Type": "application/json"}, + body: jsonEncode({ + "action": "account_history", + "account": publicAddress, + "count": "-1", + }), + proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, + ); + final data = await jsonDecode(response.body); + final transactions = + data["history"] is List ? data["history"] as List : []; + if (transactions.isEmpty) { + return; + } else { + List> transactionList = []; + for (var tx in transactions) { + var typeString = tx["type"].toString(); + TransactionType transactionType = TransactionType.unknown; + if (typeString == "send") { + transactionType = TransactionType.outgoing; + } else if (typeString == "receive") { + transactionType = TransactionType.incoming; + } + final amount = Amount( + rawValue: BigInt.parse(tx["amount"].toString()), + fractionDigits: cryptoCurrency.fractionDigits, + ); + + var transaction = Transaction( + walletId: walletId, + txid: tx["hash"].toString(), + timestamp: int.parse(tx["local_timestamp"].toString()), + type: transactionType, + subType: TransactionSubType.none, + amount: 0, + amountString: amount.toJsonString(), + fee: 0, + height: int.parse(tx["height"].toString()), + isCancelled: false, + isLelantus: false, + slateId: "", + otherData: "", + inputs: [], + outputs: [], + nonce: 0, + numberOfMessages: null, + ); + + Address address = transactionType == TransactionType.incoming + ? receivingAddress + : Address( + walletId: walletId, + publicKey: [], + value: tx["account"].toString(), + derivationIndex: 0, + derivationPath: null, + type: info.coin.primaryAddressType, + subType: AddressSubType.nonWallet, + ); + Tuple2 tuple = Tuple2(transaction, address); + transactionList.add(tuple); + } + + await mainDB.addNewTransactionData(transactionList, walletId); + } + } + + @override + Future updateBalance() async { + try { + final addressString = + (_cachedAddress ??= (await getCurrentReceivingAddress())!).value; + final body = jsonEncode({ + "action": "account_balance", + "account": addressString, + }); + final headers = { + "Content-Type": "application/json", + }; + + final response = await _httpClient.post( + url: Uri.parse(getCurrentNode().host), + headers: headers, + body: body, + proxyInfo: + prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, + ); + final data = jsonDecode(response.body); + final balance = Balance( + total: Amount( + rawValue: (BigInt.parse(data["balance"].toString()) + + BigInt.parse(data["receivable"].toString())), + fractionDigits: cryptoCurrency.fractionDigits, + ), + spendable: Amount( + rawValue: BigInt.parse(data["balance"].toString()), + fractionDigits: cryptoCurrency.fractionDigits, + ), + blockedTotal: Amount( + rawValue: BigInt.parse("0"), + fractionDigits: cryptoCurrency.fractionDigits, + ), + pendingSpendable: Amount( + rawValue: BigInt.parse(data["receivable"].toString()), + fractionDigits: cryptoCurrency.fractionDigits, + ), + ); + + await info.updateBalance(newBalance: balance, isar: mainDB.isar); + } catch (e, s) { + Logging.instance.log( + "Failed to update ${cryptoCurrency.runtimeType} balance: $e\n$s", + level: LogLevel.Warning, + ); + } + } + + @override + Future updateChainHeight() async { + try { + final String publicAddress = + (_cachedAddress ??= (await getCurrentReceivingAddress())!).value; + + final infoBody = jsonEncode({ + "action": "account_info", + "account": publicAddress, + }); + final headers = { + "Content-Type": "application/json", + }; + final infoResponse = await _httpClient.post( + url: Uri.parse(getCurrentNode().host), + headers: headers, + body: infoBody, + proxyInfo: + prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, + ); + final infoData = jsonDecode(infoResponse.body); + + final height = int.tryParse( + infoData["confirmation_height"].toString(), + ) ?? + 0; + + await info.updateCachedChainHeight(newHeight: height, isar: mainDB.isar); + } catch (e, s) { + Logging.instance.log( + "Failed to update ${cryptoCurrency.runtimeType} chain height: $e\n$s", + level: LogLevel.Warning, + ); + } + } + + @override + FilterOperation? get changeAddressFilterOperation => + FilterGroup.and(standardChangeAddressFilters); + + @override + FilterOperation? get receivingAddressFilterOperation => + FilterGroup.and(standardReceivingAddressFilters); + + @override + Future updateUTXOs() async { + // do nothing for nano based coins + } + + @override + // nano has no fees + Future estimateFeeFor(Amount amount, int feeRate) async => Amount( + rawValue: BigInt.from(0), + fractionDigits: cryptoCurrency.fractionDigits, + ); + + @override + // nano has no fees + Future get fees async => FeeObject( + numberOfBlocksFast: 1, + numberOfBlocksAverage: 1, + numberOfBlocksSlow: 1, + fast: 0, + medium: 0, + slow: 0, + ); +} diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 3b46e8dd3..f05607160 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -22,11 +22,13 @@ import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; +import 'package:stackwallet/wallets/wallet/impl/banano_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/bitcoincash_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/dogecoin_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/ecash_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/epiccash_wallet.dart'; +import 'package:stackwallet/wallets/wallet/impl/nano_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/wownero_wallet.dart'; import 'package:stackwallet/wallets/wallet/mixins/electrumx.dart'; import 'package:stackwallet/wallets/wallet/mixins/mnemonic_based_wallet.dart'; @@ -238,6 +240,9 @@ abstract class Wallet { required WalletInfo walletInfo, }) { switch (walletInfo.coin) { + case Coin.banano: + return BananoWallet(CryptoCurrencyNetwork.main); + case Coin.bitcoin: return BitcoinWallet(CryptoCurrencyNetwork.main); case Coin.bitcoinTestNet: @@ -259,6 +264,9 @@ abstract class Wallet { case Coin.epicCash: return EpiccashWallet(CryptoCurrencyNetwork.main); + case Coin.nano: + return NanoWallet(CryptoCurrencyNetwork.main); + case Coin.wownero: return WowneroWallet(CryptoCurrencyNetwork.main);