import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_nano/nano_balance.dart'; import 'package:cw_nano/nano_transaction_model.dart'; import 'package:cw_nano/nano_util.dart'; import 'package:flutter/services.dart'; import 'package:http/http.dart' as http; import 'package:nanodart/nanodart.dart'; import 'package:web3dart/web3dart.dart'; import 'package:web3dart/contracts/erc20.dart'; import 'package:cw_core/node.dart'; class NanoClient { // bit of a hack since we need access to a node in a weird location: static const String BACKUP_NODE_URI = "rpc.nano.to"; static const String DEFAULT_REPRESENTATIVE = "nano_38713x95zyjsqzx6nm1dsom1jmm668owkeb9913ax6nfgj15az3nu8xkx579"; StreamSubscription? subscription; Node? _node; bool connect(Node node) { try { _node = node; return true; } catch (e) { return false; } } Future getBalance(String address) async { final response = await http.post( _node!.uri, headers: {"Content-Type": "application/json"}, body: jsonEncode( { "action": "account_balance", "account": address, }, ), ); final data = await jsonDecode(response.body); final String currentBalance = data["balance"] as String; final String receivableBalance = data["receivable"] as String; final BigInt cur = BigInt.parse(currentBalance); final BigInt rec = BigInt.parse(receivableBalance); return NanoBalance(currentBalance: cur, receivableBalance: rec); } Future getAccountInfo(String address) async { try { final response = await http.post( _node!.uri, headers: {"Content-Type": "application/json"}, body: jsonEncode( { "action": "account_info", "representative": "true", "account": address, }, ), ); final data = await jsonDecode(response.body); return data; } catch (e) { print("error while getting account info"); rethrow; } } Future changeRep({ required String privateKey, required String repAddress, required String ourAddress, }) async { try { final accountInfo = await getAccountInfo(ourAddress); // construct the change block: Map changeBlock = { "type": "state", "account": ourAddress, "previous": accountInfo["frontier"] as String, "representative": repAddress, "balance": accountInfo["balance"] as String, "link": "0000000000000000000000000000000000000000000000000000000000000000", "link_as_account": "nano_1111111111111111111111111111111111111111111111111111hifc8npp", }; // sign the change block: final String hash = NanoBlocks.computeStateHash( NanoAccountType.NANO, changeBlock["account"]!, changeBlock["previous"]!, changeBlock["representative"]!, BigInt.parse(changeBlock["balance"]!), changeBlock["link"]!, ); final String signature = NanoSignatures.signBlock(hash, privateKey); // get PoW for the send block: final String work = await requestWork(accountInfo["frontier"] as String); changeBlock["signature"] = signature; changeBlock["work"] = work; return await processBlock(changeBlock, "change"); } catch (e) { throw Exception("error while changing representative"); } } Future requestWork(String hash) async { return http .post( Uri.parse("https://rpc.nano.to"), // TODO: make a setting 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}"); } }); } Future send({ required String privateKey, required String amountRaw, required String destinationAddress, }) async { final Map sendBlock = await constructSendBlock( privateKey: privateKey, amountRaw: amountRaw, destinationAddress: destinationAddress, ); return await processBlock(sendBlock, "send"); } Future processBlock(Map block, String subtype) async { final headers = {"Content-Type": "application/json"}; final processBody = jsonEncode({ "action": "process", "json_block": "true", "subtype": subtype, "block": block, }); final processResponse = await http.post( _node!.uri, headers: headers, body: processBody, ); final Map decoded = json.decode(processResponse.body) as Map; if (decoded.containsKey("error")) { throw Exception("Received error ${decoded["error"]}"); } // return the hash of the transaction: return decoded["hash"].toString(); } Future> constructSendBlock({ required String privateKey, required String amountRaw, required String destinationAddress, BigInt? balanceAfterTx, String? previousHash, }) async { try { // our address: final String publicAddress = NanoUtil.privateKeyToAddress(privateKey); // first get the current account balance: if (balanceAfterTx == null) { final BigInt currentBalance = (await getBalance(publicAddress)).currentBalance; final BigInt txAmount = BigInt.parse(amountRaw); balanceAfterTx = currentBalance - txAmount; } // get the account info (we need the frontier and representative): final headers = {"Content-Type": "application/json"}; final infoBody = jsonEncode({ "action": "account_info", "representative": "true", "account": publicAddress, }); final infoResponse = await http.post( _node!.uri, headers: headers, body: infoBody, ); String frontier = jsonDecode(infoResponse.body)["frontier"].toString(); // override if provided: if (previousHash != null) { frontier = previousHash; } final String representative = jsonDecode(infoResponse.body)["representative"].toString(); // link = destination address: final String link = NanoAccounts.extractPublicKey(destinationAddress); final String linkAsAccount = destinationAddress; // construct the send block: Map sendBlock = { "type": "state", "account": publicAddress, "previous": frontier, "representative": representative, "balance": balanceAfterTx.toString(), "link": link, }; // sign the send block: final String hash = NanoBlocks.computeStateHash( NanoAccountType.NANO, sendBlock["account"]!, sendBlock["previous"]!, sendBlock["representative"]!, BigInt.parse(sendBlock["balance"]!), sendBlock["link"]!, ); final String signature = NanoSignatures.signBlock(hash, privateKey); // get PoW for the send block: final String work = await requestWork(frontier); sendBlock["link_as_account"] = linkAsAccount; sendBlock["signature"] = signature; sendBlock["work"] = work; // ready to post send block: return sendBlock; } catch (e) { print(e); rethrow; } } Future receiveBlock({ required String blockHash, required String source, required String amountRaw, required String destinationAddress, required String privateKey, }) async { bool openBlock = false; final headers = { "Content-Type": "application/json", }; // first check if the account is open: // get the account info (we need the frontier and representative): final infoBody = jsonEncode({ "action": "account_info", "representative": "true", "account": destinationAddress, }); final infoResponse = await http.post( _node!.uri, 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": destinationAddress, }); final balanceResponse = await http.post( _node!.uri, 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.NANO, blockHash); // construct the receive block: Map receiveBlock = { "type": "state", "account": destinationAddress, "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 signature = NanoSignatures.signBlock(hash, privateKey); // get PoW for the receive block: String? work; if (openBlock) { work = await requestWork(NanoAccounts.extractPublicKey(destinationAddress)); } else { work = await requestWork(frontier); } receiveBlock["link_as_account"] = linkAsAccount; receiveBlock["signature"] = signature; receiveBlock["work"] = work; // process the receive block: final processBody = jsonEncode({ "action": "process", "json_block": "true", "subtype": "receive", "block": receiveBlock, }); final processResponse = await http.post( _node!.uri, headers: headers, body: processBody, ); final Map decoded = json.decode(processResponse.body) as Map; if (decoded.containsKey("error")) { throw Exception("Received error ${decoded["error"]}"); } } // returns the number of blocks received: Future confirmAllReceivable({ required String destinationAddress, required String privateKey, }) async { final receivableResponse = await http.post(_node!.uri, headers: {"Content-Type": "application/json"}, body: jsonEncode({ "action": "receivable", "account": destinationAddress, "count": "-1", "source": true, })); final receivableData = await jsonDecode(receivableResponse.body); if (receivableData["blocks"] == "" || receivableData["blocks"] == null) { return 0; } dynamic blocks; if (receivableData["blocks"] is List) { var listBlocks = receivableData["blocks"] as List; if (listBlocks.isEmpty) { return 0; } blocks = {for (var block in listBlocks) block['hash']: block}; } else { blocks = receivableData["blocks"] as Map; } blocks = blocks as Map; // confirm all receivable blocks: for (final blockHash in blocks.keys) { final block = blocks[blockHash]; final String amountRaw = block["amount"] as String; final String source = block["source"] as String; await receiveBlock( blockHash: blockHash, source: source, amountRaw: amountRaw, privateKey: privateKey, destinationAddress: destinationAddress, ); // a bit of a hack: await Future.delayed(const Duration(seconds: 2)); } return blocks.keys.length; } Future getTransactionDetails(String transactionHash) async { throw UnimplementedError(); } void stop() { subscription?.cancel(); } Future> fetchTransactions(String address) async { try { final response = await http.post(_node!.uri, headers: {"Content-Type": "application/json"}, body: jsonEncode({ "action": "account_history", "account": address, "count": "250", // TODO: pick a number // "raw": true, })); final data = await jsonDecode(response.body); final transactions = data["history"] is List ? data["history"] as List : []; // Map the transactions list to NanoTransactionModel using the factory // reversed so that the DateTime is correct when local_timestamp is absent return transactions.reversed .map((transaction) => NanoTransactionModel.fromJson(transaction)) .toList(); } catch (e) { print(e); return []; } } }