diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..eba0d6cd7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dart.lineLength": 80, +} \ No newline at end of file diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index 73d257ed2..81659ce57 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit 73d257ed2fe5b204cf3589822e226301b187b86d +Subproject commit 81659ce57952c5ab54ffe6bacfbf43da159fff3e diff --git a/lib/services/coins/nano/nano_wallet.dart b/lib/services/coins/nano/nano_wallet.dart index f019cde87..87a966721 100644 --- a/lib/services/coins/nano/nano_wallet.dart +++ b/lib/services/coins/nano/nano_wallet.dart @@ -32,7 +32,8 @@ import 'package:stackwallet/models/isar/models/isar_models.dart'; const int MINIMUM_CONFIRMATIONS = 1; -class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlInterface { +class NanoWallet extends CoinServiceAPI + with WalletCache, WalletDB, CoinControlInterface { NanoWallet({ required String walletId, required String walletName, @@ -58,7 +59,8 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlI ); @override - Future get mnemonicString => _secureStore.read(key: '${_walletId}_mnemonic'); + Future get mnemonicString => + _secureStore.read(key: '${_walletId}_mnemonic'); Future getSeedFromMnemonic() async { var mnemonic = await mnemonicString; @@ -74,8 +76,8 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlI 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))); + var address = NanoAccounts.createAccount(NanoAccountType.NANO, + NanoKeys.createPublicKey(NanoKeys.seedToPrivate(seed, 0))); return address; } @@ -130,10 +132,10 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlI Balance get balance => _balance ??= getCachedBalance(); Balance? _balance; - Future requestWork(String url, String hash) async { + Future requestWork(String hash) async { return http .post( - Uri.parse(url), + Uri.parse("https://rpc.nano.to"), headers: {'Content-type': 'application/json'}, body: json.encode( { @@ -144,7 +146,8 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlI ) .then((http.Response response) { if (response.statusCode == 200) { - final Map decoded = json.decode(response.body) as Map; + final Map decoded = + json.decode(response.body) as Map; if (decoded.containsKey("error")) { throw Exception("Received error ${decoded["error"]}"); } @@ -157,12 +160,14 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlI @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": await getAddressFromMnemonic(), + "account": publicAddress, }); final headers = { "Content-Type": "application/json", @@ -174,7 +179,8 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlI ); final balanceData = jsonDecode(balanceResponse.body); - final BigInt currentBalance = BigInt.parse(balanceData["balance"].toString()); + final BigInt currentBalance = + BigInt.parse(balanceData["balance"].toString()); final BigInt txAmount = txData["recipientAmt"].raw as BigInt; final BigInt balanceAfterTx = currentBalance - txAmount; @@ -182,7 +188,7 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlI final infoBody = jsonEncode({ "action": "account_info", "representative": "true", - "account": await getAddressFromMnemonic(), + "account": publicAddress, }); final infoResponse = await http.post( Uri.parse(getCurrentNode().host), @@ -190,16 +196,17 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlI body: infoBody, ); - final String frontier = jsonDecode(infoResponse.body)["frontier"].toString(); - final String representative = jsonDecode(infoResponse.body)["representative"].toString(); - // our address: - final String publicAddress = await getAddressFromMnemonic(); + 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 link = + NanoAccounts.extractPublicKey(txData["address"].toString()); final String linkAsAccount = txData["address"].toString(); // construct the send block: - final Map sendBlock = { + Map sendBlock = { "type": "state", "account": publicAddress, "previous": frontier, @@ -221,29 +228,20 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlI final String signature = NanoSignatures.signBlock(hash, privateKey); // get PoW for the send block: - final String? work = await requestWork("https://rpc.nano.to", frontier); + final String? work = await requestWork(frontier); if (work == null) { throw Exception("Failed to get PoW for send block"); } - // process the send block: - final Map finalSendBlock = { - "type": "state", - "account": publicAddress, - "previous": frontier, - "representative": representative, - "balance": balanceAfterTx.toString(), - "link": link, - "link_as_account": linkAsAccount, - "signature": signature, - "work": work, - }; + sendBlock["link_as_account"] = linkAsAccount; + sendBlock["signature"] = signature; + sendBlock["work"] = work; final processBody = jsonEncode({ "action": "process", "json_block": "true", "subtype": "send", - "block": finalSendBlock, + "block": sendBlock, }); final processResponse = await http.post( Uri.parse(getCurrentNode().host), @@ -251,7 +249,8 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlI body: processBody, ); - final Map decoded = json.decode(processResponse.body) as Map; + final Map decoded = + json.decode(processResponse.body) as Map; if (decoded.containsKey("error")) { throw Exception("Received error ${decoded["error"]}"); } @@ -259,7 +258,8 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlI // return the hash of the transaction: return decoded["hash"].toString(); } catch (e, s) { - Logging.instance.log("Error sending transaction $e - $s", level: LogLevel.Error); + Logging.instance + .log("Error sending transaction $e - $s", level: LogLevel.Error); rethrow; } } @@ -290,24 +290,162 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlI final headers = { "Content-Type": "application/json", }; - final response = await http.post(Uri.parse(getCurrentNode().host), headers: headers, body: body); + 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())*/) ~/ + rawValue: (BigInt.parse(data["balance"] + .toString()) /*+ BigInt.parse(data["receivable"].toString())*/) ~/ + BigInt.from(10).pow(23), + fractionDigits: 7), + spendable: Amount( + rawValue: BigInt.parse(data["balance"].toString()) ~/ BigInt.from(10).pow(23), fractionDigits: 7), - spendable: - Amount(rawValue: BigInt.parse(data["balance"].toString()) ~/ BigInt.from(10).pow(23), fractionDigits: 7), blockedTotal: Amount(rawValue: BigInt.parse("0"), fractionDigits: 30), - pendingSpendable: - Amount(rawValue: BigInt.parse(data["receivable"].toString()) ~/ BigInt.from(10).pow(23), fractionDigits: 7), + pendingSpendable: Amount( + rawValue: BigInt.parse(data["receivable"].toString()) ~/ + BigInt.from(10).pow(23), + fractionDigits: 7), ); 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; + + // 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 = 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 = jsonDecode(infoResponse.body)["frontier"].toString(); + final String representative = + jsonDecode(infoResponse.body)["representative"].toString(); + + // 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 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 { - // TODO: Implement this function + 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 { @@ -363,7 +501,8 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlI } @override - Future fullRescan(int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) async { + Future fullRescan( + int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) async { await _prefs.init(); await updateBalance(); await updateTransactions(); @@ -387,7 +526,8 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlI @override Future initializeNew() async { if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) { - throw Exception("Attempted to overwrite mnemonic on generate new wallet!"); + throw Exception( + "Attempted to overwrite mnemonic on generate new wallet!"); } await _prefs.init(); @@ -404,7 +544,8 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlI ); String privateKey = NanoKeys.seedToPrivate(seed, 0); String publicKey = NanoKeys.createPublicKey(privateKey); - String publicAddress = NanoAccounts.createAccount(NanoAccountType.NANO, publicKey); + String publicAddress = + NanoAccounts.createAccount(NanoAccountType.NANO, publicKey); final address = Address( walletId: walletId, @@ -418,7 +559,8 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlI await db.putAddress(address); - await Future.wait([updateCachedId(walletId), updateCachedIsFavorite(false)]); + await Future.wait( + [updateCachedId(walletId), updateCachedIsFavorite(false)]); } @override @@ -485,11 +627,13 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlI required int maxNumberOfIndexesToCheck, required int height}) async { try { - if ((await mnemonicString) != null || (await this.mnemonicPassphrase) != null) { + 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}_mnemonic', value: mnemonic.trim()); await _secureStore.write( key: '${_walletId}_mnemonicPassphrase', value: mnemonicPassphrase ?? "", @@ -498,21 +642,24 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlI String seed = NanoMnemomics.mnemonicListToSeed(mnemonic.split(" ")); String privateKey = NanoKeys.seedToPrivate(seed, 0); String publicKey = NanoKeys.createPublicKey(privateKey); - String publicAddress = NanoAccounts.createAccount(NanoAccountType.NANO, publicKey); + String publicAddress = + NanoAccounts.createAccount(NanoAccountType.NANO, publicKey); final address = Address( walletId: walletId, value: publicAddress, publicKey: [], // TODO: add public key derivationIndex: 0, - derivationPath: DerivationPath()..value = "0/0", // TODO: Check if this is true + derivationPath: DerivationPath() + ..value = "0/0", // TODO: Check if this is true type: AddressType.unknown, subType: AddressSubType.receiving, ); await db.putAddress(address); - await Future.wait([updateCachedId(walletId), updateCachedIsFavorite(false)]); + await Future.wait( + [updateCachedId(walletId), updateCachedIsFavorite(false)]); } catch (e) { rethrow; } @@ -530,13 +677,16 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlI NodeModel getCurrentNode() { return _xnoNode ?? - NodeService(secureStorageInterface: _secureStore).getPrimaryNodeFor(coin: coin) ?? + NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); } @override Future testNetworkConnection() { - http.get(Uri.parse("${getCurrentNode().host}?action=version")).then((response) { + http + .get(Uri.parse("${getCurrentNode().host}?action=version")) + .then((response) { if (response.statusCode == 200) { return true; } @@ -545,11 +695,13 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB, CoinControlI } @override - Future> get transactions => db.getTransactions(walletId).findAll(); + Future> get transactions => + db.getTransactions(walletId).findAll(); @override Future updateNode(bool shouldRefresh) async { - _xnoNode = NodeService(secureStorageInterface: _secureStore).getPrimaryNodeFor(coin: coin) ?? + _xnoNode = NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); if (shouldRefresh) {