cake_wallet/cw_nano/lib/nano_wallet.dart
Matthew Fosse 509b92e97f
Bitcoin derivations (#1089)
* - 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

* derivation fixes

* deterministic fix

* fix pt.2

* derivation class fixes

* review fixes from nano that also apply here

* formatting

* stuff that should've stayed deleted

* post merge fixes

* remove problematic imports and duplicate changes

* Delete lib/nano/nano.dart

* move wallet restore page proxy code to the view model

* fix dashboard page indicators being the same color

* debatably better refactoring of derivationInfo, migration needed

* additional refactor improvements

* blanket comment some stuff out to narrow down this issue

* refactor fixes

* fix nano exchange

* fix , bug, i.e. replace , with . when making a nano transaction

* fix nano sending, update restore page wording, and other minor fixes

* write migration for existing bitcoin and nano wallets

* merge fixes

* minor fixes

* use default derivation type when restoring from qr code

* fixes for restoring

* fixes

* fixes

* merge fix

* Fix issues with Creating Electrum and Restoring Bip39

* updates & fixes

* Add missing case for no transactions BIP39 wallet restore

* Make the default BIP39 the 84 derivation path

* Add Samourai Deposit

* litecoin mnemonic error fix

* Bip39 passphrase support (#1412)

* save

* passphrase working

* fix for when loading wallets + translation update

* minor fix

* Fix Nano

* minor fix [skip ci]

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>

* change error state seed conditions into throwables [skip ci]

* litecoin fixes

* Bip39 minor enhancements (#1416)

* minor enhancements

* rename bitcoin_derivations -> electrum_derivations

* Remove duplicate derivations
handle default case

* minor fix

* Enable passphrase for Litecoin

* obscure text of passphrase

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
Co-authored-by: Justin Ehrenhofer <justin.ehrenhofer@gmail.com>
Co-authored-by: fossephate <fosse@book.local>
2024-04-30 03:49:56 +03:00

503 lines
15 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/nano_account_info_response.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_nano/file.dart';
import 'package:cw_core/nano_account.dart';
import 'package:cw_core/n2_node.dart';
import 'package:cw_nano/nano_balance.dart';
import 'package:cw_nano/nano_client.dart';
import 'package:cw_nano/nano_transaction_credentials.dart';
import 'package:cw_nano/nano_transaction_history.dart';
import 'package:cw_nano/nano_transaction_info.dart';
import 'package:cw_nano/nano_wallet_keys.dart';
import 'package:cw_nano/pending_nano_transaction.dart';
import 'package:mobx/mobx.dart';
import 'dart:async';
import 'package:cw_nano/nano_wallet_addresses.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:nanodart/nanodart.dart';
import 'package:bip39/bip39.dart' as bip39;
import 'package:nanoutil/nanoutil.dart';
part 'nano_wallet.g.dart';
class NanoWallet = NanoWalletBase with _$NanoWallet;
abstract class NanoWalletBase
extends WalletBase<NanoBalance, NanoTransactionHistory, NanoTransactionInfo> with Store {
NanoWalletBase({
required WalletInfo walletInfo,
required String mnemonic,
required String password,
NanoBalance? initialBalance,
}) : syncStatus = NotConnectedSyncStatus(),
_password = password,
_mnemonic = mnemonic,
_derivationType = walletInfo.derivationInfo!.derivationType!,
_isTransactionUpdating = false,
_client = NanoClient(),
walletAddresses = NanoWalletAddresses(walletInfo),
balance = ObservableMap<CryptoCurrency, NanoBalance>.of({
CryptoCurrency.nano: initialBalance ??
NanoBalance(currentBalance: BigInt.zero, receivableBalance: BigInt.zero)
}),
super(walletInfo) {
this.walletInfo = walletInfo;
transactionHistory = NanoTransactionHistory(walletInfo: walletInfo, password: password);
if (!CakeHive.isAdapterRegistered(NanoAccount.typeId)) {
CakeHive.registerAdapter(NanoAccountAdapter());
}
}
String _mnemonic;
final String _password;
DerivationType _derivationType;
String? _privateKey;
String? _publicAddress;
String? _hexSeed;
Timer? _receiveTimer;
String? _representativeAddress;
int repScore = 100;
bool get isRepOk => repScore >= 90;
late final NanoClient _client;
bool _isTransactionUpdating;
@override
NanoWalletAddresses walletAddresses;
@override
@observable
SyncStatus syncStatus;
@override
@observable
late ObservableMap<CryptoCurrency, NanoBalance> balance;
static const int POLL_INTERVAL_SECONDS = 10;
// initialize the different forms of private / public key we'll need:
Future<void> init() async {
if (_derivationType == DerivationType.unknown) {
_derivationType = DerivationType.nano;
}
final String type = (_derivationType == DerivationType.nano) ? "standard" : "hd";
// our "mnemonic" is actually a hex form seed:
if (!_mnemonic.contains(' ')) {
_hexSeed = _mnemonic;
_mnemonic = "";
}
if (_hexSeed == null) {
if (_derivationType == DerivationType.nano) {
_hexSeed = bip39.mnemonicToEntropy(_mnemonic).toUpperCase();
} else {
_hexSeed = await NanoDerivations.hdMnemonicListToSeed(_mnemonic.split(' '));
}
}
NanoDerivationType derivationType =
type == "standard" ? NanoDerivationType.STANDARD : NanoDerivationType.HD;
_privateKey = await NanoDerivations.universalSeedToPrivate(
_hexSeed!,
index: 0,
type: derivationType,
);
_publicAddress = await NanoDerivations.universalSeedToAddress(
_hexSeed!,
index: 0,
type: derivationType,
);
this.walletInfo.address = _publicAddress!;
await walletAddresses.init();
await transactionHistory.init();
await save();
}
@override
int calculateEstimatedFee(TransactionPriority priority, int? amount) {
return 0; // always 0 :)
}
@override
Future<void> changePassword(String password) {
throw UnimplementedError("changePassword");
}
@override
void close() {
_client.stop();
_receiveTimer?.cancel();
}
@action
@override
Future<void> connectToNode({required Node node}) async {
try {
syncStatus = ConnectingSyncStatus();
final isConnected = _client.connect(node);
if (!isConnected) {
throw Exception("Nano Node connection failed");
}
try {
await _updateBalance();
await updateTransactions();
await _updateRep();
await _receiveAll();
} catch (e) {
print(e);
}
syncStatus = ConnectedSyncStatus();
} catch (e) {
print(e);
syncStatus = FailedSyncStatus();
}
}
@override
Future<void> connectToPowNode({required Node node}) async {
_client.connectPow(node);
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
credentials = credentials as NanoTransactionCredentials;
BigInt runningAmount = BigInt.zero;
await _updateBalance();
BigInt runningBalance = balance[currency]?.currentBalance ?? BigInt.zero;
final List<Map<String, String>> blocks = [];
String? previousHash;
for (var txOut in credentials.outputs) {
late BigInt amt;
if (txOut.sendAll) {
amt = balance[currency]?.currentBalance ?? BigInt.zero;
} else {
amt = BigInt.tryParse(NanoAmounts.getAmountAsRaw(
txOut.cryptoAmount?.replaceAll(',', '.') ?? "0", NanoAmounts.rawPerNano)) ??
BigInt.zero;
}
if (balance[currency]?.currentBalance != null && amt > balance[currency]!.currentBalance) {
throw Exception("Trying to send more than entire balance!");
}
runningBalance = runningBalance - amt;
final block = await _client.constructSendBlock(
amountRaw: amt.toString(),
destinationAddress: txOut.isParsedAddress ? txOut.extractedAddress! : txOut.address,
privateKey: _privateKey!,
balanceAfterTx: runningBalance,
previousHash: previousHash,
);
previousHash = NanoBlocks.computeStateHash(
NanoAccountType.NANO,
block["account"]!,
block["previous"]!,
block["representative"]!,
BigInt.parse(block["balance"]!),
block["link"]!,
);
blocks.add(block);
runningAmount += amt;
}
try {
if (runningAmount > balance[currency]!.currentBalance || runningBalance < BigInt.zero) {
throw Exception(("Trying to send more than entire balance!"));
}
} catch (e) {
rethrow;
}
return PendingNanoTransaction(
amount: runningAmount,
id: "",
nanoClient: _client,
blocks: blocks,
);
}
Future<void> _receiveAll() async {
await _updateBalance();
int blocksReceived = await this._client.confirmAllReceivable(
destinationAddress: _publicAddress!,
privateKey: _privateKey!,
);
if (blocksReceived > 0) {
await Future<void>.delayed(Duration(seconds: 3));
_updateBalance();
updateTransactions();
}
}
Future<bool> updateTransactions() async {
try {
if (_isTransactionUpdating) {
return false;
}
_isTransactionUpdating = true;
final transactions = await fetchTransactions();
transactionHistory.addMany(transactions);
await transactionHistory.save();
_isTransactionUpdating = false;
return true;
} catch (_) {
_isTransactionUpdating = false;
return false;
}
}
@override
Future<Map<String, NanoTransactionInfo>> fetchTransactions() async {
String address = _publicAddress!;
final transactions = await _client.fetchTransactions(address);
final Map<String, NanoTransactionInfo> result = {};
for (var transactionModel in transactions) {
final bool isSend = transactionModel.type == "send";
result[transactionModel.hash] = NanoTransactionInfo(
id: transactionModel.hash,
amountRaw: transactionModel.amount,
height: transactionModel.height,
direction: isSend ? TransactionDirection.outgoing : TransactionDirection.incoming,
confirmed: transactionModel.confirmed,
date: transactionModel.date ?? DateTime.now(),
confirmations: transactionModel.confirmed ? 1 : 0,
to: isSend ? transactionModel.account : address,
from: isSend ? address : transactionModel.account,
);
}
return result;
}
@override
NanoWalletKeys get keys {
return NanoWalletKeys(seedKey: _hexSeed!);
}
@override
String? get privateKey => _privateKey!;
@override
Future<void> rescan({required int height}) async {
updateTransactions();
_updateBalance();
return;
}
@override
Future<void> save() async {
await walletAddresses.updateAddressesInBox();
final path = await makePath();
await write(path: path, password: _password, data: toJSON());
await transactionHistory.save();
}
@override
String? get seed => _mnemonic.isNotEmpty ? _mnemonic : null;
String get hexSeed => _hexSeed!;
String get representative => _representativeAddress ?? "";
@action
@override
Future<void> startSync() async {
try {
syncStatus = AttemptingSyncStatus();
// setup a timer to receive transactions periodically:
_receiveTimer?.cancel();
_receiveTimer = Timer.periodic(const Duration(seconds: POLL_INTERVAL_SECONDS), (timer) async {
// get our balance:
await _updateBalance();
// if we have anything to receive, process it:
if (balance[currency]!.receivableBalance > BigInt.zero) {
await _receiveAll();
}
});
// also run once, immediately:
await _updateBalance();
bool updateSuccess = await updateTransactions();
if (!updateSuccess) {
syncStatus = FailedSyncStatus();
return;
}
syncStatus = SyncedSyncStatus();
} catch (e) {
print(e);
syncStatus = FailedSyncStatus();
rethrow;
}
}
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
String toJSON() => json.encode({
'seedKey': _hexSeed,
'mnemonic': _mnemonic,
'currentBalance': balance[currency]?.currentBalance.toString() ?? "0",
'receivableBalance': balance[currency]?.receivableBalance.toString() ?? "0",
'derivationType': _derivationType.toString()
});
static Future<NanoWallet> open({
required String name,
required String password,
required WalletInfo walletInfo,
}) async {
final path = await pathForWallet(name: name, type: walletInfo.type);
final jsonSource = await read(path: path, password: password);
final data = json.decode(jsonSource) as Map;
final mnemonic = data['mnemonic'] as String;
final balance = NanoBalance.fromRawString(
currentBalance: data['currentBalance'] as String? ?? "0",
receivableBalance: data['receivableBalance'] as String? ?? "0",
);
DerivationType derivationType = DerivationType.nano;
if (data['derivationType'] == "DerivationType.bip39") {
derivationType = DerivationType.bip39;
}
walletInfo.derivationInfo ??= DerivationInfo(derivationType: derivationType);
if (walletInfo.derivationInfo!.derivationType == null) {
walletInfo.derivationInfo!.derivationType = derivationType;
}
return NanoWallet(
walletInfo: walletInfo,
password: password,
mnemonic: mnemonic,
initialBalance: balance,
);
// init() should always be run after this!
}
Future<void> _updateBalance() async {
var oldBalance = balance[currency];
try {
balance[currency] = await _client.getBalance(_publicAddress!);
} catch (e) {
print("Failed to get balance $e");
// if we don't have a balance, we should at least create one, since it's a late binding
// otherwise, it's better to just leave it as whatever it was before:
if (balance[currency] == null) {
balance[currency] =
NanoBalance(currentBalance: BigInt.zero, receivableBalance: BigInt.zero);
}
}
// don't save unnecessarily:
// trying to save too frequently can cause problems with the file system
// since nano is updated frequently this can be a problem, so we only save if there is a change:
if (oldBalance == null ||
balance[currency]!.currentBalance != oldBalance.currentBalance ||
balance[currency]!.receivableBalance != oldBalance.receivableBalance) {
await save();
}
}
Future<void> _updateRep() async {
try {
AccountInfoResponse accountInfo = (await _client.getAccountInfo(_publicAddress!))!;
_representativeAddress = accountInfo.representative;
} catch (e) {
// account not found:
_representativeAddress = await _client.getRepFromPrefs();
throw Exception("Failed to get representative address $e");
}
repScore = await _client.getRepScore(_representativeAddress!);
}
Future<void> regenerateAddress() async {
final NanoDerivationType type = (_derivationType == DerivationType.nano)
? NanoDerivationType.STANDARD
: NanoDerivationType.HD;
_privateKey = await NanoDerivations.universalSeedToPrivate(
_hexSeed!,
index: this.walletAddresses.account!.id,
type: type,
);
_publicAddress = await NanoDerivations.universalSeedToAddress(
_hexSeed!,
index: this.walletAddresses.account!.id,
type: type,
);
this.walletInfo.address = _publicAddress!;
this.walletAddresses.address = _publicAddress!;
}
Future<void> changeRep(String address) async {
try {
final String hash = await _client.changeRep(
privateKey: _privateKey!,
repAddress: address,
ourAddress: _publicAddress!,
);
if (hash.isNotEmpty) {
_representativeAddress = address;
}
} catch (e) {
throw Exception("Failed to change representative address $e");
}
}
Future<List<N2Node>> getN2Reps() async {
return _client.getN2Reps();
}
Future<void>? updateBalance() async => await _updateBalance();
@override
Future<void> renameWalletFiles(String newWalletName) async {
final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
final currentWalletFile = File(currentWalletPath);
final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName');
// Copies current wallet files into new wallet name's dir and files
if (currentWalletFile.existsSync()) {
final newWalletPath = await pathForWallet(name: newWalletName, type: type);
await currentWalletFile.copy(newWalletPath);
}
if (currentTransactionsFile.existsSync()) {
final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName');
}
// Delete old name's dir and files
await Directory(currentDirPath).delete(recursive: true);
}
}