From 3c524b2ea4f5ad2cfe9101194c6b4946f2ffd6d8 Mon Sep 17 00:00:00 2001 From: fossephate Date: Tue, 30 May 2023 19:19:31 -0400 Subject: [PATCH 1/2] add banano support --- .../isar/models/blockchain_data/address.dart | 5 +- .../models/blockchain_data/address.g.dart | 2 + .../add_edit_node_view.dart | 1 + .../wallet_view/sub_widgets/desktop_send.dart | 6 +- lib/services/coins/coin_service.dart | 10 + lib/services/coins/nano/banano_wallet.dart | 812 ++++++++++++++++++ lib/services/price.dart | 2 +- lib/themes/color_theme.dart | 3 + lib/themes/stack_colors.dart | 2 + lib/utilities/address_utils.dart | 2 + lib/utilities/block_explorers.dart | 2 + lib/utilities/constants.dart | 19 +- lib/utilities/default_nodes.dart | 12 + lib/utilities/enums/coin_enum.dart | 20 +- .../enums/derive_path_type_enum.dart | 1 + 15 files changed, 890 insertions(+), 9 deletions(-) create mode 100644 lib/services/coins/nano/banano_wallet.dart diff --git a/lib/models/isar/models/blockchain_data/address.dart b/lib/models/isar/models/blockchain_data/address.dart index dd06ccb84..f34e75a49 100644 --- a/lib/models/isar/models/blockchain_data/address.dart +++ b/lib/models/isar/models/blockchain_data/address.dart @@ -124,7 +124,8 @@ enum AddressType { unknown, nonWallet, ethereum, - nano; + nano, + banano; String get readableName { switch (this) { @@ -146,6 +147,8 @@ enum AddressType { return "Ethereum"; case AddressType.nano: return "Nano"; + case AddressType.banano: + return "Banano"; } } } diff --git a/lib/models/isar/models/blockchain_data/address.g.dart b/lib/models/isar/models/blockchain_data/address.g.dart index 5498a0434..356904d2c 100644 --- a/lib/models/isar/models/blockchain_data/address.g.dart +++ b/lib/models/isar/models/blockchain_data/address.g.dart @@ -262,6 +262,7 @@ const _AddresstypeEnumValueMap = { 'nonWallet': 6, 'ethereum': 7, 'nano': 8, + 'banano': 9, }; const _AddresstypeValueEnumMap = { 0: AddressType.p2pkh, @@ -273,6 +274,7 @@ const _AddresstypeValueEnumMap = { 6: AddressType.nonWallet, 7: AddressType.ethereum, 8: AddressType.nano, + 9: AddressType.banano, }; Id _addressGetId(Address object) { diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index cd6ab35ff..d0c11f638 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -718,6 +718,7 @@ class _NodeFormState extends ConsumerState { case Coin.dogecoinTestNet: case Coin.epicCash: case Coin.nano: + case Coin.banano: case Coin.eCash: return false; diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index e16c976c3..ac920a966 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -1412,7 +1412,7 @@ class _DesktopSendState extends ConsumerState { const SizedBox( height: 20, ), - if (!([Coin.nano, Coin.epicCash].contains(coin))) + if (!([Coin.nano, Coin.banano, Coin.epicCash].contains(coin))) Text( "Transaction fee (${coin == Coin.ethereum ? "max" : "estimated"})", style: STextStyles.desktopTextExtraSmall(context).copyWith( @@ -1422,11 +1422,11 @@ class _DesktopSendState extends ConsumerState { ), textAlign: TextAlign.left, ), - if (!([Coin.nano, Coin.epicCash].contains(coin))) + if (!([Coin.nano, Coin.banano, Coin.epicCash].contains(coin))) const SizedBox( height: 10, ), - if (!([Coin.nano, Coin.epicCash].contains(coin))) + if (!([Coin.nano, Coin.banano, Coin.epicCash].contains(coin))) DesktopFeeDropDown( walletId: walletId, ), diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index db47342b3..2d5cceba2 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -14,6 +14,7 @@ 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/banano_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/wownero/wownero_wallet.dart'; @@ -234,6 +235,15 @@ abstract class CoinServiceAPI { secureStore: secureStorageInterface ); + case Coin.banano: + return BananoWallet( + walletId: walletId, + walletName: walletName, + coin: coin, + tracker: tracker, + secureStore: secureStorageInterface + ); + case Coin.dogecoinTestNet: return DogecoinWallet( walletId: walletId, diff --git a/lib/services/coins/nano/banano_wallet.dart b/lib/services/coins/nano/banano_wallet.dart new file mode 100644 index 000000000..cd1efa429 --- /dev/null +++ b/lib/services/coins/nano/banano_wallet.dart @@ -0,0 +1,812 @@ +import 'dart:convert'; + +import 'package:isar/isar.dart'; +import 'package:nanodart/nanodart.dart'; +import 'package:http/http.dart' as http; + +import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; +import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/services/coins/coin_service.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/coin_control_interface.dart'; +import 'package:stackwallet/services/mixins/wallet_cache.dart'; +import 'package:stackwallet/services/mixins/wallet_db.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/log_level_enum.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:tuple/tuple.dart'; + +import '../../../db/isar/main_db.dart'; +import '../../../models/isar/models/blockchain_data/address.dart'; +import '../../../models/node_model.dart'; +import '../../../utilities/default_nodes.dart'; +import '../../../utilities/flutter_secure_storage_interface.dart'; +import '../../../utilities/prefs.dart'; +import '../../node_service.dart'; +import '../../transaction_notification_tracker.dart'; + +import 'dart:async'; + +import 'package:stackwallet/models/isar/models/isar_models.dart'; + +const int MINIMUM_CONFIRMATIONS = 1; +const String DEFAULT_REPRESENTATIVE = + "ban_1ka1ium4pfue3uxtntqsrib8mumxgazsjf58gidh1xeo5te3whsq8z476goo"; + +class BananoWallet extends CoinServiceAPI + with WalletCache, WalletDB, CoinControlInterface { + 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; + + bool _shouldAutoSync = false; + + @override + bool get shouldAutoSync => _shouldAutoSync; + + @override + set shouldAutoSync(bool shouldAutoSync) => _shouldAutoSync = shouldAutoSync; + + @override + Balance get balance => _balance ??= getCachedBalance(); + Balance? _balance; + + Future requestWork(String hash) async { + return http + .post( + Uri.parse("https://rpc.nano.to"), // this should be a + headers: {'Content-type': 'application/json'}, + body: json.encode( + { + "action": "work_generate", + "hash": hash, + }, + ), + ) + .then((http.Response response) { + if (response.statusCode == 200) { + final Map decoded = + json.decode(response.body) as Map; + if (decoded.containsKey("error")) { + throw Exception("Received error ${decoded["error"]}"); + } + return decoded["work"] as String?; + } else { + throw Exception("Received error ${response.statusCode}"); + } + }); + } + + @override + Future confirmSend({required Map txData}) async { + try { + // our address: + final String publicAddress = await getAddressFromMnemonic(); + + // first get the account balance: + final balanceBody = jsonEncode({ + "action": "account_balance", + "account": publicAddress, + }); + final headers = { + "Content-Type": "application/json", + }; + final balanceResponse = await http.post( + Uri.parse(getCurrentNode().host), + headers: headers, + body: balanceBody, + ); + 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 http.post( + Uri.parse(getCurrentNode().host), + headers: headers, + body: infoBody, + ); + + 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 http.post( + Uri.parse(getCurrentNode().host), + headers: headers, + body: processBody, + ); + + final Map decoded = + json.decode(processResponse.body) as Map; + if (decoded.containsKey("error")) { + throw Exception("Received error ${decoded["error"]}"); + } + + // return the hash of the transaction: + return decoded["hash"].toString(); + } catch (e, s) { + Logging.instance + .log("Error sending transaction $e - $s", level: LogLevel.Error); + rethrow; + } + } + + @override + Future get currentReceivingAddress => 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; + } + + @override + // TODO: implement fees + Future get fees => throw UnimplementedError(); + + Future updateBalance() async { + final body = jsonEncode({ + "action": "account_balance", + "account": await getAddressFromMnemonic(), + }); + final headers = { + "Content-Type": "application/json", + }; + final response = await http.post(Uri.parse(getCurrentNode().host), + headers: headers, body: body); + 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 getAddressFromMnemonic(); + + // 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 http.post( + Uri.parse(getCurrentNode().host), + headers: headers, + body: infoBody, + ); + 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 http.post( + Uri.parse(getCurrentNode().host), + headers: headers, + body: balanceBody, + ); + + 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 http.post( + Uri.parse(getCurrentNode().host), + headers: headers, + body: processBody, + ); + + 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 http.post(Uri.parse(getCurrentNode().host), + headers: {"Content-Type": "application/json"}, + body: jsonEncode({ + "action": "receivable", + "source": "true", + "account": await getAddressFromMnemonic(), + "count": "-1", + })); + + 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 String publicAddress = await getAddressFromMnemonic(); + final response = await http.post(Uri.parse(getCurrentNode().host), + headers: {"Content-Type": "application/json"}, + body: jsonEncode({ + "action": "account_history", + "account": publicAddress, + "count": "-1", + })); + final data = await jsonDecode(response.body); + final transactions = 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, + ); + + Address address = Address( + walletId: walletId, + publicKey: [], + value: transactionType == TransactionType.incoming + ? publicAddress + : tx["account"].toString(), + derivationIndex: 0, + derivationPath: null, + type: transactionType == TransactionType.incoming + ? AddressType.nonWallet + : AddressType.banano, + subType: transactionType == TransactionType.incoming + ? AddressSubType.receiving + : AddressSubType.nonWallet, + ); + Tuple2 tuple = Tuple2(transaction, address); + transactionList.add(tuple); + } + + await db.addNewTransactionData(transactionList, walletId); + + return; + } + } + + @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() 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.unknown, + 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 { + int satAmount = amount.raw.toInt(); + int realfee = 0; + + if (balance.spendable == amount) { + satAmount = balance.spendable.raw.toInt() - realfee; + } + + Map txData = { + "fee": realfee, + "addresss": address, + "recipientAmt": Amount( + rawValue: BigInt.from(satAmount), + fractionDigits: coin.decimals, + ), + }; + + 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: [], // TODO: add public key + derivationIndex: 0, + derivationPath: null, + type: AddressType.unknown, + subType: AddressSubType.receiving, + ); + + await db.putAddress(address); + + await Future.wait( + [updateCachedId(walletId), updateCachedIsFavorite(false)]); + } catch (e) { + rethrow; + } + } + + @override + Future refresh() async { + await _prefs.init(); + + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + coin, + ), + ); + + try { + await updateChainHeight(); + await updateTransactions(); + await updateBalance(); + + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + coin, + ), + ); + } catch (e) { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + coin, + ), + ); + } + } + + @override + int get storedChainHeight => getCachedChainHeight(); + + NodeModel getCurrentNode() { + return _xnoNode ?? + NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? + DefaultNodes.getNodeFor(coin); + } + + @override + Future testNetworkConnection() { + http + .get(Uri.parse("${getCurrentNode().host}?action=version")) + .then((response) { + if (response.statusCode == 200) { + return true; + } + }); + return Future.value(false); + } + + @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) { + // TODO: implement updateSentCachedTxData + throw UnimplementedError(); + } + + @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 getAddressFromMnemonic(); + // first get the account balance: + final infoBody = jsonEncode({ + "action": "account_info", + "account": publicAddress, + }); + final headers = { + "Content-Type": "application/json", + }; + final infoResponse = await http.post( + Uri.parse(getCurrentNode().host), + headers: headers, + body: infoBody, + ); + final infoData = jsonDecode(infoResponse.body); + + final int height = int.parse(infoData["confirmation_height"].toString()); + await updateCachedChainHeight(height); + } +} diff --git a/lib/services/price.dart b/lib/services/price.dart index a38fa24e8..2095c066d 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -90,7 +90,7 @@ class PriceAPI { Uri.parse("https://api.coingecko.com/api/v3/coins/markets?vs_currency" "=${baseCurrency.toLowerCase()}" "&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin," - "bitcoin-cash,namecoin,wownero,ethereum,particl,nano" + "bitcoin-cash,namecoin,wownero,ethereum,particl,nano,ban" "&order=market_cap_desc&per_page=50&page=1&sparkline=false"); final coinGeckoResponse = await client.get( diff --git a/lib/themes/color_theme.dart b/lib/themes/color_theme.dart index 63ca1a4cb..ed7d7514f 100644 --- a/lib/themes/color_theme.dart +++ b/lib/themes/color_theme.dart @@ -19,6 +19,7 @@ class CoinThemeColorDefault { Color get wownero => const Color(0xFFED80C1); Color get particl => const Color(0xFF8175BD); Color get nano => const Color(0xFF209CE9); + Color get banano => const Color(0xFFFBDD11); Color forCoin(Coin coin) { switch (coin) { @@ -53,6 +54,8 @@ class CoinThemeColorDefault { return particl; case Coin.nano: return nano; + case Coin.banano: + return banano; } } } diff --git a/lib/themes/stack_colors.dart b/lib/themes/stack_colors.dart index 879f0a397..d14051b71 100644 --- a/lib/themes/stack_colors.dart +++ b/lib/themes/stack_colors.dart @@ -1699,6 +1699,8 @@ class StackColors extends ThemeExtension { return _coin.particl; case Coin.nano: return _coin.nano; + case Coin.banano: + return _coin.banano; } } diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index eacbfa04a..e62234869 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -77,6 +77,8 @@ class AddressUtils { return Address.validateAddress(address, particl); case Coin.nano: return NanoAccounts.isValid(NanoAccountType.NANO, address); + case Coin.banano: + return NanoAccounts.isValid(NanoAccountType.BANANO, address); case Coin.bitcoinTestNet: return Address.validateAddress(address, testnet); case Coin.litecoinTestNet: diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index 32cb361ca..8ea5cca38 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -45,6 +45,8 @@ Uri getDefaultBlockExplorerUrlFor({ return Uri.parse("https://chainz.cryptoid.info/part/tx.dws?$txid.htm"); case Coin.nano: return Uri.parse("https://www.nanolooker.com/block/$txid"); + case Coin.banano: + return Uri.parse("https://www.bananolooker.com/block/$txid"); } } diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 2ea630c13..6e63558ed 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -30,10 +30,13 @@ abstract class Constants { static final BigInt _satsPerCoinMonero = BigInt.from(1000000000000); static final BigInt _satsPerCoinWownero = BigInt.from(100000000000); static final BigInt _satsPerCoinNano = - BigInt.parse("1000000000000000000000000000000"); + BigInt.parse("1000000000000000000000000000000");// 1*10^30 + static final BigInt _satsPerCoinBanano = + BigInt.parse("100000000000000000000000000000");// 1*10^29 static final BigInt _satsPerCoin = BigInt.from(100000000); static const int _decimalPlaces = 8; static const int _decimalPlacesNano = 30; + static const int _decimalPlacesBanano = 29; static const int _decimalPlacesWownero = 11; static const int _decimalPlacesMonero = 12; static const int _decimalPlacesEthereum = 18; @@ -71,6 +74,9 @@ abstract class Constants { case Coin.nano: return _satsPerCoinNano; + case Coin.banano: + return _satsPerCoinBanano; + case Coin.wownero: return _satsPerCoinWownero; @@ -105,6 +111,9 @@ abstract class Constants { case Coin.nano: return _decimalPlacesNano; + case Coin.banano: + return _decimalPlacesBanano; + case Coin.wownero: return _decimalPlacesWownero; @@ -138,7 +147,10 @@ abstract class Constants { case Coin.namecoin: case Coin.particl: case Coin.nano: - values.addAll([24, 21, 18, 15, 12]); + values.addAll([24, 12]); + break; + case Coin.banano: + values.addAll([24, 12]); break; case Coin.monero: @@ -192,7 +204,8 @@ abstract class Constants { return 600; case Coin.nano: // TODO: Verify this - return 3; + case Coin.banano: // TODO: Verify this + return 1; } } diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index ee44df9e3..a64ba91ea 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -183,6 +183,18 @@ abstract class DefaultNodes { isDown: false ); + static NodeModel get banano => NodeModel( + host: "https://kaliumapi.appditto.com/api", + port: 443, + name: defaultName, + id: _nodeId(Coin.banano), + useSSL: true, + enabled: true, + coinName: Coin.banano.name, + isFailover: true, + isDown: false + ); + static NodeModel get bitcoinTestnet => NodeModel( host: "bitcoin-testnet.stackwallet.com", port: 51002, diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 8173e3147..6546b07d8 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -34,6 +34,7 @@ enum Coin { particl, wownero, nano, + banano, /// @@ -79,6 +80,8 @@ extension CoinExt on Coin { return "Namecoin"; case Coin.nano: return "Nano"; + case Coin.banano: + return "Banano"; case Coin.bitcoinTestNet: return "tBitcoin"; case Coin.litecoinTestNet: @@ -120,6 +123,8 @@ extension CoinExt on Coin { return "NMC"; case Coin.nano: return "XNO"; + case Coin.banano: + return "BAN"; case Coin.bitcoinTestNet: return "tBTC"; case Coin.litecoinTestNet: @@ -162,6 +167,8 @@ extension CoinExt on Coin { return "namecoin"; case Coin.nano: return "nano"; + case Coin.banano: + return "ban"; case Coin.bitcoinTestNet: return "bitcoin"; case Coin.litecoinTestNet: @@ -196,7 +203,8 @@ extension CoinExt on Coin { case Coin.ethereum: case Coin.monero: case Coin.wownero: - case Coin.nano: // TODO: Check this + case Coin.nano: + case Coin.banano: return false; } } @@ -223,6 +231,7 @@ extension CoinExt on Coin { case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.nano: + case Coin.banano: return false; } } @@ -241,6 +250,7 @@ extension CoinExt on Coin { case Coin.monero: case Coin.wownero: case Coin.nano: + case Coin.banano: case Coin.eCash: return false; @@ -267,6 +277,7 @@ extension CoinExt on Coin { case Coin.monero: case Coin.wownero: case Coin.nano: + case Coin.banano: case Coin.eCash: return this; @@ -331,6 +342,7 @@ extension CoinExt on Coin { return nmc.MINIMUM_CONFIRMATIONS; case Coin.nano: + case Coin.banano: return nano.MINIMUM_CONFIRMATIONS; } } @@ -422,6 +434,10 @@ Coin coinFromPrettyName(String name) { case "nano": return Coin.nano; + case "Banano": + case "banano": + return Coin.banano; + default: throw ArgumentError.value( name, @@ -469,6 +485,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) { return Coin.wownero; case "xno": return Coin.nano; + case "ban": + return Coin.banano; default: throw ArgumentError.value( ticker, "name", "No Coin enum value with that ticker"); diff --git a/lib/utilities/enums/derive_path_type_enum.dart b/lib/utilities/enums/derive_path_type_enum.dart index 8fa325e3b..86019ca55 100644 --- a/lib/utilities/enums/derive_path_type_enum.dart +++ b/lib/utilities/enums/derive_path_type_enum.dart @@ -38,6 +38,7 @@ extension DerivePathTypeExt on DerivePathType { case Coin.monero: case Coin.wownero: case Coin.nano: + case Coin.banano: throw UnsupportedError( "$coin does not use bitcoin style derivation paths"); } From 5ac6df5d446bd389401121e2716f5fbd916da671 Mon Sep 17 00:00:00 2001 From: fossephate Date: Tue, 30 May 2023 19:29:33 -0400 Subject: [PATCH 2/2] small fix --- lib/utilities/default_nodes.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index a64ba91ea..8dd348aa7 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -180,8 +180,7 @@ abstract class DefaultNodes { enabled: true, coinName: Coin.nano.name, isFailover: true, - isDown: false - ); + isDown: false); static NodeModel get banano => NodeModel( host: "https://kaliumapi.appditto.com/api", @@ -192,8 +191,7 @@ abstract class DefaultNodes { enabled: true, coinName: Coin.banano.name, isFailover: true, - isDown: false - ); + isDown: false); static NodeModel get bitcoinTestnet => NodeModel( host: "bitcoin-testnet.stackwallet.com", @@ -296,6 +294,9 @@ abstract class DefaultNodes { case Coin.nano: return nano; + case Coin.banano: + return banano; + case Coin.bitcoinTestNet: return bitcoinTestnet;