diff --git a/lib/models/isar/models/blockchain_data/address.dart b/lib/models/isar/models/blockchain_data/address.dart index 25281a629..dd06ccb84 100644 --- a/lib/models/isar/models/blockchain_data/address.dart +++ b/lib/models/isar/models/blockchain_data/address.dart @@ -123,7 +123,8 @@ enum AddressType { mimbleWimble, unknown, nonWallet, - ethereum; + ethereum, + nano; String get readableName { switch (this) { @@ -143,6 +144,8 @@ enum AddressType { return "Non wallet/unknown"; case AddressType.ethereum: return "Ethereum"; + case AddressType.nano: + return "Nano"; } } } diff --git a/lib/models/isar/models/blockchain_data/address.g.dart b/lib/models/isar/models/blockchain_data/address.g.dart index 49188b395..5498a0434 100644 --- a/lib/models/isar/models/blockchain_data/address.g.dart +++ b/lib/models/isar/models/blockchain_data/address.g.dart @@ -261,6 +261,7 @@ const _AddresstypeEnumValueMap = { 'unknown': 5, 'nonWallet': 6, 'ethereum': 7, + 'nano': 8, }; const _AddresstypeValueEnumMap = { 0: AddressType.p2pkh, @@ -271,6 +272,7 @@ const _AddresstypeValueEnumMap = { 5: AddressType.unknown, 6: AddressType.nonWallet, 7: AddressType.ethereum, + 8: AddressType.nano, }; Id _addressGetId(Address object) { diff --git a/lib/services/coins/nano/nano_wallet.dart b/lib/services/coins/nano/nano_wallet.dart index 87a966721..d275a7003 100644 --- a/lib/services/coins/nano/nano_wallet.dart +++ b/lib/services/coins/nano/nano_wallet.dart @@ -9,6 +9,8 @@ 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'; @@ -16,6 +18,7 @@ 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'; @@ -31,6 +34,8 @@ import 'dart:async'; import 'package:stackwallet/models/isar/models/isar_models.dart'; const int MINIMUM_CONFIRMATIONS = 1; +const String DEFAULT_REPRESENTATIVE = + "nano_38713x95zyjsqzx6nm1dsom1jmm668owkeb9913ax6nfgj15az3nu8xkx579"; class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlInterface { @@ -135,7 +140,7 @@ class NanoWallet extends CoinServiceAPI Future requestWork(String hash) async { return http .post( - Uri.parse("https://rpc.nano.to"), + Uri.parse("https://rpc.nano.to"), // this should be a headers: {'Content-type': 'application/json'}, body: json.encode( { @@ -177,8 +182,8 @@ class NanoWallet extends CoinServiceAPI 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; @@ -270,7 +275,8 @@ class NanoWallet extends CoinServiceAPI @override Future estimateFeeFor(Amount amount, int feeRate) { // fees are always 0 :) - return Future.value(Amount(rawValue: BigInt.from(0), fractionDigits: 7)); + return Future.value( + Amount(rawValue: BigInt.from(0), fractionDigits: coin.decimals)); } @override @@ -295,19 +301,17 @@ class NanoWallet extends CoinServiceAPI final data = jsonDecode(response.body); _balance = Balance( total: Amount( - rawValue: (BigInt.parse(data["balance"] - .toString()) /*+ BigInt.parse(data["receivable"].toString())*/) ~/ - BigInt.from(10).pow(23), - fractionDigits: 7), + rawValue: (BigInt.parse(data["balance"].toString()) + + BigInt.parse(data["receivable"].toString())), + fractionDigits: coin.decimals), spendable: Amount( - rawValue: BigInt.parse(data["balance"].toString()) ~/ - BigInt.from(10).pow(23), - fractionDigits: 7), - blockedTotal: Amount(rawValue: BigInt.parse("0"), fractionDigits: 30), + 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()) ~/ - BigInt.from(10).pow(23), - fractionDigits: 7), + rawValue: BigInt.parse(data["receivable"].toString()), + fractionDigits: coin.decimals), ); await updateCachedBalance(_balance!); } @@ -317,17 +321,38 @@ class NanoWallet extends CoinServiceAPI // 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 headers = { - "Content-Type": "application/json", - }; + final balanceResponse = await http.post( Uri.parse(getCurrentNode().host), headers: headers, @@ -340,21 +365,13 @@ class NanoWallet extends CoinServiceAPI final BigInt txAmount = BigInt.parse(amountRaw); 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, - ); + String frontier = infoData["frontier"].toString(); + String representative = infoData["representative"].toString(); - String frontier = jsonDecode(infoResponse.body)["frontier"].toString(); - final String representative = - jsonDecode(infoResponse.body)["representative"].toString(); + if (openBlock) { + // we don't have a representative set yet: + representative = DEFAULT_REPRESENTATIVE; + } // link = send block hash: final String link = blockHash; @@ -450,11 +467,12 @@ class NanoWallet extends CoinServiceAPI 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": await getAddressFromMnemonic(), + "account": publicAddress, "count": "-1", })); final data = await jsonDecode(response.body); @@ -462,14 +480,14 @@ class NanoWallet extends CoinServiceAPI if (transactions.isEmpty) { return; } else { - List transactionList = []; + List> transactionList = []; for (var tx in transactions) { var typeString = tx["type"].toString(); - TransactionType type = TransactionType.unknown; + TransactionType transactionType = TransactionType.unknown; if (typeString == "send") { - type = TransactionType.outgoing; + transactionType = TransactionType.outgoing; } else if (typeString == "receive") { - type = TransactionType.incoming; + transactionType = TransactionType.incoming; } final amount = Amount( rawValue: BigInt.parse(tx["amount"].toString()), @@ -477,25 +495,45 @@ class NanoWallet extends CoinServiceAPI ); var transaction = Transaction( - walletId: walletId, - txid: tx["hash"].toString(), - timestamp: int.parse(tx["local_timestamp"].toString()), - type: type, - 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); - transactionList.add(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.nano, + subType: transactionType == TransactionType.incoming + ? AddressSubType.receiving + : AddressSubType.nonWallet, + ); + Tuple2 tuple = Tuple2(transaction, address); + transactionList.add(tuple); } - await db.putTransactions(transactionList); + + await db.addNewTransactionData(transactionList, walletId); + return; } } @@ -504,8 +542,8 @@ class NanoWallet extends CoinServiceAPI Future fullRescan( int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) async { await _prefs.init(); - await updateBalance(); await updateTransactions(); + await updateBalance(); } @override @@ -552,7 +590,7 @@ class NanoWallet extends CoinServiceAPI value: publicAddress, publicKey: [], // TODO: add public key derivationIndex: 0, - derivationPath: DerivationPath(), + derivationPath: null, type: AddressType.unknown, subType: AddressSubType.receiving, ); @@ -650,8 +688,7 @@ class NanoWallet extends CoinServiceAPI value: publicAddress, publicKey: [], // TODO: add public key derivationIndex: 0, - derivationPath: DerivationPath() - ..value = "0/0", // TODO: Check if this is true + derivationPath: null, type: AddressType.unknown, subType: AddressSubType.receiving, ); @@ -668,8 +705,36 @@ class NanoWallet extends CoinServiceAPI @override Future refresh() async { await _prefs.init(); - await updateBalance(); - await updateTransactions(); + + 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 @@ -723,4 +788,25 @@ class NanoWallet extends CoinServiceAPI bool validateAddress(String address) { return NanoAccounts.isValid(NanoAccountType.NANO, 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 16bd93c6a..a38fa24e8 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" + "bitcoin-cash,namecoin,wownero,ethereum,particl,nano" "&order=market_cap_desc&per_page=50&page=1&sparkline=false"); final coinGeckoResponse = await client.get( diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index a8b353c14..2ea630c13 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -33,7 +33,7 @@ abstract class Constants { BigInt.parse("1000000000000000000000000000000"); static final BigInt _satsPerCoin = BigInt.from(100000000); static const int _decimalPlaces = 8; - static const int _decimalPlacesNano = 6; + static const int _decimalPlacesNano = 30; static const int _decimalPlacesWownero = 11; static const int _decimalPlacesMonero = 12; static const int _decimalPlacesEthereum = 18;