cake_wallet/cw_nano/lib/nano_client.dart
Matthew Fosse 4c60b178be
CW-438 add nano (#1015)
* Fix web3dart versioning issue

* Add primary receive address extracted from private key

* Implement open wallet functionality

* Implement restore wallet from seed functionality

* Fixate web3dart version as higher versions cause some issues

* Add Initial Transaction priorities for eth
Add estimated gas price

* Rename priority value to tip

* Re-order wallet types

* Change ethereum node
Fix connection issues

* Fix estimating gas for priority

* Add case for ethereum to fetch it's seeds

* Add case for ethereum to request node

* Fix Exchange screen initial pairs

* Add initial send transaction flow

* Add missing configure for ethereum class

* Add Eth address initial setup

* Fix Private key for Ethereum wallets

* Change sign/send transaction flow

* - Fix Conflicts with main
- Remove unused function from Haven configure.dart

* Add build command for ethereum package

* Add missing Node list file to pubspec

* - Fix balance display
- Fix parsing of Ethereum amount
- Add more Ethereum Nodes [skip ci]

* - Fix extracting Ethereum Private key from seeds
- Integrate signing/sending transaction with the send view model

* - Update and Fix Conflicts with main

* Add Balances for ERC20 tokens

* Fix conflicts with main

* Add erc20 abi json

* Add send erc20 tokens initial function

* add missing getHeightByDate in Haven [skip ci]

* Allow contacts and wallets from the same tag

* Add Shiba Inu icon

* Add send ERC-20 tokens initial flow

* Add missing import in generated file

* Add initial approach for transaction sending for ERC-20 tokens

* Refactor signing/sending transactions

* Add initial flow for transactions subscription

* Refactor signing/sending transactions

* Add home settings icon

* Fix conflicts with main

* Initial flow for home settings

* Add logic flow for adding erc20 tokens

* Fix initial UI

* Finalize UI for Tokens

* Integrate UI with Ethereum flow

* Add "Enable/Disable" feature for ERC20 tokens

* Add initial Erc20 tokens

* Add Sorting and Pin Native Token features

* Fix price sorting

* Sort tokens list as well when Sort criteria changes

* - Improve sorting balances flow
- Add initial add token from search bar flow

* Fix Accounts Popup UI

* Fix Pin native token

* Fix Enabling/Disabling tokens
Fix sorting by fiat once app is opened
Improve token availability mechanism

* Fix deleting token
Fix renaming tokens

* Fix issue with search

* Add more tokens

* - Fix scroll issue
- Add ERC20 tokens placeholder image in picker

* - Separate and organize default erc20 tokens
- Fix scrolling
- Add token placeholder images in picker
- Sort disabled tokens alphabetically

* Change BNB token initial availability [skip ci]

* Fix Conflicts with main

* Fix Conflicts with main

* Add Verse ERC20 token to the initial tokens list

* Add rename wallet to Ethereum

* Integrate EtherScan API for fetching address transactions
Generate Ethereum specific secrets in Ethereum package

* Adjust transactions fiat price for ERC20 tokens

* Free Up GitHub Actions Ubuntu Runner Disk Space

* Free Up GitHub Actions Ubuntu Runner Disk space (trial 2)

* Fix Transaction Fee display

* Save transaction history

* Enhance loading time for erc20 tokens transactions

* Minor Fixes and Enhancements

* Fix sending erc20
fix block explorer issue

* Fix int overflow

* Fix transaction amount conversions

* Minor: `slow` -> `Slow` [skip-ci]

* initial changes

* more base config stuff

* config changes

* successfully builds!

* save

* successfully add nano wallet

* save

* seed generation

* receive screen + node screen working

* tx history working and fiat fixes

* balance working

* derivation updates

* nano-unfinished

* sends working

* remove fees from send screen, send and receive transactions working

* fixes + auto receive incoming txs

* fix for scanning QR codes

* save

* update translations

* fixes

* more fixes

* more strings

* small fix

* fix github actions workflow

* potential fix

* potential fix

* ci/cd fix

* change rep working

* seed generation fixes

* fixes

* save

* change rep screen functional

* save

* banano changes

* fixes, start adding ui for PoW

* pow node changes

* update translations

* fix

* account changing barely working

* save

* disable account generation

* small fix

* save

* UI work

* save

* fixes after merge main

* fixes

* remove monero stuff, work on derivation ui

* lots of fixes + finish up seed derivation

* last minute fixes

* node related fixes

* more fixes

* small fix

* more fixes

* fixes

* pretty big refactor for pow, still some bugs

* finally works!

* get transactions after send

* fix

* merge conflict fixes

* save

* fix pow node showing up twice

* done

* initial changes

* small fix

* more merge fixes

* fixes

* more fixes

* fix

* save

* fix manage pow nodes setting appearing on other wallets

* fix contact bug

* fixes

* fiat fixes

* save

* save

* save

* save

* updates

* cleanup

* restore fix

* fixes

* remove deprecated alert

* fix

* small fix

* remove outdated warning

* electrum restore fixes

* fixes

* fixes

* fix

* derivation fixes

* nano fixes pt.1

* nano fixes pt.2

* bip39 fixes

* pownode refactor

* nodes pages fixes

* observer fix

* ssl fix

* remove old references

* remove unused imports

* code cleanup

* small fix

* small potential fix

* save

* undo all bitcoin related changes

* remove dead code

* review fixes

* more fixes

* fix

* fix

* review fix

* small fix

* nano derivation and nanoutil fixes

* exchange nano fix

* nano review fixes pt.1

* nano fixes pt.2

* nano fixes pt.3

* remove old imports + stop using dynamic in di

* nanoutil fixes

* add nano.dart to gitignore, configure fixes

* review fixes, getnanowalletservice removed

* fix settings screen, add changeRep to configure.dart, other minor fixes

* remove manage_pow_nodes_page, key derivation edge case handled

* remove old refs

* more small fixes

* Generic Enhancements/Minor fixes

* review fixes

* hopefully final fixes

* review fixes

* node connection fixes

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
Co-authored-by: Justin Ehrenhofer <justin.ehrenhofer@gmail.com>
Co-authored-by: fossephate <fosse@book.local>
2023-10-05 04:09:07 +03:00

424 lines
13 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:cw_core/nano_account_info_response.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:http/http.dart' as http;
import 'package:nanodart/nanodart.dart';
import 'package:cw_core/node.dart';
class NanoClient {
static const String DEFAULT_REPRESENTATIVE =
"nano_38713x95zyjsqzx6nm1dsom1jmm668owkeb9913ax6nfgj15az3nu8xkx579";
Node? _node;
Node? _powNode;
bool connect(Node node) {
try {
_node = node;
return true;
} catch (e) {
return false;
}
}
bool connectPow(Node node) {
try {
_powNode = node;
return true;
} catch (e) {
return false;
}
}
Future<NanoBalance> 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<AccountInfoResponse?> 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 AccountInfoResponse.fromJson(data as Map<String, dynamic>);
} catch (e) {
print("error while getting account info");
return null;
}
}
Future<String> changeRep({
required String privateKey,
required String repAddress,
required String ourAddress,
}) async {
try {
AccountInfoResponse? accountInfo = await getAccountInfo(ourAddress);
if (accountInfo == null) {
throw Exception("error while getting account info");
}
// construct the change block:
Map<String, String> changeBlock = {
"type": "state",
"account": ourAddress,
"previous": accountInfo.frontier,
"representative": repAddress,
"balance": accountInfo.balance,
"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);
changeBlock["signature"] = signature;
changeBlock["work"] = work;
return await processBlock(changeBlock, "change");
} catch (e) {
throw Exception("error while changing representative");
}
}
Future<String> requestWork(String hash) async {
final response = await http.post(
_powNode!.uri,
headers: {'Content-type': 'application/json'},
body: json.encode(
{
"action": "work_generate",
"hash": hash,
},
),
);
if (response.statusCode == 200) {
final Map<String, dynamic> decoded = json.decode(response.body) as Map<String, dynamic>;
if (decoded.containsKey("error")) {
throw Exception("Received error ${decoded["error"]}");
}
return decoded["work"] as String;
} else {
throw Exception("Received work error ${response.body}");
}
}
Future<String> send({
required String privateKey,
required String amountRaw,
required String destinationAddress,
}) async {
final Map<String, String> sendBlock = await constructSendBlock(
privateKey: privateKey,
amountRaw: amountRaw,
destinationAddress: destinationAddress,
);
return await processBlock(sendBlock, "send");
}
Future<String> processBlock(Map<String, String> 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<String, dynamic> decoded = json.decode(processResponse.body) as Map<String, dynamic>;
if (decoded.containsKey("error")) {
throw Exception("Received error ${decoded["error"]}");
}
// return the hash of the transaction:
return decoded["hash"].toString();
}
Future<Map<String, String>> 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):
AccountInfoResponse? infoResponse = await getAccountInfo(publicAddress);
if (infoResponse == null) {
throw Exception(
"error while getting account info! (we probably don't have an open account yet)");
}
String frontier = infoResponse.frontier;
// override if provided:
if (previousHash != null) {
frontier = previousHash;
}
final String representative = infoResponse.representative;
// link = destination address:
final String link = NanoAccounts.extractPublicKey(destinationAddress);
final String linkAsAccount = destinationAddress;
// construct the send block:
Map<String, String> 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<void> 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):
AccountInfoResponse? infoData = await getAccountInfo(destinationAddress);
String? frontier;
String? representative;
if (infoData == null) {
// account is not open yet, we need to create an open block:
openBlock = true;
// we don't have a representative set yet:
representative = DEFAULT_REPRESENTATIVE;
// we don't have a frontier yet:
frontier = "0000000000000000000000000000000000000000000000000000000000000000";
} else {
frontier = infoData.frontier;
representative = infoData.representative;
}
// first get the account balance:
final BigInt currentBalance = (await getBalance(destinationAddress)).currentBalance;
final BigInt txAmount = BigInt.parse(amountRaw);
final BigInt balanceAfterTx = currentBalance + txAmount;
// 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<String, String> receiveBlock = {
"type": "state",
"account": destinationAddress,
"previous": 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<String, dynamic> decoded = json.decode(processResponse.body) as Map<String, dynamic>;
if (decoded.containsKey("error")) {
throw Exception("Received error ${decoded["error"]}");
}
}
// returns the number of blocks received:
Future<int> 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<dynamic>) {
var listBlocks = receivableData["blocks"] as List<dynamic>;
if (listBlocks.isEmpty) {
return 0;
}
blocks = {for (var block in listBlocks) block['hash']: block};
} else {
blocks = receivableData["blocks"] as Map<String, dynamic>;
}
blocks = blocks as Map<String, dynamic>;
// 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<void>.delayed(const Duration(seconds: 2));
}
return blocks.keys.length;
}
void stop() {}
Future<List<NanoTransactionModel>> 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<dynamic> : [];
// 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<NanoTransactionModel>((transaction) => NanoTransactionModel.fromJson(transaction))
.toList();
} catch (e) {
print(e);
return [];
}
}
}