cake_wallet/cw_nano/lib/nano_wallet.dart
cyan 1ce60d62b3
CW-676 Add Linux scripts to build monero_c for linux platform (#1527)
* Revert "Revert btc address types"

This reverts commit a49e57e3

* Re-add Bitcoin Address types
Fix conflicts with main

* fix: label issues, clear spent utxo

* chore: deps

* fix: build

* fix: missing types

* feat: new electrs API & changes, fixes for last block scanning

* Update Monero

* not sure why it's failing

* Enable Exolix
Improve service updates indicator
New versions

* Add exolix Api token to limits api

* Ignore reporting network issues

* Change default bitcoin node

* Merge main and update linux version

* Update app version [skip ci]

* New versions

* Fix conflicts and update linux version

* minor fix

* feat: Scan Silent Payments homepage toggle

* chore: build configure

* feat: generic fixes, testnet UI improvements, useSSL on bitcoin nodes

* fix: invalid Object in sendData

* feat: improve addresses page & address book displays

* feat: silent payments labeled addresses disclaimer

* fix: missing i18n

* chore: print

* feat: single block scan, rescan by date working for btc mainnet

* feat: new cake features page replace market page, move sp scan toggle, auto switch node pop up alert

* feat: delete silent addresses

* fix: red dot in non ssl nodes

* fix: inconsistent connection states, fix tx history

* fix: tx & balance displays, cpfp sending

* feat: new rust lib

* chore: node path

* fix: check node based on network

* fix: missing txcount from addresses

* style: padding in feature page cards

* fix: restore not getting all wallet addresses by type

* fix: auto switch node broken

* fix: silent payment txs not being restored

* update linux version

* feat: change scanning to subscription model, sync improvements

* fix: scan re-subscription

* fix: default nodes

* fix: improve scanning by date, fix single block scan

* refactor: common function for input tx selection

* various fixes for build issues

* initial monero.dart implementation

* ...

* multiple wallets
new lib
minor fixes

* other fixes from monero.dart and monero_c

* fix: nodes & build

* update build scripts
fix polyseed

* remove unnecessary code

* Add windows app, build scripts and build guide for it.

* Minor fix in generated monero configs

* Merge and fix main

* fix: send all with multiple outs

* add missing monero_c command

* add android build script

* update version

* Merge and fix main

* undo android ndk removal

* Fix modified exception_handler.dart

* Temporarily remove haven

* fix build issues

* fix pr script

* Fixes for build monero.dart (monero_c) for windows.

* monero build script

* wip: ios build script

* refactor: unchanged file

* Added build guides for iOS and macOS. Replaced nproc call on macOS. Added macOS configuration for configure_cake_wallet.sh script.

* Update monero.dart and monero_c versions.

* Add missed windows build scripts

* Update the application configuration for windows build script.

* Update cw_monero pubspec lock file for monero.dart

* Update pr_test_build.yml

* chore: upgrade

* chore: merge changes

* refactor: unchanged files [skip ci]

* Fix conflicts with main

* fix for multiple wallets

* update app version [skip ci]

* Add tron to windows application configuration.

* Add macOS option for description message in configure_cake_wallet.sh

* fix missing encryption utils in hardware wallet functions [skip ci]

* fix conflicts

* Include missed monero dll for windows.

* reformatting [skip ci]

* fix conflicts with main

* Disable haven configuration for iOS as default. Add ability to configure cakewallet for iOS with  for configuration script. Remove cw_shared configuration for cw_monero.

* fix: scan fixes, add date, allow sending while scanning

* add missing nano secrets file [skip ci]

* ios library

* don't pull prebuilds android

* Add auto generation of manifest file for android project even for iOS, macOS, Windows.

* remove tron

* feat: sync fixes, sp settings

* feat: fix resyncing

* store crash fix

* make init async so it won't lag
disable print starts

* fix monero_c build issues

* libstdc++

* merge main and update version

* Fix MacOS saving wallet file issue
Fix Secure Storage issue (somehow)

* update pubspec.lock

* fix build script

* Use dylib as iOS framework. Use custom path for loading of iOS framework for monero.dart. Add script for generate iOS framework for monero wallet.

* fix: date from height logic, status disconnected & chain tip get

* fix: params

* feat: electrum migration if using cake electrum

* fix nodes
update versions

* re-enable tron

* update sp_scanner to work on iOS [skip ci]

* bump monero_c hash

* bump monero_c commit

* bump moneroc version

* bump monero_c commit

* Add ability to build monero wallet lib as universal lib. Update macOS build guide. Change default arch for macOS project to .

* fix: wrong socket for old electrum nodes

* update version

* Fix unchecked wallet type call

* get App Dir correctly in default_settings_migration.dart

* handle previous issue with fetching linux documents directory [skip ci]

* backup fix

* fix NTFS issues

* Add Tron
Update Linux version

* Close the wallet when the wallet gets changed

* fix: double balance

* feat: node domain

* fix: menu name

* bump monero_c commit

* fix: update tip on set scanning

* fix: connection switching back and forth

* feat: check if node is electrs, and supports sp

* chore: fix build

* minor enhancements

* fixes and enhancements

* solve conflicts with main

* Only stop wallet on rename and delete

* fix: status toggle

* minor enhancement

* Monero.com fixes

* bump monero_c commit

* update sp_scanner to include windows and linux

* merge main

* Update macOS build guide. Change brew dependencies for build unbound locally.

* fix: Tron file write, build scripts

* - merge linux with Monero Dart
- Temporarily disable Monero

* fix other issues with linux

* linux ci
fix build script

* Update pr_test_build_linux.yml

install required packages

* add linux desktop dependencies

* don't use apk in linux build releases

* don't copy the file to test-apk

* fix linux runtime issues

* remove libc++_shared.so

* fix issues with linux

* prepare both android and linux (because otherwise it will fail)

* ci script updates

* run apt update

* bump image to ubuntu 22.04
note: remember to put it down later

* bump python version

* remove some dependencies

* remove unused import

* add missing dependencies

* fix dependencies

* some fixes

* remove print [skip ci]

* Add back RunnerBase.entitlements
minor fixes [skip ci]

* fix memory leak / infinite recurrsion when opening xmr wallet

* url_launcher_linux: 3.1.1 # https://github.com/flutter/flutter/issues/153083

* fix conflicts with main

* handle walletKeysFile with encryptionUtils

* update app version [skip ci]

* add wownero [skip ci]

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
Co-authored-by: Rafael Saes <git@rafael.saes.dev>
Co-authored-by: M <m@cakewallet.com>
Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com>
2024-08-13 01:18:14 +03:00

538 lines
16 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:bip39/bip39.dart' as bip39;
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/n2_node.dart';
import 'package:cw_core/nano_account.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_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.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_addresses.dart';
import 'package:cw_nano/nano_wallet_keys.dart';
import 'package:cw_nano/pending_nano_transaction.dart';
import 'package:mobx/mobx.dart';
import 'package:nanodart/nanodart.dart';
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, WalletKeysFile {
NanoWalletBase({
required WalletInfo walletInfo,
required String mnemonic,
required String password,
NanoBalance? initialBalance,
required EncryptionFileUtils encryptionFileUtils,
}) : syncStatus = NotConnectedSyncStatus(),
_password = password,
_mnemonic = mnemonic,
_derivationType = walletInfo.derivationInfo!.derivationType!,
_isTransactionUpdating = false,
_encryptionFileUtils = encryptionFileUtils,
_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,
encryptionFileUtils: encryptionFileUtils,
);
if (!CakeHive.isAdapterRegistered(NanoAccount.typeId)) {
CakeHive.registerAdapter(NanoAccountAdapter());
}
}
String _mnemonic;
final String _password;
DerivationType _derivationType;
final EncryptionFileUtils _encryptionFileUtils;
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;
@override
String get password => _password;
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) => 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 => NanoWalletKeys(seedKey: _hexSeed!);
@override
String? get privateKey => _privateKey!;
@override
Future<void> rescan({required int height}) async {
updateTransactions();
_updateBalance();
return;
}
@override
Future<void> save() async {
if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) {
await saveKeysFile(_password, _encryptionFileUtils);
saveKeysFile(_password, _encryptionFileUtils, true);
}
await walletAddresses.updateAddressesInBox();
final path = await makePath();
await _encryptionFileUtils.write(path: path, password: _password, data: toJSON());
await transactionHistory.save();
}
@override
String? get seed => _mnemonic.isNotEmpty ? _mnemonic : null;
String get hexSeed => _hexSeed!;
@override
WalletKeysData get walletKeysData => WalletKeysData(mnemonic: _mnemonic, altMnemonic: 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;
}
}
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,
required EncryptionFileUtils encryptionFileUtils,
}) async {
final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
final path = await pathForWallet(name: name, type: walletInfo.type);
Map<String, dynamic>? data = null;
try {
final jsonSource = await encryptionFileUtils.read(path: path, password: password);
data = json.decode(jsonSource) as Map<String, dynamic>;
} catch (e) {
if (!hasKeysFile) rethrow;
}
final balance = NanoBalance.fromRawString(
currentBalance: data?['currentBalance'] as String? ?? "0",
receivableBalance: data?['receivableBalance'] as String? ?? "0",
);
final WalletKeysData keysData;
// Migrate wallet from the old scheme to then new .keys file scheme
if (!hasKeysFile) {
final mnemonic = data!['mnemonic'] as String;
final isHexSeed = !mnemonic.contains(' ');
keysData = WalletKeysData(
mnemonic: isHexSeed ? null : mnemonic, altMnemonic: isHexSeed ? mnemonic : null);
} else {
keysData = await WalletKeysFile.readKeysFile(
name,
walletInfo.type,
password,
encryptionFileUtils,
);
}
DerivationType derivationType = DerivationType.nano;
if (data?['derivationType'] == "DerivationType.bip39") {
derivationType = DerivationType.bip39;
}
walletInfo.derivationInfo ??= DerivationInfo(derivationType: derivationType);
walletInfo.derivationInfo!.derivationType ??= derivationType;
return NanoWallet(
walletInfo: walletInfo,
password: password,
mnemonic: keysData.mnemonic!,
initialBalance: balance,
encryptionFileUtils: encryptionFileUtils,
);
// 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);
}
}