diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index 0ce42fe2f..471c8ee03 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -55,7 +55,7 @@ jobs: /opt/android/cake_wallet/cw_monero/android/.cxx /opt/android/cake_wallet/cw_monero/ios/External /opt/android/cake_wallet/cw_shared_external/ios/External - key: ${{ hashFiles('**/build_monero.sh', '**/build_haven.sh') }} + key: ${{ hashFiles('**/build_monero.sh', '**/build_haven.sh', '**/monero_api.cpp') }} - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} name: Generate Externals diff --git a/cw_bitcoin/lib/bitcoin_transaction_no_inputs_exception.dart b/cw_bitcoin/lib/bitcoin_transaction_no_inputs_exception.dart index d4397dead..fac7e93c4 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_no_inputs_exception.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_no_inputs_exception.dart @@ -1,4 +1,4 @@ class BitcoinTransactionNoInputsException implements Exception { @override - String toString() => 'Not enough inputs available'; + String toString() => 'Not enough inputs available. Please select more under Coin Control'; } diff --git a/cw_core/lib/address_info.dart b/cw_core/lib/address_info.dart new file mode 100644 index 000000000..63dc023ab --- /dev/null +++ b/cw_core/lib/address_info.dart @@ -0,0 +1,21 @@ +import 'package:cw_core/hive_type_ids.dart'; +import 'package:hive/hive.dart'; + +part 'address_info.g.dart'; + +@HiveType(typeId: ADDRESS_INFO_TYPE_ID) +class AddressInfo extends HiveObject { + AddressInfo({required this.address, this.accountIndex, required this.label}); + + static const typeId = ADDRESS_INFO_TYPE_ID; + static const boxName = 'AddressInfo'; + + @HiveField(0) + int? accountIndex; + + @HiveField(1, defaultValue: '') + String address; + + @HiveField(2, defaultValue: '') + String label; +} diff --git a/cw_core/lib/hive_type_ids.dart b/cw_core/lib/hive_type_ids.dart index efaf026e4..4d4d1a6a8 100644 --- a/cw_core/lib/hive_type_ids.dart +++ b/cw_core/lib/hive_type_ids.dart @@ -1,16 +1,16 @@ -const CONTACT_TYPE_ID = 0; -const NODE_TYPE_ID = 1; -const TRANSACTION_TYPE_ID = 2; -const TRADE_TYPE_ID = 3; -const WALLET_INFO_TYPE_ID = 4; -const WALLET_TYPE_TYPE_ID = 5; -const TEMPLATE_TYPE_ID = 6; -const EXCHANGE_TEMPLATE_TYPE_ID = 7; -const ORDER_TYPE_ID = 8; -const UNSPENT_COINS_INFO_TYPE_ID = 9; -const ANONPAY_INVOICE_INFO_TYPE_ID = 10; -const DERIVATION_TYPE_TYPE_ID = 15; - -const ERC20_TOKEN_TYPE_ID = 12; -const NANO_ACCOUNT_TYPE_ID = 13; -const POW_NODE_TYPE_ID = 14; +const CONTACT_TYPE_ID = 0; +const NODE_TYPE_ID = 1; +const TRANSACTION_TYPE_ID = 2; +const TRADE_TYPE_ID = 3; +const WALLET_INFO_TYPE_ID = 4; +const WALLET_TYPE_TYPE_ID = 5; +const TEMPLATE_TYPE_ID = 6; +const EXCHANGE_TEMPLATE_TYPE_ID = 7; +const ORDER_TYPE_ID = 8; +const UNSPENT_COINS_INFO_TYPE_ID = 9; +const ANONPAY_INVOICE_INFO_TYPE_ID = 10; +const ADDRESS_INFO_TYPE_ID = 11; +const ERC20_TOKEN_TYPE_ID = 12; +const NANO_ACCOUNT_TYPE_ID = 13; +const POW_NODE_TYPE_ID = 14; +const DERIVATION_TYPE_TYPE_ID = 15; \ No newline at end of file diff --git a/cw_core/lib/monero_balance.dart b/cw_core/lib/monero_balance.dart index 7d569ef2f..bf30110a3 100644 --- a/cw_core/lib/monero_balance.dart +++ b/cw_core/lib/monero_balance.dart @@ -2,24 +2,31 @@ import 'package:cw_core/balance.dart'; import 'package:cw_core/monero_amount_format.dart'; class MoneroBalance extends Balance { - MoneroBalance({required this.fullBalance, required this.unlockedBalance}) + MoneroBalance({required this.fullBalance, required this.unlockedBalance, this.frozenBalance = 0}) : formattedFullBalance = moneroAmountToString(amount: fullBalance), - formattedUnlockedBalance = - moneroAmountToString(amount: unlockedBalance), + formattedUnlockedBalance = moneroAmountToString(amount: unlockedBalance), + frozenFormatted = moneroAmountToString(amount: frozenBalance), super(unlockedBalance, fullBalance); MoneroBalance.fromString( {required this.formattedFullBalance, - required this.formattedUnlockedBalance}) + required this.formattedUnlockedBalance, + this.frozenFormatted = '0.0'}) : fullBalance = moneroParseAmount(amount: formattedFullBalance), unlockedBalance = moneroParseAmount(amount: formattedUnlockedBalance), + frozenBalance = moneroParseAmount(amount: frozenFormatted), super(moneroParseAmount(amount: formattedUnlockedBalance), moneroParseAmount(amount: formattedFullBalance)); final int fullBalance; final int unlockedBalance; + final int frozenBalance; final String formattedFullBalance; final String formattedUnlockedBalance; + final String frozenFormatted; + + @override + String get formattedFrozenBalance => frozenFormatted == '0.0' ? '' : frozenFormatted; @override String get formattedAvailableBalance => formattedUnlockedBalance; diff --git a/cw_core/lib/unspent_coins_info.dart b/cw_core/lib/unspent_coins_info.dart index 33be2eb2c..68bbcbfd2 100644 --- a/cw_core/lib/unspent_coins_info.dart +++ b/cw_core/lib/unspent_coins_info.dart @@ -13,7 +13,9 @@ class UnspentCoinsInfo extends HiveObject { required this.noteRaw, required this.address, required this.vout, - required this.value}); + required this.value, + this.keyImage = null + }); static const typeId = UNSPENT_COINS_INFO_TYPE_ID; static const boxName = 'Unspent'; @@ -43,6 +45,9 @@ class UnspentCoinsInfo extends HiveObject { @HiveField(7, defaultValue: 0) int vout; + @HiveField(8, defaultValue: null) + String? keyImage; + String get note => noteRaw ?? ''; set note(String value) => noteRaw = value; diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index a34101a88..27b5468c5 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -1,8 +1,10 @@ +import 'package:cw_core/address_info.dart'; import 'package:cw_core/wallet_info.dart'; abstract class WalletAddresses { WalletAddresses(this.walletInfo) - : addressesMap = {}; + : addressesMap = {}, + addressInfos = {}; final WalletInfo walletInfo; @@ -12,6 +14,10 @@ abstract class WalletAddresses { Map<String, String> addressesMap; + Map<int, List<AddressInfo>> addressInfos; + + Set<String> usedAddresses = {}; + Future<void> init(); Future<void> updateAddressesInBox(); @@ -20,6 +26,8 @@ abstract class WalletAddresses { try { walletInfo.address = address; walletInfo.addresses = addressesMap; + walletInfo.addressInfos = addressInfos; + walletInfo.usedAddresses = usedAddresses.toList(); if (walletInfo.isInBox) { await walletInfo.save(); diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index a2d25f7b9..42fb22e75 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -53,6 +53,10 @@ abstract class WalletBase< late HistoryType transactionHistory; + set isEnabledAutoGenerateSubaddress(bool value) {} + + bool get isEnabledAutoGenerateSubaddress => false; + Future<void> connectToNode({required Node node}); // there is a default definition here because only coins with a pow node (nano based) need to override this diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index e2413745c..c4ccea00a 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:cw_core/address_info.dart'; import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart'; @@ -136,9 +137,15 @@ class WalletInfo extends HiveObject { bool? showIntroCakePayCard; @HiveField(14) - DerivationType? derivationType; + Map<int, List<AddressInfo>>? addressInfos; @HiveField(15) + List<String>? usedAddresses; + + @HiveField(16) + DerivationType? derivationType; + + @HiveField(17) String? derivationPath; String get yatLastUsedAddress => yatLastUsedAddressRaw ?? ''; diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index 226ace6a1..e639be4b9 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -12,8 +12,7 @@ import 'package:cw_core/monero_wallet_utils.dart'; import 'package:cw_haven/api/structs/pending_transaction.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; -import 'package:cw_haven/api/transaction_history.dart' - as haven_transaction_history; +import 'package:cw_haven/api/transaction_history.dart' as haven_transaction_history; //import 'package:cw_haven/wallet.dart'; import 'package:cw_haven/api/wallet.dart' as haven_wallet; import 'package:cw_haven/api/transaction_history.dart' as transaction_history; @@ -37,8 +36,8 @@ const moneroBlockSize = 1000; class HavenWallet = HavenWalletBase with _$HavenWallet; -abstract class HavenWalletBase extends WalletBase<MoneroBalance, - HavenTransactionHistory, HavenTransactionInfo> with Store { +abstract class HavenWalletBase + extends WalletBase<MoneroBalance, HavenTransactionHistory, HavenTransactionInfo> with Store { HavenWalletBase({required WalletInfo walletInfo}) : balance = ObservableMap.of(getHavenBalance(accountIndex: 0)), _isTransactionUpdating = false, @@ -47,8 +46,7 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance, syncStatus = NotConnectedSyncStatus(), super(walletInfo) { transactionHistory = HavenTransactionHistory(); - _onAccountChangeReaction = reaction((_) => walletAddresses.account, - (Account? account) { + _onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) { if (account == null) { return; } @@ -96,14 +94,12 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance, haven_wallet.setRecoveringFromSeed(isRecovery: walletInfo.isRecovery); if (haven_wallet.getCurrentHeight() <= 1) { - haven_wallet.setRefreshFromBlockHeight( - height: walletInfo.restoreHeight); + haven_wallet.setRefreshFromBlockHeight(height: walletInfo.restoreHeight); } } - _autoSaveTimer = Timer.periodic( - Duration(seconds: _autoSaveInterval), - (_) async => await save()); + _autoSaveTimer = + Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save()); } @override @@ -115,7 +111,7 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance, _onAccountChangeReaction?.reaction.dispose(); _autoSaveTimer?.cancel(); } - + @override Future<void> connectToNode({required Node node}) async { try { @@ -170,26 +166,25 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance, } if (hasMultiDestination) { - if (outputs.any((item) => item.sendAll - || (item.formattedCryptoAmount ?? 0) <= 0)) { - throw HavenTransactionCreationException('You do not have enough coins to send this amount.'); + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + throw HavenTransactionCreationException( + 'You do not have enough coins to send this amount.'); } - final int totalAmount = outputs.fold(0, (acc, value) => - acc + (value.formattedCryptoAmount ?? 0)); + final int totalAmount = + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); if (unlockedBalance < totalAmount) { - throw HavenTransactionCreationException('You do not have enough coins to send this amount.'); + throw HavenTransactionCreationException( + 'You do not have enough coins to send this amount.'); } - final moneroOutputs = outputs.map((output) => - MoneroOutput( - address: output.address, - amount: output.cryptoAmount!.replaceAll(',', '.'))) + final moneroOutputs = outputs + .map((output) => MoneroOutput( + address: output.address, amount: output.cryptoAmount!.replaceAll(',', '.'))) .toList(); - pendingTransactionDescription = - await transaction_history.createTransactionMultDest( + pendingTransactionDescription = await transaction_history.createTransactionMultDest( outputs: moneroOutputs, priorityRaw: _credentials.priority.serialize(), accountIndex: walletAddresses.account!.id); @@ -198,12 +193,8 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance, final address = output.isParsedAddress && (output.extractedAddress?.isNotEmpty ?? false) ? output.extractedAddress! : output.address; - final amount = output.sendAll - ? null - : output.cryptoAmount!.replaceAll(',', '.'); - final int? formattedAmount = output.sendAll - ? null - : output.formattedCryptoAmount; + final amount = output.sendAll ? null : output.cryptoAmount!.replaceAll(',', '.'); + final int? formattedAmount = output.sendAll ? null : output.formattedCryptoAmount; if ((formattedAmount != null && unlockedBalance < formattedAmount) || (formattedAmount == null && unlockedBalance <= 0)) { @@ -213,8 +204,7 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance, 'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.'); } - pendingTransactionDescription = - await transaction_history.createTransaction( + pendingTransactionDescription = await transaction_history.createTransaction( address: address, assetType: _credentials.assetType, amount: amount, @@ -307,16 +297,14 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance, } String getTransactionAddress(int accountIndex, int addressIndex) => - haven_wallet.getAddress( - accountIndex: accountIndex, - addressIndex: addressIndex); + haven_wallet.getAddress(accountIndex: accountIndex, addressIndex: addressIndex); @override Future<Map<String, HavenTransactionInfo>> fetchTransactions() async { haven_transaction_history.refreshTransactions(); - return _getAllTransactions(null).fold<Map<String, HavenTransactionInfo>>( - <String, HavenTransactionInfo>{}, - (Map<String, HavenTransactionInfo> acc, HavenTransactionInfo tx) { + return _getAllTransactions(null) + .fold<Map<String, HavenTransactionInfo>>(<String, HavenTransactionInfo>{}, + (Map<String, HavenTransactionInfo> acc, HavenTransactionInfo tx) { acc[tx.id] = tx; return acc; }); @@ -340,9 +328,9 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance, } List<HavenTransactionInfo> _getAllTransactions(dynamic _) => haven_transaction_history - .getAllTransations() - .map((row) => HavenTransactionInfo.fromRow(row)) - .toList(); + .getAllTransations() + .map((row) => HavenTransactionInfo.fromRow(row)) + .toList(); void _setListeners() { _listener?.stop(); @@ -364,8 +352,7 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance, } int _getHeightDistance(DateTime date) { - final distance = - DateTime.now().millisecondsSinceEpoch - date.millisecondsSinceEpoch; + final distance = DateTime.now().millisecondsSinceEpoch - date.millisecondsSinceEpoch; final daysTmp = (distance / 86400).round(); final days = daysTmp < 1 ? 1 : daysTmp; @@ -386,8 +373,7 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance, void _askForUpdateBalance() => balance.addAll(getHavenBalance(accountIndex: walletAddresses.account!.id)); - Future<void> _askForUpdateTransactionHistory() async => - await updateTransactions(); + Future<void> _askForUpdateTransactionHistory() async => await updateTransactions(); void _onNewBlock(int height, int blocksLeft, double ptc) async { try { @@ -404,9 +390,9 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance, syncStatus = SyncedSyncStatus(); if (!_hasSyncAfterStartup) { - _hasSyncAfterStartup = true; - await save(); - } + _hasSyncAfterStartup = true; + await save(); + } if (walletInfo.isRecovery) { await setAsRecovered(); diff --git a/cw_monero/ios/Classes/monero_api.cpp b/cw_monero/ios/Classes/monero_api.cpp index 780fc5b14..66b8605c6 100644 --- a/cw_monero/ios/Classes/monero_api.cpp +++ b/cw_monero/ios/Classes/monero_api.cpp @@ -6,6 +6,7 @@ #include <fstream> #include <unistd.h> #include <mutex> +#include <list> #include "thread" #include "CwWalletListener.h" #if __APPLE__ @@ -182,6 +183,62 @@ extern "C" } }; + struct CoinsInfoRow + { + uint64_t blockHeight; + char *hash; + uint64_t internalOutputIndex; + uint64_t globalOutputIndex; + bool spent; + bool frozen; + uint64_t spentHeight; + uint64_t amount; + bool rct; + bool keyImageKnown; + uint64_t pkIndex; + uint32_t subaddrIndex; + uint32_t subaddrAccount; + char *address; + char *addressLabel; + char *keyImage; + uint64_t unlockTime; + bool unlocked; + char *pubKey; + bool coinbase; + char *description; + + CoinsInfoRow(Monero::CoinsInfo *coinsInfo) + { + blockHeight = coinsInfo->blockHeight(); + std::string *hash_str = new std::string(coinsInfo->hash()); + hash = strdup(hash_str->c_str()); + internalOutputIndex = coinsInfo->internalOutputIndex(); + globalOutputIndex = coinsInfo->globalOutputIndex(); + spent = coinsInfo->spent(); + frozen = coinsInfo->frozen(); + spentHeight = coinsInfo->spentHeight(); + amount = coinsInfo->amount(); + rct = coinsInfo->rct(); + keyImageKnown = coinsInfo->keyImageKnown(); + pkIndex = coinsInfo->pkIndex(); + subaddrIndex = coinsInfo->subaddrIndex(); + subaddrAccount = coinsInfo->subaddrAccount(); + address = strdup(coinsInfo->address().c_str()) ; + addressLabel = strdup(coinsInfo->addressLabel().c_str()); + keyImage = strdup(coinsInfo->keyImage().c_str()); + unlockTime = coinsInfo->unlockTime(); + unlocked = coinsInfo->unlocked(); + pubKey = strdup(coinsInfo->pubKey().c_str()); + coinbase = coinsInfo->coinbase(); + description = strdup(coinsInfo->description().c_str()); + } + + void setUnlocked(bool unlocked); + + }; + + Monero::Coins *m_coins; + Monero::Wallet *m_wallet; Monero::TransactionHistory *m_transaction_history; MoneroWalletListener *m_listener; @@ -189,6 +246,7 @@ extern "C" Monero::SubaddressAccount *m_account; uint64_t m_last_known_wallet_height; uint64_t m_cached_syncing_blockchain_height = 0; + std::list<Monero::CoinsInfo*> m_coins_info; std::mutex store_lock; bool is_storing = false; @@ -224,6 +282,17 @@ extern "C" { m_subaddress = nullptr; } + + m_coins_info = std::list<Monero::CoinsInfo*>(); + + if (wallet != nullptr) + { + m_coins = wallet->coins(); + } + else + { + m_coins = nullptr; + } } Monero::Wallet *get_current_wallet() @@ -487,9 +556,18 @@ extern "C" } bool transaction_create(char *address, char *payment_id, char *amount, - uint8_t priority_raw, uint32_t subaddr_account, Utf8Box &error, PendingTransactionRaw &pendingTransaction) + uint8_t priority_raw, uint32_t subaddr_account, + char **preferred_inputs, uint32_t preferred_inputs_size, + Utf8Box &error, PendingTransactionRaw &pendingTransaction) { nice(19); + + std::set<std::string> _preferred_inputs; + + for (int i = 0; i < preferred_inputs_size; i++) { + _preferred_inputs.insert(std::string(*preferred_inputs)); + preferred_inputs++; + } auto priority = static_cast<Monero::PendingTransaction::Priority>(priority_raw); std::string _payment_id; @@ -503,11 +581,11 @@ extern "C" if (amount != nullptr) { uint64_t _amount = Monero::Wallet::amountFromString(std::string(amount)); - transaction = m_wallet->createTransaction(std::string(address), _payment_id, _amount, m_wallet->defaultMixin(), priority, subaddr_account); + transaction = m_wallet->createTransaction(std::string(address), _payment_id, _amount, m_wallet->defaultMixin(), priority, subaddr_account, {}, _preferred_inputs); } else { - transaction = m_wallet->createTransaction(std::string(address), _payment_id, Monero::optional<uint64_t>(), m_wallet->defaultMixin(), priority, subaddr_account); + transaction = m_wallet->createTransaction(std::string(address), _payment_id, Monero::optional<uint64_t>(), m_wallet->defaultMixin(), priority, subaddr_account, {}, _preferred_inputs); } int status = transaction->status(); @@ -527,7 +605,9 @@ extern "C" } bool transaction_create_mult_dest(char **addresses, char *payment_id, char **amounts, uint32_t size, - uint8_t priority_raw, uint32_t subaddr_account, Utf8Box &error, PendingTransactionRaw &pendingTransaction) + uint8_t priority_raw, uint32_t subaddr_account, + char **preferred_inputs, uint32_t preferred_inputs_size, + Utf8Box &error, PendingTransactionRaw &pendingTransaction) { nice(19); @@ -541,6 +621,13 @@ extern "C" amounts++; } + std::set<std::string> _preferred_inputs; + + for (int i = 0; i < preferred_inputs_size; i++) { + _preferred_inputs.insert(std::string(*preferred_inputs)); + preferred_inputs++; + } + auto priority = static_cast<Monero::PendingTransaction::Priority>(priority_raw); std::string _payment_id; Monero::PendingTransaction *transaction; @@ -800,6 +887,91 @@ extern "C" return m_wallet->trustedDaemon(); } + CoinsInfoRow* coin(int index) + { + if (index >= 0 && index < m_coins_info.size()) { + std::list<Monero::CoinsInfo*>::iterator it = m_coins_info.begin(); + std::advance(it, index); + Monero::CoinsInfo* element = *it; + std::cout << "Element at index " << index << ": " << element << std::endl; + return new CoinsInfoRow(element); + } else { + std::cout << "Invalid index." << std::endl; + return nullptr; // Return a default value (nullptr) for invalid index + } + } + + void refresh_coins(uint32_t accountIndex) + { + m_coins_info.clear(); + + m_coins->refresh(); + for (const auto i : m_coins->getAll()) { + if (i->subaddrAccount() == accountIndex && !(i->spent())) { + m_coins_info.push_back(i); + } + } + } + + uint64_t coins_count() + { + return m_coins_info.size(); + } + + CoinsInfoRow** coins_from_account(uint32_t accountIndex) + { + std::vector<CoinsInfoRow*> matchingCoins; + + for (int i = 0; i < coins_count(); i++) { + CoinsInfoRow* coinInfo = coin(i); + if (coinInfo->subaddrAccount == accountIndex) { + matchingCoins.push_back(coinInfo); + } + } + + CoinsInfoRow** result = new CoinsInfoRow*[matchingCoins.size()]; + std::copy(matchingCoins.begin(), matchingCoins.end(), result); + return result; + } + + CoinsInfoRow** coins_from_txid(const char* txid, size_t* count) + { + std::vector<CoinsInfoRow*> matchingCoins; + + for (int i = 0; i < coins_count(); i++) { + CoinsInfoRow* coinInfo = coin(i); + if (std::string(coinInfo->hash) == txid) { + matchingCoins.push_back(coinInfo); + } + } + + *count = matchingCoins.size(); + CoinsInfoRow** result = new CoinsInfoRow*[*count]; + std::copy(matchingCoins.begin(), matchingCoins.end(), result); + return result; + } + + CoinsInfoRow** coins_from_key_image(const char** keyimages, size_t keyimageCount, size_t* count) + { + std::vector<CoinsInfoRow*> matchingCoins; + + for (int i = 0; i < coins_count(); i++) { + CoinsInfoRow* coinsInfoRow = coin(i); + for (size_t j = 0; j < keyimageCount; j++) { + if (coinsInfoRow->keyImageKnown && std::string(coinsInfoRow->keyImage) == keyimages[j]) { + matchingCoins.push_back(coinsInfoRow); + break; + } + } + } + + *count = matchingCoins.size(); + CoinsInfoRow** result = new CoinsInfoRow*[*count]; + std::copy(matchingCoins.begin(), matchingCoins.end(), result); + return result; + } + + #ifdef __cplusplus } #endif diff --git a/cw_monero/lib/api/coins_info.dart b/cw_monero/lib/api/coins_info.dart new file mode 100644 index 000000000..9a5303f9d --- /dev/null +++ b/cw_monero/lib/api/coins_info.dart @@ -0,0 +1,23 @@ +import 'dart:ffi'; +import 'package:cw_monero/api/signatures.dart'; +import 'package:cw_monero/api/structs/coins_info_row.dart'; +import 'package:cw_monero/api/types.dart'; +import 'package:cw_monero/api/monero_api.dart'; + +final refreshCoinsNative = moneroApi + .lookup<NativeFunction<refresh_coins>>('refresh_coins') + .asFunction<RefreshCoins>(); + +final coinsCountNative = moneroApi + .lookup<NativeFunction<coins_count>>('coins_count') + .asFunction<CoinsCount>(); + +final coinNative = moneroApi + .lookup<NativeFunction<coin>>('coin') + .asFunction<GetCoin>(); + +void refreshCoins(int accountIndex) => refreshCoinsNative(accountIndex); + +int countOfCoins() => coinsCountNative(); + +CoinsInfoRow getCoin(int index) => coinNative(index).ref; diff --git a/cw_monero/lib/api/signatures.dart b/cw_monero/lib/api/signatures.dart index 82bc7801e..e208414c8 100644 --- a/cw_monero/lib/api/signatures.dart +++ b/cw_monero/lib/api/signatures.dart @@ -1,4 +1,5 @@ import 'dart:ffi'; +import 'package:cw_monero/api/structs/coins_info_row.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; import 'package:cw_monero/api/structs/ut8_box.dart'; import 'package:ffi/ffi.dart'; @@ -9,8 +10,8 @@ typedef create_wallet = Int8 Function( typedef restore_wallet_from_seed = Int8 Function( Pointer<Utf8>, Pointer<Utf8>, Pointer<Utf8>, Int32, Int64, Pointer<Utf8>); -typedef restore_wallet_from_keys = Int8 Function(Pointer<Utf8>, Pointer<Utf8>, - Pointer<Utf8>, Pointer<Utf8>, Pointer<Utf8>, Pointer<Utf8>, Int32, Int64, Pointer<Utf8>); +typedef restore_wallet_from_keys = Int8 Function(Pointer<Utf8>, Pointer<Utf8>, Pointer<Utf8>, + Pointer<Utf8>, Pointer<Utf8>, Pointer<Utf8>, Int32, Int64, Pointer<Utf8>); typedef is_wallet_exist = Int8 Function(Pointer<Utf8>); @@ -63,8 +64,7 @@ typedef subaddrress_refresh = Void Function(Int32); typedef subaddress_get_all = Pointer<Int64> Function(); -typedef subaddress_add_new = Void Function( - Int32 accountIndex, Pointer<Utf8> label); +typedef subaddress_add_new = Void Function(Int32 accountIndex, Pointer<Utf8> label); typedef subaddress_set_label = Void Function( Int32 accountIndex, Int32 addressIndex, Pointer<Utf8> label); @@ -77,8 +77,7 @@ typedef account_get_all = Pointer<Int64> Function(); typedef account_add_new = Void Function(Pointer<Utf8> label); -typedef account_set_label = Void Function( - Int32 accountIndex, Pointer<Utf8> label); +typedef account_set_label = Void Function(Int32 accountIndex, Pointer<Utf8> label); typedef transactions_refresh = Void Function(); @@ -94,6 +93,8 @@ typedef transaction_create = Int8 Function( Pointer<Utf8> amount, Int8 priorityRaw, Int32 subaddrAccount, + Pointer<Pointer<Utf8>> preferredInputs, + Int32 preferredInputsSize, Pointer<Utf8Box> error, Pointer<PendingTransactionRaw> pendingTransaction); @@ -104,6 +105,8 @@ typedef transaction_create_mult_dest = Int8 Function( Int32 size, Int8 priorityRaw, Int32 subaddrAccount, + Pointer<Pointer<Utf8>> preferredInputs, + Int32 preferredInputsSize, Pointer<Utf8Box> error, Pointer<PendingTransactionRaw> pendingTransaction); @@ -123,10 +126,16 @@ typedef on_startup = Void Function(); typedef rescan_blockchain = Void Function(); -typedef get_subaddress_label = Pointer<Utf8> Function( - Int32 accountIndex, - Int32 addressIndex); +typedef get_subaddress_label = Pointer<Utf8> Function(Int32 accountIndex, Int32 addressIndex); typedef set_trusted_daemon = Void Function(Int8 trusted); -typedef trusted_daemon = Int8 Function(); \ No newline at end of file +typedef trusted_daemon = Int8 Function(); + +typedef refresh_coins = Void Function(Int32 accountIndex); + +typedef coins_count = Int64 Function(); + +// typedef coins_from_txid = Pointer<CoinsInfoRow> Function(Pointer<Utf8> txid); + +typedef coin = Pointer<CoinsInfoRow> Function(Int32 index); diff --git a/cw_monero/lib/api/structs/coins_info_row.dart b/cw_monero/lib/api/structs/coins_info_row.dart new file mode 100644 index 000000000..ff6f6ce73 --- /dev/null +++ b/cw_monero/lib/api/structs/coins_info_row.dart @@ -0,0 +1,73 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class CoinsInfoRow extends Struct { + @Int64() + external int blockHeight; + + external Pointer<Utf8> hash; + + @Uint64() + external int internalOutputIndex; + + @Uint64() + external int globalOutputIndex; + + @Int8() + external int spent; + + @Int8() + external int frozen; + + @Uint64() + external int spentHeight; + + @Uint64() + external int amount; + + @Int8() + external int rct; + + @Int8() + external int keyImageKnown; + + @Uint64() + external int pkIndex; + + @Uint32() + external int subaddrIndex; + + @Uint32() + external int subaddrAccount; + + external Pointer<Utf8> address; + + external Pointer<Utf8> addressLabel; + + external Pointer<Utf8> keyImage; + + @Uint64() + external int unlockTime; + + @Int8() + external int unlocked; + + external Pointer<Utf8> pubKey; + + @Int8() + external int coinbase; + + external Pointer<Utf8> description; + + String getHash() => hash.toDartString(); + + String getAddress() => address.toDartString(); + + String getAddressLabel() => addressLabel.toDartString(); + + String getKeyImage() => keyImage.toDartString(); + + String getPubKey() => pubKey.toDartString(); + + String getDescription() => description.toDartString(); +} diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index 0fc507500..6f848cb3d 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -35,9 +35,8 @@ final transactionCommitNative = moneroApi .lookup<NativeFunction<transaction_commit>>('transaction_commit') .asFunction<TransactionCommit>(); -final getTxKeyNative = moneroApi - .lookup<NativeFunction<get_tx_key>>('get_tx_key') - .asFunction<GetTxKey>(); +final getTxKeyNative = + moneroApi.lookup<NativeFunction<get_tx_key>>('get_tx_key').asFunction<GetTxKey>(); String getTxKey(String txId) { final txIdPointer = txId.toNativeUtf8(); @@ -71,10 +70,21 @@ PendingTransactionDescription createTransactionSync( required String paymentId, required int priorityRaw, String? amount, - int accountIndex = 0}) { + int accountIndex = 0, + List<String> preferredInputs = const []}) { final addressPointer = address.toNativeUtf8(); final paymentIdPointer = paymentId.toNativeUtf8(); final amountPointer = amount != null ? amount.toNativeUtf8() : nullptr; + + final int preferredInputsSize = preferredInputs.length; + final List<Pointer<Utf8>> preferredInputsPointers = + preferredInputs.map((output) => output.toNativeUtf8()).toList(); + final Pointer<Pointer<Utf8>> preferredInputsPointerPointer = calloc(preferredInputsSize); + + for (int i = 0; i < preferredInputsSize; i++) { + preferredInputsPointerPointer[i] = preferredInputsPointers[i]; + } + final errorMessagePointer = calloc<Utf8Box>(); final pendingTransactionRawPointer = calloc<PendingTransactionRaw>(); final created = transactionCreateNative( @@ -83,10 +93,16 @@ PendingTransactionDescription createTransactionSync( amountPointer, priorityRaw, accountIndex, + preferredInputsPointerPointer, + preferredInputsSize, errorMessagePointer, pendingTransactionRawPointer) != 0; + calloc.free(preferredInputsPointerPointer); + + preferredInputsPointers.forEach((element) => calloc.free(element)); + calloc.free(addressPointer); calloc.free(paymentIdPointer); @@ -111,15 +127,16 @@ PendingTransactionDescription createTransactionSync( PendingTransactionDescription createTransactionMultDestSync( {required List<MoneroOutput> outputs, - required String paymentId, - required int priorityRaw, - int accountIndex = 0}) { + required String paymentId, + required int priorityRaw, + int accountIndex = 0, + List<String> preferredInputs = const []}) { final int size = outputs.length; - final List<Pointer<Utf8>> addressesPointers = outputs.map((output) => - output.address.toNativeUtf8()).toList(); + final List<Pointer<Utf8>> addressesPointers = + outputs.map((output) => output.address.toNativeUtf8()).toList(); final Pointer<Pointer<Utf8>> addressesPointerPointer = calloc(size); - final List<Pointer<Utf8>> amountsPointers = outputs.map((output) => - output.amount.toNativeUtf8()).toList(); + final List<Pointer<Utf8>> amountsPointers = + outputs.map((output) => output.amount.toNativeUtf8()).toList(); final Pointer<Pointer<Utf8>> amountsPointerPointer = calloc(size); for (int i = 0; i < size; i++) { @@ -127,25 +144,38 @@ PendingTransactionDescription createTransactionMultDestSync( amountsPointerPointer[i] = amountsPointers[i]; } + final int preferredInputsSize = preferredInputs.length; + final List<Pointer<Utf8>> preferredInputsPointers = + preferredInputs.map((output) => output.toNativeUtf8()).toList(); + final Pointer<Pointer<Utf8>> preferredInputsPointerPointer = calloc(preferredInputsSize); + + for (int i = 0; i < preferredInputsSize; i++) { + preferredInputsPointerPointer[i] = preferredInputsPointers[i]; + } + final paymentIdPointer = paymentId.toNativeUtf8(); final errorMessagePointer = calloc<Utf8Box>(); final pendingTransactionRawPointer = calloc<PendingTransactionRaw>(); final created = transactionCreateMultDestNative( - addressesPointerPointer, - paymentIdPointer, - amountsPointerPointer, - size, - priorityRaw, - accountIndex, - errorMessagePointer, - pendingTransactionRawPointer) != + addressesPointerPointer, + paymentIdPointer, + amountsPointerPointer, + size, + priorityRaw, + accountIndex, + preferredInputsPointerPointer, + preferredInputsSize, + errorMessagePointer, + pendingTransactionRawPointer) != 0; calloc.free(addressesPointerPointer); calloc.free(amountsPointerPointer); + calloc.free(preferredInputsPointerPointer); addressesPointers.forEach((element) => calloc.free(element)); amountsPointers.forEach((element) => calloc.free(element)); + preferredInputsPointers.forEach((element) => calloc.free(element)); calloc.free(paymentIdPointer); @@ -164,13 +194,12 @@ PendingTransactionDescription createTransactionMultDestSync( pointerAddress: pendingTransactionRawPointer.address); } -void commitTransactionFromPointerAddress({required int address}) => commitTransaction( - transactionPointer: Pointer<PendingTransactionRaw>.fromAddress(address)); +void commitTransactionFromPointerAddress({required int address}) => + commitTransaction(transactionPointer: Pointer<PendingTransactionRaw>.fromAddress(address)); void commitTransaction({required Pointer<PendingTransactionRaw> transactionPointer}) { final errorMessagePointer = calloc<Utf8Box>(); - final isCommited = - transactionCommitNative(transactionPointer, errorMessagePointer) != 0; + final isCommited = transactionCommitNative(transactionPointer, errorMessagePointer) != 0; if (!isCommited) { final message = errorMessagePointer.ref.getValue(); @@ -185,13 +214,15 @@ PendingTransactionDescription _createTransactionSync(Map args) { final amount = args['amount'] as String?; final priorityRaw = args['priorityRaw'] as int; final accountIndex = args['accountIndex'] as int; + final preferredInputs = args['preferredInputs'] as List<String>; return createTransactionSync( address: address, paymentId: paymentId, amount: amount, priorityRaw: priorityRaw, - accountIndex: accountIndex); + accountIndex: accountIndex, + preferredInputs: preferredInputs); } PendingTransactionDescription _createTransactionMultDestSync(Map args) { @@ -199,12 +230,14 @@ PendingTransactionDescription _createTransactionMultDestSync(Map args) { final paymentId = args['paymentId'] as String; final priorityRaw = args['priorityRaw'] as int; final accountIndex = args['accountIndex'] as int; + final preferredInputs = args['preferredInputs'] as List<String>; return createTransactionMultDestSync( outputs: outputs, paymentId: paymentId, priorityRaw: priorityRaw, - accountIndex: accountIndex); + accountIndex: accountIndex, + preferredInputs: preferredInputs); } Future<PendingTransactionDescription> createTransaction( @@ -212,23 +245,27 @@ Future<PendingTransactionDescription> createTransaction( required int priorityRaw, String? amount, String paymentId = '', - int accountIndex = 0}) => + int accountIndex = 0, + List<String> preferredInputs = const []}) => compute(_createTransactionSync, { 'address': address, 'paymentId': paymentId, 'amount': amount, 'priorityRaw': priorityRaw, - 'accountIndex': accountIndex + 'accountIndex': accountIndex, + 'preferredInputs': preferredInputs }); Future<PendingTransactionDescription> createTransactionMultDest( - {required List<MoneroOutput> outputs, - required int priorityRaw, - String paymentId = '', - int accountIndex = 0}) => + {required List<MoneroOutput> outputs, + required int priorityRaw, + String paymentId = '', + int accountIndex = 0, + List<String> preferredInputs = const []}) => compute(_createTransactionMultDestSync, { 'outputs': outputs, 'paymentId': paymentId, 'priorityRaw': priorityRaw, - 'accountIndex': accountIndex + 'accountIndex': accountIndex, + 'preferredInputs': preferredInputs }); diff --git a/cw_monero/lib/api/types.dart b/cw_monero/lib/api/types.dart index 051f317c9..2c92f2d80 100644 --- a/cw_monero/lib/api/types.dart +++ b/cw_monero/lib/api/types.dart @@ -1,4 +1,5 @@ import 'dart:ffi'; +import 'package:cw_monero/api/structs/coins_info_row.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; import 'package:cw_monero/api/structs/ut8_box.dart'; import 'package:ffi/ffi.dart'; @@ -92,6 +93,8 @@ typedef TransactionCreate = int Function( Pointer<Utf8> amount, int priorityRaw, int subaddrAccount, + Pointer<Pointer<Utf8>> preferredInputs, + int preferredInputsSize, Pointer<Utf8Box> error, Pointer<PendingTransactionRaw> pendingTransaction); @@ -102,6 +105,8 @@ typedef TransactionCreateMultDest = int Function( int size, int priorityRaw, int subaddrAccount, + Pointer<Pointer<Utf8>> preferredInputs, + int preferredInputsSize, Pointer<Utf8Box> error, Pointer<PendingTransactionRaw> pendingTransaction); @@ -127,4 +132,10 @@ typedef GetSubaddressLabel = Pointer<Utf8> Function( typedef SetTrustedDaemon = void Function(int); -typedef TrustedDaemon = int Function(); \ No newline at end of file +typedef TrustedDaemon = int Function(); + +typedef RefreshCoins = void Function(int); + +typedef CoinsCount = int Function(); + +typedef GetCoin = Pointer<CoinsInfoRow> Function(int); diff --git a/cw_monero/lib/monero_transaction_creation_exception.dart b/cw_monero/lib/exceptions/monero_transaction_creation_exception.dart similarity index 100% rename from cw_monero/lib/monero_transaction_creation_exception.dart rename to cw_monero/lib/exceptions/monero_transaction_creation_exception.dart diff --git a/cw_monero/lib/exceptions/monero_transaction_no_inputs_exception.dart b/cw_monero/lib/exceptions/monero_transaction_no_inputs_exception.dart new file mode 100644 index 000000000..5d808be8f --- /dev/null +++ b/cw_monero/lib/exceptions/monero_transaction_no_inputs_exception.dart @@ -0,0 +1,4 @@ +class MoneroTransactionNoInputsException implements Exception { + @override + String toString() => 'Not enough inputs available. Please select more under Coin Control'; +} diff --git a/cw_monero/lib/monero_subaddress_list.dart b/cw_monero/lib/monero_subaddress_list.dart index 8d8eeb469..dbd1a89ae 100644 --- a/cw_monero/lib/monero_subaddress_list.dart +++ b/cw_monero/lib/monero_subaddress_list.dart @@ -1,19 +1,20 @@ -import 'package:cw_monero/api/structs/subaddress_row.dart'; import 'package:flutter/services.dart'; import 'package:mobx/mobx.dart'; +import 'package:cw_monero/api/coins_info.dart'; import 'package:cw_monero/api/subaddress_list.dart' as subaddress_list; import 'package:cw_core/subaddress.dart'; part 'monero_subaddress_list.g.dart'; -class MoneroSubaddressList = MoneroSubaddressListBase - with _$MoneroSubaddressList; +class MoneroSubaddressList = MoneroSubaddressListBase with _$MoneroSubaddressList; abstract class MoneroSubaddressListBase with Store { MoneroSubaddressListBase() - : _isRefreshing = false, - _isUpdating = false, - subaddresses = ObservableList<Subaddress>(); + : _isRefreshing = false, + _isUpdating = false, + subaddresses = ObservableList<Subaddress>(); + + final List<String> _usedAddresses = []; @observable ObservableList<Subaddress> subaddresses; @@ -22,6 +23,8 @@ abstract class MoneroSubaddressListBase with Store { bool _isUpdating; void update({required int accountIndex}) { + refreshCoins(accountIndex); + if (_isUpdating) { return; } @@ -47,20 +50,24 @@ abstract class MoneroSubaddressListBase with Store { subaddresses = [primary] + rest.toList(); } - return subaddresses - .map((subaddressRow) => Subaddress( + return subaddresses.map((subaddressRow) { + final hasDefaultAddressName = + subaddressRow.getLabel().toLowerCase() == 'Primary account'.toLowerCase() || + subaddressRow.getLabel().toLowerCase() == 'Untitled account'.toLowerCase(); + final isPrimaryAddress = subaddressRow.getId() == 0 && hasDefaultAddressName; + return Subaddress( id: subaddressRow.getId(), address: subaddressRow.getAddress(), - label: subaddressRow.getId() == 0 && - subaddressRow.getLabel().toLowerCase() == 'Primary account'.toLowerCase() - ? 'Primary address' - : subaddressRow.getLabel())) - .toList(); + label: isPrimaryAddress + ? 'Primary address' + : hasDefaultAddressName + ? '' + : subaddressRow.getLabel()); + }).toList(); } Future<void> addSubaddress({required int accountIndex, required String label}) async { - await subaddress_list.addSubaddress( - accountIndex: accountIndex, label: label); + await subaddress_list.addSubaddress(accountIndex: accountIndex, label: label); update(accountIndex: accountIndex); } @@ -86,4 +93,59 @@ abstract class MoneroSubaddressListBase with Store { rethrow; } } + + Future<void> updateWithAutoGenerate({ + required int accountIndex, + required String defaultLabel, + required List<String> usedAddresses, + }) async { + _usedAddresses.addAll(usedAddresses); + if (_isUpdating) { + return; + } + + try { + _isUpdating = true; + refresh(accountIndex: accountIndex); + subaddresses.clear(); + final newSubAddresses = + await _getAllUnusedAddresses(accountIndex: accountIndex, label: defaultLabel); + subaddresses.addAll(newSubAddresses); + } catch (e) { + rethrow; + } finally { + _isUpdating = false; + } + } + + Future<List<Subaddress>> _getAllUnusedAddresses( + {required int accountIndex, required String label}) async { + final allAddresses = subaddress_list.getAllSubaddresses(); + + if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.last.getAddress())) { + final isAddressUnused = await _newSubaddress(accountIndex: accountIndex, label: label); + if (!isAddressUnused) { + return await _getAllUnusedAddresses(accountIndex: accountIndex, label: label); + } + } + + return allAddresses + .map((subaddressRow) => Subaddress( + id: subaddressRow.getId(), + address: subaddressRow.getAddress(), + label: subaddressRow.getId() == 0 && + subaddressRow.getLabel().toLowerCase() == 'Primary account'.toLowerCase() + ? 'Primary address' + : subaddressRow.getLabel())) + .toList(); + } + + Future<bool> _newSubaddress({required int accountIndex, required String label}) async { + await subaddress_list.addSubaddress(accountIndex: accountIndex, label: label); + + return subaddress_list + .getAllSubaddresses() + .where((subaddressRow) => !_usedAddresses.contains(subaddressRow.getAddress())) + .isNotEmpty; + } } diff --git a/cw_monero/lib/monero_unspent.dart b/cw_monero/lib/monero_unspent.dart new file mode 100644 index 000000000..c2ff9f9db --- /dev/null +++ b/cw_monero/lib/monero_unspent.dart @@ -0,0 +1,28 @@ +import 'package:cw_monero/api/structs/coins_info_row.dart'; + +class MoneroUnspent { + MoneroUnspent(this.address, this.hash, this.keyImage, this.value, this.isFrozen, this.isUnlocked) + : isSending = true, + note = ''; + + MoneroUnspent.fromCoinsInfoRow(CoinsInfoRow coinsInfoRow) + : address = coinsInfoRow.getAddress(), + hash = coinsInfoRow.getHash(), + keyImage = coinsInfoRow.getKeyImage(), + value = coinsInfoRow.amount, + isFrozen = coinsInfoRow.frozen == 1, + isUnlocked = coinsInfoRow.unlocked == 1, + isSending = true, + note = ''; + + final String address; + final String hash; + final String keyImage; + final int value; + + final bool isUnlocked; + + bool isFrozen; + bool isSending; + String note; +} diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index ef25b6b93..39c0604a3 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -1,33 +1,35 @@ import 'dart:async'; import 'dart:io'; -import 'package:cw_core/pathForWallet.dart'; -import 'package:cw_core/transaction_priority.dart'; -import 'package:cw_core/monero_amount_format.dart'; -import 'package:cw_monero/monero_transaction_creation_exception.dart'; -import 'package:cw_monero/monero_transaction_info.dart'; -import 'package:cw_monero/monero_wallet_addresses.dart'; -import 'package:cw_core/monero_wallet_utils.dart'; -import 'package:cw_monero/api/structs/pending_transaction.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cw_monero/api/transaction_history.dart' - as monero_transaction_history; -import 'package:cw_monero/api/wallet.dart'; -import 'package:cw_monero/api/wallet.dart' as monero_wallet; -import 'package:cw_monero/api/transaction_history.dart' as transaction_history; -import 'package:cw_monero/api/monero_output.dart'; -import 'package:cw_monero/monero_transaction_creation_credentials.dart'; -import 'package:cw_monero/pending_monero_transaction.dart'; -import 'package:cw_core/monero_wallet_keys.dart'; -import 'package:cw_core/monero_balance.dart'; -import 'package:cw_monero/monero_transaction_history.dart'; import 'package:cw_core/account.dart'; -import 'package:cw_core/pending_transaction.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/sync_status.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/node.dart'; -import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/monero_amount_format.dart'; +import 'package:cw_core/monero_balance.dart'; +import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:cw_core/monero_wallet_keys.dart'; +import 'package:cw_core/monero_wallet_utils.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_priority.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_monero/api/coins_info.dart'; +import 'package:cw_monero/api/monero_output.dart'; +import 'package:cw_monero/api/structs/pending_transaction.dart'; +import 'package:cw_monero/api/transaction_history.dart' as transaction_history; +import 'package:cw_monero/api/wallet.dart' as monero_wallet; +import 'package:cw_monero/exceptions/monero_transaction_creation_exception.dart'; +import 'package:cw_monero/exceptions/monero_transaction_no_inputs_exception.dart'; +import 'package:cw_monero/pending_monero_transaction.dart'; +import 'package:cw_monero/monero_transaction_creation_credentials.dart'; +import 'package:cw_monero/monero_transaction_history.dart'; +import 'package:cw_monero/monero_transaction_info.dart'; +import 'package:cw_monero/monero_unspent.dart'; +import 'package:cw_monero/monero_wallet_addresses.dart'; +import 'package:mobx/mobx.dart'; +import 'package:hive/hive.dart'; part 'monero_wallet.g.dart'; @@ -37,39 +39,51 @@ class MoneroWallet = MoneroWalletBase with _$MoneroWallet; abstract class MoneroWalletBase extends WalletBase<MoneroBalance, MoneroTransactionHistory, MoneroTransactionInfo> with Store { - MoneroWalletBase({required WalletInfo walletInfo}) + MoneroWalletBase({required WalletInfo walletInfo, + required Box<UnspentCoinsInfo> unspentCoinsInfo}) : balance = ObservableMap<CryptoCurrency, MoneroBalance>.of({ - CryptoCurrency.xmr: MoneroBalance( + CryptoCurrency.xmr: MoneroBalance( fullBalance: monero_wallet.getFullBalance(accountIndex: 0), unlockedBalance: monero_wallet.getFullBalance(accountIndex: 0)) - }), + }), _isTransactionUpdating = false, _hasSyncAfterStartup = false, - walletAddresses = MoneroWalletAddresses(walletInfo), + isEnabledAutoGenerateSubaddress = false, syncStatus = NotConnectedSyncStatus(), + unspentCoins = [], + this.unspentCoinsInfo = unspentCoinsInfo, super(walletInfo) { transactionHistory = MoneroTransactionHistory(); - _onAccountChangeReaction = reaction((_) => walletAddresses.account, - (Account? account) { + walletAddresses = MoneroWalletAddresses(walletInfo, transactionHistory); + + _onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) { if (account == null) { return; } - balance = ObservableMap<CryptoCurrency, MoneroBalance>.of( - <CryptoCurrency, MoneroBalance>{ - currency: MoneroBalance( + balance = ObservableMap<CryptoCurrency, MoneroBalance>.of(<CryptoCurrency, MoneroBalance>{ + currency: MoneroBalance( fullBalance: monero_wallet.getFullBalance(accountIndex: account.id), - unlockedBalance: - monero_wallet.getUnlockedBalance(accountIndex: account.id)) - }); - walletAddresses.updateSubaddressList(accountIndex: account.id); + unlockedBalance: monero_wallet.getUnlockedBalance(accountIndex: account.id)) + }); + _updateSubAddress(isEnabledAutoGenerateSubaddress, account: account); + }); + + reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) { + _updateSubAddress(enabled, account: walletAddresses.account); }); } static const int _autoSaveInterval = 30; + Box<UnspentCoinsInfo> unspentCoinsInfo; + @override - MoneroWalletAddresses walletAddresses; + late MoneroWalletAddresses walletAddresses; + + @override + @observable + bool isEnabledAutoGenerateSubaddress; @override @observable @@ -89,11 +103,12 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance, publicSpendKey: monero_wallet.getPublicSpendKey(), publicViewKey: monero_wallet.getPublicViewKey()); - SyncListener? _listener; + monero_wallet.SyncListener? _listener; ReactionDisposer? _onAccountChangeReaction; bool _isTransactionUpdating; bool _hasSyncAfterStartup; Timer? _autoSaveTimer; + List<MoneroUnspent> unspentCoins; Future<void> init() async { await walletAddresses.init(); @@ -170,10 +185,12 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance, @override Future<PendingTransaction> createTransaction(Object credentials) async { final _credentials = credentials as MoneroTransactionCreationCredentials; + final inputs = <String>[]; final outputs = _credentials.outputs; final hasMultiDestination = outputs.length > 1; final unlockedBalance = monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); + var allInputsAmount = 0; PendingTransactionDescription pendingTransactionDescription; @@ -181,6 +198,21 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance, throw MoneroTransactionCreationException('The wallet is not synced.'); } + if (unspentCoins.isEmpty) { + await updateUnspent(); + } + + for (final utx in unspentCoins) { + if (utx.isSending) { + allInputsAmount += utx.value; + inputs.add(utx.keyImage); + } + } + + if (inputs.isEmpty) { + throw MoneroTransactionNoInputsException(); + } + if (hasMultiDestination) { if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { @@ -208,7 +240,8 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance, await transaction_history.createTransactionMultDest( outputs: moneroOutputs, priorityRaw: _credentials.priority.serialize(), - accountIndex: walletAddresses.account!.id); + accountIndex: walletAddresses.account!.id, + preferredInputs: inputs); } else { final output = outputs.first; final address = output.isParsedAddress @@ -229,12 +262,12 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance, 'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.'); } - pendingTransactionDescription = - await transaction_history.createTransaction( + pendingTransactionDescription = await transaction_history.createTransaction( address: address!, amount: amount, priorityRaw: _credentials.priority.serialize(), - accountIndex: walletAddresses.account!.id); + accountIndex: walletAddresses.account!.id, + preferredInputs: inputs); } return PendingMoneroTransaction(pendingTransactionDescription); @@ -264,6 +297,14 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance, @override Future<void> save() async { + await walletAddresses.updateUsedSubaddress(); + + if (isEnabledAutoGenerateSubaddress) { + walletAddresses.updateUnusedSubaddress( + accountIndex: walletAddresses.account?.id ?? 0, + defaultLabel: walletAddresses.account?.label ?? ''); + } + await walletAddresses.updateAddressesInBox(); await backupWalletFiles(name); await monero_wallet.store(); @@ -354,6 +395,85 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance, await walletInfo.save(); } + Future<void> updateUnspent() async { + refreshCoins(walletAddresses.account!.id); + + unspentCoins.clear(); + + final coinCount = countOfCoins(); + for (var i = 0; i < coinCount; i++) { + final coin = getCoin(i); + if (coin.spent == 0) { + unspentCoins.add(MoneroUnspent.fromCoinsInfoRow(coin)); + } + } + + if (unspentCoinsInfo.isEmpty) { + unspentCoins.forEach((coin) => _addCoinInfo(coin)); + return; + } + + if (unspentCoins.isNotEmpty) { + unspentCoins.forEach((coin) { + final coinInfoList = unspentCoinsInfo.values.where((element) => + element.walletId.contains(id) && element.hash.contains(coin.hash)); + + if (coinInfoList.isNotEmpty) { + final coinInfo = coinInfoList.first; + + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + } else { + _addCoinInfo(coin); + } + }); + } + + await _refreshUnspentCoinsInfo(); + _askForUpdateBalance(); + } + + Future<void> _addCoinInfo(MoneroUnspent coin) async { + final newInfo = UnspentCoinsInfo( + walletId: id, + hash: coin.hash, + isFrozen: coin.isFrozen, + isSending: coin.isSending, + noteRaw: coin.note, + address: coin.address, + value: coin.value, + vout: 0, + keyImage: coin.keyImage + ); + + await unspentCoinsInfo.add(newInfo); + } + + Future<void> _refreshUnspentCoinsInfo() async { + try { + final List<dynamic> keys = <dynamic>[]; + final currentWalletUnspentCoins = unspentCoinsInfo.values + .where((element) => element.walletId.contains(id)); + + if (currentWalletUnspentCoins.isNotEmpty) { + currentWalletUnspentCoins.forEach((element) { + final existUnspentCoins = unspentCoins.where((coin) => element.hash.contains(coin.hash)); + + if (existUnspentCoins.isEmpty) { + keys.add(element.key); + } + }); + } + + if (keys.isNotEmpty) { + await unspentCoinsInfo.deleteAll(keys); + } + } catch (e) { + print(e.toString()); + } + } + String getTransactionAddress(int accountIndex, int addressIndex) => monero_wallet.getAddress( accountIndex: accountIndex, @@ -361,7 +481,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance, @override Future<Map<String, MoneroTransactionInfo>> fetchTransactions() async { - monero_transaction_history.refreshTransactions(); + transaction_history.refreshTransactions(); return _getAllTransactions(null).fold<Map<String, MoneroTransactionInfo>>( <String, MoneroTransactionInfo>{}, (Map<String, MoneroTransactionInfo> acc, MoneroTransactionInfo tx) { @@ -392,7 +512,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance, } List<MoneroTransactionInfo> _getAllTransactions(dynamic _) => - monero_transaction_history + transaction_history .getAllTransations() .map((row) => MoneroTransactionInfo.fromRow(row)) .toList(); @@ -407,7 +527,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance, return; } - final currentHeight = getCurrentHeight(); + final currentHeight = monero_wallet.getCurrentHeight(); if (currentHeight <= 1) { final height = _getHeightByDate(walletInfo.date); @@ -439,11 +559,13 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance, void _askForUpdateBalance() { final unlockedBalance = _getUnlockedBalance(); final fullBalance = _getFullBalance(); + final frozenBalance = _getFrozenBalance(); if (balance[currency]!.fullBalance != fullBalance || - balance[currency]!.unlockedBalance != unlockedBalance) { + balance[currency]!.unlockedBalance != unlockedBalance || + balance[currency]!.frozenBalance != frozenBalance) { balance[currency] = MoneroBalance( - fullBalance: fullBalance, unlockedBalance: unlockedBalance); + fullBalance: fullBalance, unlockedBalance: unlockedBalance, frozenBalance: frozenBalance); } } @@ -456,6 +578,17 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance, int _getUnlockedBalance() => monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); + int _getFrozenBalance() { + var frozenBalance = 0; + + for (var coin in unspentCoinsInfo.values) { + if (coin.isFrozen) + frozenBalance += coin.value; + } + + return frozenBalance; + } + void _onNewBlock(int height, int blocksLeft, double ptc) async { try { if (walletInfo.isRecovery) { @@ -495,4 +628,15 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance, print(e.toString()); } } + + void _updateSubAddress(bool enableAutoGenerate, {Account? account}) { + if (enableAutoGenerate) { + walletAddresses.updateUnusedSubaddress( + accountIndex: account?.id ?? 0, + defaultLabel: account?.label ?? '', + ); + } else { + walletAddresses.updateSubaddressList(accountIndex: account?.id ?? 0); + } + } } diff --git a/cw_monero/lib/monero_wallet_addresses.dart b/cw_monero/lib/monero_wallet_addresses.dart index 2002e789a..fcc3576f4 100644 --- a/cw_monero/lib/monero_wallet_addresses.dart +++ b/cw_monero/lib/monero_wallet_addresses.dart @@ -1,27 +1,32 @@ +import 'package:cw_core/address_info.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/account.dart'; +import 'package:cw_monero/api/wallet.dart'; import 'package:cw_monero/monero_account_list.dart'; import 'package:cw_monero/monero_subaddress_list.dart'; import 'package:cw_core/subaddress.dart'; +import 'package:cw_monero/monero_transaction_history.dart'; import 'package:mobx/mobx.dart'; part 'monero_wallet_addresses.g.dart'; -class MoneroWalletAddresses = MoneroWalletAddressesBase - with _$MoneroWalletAddresses; +class MoneroWalletAddresses = MoneroWalletAddressesBase with _$MoneroWalletAddresses; abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { - MoneroWalletAddressesBase(WalletInfo walletInfo) - : accountList = MoneroAccountList(), - subaddressList = MoneroSubaddressList(), - address = '', - super(walletInfo); + MoneroWalletAddressesBase( + WalletInfo walletInfo, MoneroTransactionHistory moneroTransactionHistory) + : accountList = MoneroAccountList(), + _moneroTransactionHistory = moneroTransactionHistory, + subaddressList = MoneroSubaddressList(), + address = '', + super(walletInfo); + final MoneroTransactionHistory _moneroTransactionHistory; @override @observable String address; - + @observable Account? account; @@ -36,7 +41,6 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { Future<void> init() async { accountList.update(); account = accountList.accounts.first; - updateSubaddressList(accountIndex: account?.id ?? 0); await updateAddressesInBox(); } @@ -46,11 +50,15 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { final _subaddressList = MoneroSubaddressList(); addressesMap.clear(); + addressInfos.clear(); accountList.accounts.forEach((account) { _subaddressList.update(accountIndex: account.id); _subaddressList.subaddresses.forEach((subaddress) { addressesMap[subaddress.address] = subaddress.label; + addressInfos[account.id] ??= []; + addressInfos[account.id]?.add(AddressInfo( + address: subaddress.address, label: subaddress.label, accountIndex: account.id)); }); }); @@ -62,14 +70,14 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { bool validate() { accountList.update(); - final accountListLength = accountList.accounts.length ?? 0; + final accountListLength = accountList.accounts.length; if (accountListLength <= 0) { return false; } subaddressList.update(accountIndex: accountList.accounts.first.id); - final subaddressListLength = subaddressList.subaddresses.length ?? 0; + final subaddressListLength = subaddressList.subaddresses.length; if (subaddressListLength <= 0) { return false; @@ -83,4 +91,24 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { subaddress = subaddressList.subaddresses.first; address = subaddress!.address; } -} \ No newline at end of file + + Future<void> updateUsedSubaddress() async { + final transactions = _moneroTransactionHistory.transactions.values.toList(); + + transactions.forEach((element) { + final accountIndex = element.accountIndex; + final addressIndex = element.addressIndex; + usedAddresses.add(getAddress(accountIndex: accountIndex, addressIndex: addressIndex)); + }); + } + + Future<void> updateUnusedSubaddress( + {required int accountIndex, required String defaultLabel}) async { + await subaddressList.updateWithAutoGenerate( + accountIndex: accountIndex, + defaultLabel: defaultLabel, + usedAddresses: usedAddresses.toList()); + subaddress = subaddressList.subaddresses.last; + address = subaddress!.address; + } +} diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index 6539d58a5..90c63fb72 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -1,16 +1,16 @@ import 'dart:io'; -import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/monero_wallet_utils.dart'; -import 'package:hive/hive.dart'; -import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager; -import 'package:cw_monero/api/wallet.dart' as monero_wallet; -import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart'; -import 'package:cw_monero/monero_wallet.dart'; -import 'package:cw_core/wallet_credentials.dart'; -import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart'; +import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager; +import 'package:cw_monero/monero_wallet.dart'; +import 'package:hive/hive.dart'; class MoneroNewWalletCredentials extends WalletCredentials { MoneroNewWalletCredentials({required String name, required this.language, String? password}) @@ -53,9 +53,10 @@ class MoneroWalletService extends WalletService< MoneroNewWalletCredentials, MoneroRestoreWalletFromSeedCredentials, MoneroRestoreWalletFromKeysCredentials> { - MoneroWalletService(this.walletInfoSource); + MoneroWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box<WalletInfo> walletInfoSource; + final Box<UnspentCoinsInfo> unspentCoinsInfoSource; static bool walletFilesExist(String path) => !File(path).existsSync() && !File('$path.keys').existsSync(); @@ -71,7 +72,8 @@ class MoneroWalletService extends WalletService< path: path, password: credentials.password!, language: credentials.language); - final wallet = MoneroWallet(walletInfo: credentials.walletInfo!); + final wallet = MoneroWallet( + walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); await wallet.init(); return wallet; @@ -107,7 +109,7 @@ class MoneroWalletService extends WalletService< .openWalletAsync({'path': path, 'password': password}); final walletInfo = walletInfoSource.values.firstWhere( (info) => info.id == WalletBase.idFor(name, getType())); - final wallet = MoneroWallet(walletInfo: walletInfo); + final wallet = MoneroWallet(walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource); final isValid = wallet.walletAddresses.validate(); if (!isValid) { @@ -157,7 +159,8 @@ class MoneroWalletService extends WalletService< String currentName, String password, String newName) async { final currentWalletInfo = walletInfoSource.values.firstWhere( (info) => info.id == WalletBase.idFor(currentName, getType())); - final currentWallet = MoneroWallet(walletInfo: currentWalletInfo); + final currentWallet = + MoneroWallet(walletInfo: currentWalletInfo, unspentCoinsInfo: unspentCoinsInfoSource); await currentWallet.renameWalletFiles(newName); @@ -181,7 +184,8 @@ class MoneroWalletService extends WalletService< address: credentials.address, viewKey: credentials.viewKey, spendKey: credentials.spendKey); - final wallet = MoneroWallet(walletInfo: credentials.walletInfo!); + final wallet = MoneroWallet( + walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); await wallet.init(); return wallet; @@ -202,7 +206,8 @@ class MoneroWalletService extends WalletService< password: credentials.password!, seed: credentials.mnemonic, restoreHeight: credentials.height!); - final wallet = MoneroWallet(walletInfo: credentials.walletInfo!); + final wallet = MoneroWallet( + walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); await wallet.init(); return wallet; diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 74bc1ff3b..b60fb919a 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -18,6 +18,19 @@ class CWBitcoin extends Bitcoin { derivationType: derivationType, derivationPath: derivationPath); + String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate) => + (priority as BitcoinTransactionPriority).labelWithRate(rate); + + void updateUnspents(Object wallet) async { + final bitcoinWallet = wallet as ElectrumWallet; + await bitcoinWallet.updateUnspent(); + } + + WalletService createLitecoinWalletService( + Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource) { + return LitecoinWalletService(walletInfoSource, unspentCoinSource); + } + @override WalletCredentials createBitcoinRestoreWalletFromWIFCredentials( {required String name, @@ -122,10 +135,6 @@ class CWBitcoin extends Bitcoin { @override int formatterStringDoubleToBitcoinAmount(String amount) => stringDoubleToBitcoinAmount(amount); - @override - String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate) => - (priority as BitcoinTransactionPriority).labelWithRate(rate); - @override List<Unspent> getUnspents(Object wallet) { final bitcoinWallet = wallet as ElectrumWallet; @@ -135,21 +144,11 @@ class CWBitcoin extends Bitcoin { .toList(); } - void updateUnspents(Object wallet) async { - final bitcoinWallet = wallet as ElectrumWallet; - await bitcoinWallet.updateUnspent(); - } - WalletService createBitcoinWalletService( Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource) { return BitcoinWalletService(walletInfoSource, unspentCoinSource); } - WalletService createLitecoinWalletService( - Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource) { - return LitecoinWalletService(walletInfoSource, unspentCoinSource); - } - @override TransactionPriority getBitcoinTransactionPriorityMedium() => BitcoinTransactionPriority.medium; diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 6476891ed..3f430b7e9 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -246,6 +246,7 @@ class BackupService { final useEtherscan = data[PreferencesKey.useEtherscan] as bool?; final syncAll = data[PreferencesKey.syncAllKey] as bool?; final syncMode = data[PreferencesKey.syncModeKey] as int?; + final autoGenerateSubaddressStatus = data[PreferencesKey.autoGenerateSubaddressStatusKey] as int?; await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName); @@ -296,6 +297,9 @@ class BackupService { if (fiatApiMode != null) await _sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, fiatApiMode); + if (autoGenerateSubaddressStatus != null) + await _sharedPreferences.setInt(PreferencesKey.autoGenerateSubaddressStatusKey, + autoGenerateSubaddressStatus); if (currentPinLength != null) await _sharedPreferences.setInt(PreferencesKey.currentPinLength, currentPinLength); @@ -523,6 +527,8 @@ class BackupService { _sharedPreferences.getInt(PreferencesKey.syncModeKey), PreferencesKey.syncAllKey: _sharedPreferences.getBool(PreferencesKey.syncAllKey), + PreferencesKey.autoGenerateSubaddressStatusKey: + _sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey), }; return json.encode(preferences); diff --git a/lib/di.dart b/lib/di.dart index 4dcf6fa84..ce7408669 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/buy/payfura/payfura_buy_provider.dart'; import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/entities/background_tasks.dart'; +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/receive_page_option.dart'; @@ -232,10 +233,10 @@ late Box<Template> _templates; late Box<ExchangeTemplate> _exchangeTemplates; late Box<TransactionDescription> _transactionDescriptionBox; late Box<Order> _ordersSource; -late Box<UnspentCoinsInfo>? _unspentCoinsInfoSource; +late Box<UnspentCoinsInfo> _unspentCoinsInfoSource; late Box<AnonpayInvoiceInfo> _anonpayInvoiceInfoSource; -Future setup({ +Future<void> setup({ required Box<WalletInfo> walletInfoSource, required Box<Node> nodeSource, required Box<PowNode> powNodeSource, @@ -245,7 +246,7 @@ Future setup({ required Box<ExchangeTemplate> exchangeTemplates, required Box<TransactionDescription> transactionDescriptionBox, required Box<Order> ordersSource, - Box<UnspentCoinsInfo>? unspentCoinsInfoSource, + required Box<UnspentCoinsInfo> unspentCoinsInfoSource, required Box<AnonpayInvoiceInfo> anonpayInvoiceInfoSource, }) async { _walletInfoSource = walletInfoSource; @@ -263,7 +264,6 @@ Future setup({ if (!_isSetupFinished) { getIt.registerSingletonAsync<SharedPreferences>(() => SharedPreferences.getInstance()); } - if (!_isSetupFinished) { getIt.registerFactory(() => BackgroundTasks()); } @@ -805,11 +805,11 @@ Future setup({ case WalletType.haven: return haven!.createHavenWalletService(_walletInfoSource); case WalletType.monero: - return monero!.createMoneroWalletService(_walletInfoSource); + return monero!.createMoneroWalletService(_walletInfoSource, _unspentCoinsInfoSource); case WalletType.bitcoin: - return bitcoin!.createBitcoinWalletService(_walletInfoSource, _unspentCoinsInfoSource!); + return bitcoin!.createBitcoinWalletService(_walletInfoSource, _unspentCoinsInfoSource); case WalletType.litecoin: - return bitcoin!.createLitecoinWalletService(_walletInfoSource, _unspentCoinsInfoSource!); + return bitcoin!.createLitecoinWalletService(_walletInfoSource, _unspentCoinsInfoSource); case WalletType.ethereum: return ethereum!.createEthereumWalletService(_walletInfoSource); case WalletType.nano: diff --git a/lib/entities/auto_generate_subaddress_status.dart b/lib/entities/auto_generate_subaddress_status.dart new file mode 100644 index 000000000..6d6cc406c --- /dev/null +++ b/lib/entities/auto_generate_subaddress_status.dart @@ -0,0 +1,13 @@ + +enum AutoGenerateSubaddressStatus { + initialized(1), + enabled(2), + disabled(3); + + const AutoGenerateSubaddressStatus(this.value); + final int value; + + static AutoGenerateSubaddressStatus deserialize({required int raw}) => + AutoGenerateSubaddressStatus.values.firstWhere((e) => e.value == raw); + +} \ No newline at end of file diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index fe4f2710a..aa1ebe6ce 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -54,6 +54,7 @@ class PreferencesKey { '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; static const exchangeProvidersSelection = 'exchange-providers-selection'; + static const autoGenerateSubaddressStatusKey = 'auto_generate_subaddress_status'; static const clearnetDonationLink = 'clearnet_donation_link'; static const onionDonationLink = 'onion_donation_link'; static const lastSeenAppVersion = 'last_seen_app_version'; diff --git a/lib/entities/unspent_transaction_output.dart b/lib/entities/unspent_transaction_output.dart new file mode 100644 index 000000000..6827f4c01 --- /dev/null +++ b/lib/entities/unspent_transaction_output.dart @@ -0,0 +1,18 @@ +class Unspent { + Unspent(this.address, this.hash, this.value, this.vout, this.keyImage) + : isSending = true, + isFrozen = false, + note = ''; + + final String address; + final String hash; + final int value; + final int vout; + final String? keyImage; + + bool isSending; + bool isFrozen; + String note; + + bool get isP2wpkh => address.startsWith('bc') || address.startsWith('ltc'); +} diff --git a/lib/exchange/changenow/changenow_exchange_provider.dart b/lib/exchange/changenow/changenow_exchange_provider.dart index ecfe888e7..6166a8875 100644 --- a/lib/exchange/changenow/changenow_exchange_provider.dart +++ b/lib/exchange/changenow/changenow_exchange_provider.dart @@ -68,14 +68,12 @@ class ChangeNowExchangeProvider extends ExchangeProvider { required CryptoCurrency to, required bool isFixedRateMode}) async { final headers = {apiHeaderKey: apiKey}; - final normalizedFrom = normalizeCryptoCurrency(from); - final normalizedTo = normalizeCryptoCurrency(to); final flow = getFlow(isFixedRateMode); final params = <String, String>{ - 'fromCurrency': normalizedFrom, - 'toCurrency': normalizedTo, - 'fromNetwork': networkFor(from), - 'toNetwork': networkFor(to), + 'fromCurrency': _normalizeCurrency(from), + 'toCurrency': _normalizeCurrency(to), + 'fromNetwork': _networkFor(from), + 'toNetwork': _networkFor(to), 'flow': flow }; final uri = Uri.https(apiAuthority, rangePath, params); @@ -112,10 +110,10 @@ class ChangeNowExchangeProvider extends ExchangeProvider { final flow = getFlow(isFixedRateMode); final type = isFixedRateMode ? 'reverse' : 'direct'; final body = <String, dynamic>{ - 'fromCurrency': normalizeCryptoCurrency(_request.from), - 'toCurrency': normalizeCryptoCurrency(_request.to), - 'fromNetwork': networkFor(_request.from), - 'toNetwork': networkFor(_request.to), + 'fromCurrency': _normalizeCurrency(_request.from), + 'toCurrency': _normalizeCurrency(_request.to), + 'fromNetwork': _networkFor(_request.from), + 'toNetwork': _networkFor(_request.to), if (!isFixedRateMode) 'fromAmount': _request.fromAmount, if (isFixedRateMode) 'toAmount': _request.toAmount, 'address': _request.address, @@ -241,10 +239,10 @@ class ChangeNowExchangeProvider extends ExchangeProvider { final type = isReverse ? 'reverse' : 'direct'; final flow = getFlow(isFixedRateMode); final params = <String, String>{ - 'fromCurrency': normalizeCryptoCurrency(from), - 'toCurrency': normalizeCryptoCurrency(to), - 'fromNetwork': networkFor(from), - 'toNetwork': networkFor(to), + 'fromCurrency': _normalizeCurrency(from), + 'toCurrency': _normalizeCurrency(to), + 'fromNetwork': _networkFor(from), + 'toNetwork': _networkFor(to), 'type': type, 'flow': flow }; @@ -273,25 +271,34 @@ class ChangeNowExchangeProvider extends ExchangeProvider { } } - String networkFor(CryptoCurrency currency) { + String _networkFor(CryptoCurrency currency) { switch (currency) { case CryptoCurrency.usdt: - return CryptoCurrency.btc.title.toLowerCase(); + return 'btc'; default: - return currency.tag != null ? currency.tag!.toLowerCase() : currency.title.toLowerCase(); + return currency.tag != null ? _normalizeTag(currency.tag!) : currency.title.toLowerCase(); } } -} -String normalizeCryptoCurrency(CryptoCurrency currency) { - switch (currency) { - case CryptoCurrency.zec: - return 'zec'; - case CryptoCurrency.usdcpoly: - return 'usdcmatic'; - case CryptoCurrency.maticpoly: - return 'maticmainnet'; - default: - return currency.title.toLowerCase(); + String _normalizeCurrency(CryptoCurrency currency) { + switch (currency) { + case CryptoCurrency.zec: + return 'zec'; + default: + return currency.title.toLowerCase(); + } } -} + + String _normalizeTag(String tag) { + switch (tag) { + case 'POLY': + return 'matic'; + case 'LN': + return 'lightning'; + case 'AVAXC': + return 'cchain'; + default: + return tag.toLowerCase(); + } + } +} \ No newline at end of file diff --git a/lib/exchange/sideshift/sideshift_exchange_provider.dart b/lib/exchange/sideshift/sideshift_exchange_provider.dart index 26575f2c1..257d339cf 100644 --- a/lib/exchange/sideshift/sideshift_exchange_provider.dart +++ b/lib/exchange/sideshift/sideshift_exchange_provider.dart @@ -28,7 +28,6 @@ class SideShiftExchangeProvider extends ExchangeProvider { CryptoCurrency.xhv, CryptoCurrency.dcr, CryptoCurrency.kmd, - CryptoCurrency.mkr, CryptoCurrency.oxt, CryptoCurrency.pivx, CryptoCurrency.rune, diff --git a/lib/exchange/trocador/trocador_exchange_provider.dart b/lib/exchange/trocador/trocador_exchange_provider.dart index fb6109bdf..b42291ed7 100644 --- a/lib/exchange/trocador/trocador_exchange_provider.dart +++ b/lib/exchange/trocador/trocador_exchange_provider.dart @@ -20,7 +20,6 @@ class TrocadorExchangeProvider extends ExchangeProvider { bool useTorOnly; static const List<CryptoCurrency> _notSupported = [ - CryptoCurrency.scrt, CryptoCurrency.stx, CryptoCurrency.zaddr, ]; @@ -60,8 +59,8 @@ class TrocadorExchangeProvider extends ExchangeProvider { }) async { final params = <String, String>{ 'api_key': apiKey, - 'ticker_from': request.from.title.toLowerCase(), - 'ticker_to': request.to.title.toLowerCase(), + 'ticker_from': _normalizeCurrency(request.from), + 'ticker_to': _normalizeCurrency(request.to), 'network_from': _networkFor(request.from), 'network_to': _networkFor(request.to), 'payment': isFixedRateMode ? 'True' : 'False', @@ -137,7 +136,7 @@ class TrocadorExchangeProvider extends ExchangeProvider { required bool isFixedRateMode}) async { final params = <String, String>{ 'api_key': apiKey, - 'ticker': from.title.toLowerCase(), + 'ticker': _normalizeCurrency(from), 'name': from.name, }; @@ -177,8 +176,8 @@ class TrocadorExchangeProvider extends ExchangeProvider { final params = <String, String>{ 'api_key': apiKey, - 'ticker_from': from.title.toLowerCase(), - 'ticker_to': to.title.toLowerCase(), + 'ticker_from': _normalizeCurrency(from), + 'ticker_to': _normalizeCurrency(to), 'network_from': _networkFor(from), 'network_to': _networkFor(to), if (!isFixedRateMode) 'amount_from': amount.toString(), @@ -279,6 +278,15 @@ class TrocadorExchangeProvider extends ExchangeProvider { } } + String _normalizeCurrency(CryptoCurrency currency) { + switch (currency) { + case CryptoCurrency.zec: + return 'zec'; + default: + return currency.title.toLowerCase(); + } + } + String _normalizeTag(String tag) { switch (tag) { case 'ETH': diff --git a/lib/main.dart b/lib/main.dart index da158c57a..3ac83061a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/locales/locale.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cw_core/address_info.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/pow_node.dart'; @@ -94,6 +95,10 @@ Future<void> initializeAppConfigs() async { CakeHive.registerAdapter(TradeAdapter()); } + if (!CakeHive.isAdapterRegistered(AddressInfo.typeId)) { + CakeHive.registerAdapter(AddressInfoAdapter()); + } + if (!CakeHive.isAdapterRegistered(WalletInfo.typeId)) { CakeHive.registerAdapter(WalletInfoAdapter()); } @@ -143,11 +148,7 @@ Future<void> initializeAppConfigs() async { final templates = await CakeHive.openBox<Template>(Template.boxName); final exchangeTemplates = await CakeHive.openBox<ExchangeTemplate>(ExchangeTemplate.boxName); final anonpayInvoiceInfo = await CakeHive.openBox<AnonpayInvoiceInfo>(AnonpayInvoiceInfo.boxName); - Box<UnspentCoinsInfo>? unspentCoinsInfoSource; - - if (!isMoneroOnly) { - unspentCoinsInfoSource = await CakeHive.openBox<UnspentCoinsInfo>(UnspentCoinsInfo.boxName); - } + final unspentCoinsInfoSource = await CakeHive.openBox<UnspentCoinsInfo>(UnspentCoinsInfo.boxName); await initialSetup( sharedPreferences: await SharedPreferences.getInstance(), @@ -181,7 +182,7 @@ Future<void> initialSetup( required Box<TransactionDescription> transactionDescriptions, required FlutterSecureStorage secureStorage, required Box<AnonpayInvoiceInfo> anonpayInvoiceInfo, - Box<UnspentCoinsInfo>? unspentCoinsInfoSource, + required Box<UnspentCoinsInfo> unspentCoinsInfoSource, int initialMigrationVersion = 15}) async { LanguageService.loadLocaleList(); await defaultSettingsMigration( diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index 5dfe2e467..4f45bc974 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -1,361 +1,363 @@ part of 'monero.dart'; class CWMoneroAccountList extends MoneroAccountList { - CWMoneroAccountList(this._wallet); - final Object _wallet; + CWMoneroAccountList(this._wallet); - @override - @computed + final Object _wallet; + + @override + @computed ObservableList<Account> get accounts { - final moneroWallet = _wallet as MoneroWallet; - final accounts = moneroWallet.walletAddresses.accountList - .accounts - .map((acc) => Account(id: acc.id, label: acc.label, balance: acc.balance)) - .toList(); - return ObservableList<Account>.of(accounts); + final moneroWallet = _wallet as MoneroWallet; + final accounts = moneroWallet.walletAddresses.accountList.accounts + .map((acc) => Account(id: acc.id, label: acc.label, balance: acc.balance)) + .toList(); + return ObservableList<Account>.of(accounts); } @override void update(Object wallet) { - final moneroWallet = wallet as MoneroWallet; - moneroWallet.walletAddresses.accountList.update(); + final moneroWallet = wallet as MoneroWallet; + moneroWallet.walletAddresses.accountList.update(); } @override - void refresh(Object wallet) { - final moneroWallet = wallet as MoneroWallet; - moneroWallet.walletAddresses.accountList.refresh(); - } + void refresh(Object wallet) { + final moneroWallet = wallet as MoneroWallet; + moneroWallet.walletAddresses.accountList.refresh(); + } - @override + @override List<Account> getAll(Object wallet) { - final moneroWallet = wallet as MoneroWallet; - return moneroWallet.walletAddresses.accountList - .getAll() - .map((acc) => Account(id: acc.id, label: acc.label, balance: acc.balance)) - .toList(); + final moneroWallet = wallet as MoneroWallet; + return moneroWallet.walletAddresses.accountList + .getAll() + .map((acc) => Account(id: acc.id, label: acc.label, balance: acc.balance)) + .toList(); } @override Future<void> addAccount(Object wallet, {required String label}) async { - final moneroWallet = wallet as MoneroWallet; - await moneroWallet.walletAddresses.accountList.addAccount(label: label); + final moneroWallet = wallet as MoneroWallet; + await moneroWallet.walletAddresses.accountList.addAccount(label: label); } @override - Future<void> setLabelAccount(Object wallet, {required int accountIndex, required String label}) async { - final moneroWallet = wallet as MoneroWallet; - await moneroWallet.walletAddresses.accountList - .setLabelAccount( - accountIndex: accountIndex, - label: label); + Future<void> setLabelAccount(Object wallet, + {required int accountIndex, required String label}) async { + final moneroWallet = wallet as MoneroWallet; + await moneroWallet.walletAddresses.accountList + .setLabelAccount(accountIndex: accountIndex, label: label); } } class CWMoneroSubaddressList extends MoneroSubaddressList { - CWMoneroSubaddressList(this._wallet); - final Object _wallet; + CWMoneroSubaddressList(this._wallet); - @override - @computed + final Object _wallet; + + @override + @computed ObservableList<Subaddress> get subaddresses { - final moneroWallet = _wallet as MoneroWallet; - final subAddresses = moneroWallet.walletAddresses.subaddressList - .subaddresses - .map((sub) => Subaddress( - id: sub.id, - address: sub.address, - label: sub.label)) - .toList(); - return ObservableList<Subaddress>.of(subAddresses); + final moneroWallet = _wallet as MoneroWallet; + final subAddresses = moneroWallet.walletAddresses.subaddressList.subaddresses + .map((sub) => Subaddress(id: sub.id, address: sub.address, label: sub.label)) + .toList(); + return ObservableList<Subaddress>.of(subAddresses); } @override void update(Object wallet, {required int accountIndex}) { - final moneroWallet = wallet as MoneroWallet; - moneroWallet.walletAddresses.subaddressList.update(accountIndex: accountIndex); + final moneroWallet = wallet as MoneroWallet; + moneroWallet.walletAddresses.subaddressList.update(accountIndex: accountIndex); } @override void refresh(Object wallet, {required int accountIndex}) { - final moneroWallet = wallet as MoneroWallet; - moneroWallet.walletAddresses.subaddressList.refresh(accountIndex: accountIndex); + final moneroWallet = wallet as MoneroWallet; + moneroWallet.walletAddresses.subaddressList.refresh(accountIndex: accountIndex); } @override List<Subaddress> getAll(Object wallet) { - final moneroWallet = wallet as MoneroWallet; - return moneroWallet.walletAddresses - .subaddressList - .getAll() - .map((sub) => Subaddress(id: sub.id, label: sub.label, address: sub.address)) - .toList(); + final moneroWallet = wallet as MoneroWallet; + return moneroWallet.walletAddresses.subaddressList + .getAll() + .map((sub) => Subaddress(id: sub.id, label: sub.label, address: sub.address)) + .toList(); } @override - Future<void> addSubaddress(Object wallet, {required int accountIndex, required String label}) async { - final moneroWallet = wallet as MoneroWallet; - await moneroWallet.walletAddresses.subaddressList - .addSubaddress( - accountIndex: accountIndex, - label: label); + Future<void> addSubaddress(Object wallet, + {required int accountIndex, required String label}) async { + final moneroWallet = wallet as MoneroWallet; + await moneroWallet.walletAddresses.subaddressList + .addSubaddress(accountIndex: accountIndex, label: label); } @override Future<void> setLabelSubaddress(Object wallet, {required int accountIndex, required int addressIndex, required String label}) async { - final moneroWallet = wallet as MoneroWallet; - await moneroWallet.walletAddresses.subaddressList - .setLabelSubaddress( - accountIndex: accountIndex, - addressIndex: addressIndex, - label: label); + final moneroWallet = wallet as MoneroWallet; + await moneroWallet.walletAddresses.subaddressList + .setLabelSubaddress(accountIndex: accountIndex, addressIndex: addressIndex, label: label); } } class CWMoneroWalletDetails extends MoneroWalletDetails { - CWMoneroWalletDetails(this._wallet); - final Object _wallet; + CWMoneroWalletDetails(this._wallet); - @computed + final Object _wallet; + + @computed @override Account get account { - final moneroWallet = _wallet as MoneroWallet; - final acc = moneroWallet.walletAddresses.account; - return Account(id: acc!.id, label: acc.label, balance: acc.balance); + final moneroWallet = _wallet as MoneroWallet; + final acc = moneroWallet.walletAddresses.account; + return Account(id: acc!.id, label: acc.label, balance: acc.balance); } @computed @override - MoneroBalance get balance { - final moneroWallet = _wallet as MoneroWallet; - final balance = moneroWallet.balance; + MoneroBalance get balance { + final moneroWallet = _wallet as MoneroWallet; + final balance = moneroWallet.balance; throw Exception('Unimplemented'); - // return MoneroBalance(); - //return MoneroBalance( - // fullBalance: balance.fullBalance, - // unlockedBalance: balance.unlockedBalance); - } + // return MoneroBalance(); + //return MoneroBalance( + // fullBalance: balance.fullBalance, + // unlockedBalance: balance.unlockedBalance); + } } class CWMonero extends Monero { @override - MoneroAccountList getAccountList(Object wallet) { - return CWMoneroAccountList(wallet); - } - - @override - MoneroSubaddressList getSubaddressList(Object wallet) { - return CWMoneroSubaddressList(wallet); - } - - @override - TransactionHistoryBase getTransactionHistory(Object wallet) { - final moneroWallet = wallet as MoneroWallet; - return moneroWallet.transactionHistory; - } - - @override - MoneroWalletDetails getMoneroWalletDetails(Object wallet) { - return CWMoneroWalletDetails(wallet); - } - - @override - int getHeigthByDate({required DateTime date}) { - return getMoneroHeigthByDate(date: date); - } - - @override - TransactionPriority getDefaultTransactionPriority() { - return MoneroTransactionPriority.automatic; - } + MoneroAccountList getAccountList(Object wallet) { + return CWMoneroAccountList(wallet); + } @override - TransactionPriority getMoneroTransactionPrioritySlow() - => MoneroTransactionPriority.slow; + MoneroSubaddressList getSubaddressList(Object wallet) { + return CWMoneroSubaddressList(wallet); + } @override - TransactionPriority getMoneroTransactionPriorityAutomatic() - => MoneroTransactionPriority.automatic; + TransactionHistoryBase getTransactionHistory(Object wallet) { + final moneroWallet = wallet as MoneroWallet; + return moneroWallet.transactionHistory; + } - @override - TransactionPriority deserializeMoneroTransactionPriority({required int raw}) { - return MoneroTransactionPriority.deserialize(raw: raw); - } + @override + MoneroWalletDetails getMoneroWalletDetails(Object wallet) { + return CWMoneroWalletDetails(wallet); + } - @override - List<TransactionPriority> getTransactionPriorities() { - return MoneroTransactionPriority.all; - } + @override + int getHeigthByDate({required DateTime date}) { + return getMoneroHeigthByDate(date: date); + } - @override - List<String> getMoneroWordList(String language) { - switch (language.toLowerCase()) { - case 'english': - return EnglishMnemonics.words; - case 'chinese (simplified)': - return ChineseSimplifiedMnemonics.words; - case 'dutch': - return DutchMnemonics.words; - case 'german': - return GermanMnemonics.words; - case 'japanese': - return JapaneseMnemonics.words; - case 'portuguese': - return PortugueseMnemonics.words; - case 'russian': - return RussianMnemonics.words; - case 'spanish': - return SpanishMnemonics.words; - case 'french': - return FrenchMnemonics.words; - case 'italian': - return ItalianMnemonics.words; - default: - return EnglishMnemonics.words; - } - } + @override + TransactionPriority getDefaultTransactionPriority() { + return MoneroTransactionPriority.automatic; + } - @override - WalletCredentials createMoneroRestoreWalletFromKeysCredentials({ - required String name, - required String spendKey, - required String viewKey, - required String address, - required String password, - required String language, - required int height}) { - return MoneroRestoreWalletFromKeysCredentials( - name: name, - spendKey: spendKey, - viewKey: viewKey, - address: address, - password: password, - language: language, - height: height); - } - - @override - WalletCredentials createMoneroRestoreWalletFromSeedCredentials({ - required String name, - required String password, - required int height, - required String mnemonic}) { - return MoneroRestoreWalletFromSeedCredentials( - name: name, - password: password, - height: height, - mnemonic: mnemonic); - } + @override + TransactionPriority getMoneroTransactionPrioritySlow() => MoneroTransactionPriority.slow; - @override - WalletCredentials createMoneroNewWalletCredentials({ + @override + TransactionPriority getMoneroTransactionPriorityAutomatic() => + MoneroTransactionPriority.automatic; + + @override + TransactionPriority deserializeMoneroTransactionPriority({required int raw}) { + return MoneroTransactionPriority.deserialize(raw: raw); + } + + @override + List<TransactionPriority> getTransactionPriorities() { + return MoneroTransactionPriority.all; + } + + @override + List<String> getMoneroWordList(String language) { + switch (language.toLowerCase()) { + case 'english': + return EnglishMnemonics.words; + case 'chinese (simplified)': + return ChineseSimplifiedMnemonics.words; + case 'dutch': + return DutchMnemonics.words; + case 'german': + return GermanMnemonics.words; + case 'japanese': + return JapaneseMnemonics.words; + case 'portuguese': + return PortugueseMnemonics.words; + case 'russian': + return RussianMnemonics.words; + case 'spanish': + return SpanishMnemonics.words; + case 'french': + return FrenchMnemonics.words; + case 'italian': + return ItalianMnemonics.words; + default: + return EnglishMnemonics.words; + } + } + + @override + WalletCredentials createMoneroRestoreWalletFromKeysCredentials( + {required String name, + required String spendKey, + required String viewKey, + required String address, + required String password, + required String language, + required int height}) { + return MoneroRestoreWalletFromKeysCredentials( + name: name, + spendKey: spendKey, + viewKey: viewKey, + address: address, + password: password, + language: language, + height: height); + } + + @override + WalletCredentials createMoneroRestoreWalletFromSeedCredentials( + {required String name, + required String password, + required int height, + required String mnemonic}) { + return MoneroRestoreWalletFromSeedCredentials( + name: name, password: password, height: height, mnemonic: mnemonic); + } + + @override + WalletCredentials createMoneroNewWalletCredentials({ required String name, required String language, - String? password,}) { - return MoneroNewWalletCredentials( - name: name, - password: password, - language: language); - } + String? password, + }) { + return MoneroNewWalletCredentials(name: name, password: password, language: language); + } - @override - Map<String, String> getKeys(Object wallet) { - final moneroWallet = wallet as MoneroWallet; - final keys = moneroWallet.keys; - return <String, String>{ - 'privateSpendKey': keys.privateSpendKey, + @override + Map<String, String> getKeys(Object wallet) { + final moneroWallet = wallet as MoneroWallet; + final keys = moneroWallet.keys; + return <String, String>{ + 'privateSpendKey': keys.privateSpendKey, 'privateViewKey': keys.privateViewKey, 'publicSpendKey': keys.publicSpendKey, - 'publicViewKey': keys.publicViewKey}; - } + 'publicViewKey': keys.publicViewKey + }; + } - @override - Object createMoneroTransactionCreationCredentials({ - required List<Output> outputs, - required TransactionPriority priority}) { - return MoneroTransactionCreationCredentials( - outputs: outputs.map((out) => OutputInfo( - fiatAmount: out.fiatAmount, - cryptoAmount: out.cryptoAmount, - address: out.address, - note: out.note, - sendAll: out.sendAll, - extractedAddress: out.extractedAddress, - isParsedAddress: out.isParsedAddress, - formattedCryptoAmount: out.formattedCryptoAmount)) - .toList(), - priority: priority as MoneroTransactionPriority); - } + @override + Object createMoneroTransactionCreationCredentials( + {required List<Output> outputs, required TransactionPriority priority}) { + return MoneroTransactionCreationCredentials( + outputs: outputs + .map((out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount)) + .toList(), + priority: priority as MoneroTransactionPriority); + } - @override - Object createMoneroTransactionCreationCredentialsRaw({ - required List<OutputInfo> outputs, - required TransactionPriority priority}) { - return MoneroTransactionCreationCredentials( - outputs: outputs, - priority: priority as MoneroTransactionPriority); - } + @override + Object createMoneroTransactionCreationCredentialsRaw( + {required List<OutputInfo> outputs, required TransactionPriority priority}) { + return MoneroTransactionCreationCredentials( + outputs: outputs, priority: priority as MoneroTransactionPriority); + } - @override - String formatterMoneroAmountToString({required int amount}) { - return moneroAmountToString(amount: amount); - } + @override + String formatterMoneroAmountToString({required int amount}) { + return moneroAmountToString(amount: amount); + } - @override - double formatterMoneroAmountToDouble({required int amount}) { - return moneroAmountToDouble(amount: amount); - } + @override + double formatterMoneroAmountToDouble({required int amount}) { + return moneroAmountToDouble(amount: amount); + } - @override - int formatterMoneroParseAmount({required String amount}) { - return moneroParseAmount(amount: amount); - } + @override + int formatterMoneroParseAmount({required String amount}) { + return moneroParseAmount(amount: amount); + } - @override - Account getCurrentAccount(Object wallet) { - final moneroWallet = wallet as MoneroWallet; - final acc = moneroWallet.walletAddresses.account; - return Account(id: acc!.id, label: acc.label, balance: acc.balance); - } + @override + Account getCurrentAccount(Object wallet) { + final moneroWallet = wallet as MoneroWallet; + final acc = moneroWallet.walletAddresses.account; + return Account(id: acc!.id, label: acc.label, balance: acc.balance); + } - @override - void setCurrentAccount(Object wallet, int id, String label, String? balance) { - final moneroWallet = wallet as MoneroWallet; - moneroWallet.walletAddresses.account = monero_account.Account(id: id, label: label, balance: balance); - } + @override + void setCurrentAccount(Object wallet, int id, String label, String? balance) { + final moneroWallet = wallet as MoneroWallet; + moneroWallet.walletAddresses.account = + monero_account.Account(id: id, label: label, balance: balance); + } - @override - void onStartup() { - monero_wallet_api.onStartup(); - } + @override + void onStartup() { + monero_wallet_api.onStartup(); + } - @override - int getTransactionInfoAccountId(TransactionInfo tx) { - final moneroTransactionInfo = tx as MoneroTransactionInfo; - return moneroTransactionInfo.accountIndex; - } + @override + int getTransactionInfoAccountId(TransactionInfo tx) { + final moneroTransactionInfo = tx as MoneroTransactionInfo; + return moneroTransactionInfo.accountIndex; + } - @override - WalletService createMoneroWalletService(Box<WalletInfo> walletInfoSource) { - return MoneroWalletService(walletInfoSource); - } + @override + WalletService createMoneroWalletService( + Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource) { + return MoneroWalletService(walletInfoSource, unspentCoinSource); + } - @override - String getTransactionAddress(Object wallet, int accountIndex, int addressIndex) { - final moneroWallet = wallet as MoneroWallet; - return moneroWallet.getTransactionAddress(accountIndex, addressIndex); - } + @override + String getTransactionAddress(Object wallet, int accountIndex, int addressIndex) { + final moneroWallet = wallet as MoneroWallet; + return moneroWallet.getTransactionAddress(accountIndex, addressIndex); + } - @override - String getSubaddressLabel(Object wallet, int accountIndex, int addressIndex) { - final moneroWallet = wallet as MoneroWallet; - return moneroWallet.getSubaddressLabel(accountIndex, addressIndex); - } + @override + String getSubaddressLabel(Object wallet, int accountIndex, int addressIndex) { + final moneroWallet = wallet as MoneroWallet; + return moneroWallet.getSubaddressLabel(accountIndex, addressIndex); + } - @override - Map<String, String> pendingTransactionInfo(Object transaction) { - final ptx = transaction as PendingMoneroTransaction; - return {'id': ptx.id, 'hex': ptx.hex, 'key': ptx.txKey}; - } + @override + Map<String, String> pendingTransactionInfo(Object transaction) { + final ptx = transaction as PendingMoneroTransaction; + return {'id': ptx.id, 'hex': ptx.hex, 'key': ptx.txKey}; + } + + @override + List<Unspent> getUnspents(Object wallet) { + final moneroWallet = wallet as MoneroWallet; + return moneroWallet.unspentCoins + .map((MoneroUnspent moneroUnspent) => Unspent(moneroUnspent.address, moneroUnspent.hash, + moneroUnspent.value, 0, moneroUnspent.keyImage)) + .toList(); + } + + @override + void updateUnspents(Object wallet) async { + final moneroWallet = wallet as MoneroWallet; + await moneroWallet.updateUnspent(); + } } diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 4e9c48e58..cf1cf7b81 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/update_haven_rate.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; @@ -57,7 +58,7 @@ void startCurrentWalletChangeReaction( } final node = settingsStore.getCurrentNode(wallet.type); - + startWalletSyncStatusChangeReaction(wallet, fiatConversionStore); startCheckConnectionReaction(wallet, settingsStore); await getIt.get<SharedPreferences>().setString(PreferencesKey.currentWalletName, wallet.name); @@ -65,6 +66,10 @@ void startCurrentWalletChangeReaction( .get<SharedPreferences>() .setInt(PreferencesKey.currentWalletType, serializeToInt(wallet.type)); + if (wallet.type == WalletType.monero) { + _setAutoGenerateSubaddressStatus(wallet, settingsStore); + } + await wallet.connectToNode(node: node); if (wallet.type == WalletType.nano || wallet.type == WalletType.banano) { final powNode = settingsStore.getCurrentPowNode(wallet.type); @@ -119,3 +124,17 @@ void startCurrentWalletChangeReaction( } }); } + +void _setAutoGenerateSubaddressStatus( + WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet, + SettingsStore settingsStore, +) async { + final walletHasAddresses = await wallet.walletAddresses.addressesMap.length > 1; + if (settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.initialized && + walletHasAddresses) { + settingsStore.autoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.disabled; + } + wallet.isEnabledAutoGenerateSubaddress = + settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.enabled || + settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.initialized; +} diff --git a/lib/src/screens/dashboard/widgets/address_page.dart b/lib/src/screens/dashboard/widgets/address_page.dart index fe34ba118..7a707185d 100644 --- a/lib/src/screens/dashboard/widgets/address_page.dart +++ b/lib/src/screens/dashboard/widgets/address_page.dart @@ -1,8 +1,10 @@ import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/monero_accounts/monero_account_list_page.dart'; import 'package:cake_wallet/anonpay/anonpay_donation_link_info.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/receive_page_option.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/present_receive_option_picker.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/gradient_background.dart'; @@ -24,7 +26,6 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; @@ -173,7 +174,11 @@ class AddressPage extends BasePage { Observer(builder: (_) { if (addressListViewModel.hasAddressList) { return GestureDetector( - onTap: () => Navigator.of(context).pushNamed(Routes.receive), + onTap: () async => dashboardViewModel.isAutoGenerateSubaddressesEnabled + ? await showPopUp<void>( + context: context, + builder: (_) => getIt.get<MoneroAccountListPage>()) + : Navigator.of(context).pushNamed(Routes.receive), child: Container( height: 50, padding: EdgeInsets.only(left: 24, right: 12), @@ -192,16 +197,26 @@ class AddressPage extends BasePage { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Observer( - builder: (_) => Text( - addressListViewModel.hasAccounts - ? S.of(context).accounts_subaddresses - : S.of(context).addresses, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Theme.of(context) - .extension<SyncIndicatorTheme>()!.textColor), - )), + builder: (_) { + String label = addressListViewModel.hasAccounts + ? S.of(context).accounts_subaddresses + : S.of(context).addresses; + + if (dashboardViewModel.isAutoGenerateSubaddressesEnabled) { + label = addressListViewModel.hasAccounts + ? S.of(context).accounts + : S.of(context).account; + } + return Text( + label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .extension<SyncIndicatorTheme>()! + .textColor), + ); + },), Icon( Icons.arrow_forward_ios, size: 14, @@ -211,7 +226,7 @@ class AddressPage extends BasePage { ), ), ); - } else if (addressListViewModel.showElectrumAddressDisclaimer) { + } else if (dashboardViewModel.isAutoGenerateSubaddressesEnabled || addressListViewModel.showElectrumAddressDisclaimer) { return Text(S.of(context).electrum_address_disclaimer, textAlign: TextAlign.center, style: TextStyle( diff --git a/lib/src/screens/monero_accounts/widgets/account_tile.dart b/lib/src/screens/monero_accounts/widgets/account_tile.dart index fa8221513..3e428f355 100644 --- a/lib/src/screens/monero_accounts/widgets/account_tile.dart +++ b/lib/src/screens/monero_accounts/widgets/account_tile.dart @@ -5,13 +5,14 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:cake_wallet/generated/i18n.dart'; class AccountTile extends StatelessWidget { - AccountTile( - {required this.isCurrent, - required this.accountName, - this.accountBalance, - required this.currency, - required this.onTap, - required this.onEdit}); + AccountTile({ + required this.isCurrent, + required this.accountName, + this.accountBalance, + required this.currency, + required this.onTap, + required this.onEdit, + }); final bool isCurrent; final String accountName; diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index 74e33b87a..3adad4379 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -73,7 +73,7 @@ class RestoreOptionsPage extends BasePage { await restoreFromQRViewModel.create(restoreWallet: restoreWallet); if (restoreFromQRViewModel.state is FailureState) { _onWalletCreateFailure(context, - 'Create wallet state: ${restoreFromQRViewModel.state.runtimeType.toString()}'); + 'Create wallet state: ${(restoreFromQRViewModel.state as FailureState).error}'); } } catch (e) { _onWalletCreateFailure(context, e.toString()); diff --git a/lib/src/screens/restore/widgets/backup_file_button.dart b/lib/src/screens/restore/widgets/backup_file_button.dart new file mode 100644 index 000000000..e69de29bb diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index df148e83b..9aa0c8617 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -96,7 +96,7 @@ class SendPage extends BasePage { AppBarStyle get appBarStyle => AppBarStyle.transparent; double _sendCardHeight(BuildContext context) { - final double initialHeight = sendViewModel.isElectrumWallet ? 490 : 465; + final double initialHeight = sendViewModel.hasCoinControl ? 490 : 465; if (!ResponsiveLayoutUtil.instance.isMobile) { return initialHeight - 66; diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 61e8d2f86..ec1ee5087 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -520,7 +520,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S ), ), ), - if (sendViewModel.isElectrumWallet) + if (sendViewModel.hasCoinControl) Padding( padding: EdgeInsets.only(top: 6), child: GestureDetector( diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index ae6bfe6c8..e953fd4ee 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -50,6 +50,14 @@ class PrivacyPage extends BasePage { onValueChange: (BuildContext _, bool value) { _privacySettingsViewModel.setShouldSaveRecipientAddress(value); }), + if (_privacySettingsViewModel.isAutoGenerateSubaddressesVisible) + SettingsSwitcherCell( + title: S.current.auto_generate_subaddresses, + value: _privacySettingsViewModel.isAutoGenerateSubaddressesEnabled, + onValueChange: (BuildContext _, bool value) { + _privacySettingsViewModel.setAutoGenerateSubaddresses(value); + }, + ), if (DeviceInfo.instance.isMobile) SettingsSwitcherCell( title: S.current.prevent_screenshots, diff --git a/lib/src/screens/transaction_details/transaction_details_page.dart b/lib/src/screens/transaction_details/transaction_details_page.dart index 96f67424c..a6f60a52d 100644 --- a/lib/src/screens/transaction_details/transaction_details_page.dart +++ b/lib/src/screens/transaction_details/transaction_details_page.dart @@ -21,7 +21,6 @@ class TransactionDetailsPage extends BasePage { @override Widget body(BuildContext context) { - // FIX-ME: Added `context` it was not used here before, maby bug ? return SectionStandardList( sectionCount: 1, itemCounter: (int _) => transactionDetailsViewModel.items.length, diff --git a/lib/src/screens/transaction_details/widgets/textfield_list_row.dart b/lib/src/screens/transaction_details/widgets/textfield_list_row.dart index 2c7934d19..ff5513502 100644 --- a/lib/src/screens/transaction_details/widgets/textfield_list_row.dart +++ b/lib/src/screens/transaction_details/widgets/textfield_list_row.dart @@ -6,11 +6,12 @@ import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; class TextFieldListRow extends StatelessWidget { TextFieldListRow( {required this.title, - required this.value, - this.titleFontSize = 14, - this.valueFontSize = 16, - this.onSubmitted}) - : _textController = TextEditingController() { + required this.value, + this.titleFontSize = 14, + this.valueFontSize = 16, + this.onSubmitted, + this.onTapOutside}) + : _textController = TextEditingController() { _textController.text = value; } @@ -19,6 +20,7 @@ class TextFieldListRow extends StatelessWidget { final double titleFontSize; final double valueFontSize; final Function(String value)? onSubmitted; + final Function(String value)? onTapOutside; final TextEditingController _textController; @override @@ -58,6 +60,7 @@ class TextFieldListRow extends StatelessWidget { fontWeight: FontWeight.w500, color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor), border: InputBorder.none), + onTapOutside: (_) => onTapOutside?.call(_textController.text), onSubmitted: (value) => onSubmitted?.call(value), ) ]), diff --git a/lib/src/screens/unspent_coins/unspent_coins_details_page.dart b/lib/src/screens/unspent_coins/unspent_coins_details_page.dart index dfa8e8435..61689b52a 100644 --- a/lib/src/screens/unspent_coins/unspent_coins_details_page.dart +++ b/lib/src/screens/unspent_coins/unspent_coins_details_page.dart @@ -24,7 +24,6 @@ class UnspentCoinsDetailsPage extends BasePage { @override Widget body(BuildContext context) { - // FIX-ME: Added `context` it was not used here before, maby bug ? return SectionStandardList( sectionCount: 1, itemCounter: (int _) => unspentCoinsDetailsViewModel.items.length, @@ -45,6 +44,7 @@ class UnspentCoinsDetailsPage extends BasePage { return TextFieldListRow( title: item.title, value: item.value, + onTapOutside: item.onSubmitted, onSubmitted: item.onSubmitted, ); } diff --git a/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart b/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart index 154c3a2f5..93cf27af1 100644 --- a/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart +++ b/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart @@ -94,7 +94,7 @@ class UnspentCoinsListItem extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ AutoSizeText( - address, + '${address.substring(0, 5)}...${address.substring(address.length-5)}', // ToDo: Maybe use address label style: TextStyle( color: addressColor, fontSize: 12, diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index bce4db67d..4b95f9bf1 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/cake_2fa_preset_options.dart'; import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; @@ -43,6 +44,7 @@ abstract class SettingsStoreBase with Store { required FiatCurrency initialFiatCurrency, required BalanceDisplayMode initialBalanceDisplayMode, required bool initialSaveRecipientAddress, + required AutoGenerateSubaddressStatus initialAutoGenerateSubaddressStatus, required bool initialAppSecure, required bool initialDisableBuy, required bool initialDisableSell, @@ -90,6 +92,7 @@ abstract class SettingsStoreBase with Store { fiatCurrency = initialFiatCurrency, balanceDisplayMode = initialBalanceDisplayMode, shouldSaveRecipientAddress = initialSaveRecipientAddress, + autoGenerateSubaddressStatus = initialAutoGenerateSubaddressStatus, fiatApiMode = initialFiatMode, allowBiometricalAuthentication = initialAllowBiometricalAuthentication, selectedCake2FAPreset = initialCake2FAPresetOptions, @@ -200,6 +203,11 @@ abstract class SettingsStoreBase with Store { (bool disableSell) => sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell)); + reaction( + (_) => autoGenerateSubaddressStatus, + (AutoGenerateSubaddressStatus autoGenerateSubaddressStatus) => sharedPreferences.setInt( + PreferencesKey.autoGenerateSubaddressStatusKey, autoGenerateSubaddressStatus.value)); + reaction( (_) => fiatApiMode, (FiatApiMode mode) => @@ -346,6 +354,7 @@ abstract class SettingsStoreBase with Store { static const defaultPinLength = 4; static const defaultActionsMode = 11; static const defaultPinCodeTimeOutDuration = PinCodeRequiredDuration.tenminutes; + static const defaultAutoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.initialized; @observable FiatCurrency fiatCurrency; @@ -368,6 +377,9 @@ abstract class SettingsStoreBase with Store { @observable bool shouldSaveRecipientAddress; + @observable + AutoGenerateSubaddressStatus autoGenerateSubaddressStatus; + @observable bool isAppSecure; @@ -626,7 +638,12 @@ abstract class SettingsStoreBase with Store { final packageInfo = await PackageInfo.fromPlatform(); final deviceName = await _getDeviceName() ?? ''; final shouldShowYatPopup = sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? true; + final generateSubaddresses = + sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey); + final autoGenerateSubaddressStatus = generateSubaddresses != null + ? AutoGenerateSubaddressStatus.deserialize(raw: generateSubaddresses) + : defaultAutoGenerateSubaddressStatus; final nodes = <WalletType, Node>{}; final powNodes = <WalletType, PowNode>{}; @@ -673,6 +690,7 @@ abstract class SettingsStoreBase with Store { initialFiatCurrency: currentFiatCurrency, initialBalanceDisplayMode: currentBalanceDisplayMode, initialSaveRecipientAddress: shouldSaveRecipientAddress, + initialAutoGenerateSubaddressStatus: autoGenerateSubaddressStatus, initialAppSecure: isAppSecure, initialDisableBuy: disableBuy, initialDisableSell: disableSell, @@ -742,6 +760,13 @@ abstract class SettingsStoreBase with Store { priority[WalletType.ethereum]!; } + final generateSubaddresses = + sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey); + + autoGenerateSubaddressStatus = generateSubaddresses != null + ? AutoGenerateSubaddressStatus.deserialize(raw: generateSubaddresses) + : defaultAutoGenerateSubaddressStatus; + balanceDisplayMode = BalanceDisplayMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!); shouldSaveRecipientAddress = @@ -752,8 +777,6 @@ abstract class SettingsStoreBase with Store { numberOfFailedTokenTrials = sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials; - sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? - shouldSaveRecipientAddress; isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure; disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? disableBuy; disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? disableSell; diff --git a/lib/view_model/contact_list/contact_list_view_model.dart b/lib/view_model/contact_list/contact_list_view_model.dart index 5abb4674f..0dcd8108a 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/contact_base.dart'; import 'package:cake_wallet/entities/wallet_contact.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -10,6 +11,7 @@ import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/utils/mobx.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:collection/collection.dart'; part 'contact_list_view_model.g.dart'; @@ -19,11 +21,26 @@ abstract class ContactListViewModelBase with Store { ContactListViewModelBase( this.contactSource, this.walletInfoSource, this._currency, this.settingsStore) : contacts = ObservableList<ContactRecord>(), - walletContacts = [] { + walletContacts = [], + isAutoGenerateEnabled = + settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.enabled { walletInfoSource.values.forEach((info) { - if (info.addresses?.isNotEmpty ?? false) { - info.addresses?.forEach((address, label) { - final name = label.isNotEmpty ? info.name + ' ($label)' : info.name; + if (isAutoGenerateEnabled && info.type == WalletType.monero && info.addressInfos != null) { + info.addressInfos!.forEach((key, value) { + final nextUnusedAddress = value.firstWhereOrNull( + (addressInfo) => !(info.usedAddresses?.contains(addressInfo.address) ?? false)); + if (nextUnusedAddress != null) { + final name = _createName(info.name, nextUnusedAddress.label); + walletContacts.add(WalletContact( + nextUnusedAddress.address, + name, + walletTypeToCryptoCurrency(info.type), + )); + } + }); + } else if (info.addresses?.isNotEmpty == true) { + info.addresses!.forEach((address, label) { + final name = _createName(info.name, label); walletContacts.add(WalletContact( address, name, @@ -44,6 +61,11 @@ abstract class ContactListViewModelBase with Store { initialFire: true); } + String _createName(String walletName, String label) { + return label.isNotEmpty ? '$walletName ($label)' : walletName; + } + + final bool isAutoGenerateEnabled; final Box<Contact> contactSource; final Box<WalletInfo> walletInfoSource; final ObservableList<ContactRecord> contacts; diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 8e4c70232..7cae4fade 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; @@ -236,6 +237,10 @@ abstract class DashboardViewModelBase with Store { @computed double get price => balanceViewModel.price; + @computed + bool get isAutoGenerateSubaddressesEnabled => + settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled; + @computed List<ActionListItem> get items { final _items = <ActionListItem>[]; diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 015b9c82a..78bde55db 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -178,6 +178,10 @@ abstract class SendViewModelBase with Store { .where((template) => _isEqualCurrency(template.cryptoCurrency)) .toList(); + @computed + bool get hasCoinControl => + _wallet.type == WalletType.bitcoin || _wallet.type == WalletType.litecoin || _wallet.type == WalletType.monero; + @computed bool get isElectrumWallet => _wallet.type == WalletType.bitcoin || _wallet.type == WalletType.litecoin; @@ -214,12 +218,12 @@ abstract class SendViewModelBase with Store { @computed List<ContactRecord> get contactsToShow => contactListViewModel.contacts - .where((element) => selectedCryptoCurrency == null || element.type == selectedCryptoCurrency) + .where((element) => element.type == selectedCryptoCurrency) .toList(); @computed List<WalletContact> get walletContactsToShow => contactListViewModel.walletContacts - .where((element) => selectedCryptoCurrency == null || element.type == selectedCryptoCurrency) + .where((element) => element.type == selectedCryptoCurrency) .toList(); @action diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index 5dbfd61dd..27ce919df 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -1,6 +1,10 @@ +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/balance.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; @@ -14,11 +18,27 @@ abstract class PrivacySettingsViewModelBase with Store { PrivacySettingsViewModelBase(this._settingsStore, this._wallet); final SettingsStore _settingsStore; - final WalletBase _wallet; + final WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> _wallet; @computed ExchangeApiMode get exchangeStatus => _settingsStore.exchangeStatus; + @computed + bool get isAutoGenerateSubaddressesEnabled => + _settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled; + + @action + void setAutoGenerateSubaddresses(bool value) { + _wallet.isEnabledAutoGenerateSubaddress = value; + if (value) { + _settingsStore.autoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.enabled; + } else { + _settingsStore.autoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.disabled; + } + } + + bool get isAutoGenerateSubaddressesVisible => _wallet.type == WalletType.monero; + @computed bool get shouldSaveRecipientAddress => _settingsStore.shouldSaveRecipientAddress; diff --git a/lib/view_model/unspent_coins/unspent_coins_details_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_details_view_model.dart index 098296036..992991147 100644 --- a/lib/view_model/unspent_coins/unspent_coins_details_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_details_view_model.dart @@ -23,6 +23,7 @@ abstract class UnspentCoinsDetailsViewModelBase with Store { note = unspentCoinsItem.note { items = [ StandartListItem(title: S.current.transaction_details_amount, value: unspentCoinsItem.amount), + StandartListItem(title: S.current.transaction_details_transaction_id, value: unspentCoinsItem.hash), StandartListItem(title: S.current.widgets_address, value: unspentCoinsItem.address), TextFieldListItem( title: S.current.note_tap_to_change, @@ -42,19 +43,22 @@ abstract class UnspentCoinsDetailsViewModelBase with Store { unspentCoinsItem.isSending = !value; } await unspentCoinsListViewModel.saveUnspentCoinInfo(unspentCoinsItem); - }), - BlockExplorerListItem( - title: S.current.view_in_block_explorer, - value: _explorerDescription(unspentCoinsListViewModel.wallet.type), - onTap: () { - try { - final url = Uri.parse( - _explorerUrl(unspentCoinsListViewModel.wallet.type, unspentCoinsItem.hash)); - return launchUrl(url); - } catch (e) {} - }) ]; + + if ([WalletType.bitcoin, WalletType.litecoin].contains(unspentCoinsListViewModel.wallet.type)) { + items.add(BlockExplorerListItem( + title: S.current.view_in_block_explorer, + value: _explorerDescription(unspentCoinsListViewModel.wallet.type), + onTap: () { + try { + final url = Uri.parse( + _explorerUrl(unspentCoinsListViewModel.wallet.type, unspentCoinsItem.hash)); + return launchUrl(url); + } catch (e) {} + }, + )); + } } String _explorerUrl(WalletType type, String txId) { diff --git a/lib/view_model/unspent_coins/unspent_coins_item.dart b/lib/view_model/unspent_coins/unspent_coins_item.dart index 2f0d75571..9d1f6c71c 100644 --- a/lib/view_model/unspent_coins/unspent_coins_item.dart +++ b/lib/view_model/unspent_coins/unspent_coins_item.dart @@ -13,7 +13,9 @@ abstract class UnspentCoinsItemBase with Store { required this.note, required this.isSending, required this.amountRaw, - required this.vout}); + required this.vout, + required this.keyImage + }); @observable String address; @@ -38,4 +40,7 @@ abstract class UnspentCoinsItemBase with Store { @observable int vout; -} \ No newline at end of file + + @observable + String? keyImage; +} diff --git a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart index 4df4b9a66..9ecb381fd 100644 --- a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart @@ -1,10 +1,15 @@ -import 'package:cw_core/unspent_coins_info.dart'; +import 'package:collection/collection.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cw_core/wallet_base.dart'; +import 'package:cake_wallet/entities/unspent_transaction_output.dart'; +import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; +import 'package:cw_bitcoin/bitcoin_wallet.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_monero/monero_wallet.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; -import 'package:collection/collection.dart'; part 'unspent_coins_list_view_model.g.dart'; @@ -14,7 +19,7 @@ abstract class UnspentCoinsListViewModelBase with Store { UnspentCoinsListViewModelBase( {required this.wallet, required Box<UnspentCoinsInfo> unspentCoinsInfo}) : _unspentCoinsInfo = unspentCoinsInfo { - bitcoin!.updateUnspents(wallet); + _updateUnspents(); } WalletBase wallet; @@ -22,11 +27,10 @@ abstract class UnspentCoinsListViewModelBase with Store { @computed ObservableList<UnspentCoinsItem> get items => - ObservableList.of(bitcoin!.getUnspents(wallet).map((elem) { - final amount = bitcoin!.formatterBitcoinAmountToString(amount: elem.value) + - ' ${wallet.currency.title}'; + ObservableList.of(_getUnspents().map((elem) { + final amount = formatAmountToString(elem.value) + ' ${wallet.currency.title}'; - final info = getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout); + final info = getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage); return UnspentCoinsItem( address: elem.address, @@ -36,12 +40,14 @@ abstract class UnspentCoinsListViewModelBase with Store { note: info?.note ?? '', isSending: info?.isSending ?? true, amountRaw: elem.value, - vout: elem.vout); + vout: elem.vout, + keyImage: elem.keyImage + ); })); Future<void> saveUnspentCoinInfo(UnspentCoinsItem item) async { try { - final info = getUnspentCoinInfo(item.hash, item.address, item.amountRaw, item.vout); + final info = getUnspentCoinInfo(item.hash, item.address, item.amountRaw, item.vout, item.keyImage); if (info == null) { final newInfo = UnspentCoinsInfo( walletId: wallet.id, @@ -51,10 +57,12 @@ abstract class UnspentCoinsListViewModelBase with Store { vout: item.vout, isFrozen: item.isFrozen, isSending: item.isSending, - noteRaw: item.note); + noteRaw: item.note, + keyImage: item.keyImage + ); await _unspentCoinsInfo.add(newInfo); - bitcoin!.updateUnspents(wallet); + _updateUnspents(); wallet.updateBalance(); return; } @@ -63,19 +71,45 @@ abstract class UnspentCoinsListViewModelBase with Store { info.note = item.note; await info.save(); - bitcoin!.updateUnspents(wallet); + _updateUnspents(); wallet.updateBalance(); } catch (e) { print(e.toString()); } } - UnspentCoinsInfo? getUnspentCoinInfo(String hash, String address, int value, int vout) { + UnspentCoinsInfo? getUnspentCoinInfo(String hash, String address, int value, int vout, String? keyImage) { return _unspentCoinsInfo.values.firstWhereOrNull((element) => element.walletId == wallet.id && element.hash == hash && element.address == address && element.value == value && - element.vout == vout); + element.vout == vout && + element.keyImage == keyImage + ); + } + + String formatAmountToString(int fullBalance) { + if (wallet.type == WalletType.monero) + return monero!.formatterMoneroAmountToString(amount: fullBalance); + if ([WalletType.bitcoin, WalletType.litecoin].contains(wallet.type)) + return bitcoin!.formatterBitcoinAmountToString(amount: fullBalance); + return ''; + } + + + void _updateUnspents() { + if (wallet.type == WalletType.monero) + return monero!.updateUnspents(wallet); + if ([WalletType.bitcoin, WalletType.litecoin].contains(wallet.type)) + return bitcoin!.updateUnspents(wallet); + } + + List<Unspent> _getUnspents() { + if (wallet.type == WalletType.monero) + return monero!.getUnspents(wallet); + if ([WalletType.bitcoin, WalletType.litecoin].contains(wallet.type)) + return bitcoin!.getUnspents(wallet); + return List.empty(); } } diff --git a/pubspec_base.yaml b/pubspec_base.yaml index c8c6d757e..45868105c 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -99,7 +99,10 @@ dev_dependencies: # check flutter_launcher_icons for usage pedantic: ^1.8.0 # replace https://github.com/dart-lang/lints#migrating-from-packagepedantic -# translator: ^0.1.7 + translator: + git: + url: https://github.com/cake-tech/google-translator.git + version: 1.0.0 flutter_icons: image_path: "assets/images/app_logo.png" diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 176413710..1f68afa98 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -688,5 +688,7 @@ "choose_derivation": "اختر اشتقاق المحفظة", "new_first_wallet_text": "حافظ بسهولة على أمان العملة المشفرة", "select_destination": ".ﻲﻃﺎﻴﺘﺣﻻﺍ ﺦﺴﻨﻟﺍ ﻒﻠﻣ ﺔﻬﺟﻭ ﺪﻳﺪﺤﺗ ءﺎﺟﺮﻟﺍ", - "save_to_downloads": "ﺕﻼﻳﺰﻨﺘﻟﺍ ﻲﻓ ﻆﻔﺣ" + "save_to_downloads": "ﺕﻼﻳﺰﻨﺘﻟﺍ ﻲﻓ ﻆﻔﺣ", + "support_description_other_links": "انضم إلى مجتمعاتنا أو تصل إلينا شركائنا من خلال أساليب أخرى", + "auto_generate_subaddresses": "تلقائي توليد subddresses" } diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 7570d21c4..b35fe257e 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -626,6 +626,7 @@ "setup_totp_recommended": "Настройка на TOTP (препоръчително)", "disable_buy": "Деактивирайте действието за покупка", "disable_sell": "Деактивирайте действието за продажба", + "auto_generate_subaddresses": "Автоматично генериране на подадреси", "cake_2fa_preset": "Торта 2FA Preset", "narrow": "Тесен", "normal": "нормално", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 83d994bac..dcb093a69 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -626,6 +626,7 @@ "setup_totp_recommended": "Nastavit TOTP (doporučeno)", "disable_buy": "Zakázat akci nákupu", "disable_sell": "Zakázat akci prodeje", + "auto_generate_subaddresses": "Automaticky generovat podadresy", "cake_2fa_preset": "Předvolba Cake 2FA", "narrow": "Úzký", "normal": "Normální", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index a3c79098b..736261572 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -5,10 +5,10 @@ "please_make_selection": "Bitte treffen Sie unten eine Auswahl zum Erstellen oder Wiederherstellen Ihrer Wallet.", "create_new": "Neue Wallet erstellen", "restore_wallet": "Wallet wiederherstellen", - "monero_com": "Monero.com by Cake Wallet", - "monero_com_wallet_text": "Awesome wallet for Monero", - "haven_app": "Haven by Cake Wallet", - "haven_app_wallet_text": "Awesome wallet for Haven", + "monero_com": "Monero.com von Cake Wallet", + "monero_com_wallet_text": "Eine großartige Wallet für Monero", + "haven_app": "Haven von Cake Wallet", + "haven_app_wallet_text": "Eine großartige Wallet für Haven", "accounts": "Konten", "edit": "Bearbeiten", "account": "Konto", @@ -152,10 +152,10 @@ "restore_wallet_restore_description": "Beschreibung zur Wallet-Wiederherstellung", "restore_new_seed": "Neuer Seed", "restore_active_seed": "Aktiver Seed", - "restore_bitcoin_description_from_seed": "Stellen Sie Ihre Brieftasche aus dem 24-Wort-Kombinationscode wieder her", - "restore_bitcoin_description_from_keys": "Stellen Sie Ihre Brieftasche aus der generierten WIF-Zeichenfolge aus Ihren privaten Schlüsseln wieder her", + "restore_bitcoin_description_from_seed": "Stellen Sie Ihre Wallet aus dem 24-Wort-Kombinationscode wieder her", + "restore_bitcoin_description_from_keys": "Stellen Sie Ihre Wallet aus der generierten WIF-Zeichenfolge aus Ihren privaten Schlüsseln wieder her", "restore_bitcoin_title_from_keys": "Aus WIF wiederherstellen", - "restore_from_date_or_blockheight": "Bitte geben Sie ein Datum ein, das einige Tage vor dem Erstellen dieser Brieftasche liegt. Oder wenn Sie die Blockhöhe kennen, geben Sie stattdessen diese ein", + "restore_from_date_or_blockheight": "Bitte geben Sie ein Datum ein, das einige Tage vor dem Erstellen dieser Wallet liegt. Oder wenn Sie die Blockhöhe kennen, geben Sie stattdessen diese ein", "seed_reminder": "Bitte notieren Sie diese für den Fall, dass Sie Ihr Telefon verlieren oder es kaputtgeht", "seed_title": "Seed", "seed_share": "Seed teilen", @@ -403,7 +403,7 @@ "buy_bitcoin": "Bitcoin kaufen", "buy_with": "Kaufen mit", "moonpay_alert_text": "Der Wert des Betrags muss größer oder gleich ${minAmount} ${fiatCurrency} sein", - "outdated_electrum_wallet_receive_warning": "Wenn diese Brieftasche einen 12-Wort-Seed hat und in Cake erstellt wurde, zahlen Sie KEINE Bitcoins in diese Brieftasche ein. Alle auf diese Wallet übertragenen BTC können verloren gehen. Erstellen Sie eine neue 24-Wort-Wallet (tippen Sie auf das Menü oben rechts, wählen Sie Wallets, wählen Sie Neue Wallet erstellen und dann Bitcoin) und verschieben Sie Ihre BTC SOFORT dorthin. Neue (24-Wort-)BTC-Wallets von Cake sind sicher", + "outdated_electrum_wallet_receive_warning": "Wenn diese Wallet einen 12-Wort-Seed hat und in Cake erstellt wurde, zahlen Sie KEINE Bitcoins in diese Wallet ein. Alle auf diese Wallet übertragenen BTC können verloren gehen. Erstellen Sie eine neue 24-Wort-Wallet (tippen Sie auf das Menü oben rechts, wählen Sie Wallets, wählen Sie Neue Wallet erstellen und dann Bitcoin) und verschieben Sie Ihre BTC SOFORT dorthin. Neue (24-Wort-)BTC-Wallets von Cake sind sicher", "do_not_show_me": "Zeig mir das nicht noch einmal", "unspent_coins_title": "Nicht ausgegebene Münzen", "unspent_coins_details_title": "Details zu nicht ausgegebenen Münzen", @@ -596,10 +596,10 @@ "sweeping_wallet_alert": "Das sollte nicht lange dauern. VERLASSEN SIE DIESEN BILDSCHIRM NICHT, ANDERNFALLS KÖNNEN DIE SWEPT-GELDER VERLOREN GEHEN", "decimal_places_error": "Zu viele Nachkommastellen", "edit_node": "Knoten bearbeiten", - "frozen_balance": "Gefrorenes Gleichgewicht", + "frozen_balance": "Gefrorenes Guthaben", "invoice_details": "Rechnungs-Details", "donation_link_details": "Details zum Spendenlink", - "anonpay_description": "Generieren Sie ${type}. Der Empfänger kann ${method} mit jeder unterstützten Kryptowährung verwenden, und Sie erhalten Geld in dieser Brieftasche.", + "anonpay_description": "Generieren Sie ${type}. Der Empfänger kann ${method} mit jeder unterstützten Kryptowährung verwenden, und Sie erhalten Geld in dieser Wallet.", "create_invoice": "Rechnung erstellen", "create_donation_link": "Spendenlink erstellen", "optional_email_hint": "Optionale Benachrichtigungs-E-Mail für den Zahlungsempfänger", @@ -615,22 +615,22 @@ "prevent_screenshots": "Verhindern Sie Screenshots und Bildschirmaufzeichnungen", "profile": "Profil", "close": "Schließen", - "modify_2fa": "Kuchen 2FA ändern", - "disable_cake_2fa": "Kuchen 2FA deaktivieren", - "question_to_disable_2fa": "Sind Sie sicher, dass Sie Cake 2FA deaktivieren möchten? Für den Zugriff auf die Brieftasche und bestimmte Funktionen wird kein 2FA-Code mehr benötigt.", + "modify_2fa": "Cake 2FA ändern", + "disable_cake_2fa": "Cake 2FA deaktivieren", + "question_to_disable_2fa": "Sind Sie sicher, dass Sie Cake 2FA deaktivieren möchten? Für den Zugriff auf die Wallet und bestimmte Funktionen wird kein 2FA-Code mehr benötigt.", "disable": "Deaktivieren", - "setup_2fa": "Setup-Kuchen 2FA", + "setup_2fa": "Setup-Cake 2FA", "verify_with_2fa": "Verifizieren Sie mit Cake 2FA", "totp_code": "TOTP-Code", "please_fill_totp": "Bitte geben Sie den 8-stelligen Code ein, der auf Ihrem anderen Gerät vorhanden ist", - "totp_2fa_success": "Erfolg! Cake 2FA für dieses Wallet aktiviert. Denken Sie daran, Ihren mnemonischen Seed zu speichern, falls Sie den Zugriff auf die Brieftasche verlieren.", + "totp_2fa_success": "Erfolg! Cake 2FA für dieses Wallet aktiviert. Denken Sie daran, Ihren mnemonischen Seed zu speichern, falls Sie den Zugriff auf die Wallet verlieren.", "totp_verification_success": "Verifizierung erfolgreich!", "totp_2fa_failure": "Falscher Code. Bitte versuchen Sie es mit einem anderen Code oder generieren Sie einen neuen geheimen Schlüssel. Verwenden Sie eine kompatible 2FA-App, die 8-stellige Codes und SHA512 unterstützt.", "enter_totp_code": "Bitte geben Sie den TOTP-Code ein.", "add_secret_code": "Fügen Sie diesen Geheimcode einem anderen Gerät hinzu", "totp_secret_code": "TOTP-Geheimcode", "important_note": "Wichtiger Hinweis", - "setup_2fa_text": "Cake 2FA ist NICHT so sicher wie eine Kühllagerung. 2FA schützt vor grundlegenden Arten von Angriffen, z. B. wenn Ihr Freund Ihren Fingerabdruck bereitstellt, während Sie schlafen.\n\n Cake 2FA schützt NICHT vor einem kompromittierten Gerät durch einen raffinierten Angreifer.\n\n Wenn Sie den Zugriff auf Ihre 2FA-Codes verlieren , VERLIEREN SIE DEN ZUGANG ZU DIESEM WALLET. Sie müssen Ihre Brieftasche aus mnemonic Seed wiederherstellen. SIE MÜSSEN DESHALB IHRE MNEMONISCHEN SEEDS SICHERN! Außerdem kann jemand mit Zugriff auf Ihre mnemonischen Seed(s) Ihr Geld stehlen und Cake 2FA umgehen.\n\n Cake-Supportmitarbeiter können Ihnen nicht helfen, wenn Sie den Zugriff auf Ihre mnemonischen Seed(s) verlieren, da Cake ein Brieftasche ohne Verwahrung.", + "setup_2fa_text": "Cake 2FA ist NICHT so sicher wie eine Kühllagerung. 2FA schützt vor grundlegenden Arten von Angriffen, z. B. wenn Ihr Freund Ihren Fingerabdruck bereitstellt, während Sie schlafen.\n\n Cake 2FA schützt NICHT vor einem kompromittierten Gerät durch einen raffinierten Angreifer.\n\n Wenn Sie den Zugriff auf Ihre 2FA-Codes verlieren , VERLIEREN SIE DEN ZUGANG ZU DIESEM WALLET. Sie müssen Ihre Wallet aus mnemonic Seed wiederherstellen. SIE MÜSSEN DESHALB IHRE MNEMONISCHEN SEEDS SICHERN! Außerdem kann jemand mit Zugriff auf Ihre mnemonischen Seed(s) Ihr Geld stehlen und Cake 2FA umgehen.\n\n Cake-Supportmitarbeiter können Ihnen nicht helfen, wenn Sie den Zugriff auf Ihre mnemonischen Seed(s) verlieren, da Cake Wallet eine Wallet ohne treuhänderische Verwahrung ist.", "setup_totp_recommended": "TOTP einrichten (empfohlen)", "disable_buy": "Kaufaktion deaktivieren", "disable_sell": "Verkaufsaktion deaktivieren", @@ -641,6 +641,7 @@ "high_contrast_theme": "Kontrastreiches Thema", "matrix_green_dark_theme": "Matrix Green Dark Theme", "monero_light_theme": "Monero Light-Thema", + "auto_generate_subaddresses": "Unteradressen automatisch generieren", "narrow": "Eng", "normal": "Normal", "aggressive": "Übereifrig", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 5146107f8..ee7cbc6a1 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -568,6 +568,7 @@ "privacy": "Privacy", "display_settings": "Display settings", "other_settings": "Other settings", + "auto_generate_subaddresses": "Auto generate subaddresses", "require_pin_after": "Require PIN after", "always": "Always", "minutes_to_pin_code": "${minute} minutes", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 5244b549e..1370ff5dd 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -641,6 +641,7 @@ "high_contrast_theme": "Tema de alto contraste", "matrix_green_dark_theme": "Matrix verde oscuro tema", "monero_light_theme": "Tema ligero de Monero", + "auto_generate_subaddresses": "Generar subdirecciones automáticamente", "narrow": "Angosto", "normal": "Normal", "aggressive": "Demasiado entusiasta", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 653853027..dd9cd1dce 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -641,6 +641,7 @@ "high_contrast_theme": "Thème à contraste élevé", "matrix_green_dark_theme": "Thème Matrix Green Dark", "monero_light_theme": "Thème de lumière Monero", + "auto_generate_subaddresses": "Générer automatiquement des sous-adresses", "narrow": "Étroit", "normal": "Normal", "aggressive": "Trop zélé", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 947e75446..bb3e495ec 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -619,6 +619,7 @@ "high_contrast_theme": "Babban Jigon Kwatance", "matrix_green_dark_theme": "Matrix Green Dark Jigo", "monero_light_theme": "Jigon Hasken Monero", + "auto_generate_subaddresses": "Saɓaƙa subaddresses ta kai tsaye", "narrow": "kunkuntar", "normal": "Na al'ada", "aggressive": "Mai tsananin kishi", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 780bec5b3..a2a138aa1 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -641,6 +641,7 @@ "high_contrast_theme": "उच्च कंट्रास्ट थीम", "matrix_green_dark_theme": "मैट्रिक्स ग्रीन डार्क थीम", "monero_light_theme": "मोनेरो लाइट थीम", + "auto_generate_subaddresses": "स्वचालित रूप से उप-पते उत्पन्न करें", "narrow": "सँकरा", "normal": "सामान्य", "aggressive": "ज्यादा", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 35d03d251..e6f0c1b14 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -641,6 +641,7 @@ "high_contrast_theme": "Tema visokog kontrasta", "matrix_green_dark_theme": "Matrix Green Dark Theme", "monero_light_theme": "Monero lagana tema", + "auto_generate_subaddresses": "Automatski generirajte podadrese", "narrow": "Usko", "normal": "Normalno", "aggressive": "Preterano", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 8aa3732fb..b41881488 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -622,6 +622,7 @@ "setup_totp_recommended": "Siapkan TOTP (Disarankan)", "disable_buy": "Nonaktifkan tindakan beli", "disable_sell": "Nonaktifkan aksi jual", + "auto_generate_subaddresses": "Menghasilkan subalamat secara otomatis", "cake_2fa_preset": "Preset Kue 2FA", "narrow": "Sempit", "normal": "Normal", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 8505326c1..f356fe9e1 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -634,6 +634,7 @@ "setup_totp_recommended": "Imposta TOTP (consigliato)", "disable_buy": "Disabilita l'azione di acquisto", "disable_sell": "Disabilita l'azione di vendita", + "auto_generate_subaddresses": "Genera automaticamente sottindirizzi", "cake_2fa_preset": "Torta 2FA Preset", "narrow": "Stretto", "normal": "Normale", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 3fb62b55b..e9a610d15 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -692,5 +692,6 @@ "choose_derivation": "ウォレット派生を選択します", "new_first_wallet_text": "暗号通貨を簡単に安全に保ちます", "select_destination": "バックアップファイルの保存先を選択してください。", - "save_to_downloads": "ダウンロードに保存" + "save_to_downloads": "ダウンロードに保存", + "auto_generate_subaddresses": "Autoはサブアドレスを生成します" } diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 2094b3729..8f0b9ee0a 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -690,5 +690,6 @@ "choose_derivation": "지갑 파생을 선택하십시오", "new_first_wallet_text": "cryptocurrency를 쉽게 안전하게 유지하십시오", "select_destination": "백업 파일의 대상을 선택하십시오.", - "save_to_downloads": "다운로드에 저장" + "save_to_downloads": "다운로드에 저장", + "auto_generate_subaddresses": "자동 생성 서브 아드 드레스" } diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index c19c6b641..f0a97d740 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -690,5 +690,6 @@ "choose_derivation": "ပိုက်ဆံအိတ်ကိုရွေးချယ်ပါ", "new_first_wallet_text": "သင့်ရဲ့ cryptocurrencrencres ကိုအလွယ်တကူလုံခြုံစွာထားရှိပါ", "select_destination": "အရန်ဖိုင်အတွက် ဦးတည်ရာကို ရွေးပါ။", - "save_to_downloads": "ဒေါင်းလုဒ်များထံ သိမ်းဆည်းပါ။" + "save_to_downloads": "ဒေါင်းလုဒ်များထံ သိမ်းဆည်းပါ။", + "auto_generate_subaddresses": "အော်တို Generate Subaddresses" } diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 0c99c8373..72da05311 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -634,6 +634,7 @@ "setup_totp_recommended": "TOTP instellen (aanbevolen)", "disable_buy": "Koopactie uitschakelen", "disable_sell": "Verkoopactie uitschakelen", + "auto_generate_subaddresses": "Automatisch subadressen genereren", "cake_2fa_preset": "Taart 2FA Voorinstelling", "narrow": "Smal", "normal": "Normaal", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index a4d3d4cbe..f5d0826e6 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -634,6 +634,7 @@ "setup_totp_recommended": "Skonfiguruj TOTP (zalecane)", "disable_buy": "Wyłącz akcję kupna", "disable_sell": "Wyłącz akcję sprzedaży", + "auto_generate_subaddresses": "Automatycznie generuj podadresy", "cake_2fa_preset": "Ciasto 2FA Preset", "narrow": "Wąski", "normal": "Normalna", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 3b8ffa13a..8c741b1c8 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -633,6 +633,7 @@ "setup_totp_recommended": "Configurar TOTP (recomendado)", "disable_buy": "Desativar ação de compra", "disable_sell": "Desativar ação de venda", + "auto_generate_subaddresses": "Gerar subendereços automaticamente", "cake_2fa_preset": "Predefinição de bolo 2FA", "narrow": "Estreito", "normal": "Normal", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 750294dc0..b57484606 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -692,5 +692,6 @@ "choose_derivation": "Выберите вывод кошелька", "new_first_wallet_text": "Легко сохранить свою криптовалюту в безопасности", "select_destination": "Пожалуйста, выберите место для файла резервной копии.", - "save_to_downloads": "Сохранить в загрузках" + "save_to_downloads": "Сохранить в загрузках", + "auto_generate_subaddresses": "Авто генерируйте Subaddresses" } diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 0bb802ec6..0e3405cca 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -690,5 +690,6 @@ "choose_derivation": "เลือก Wallet Derivation", "new_first_wallet_text": "ทำให้สกุลเงินดิจิตอลของคุณปลอดภัยได้อย่างง่ายดาย", "select_destination": "โปรดเลือกปลายทางสำหรับไฟล์สำรอง", - "save_to_downloads": "บันทึกลงดาวน์โหลด" + "save_to_downloads": "บันทึกลงดาวน์โหลด", + "auto_generate_subaddresses": "Auto สร้าง subaddresses" } diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 29179b1ac..9185183e5 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -632,6 +632,7 @@ "setup_totp_recommended": "TOTP'yi kurun (Önerilir)", "disable_buy": "Satın alma işlemini devre dışı bırak", "disable_sell": "Satış işlemini devre dışı bırak", + "auto_generate_subaddresses": "Alt adresleri otomatik olarak oluştur", "cake_2fa_preset": "Kek 2FA Ön Ayarı", "narrow": "Dar", "normal": "Normal", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 9f044cf70..694942833 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -634,6 +634,7 @@ "setup_totp_recommended": "Налаштувати TOTP (рекомендовано)", "disable_buy": "Вимкнути дію покупки", "disable_sell": "Вимкнути дію продажу", + "auto_generate_subaddresses": "Автоматично генерувати підадреси", "cake_2fa_preset": "Торт 2FA Preset", "narrow": "вузькі", "normal": "нормальний", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 2517e6936..23586d159 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -684,5 +684,6 @@ "choose_derivation": "پرس سے ماخوذ منتخب کریں", "new_first_wallet_text": "آسانی سے اپنے cryptocurrency محفوظ رکھیں", "select_destination": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﻝﺰﻨﻣ ﮯﯿﻟ ﮯﮐ ﻞﺋﺎﻓ ﭖﺍ ﮏﯿﺑ ﻡﺮﮐ ﮦﺍﺮﺑ", - "save_to_downloads": "۔ﮟﯾﺮﮐ ﻅﻮﻔﺤﻣ ﮟﯿﻣ ﺯﮈﻮﻟ ﻥﺅﺍﮈ" + "save_to_downloads": "۔ﮟﯾﺮﮐ ﻅﻮﻔﺤﻣ ﮟﯿﻣ ﺯﮈﻮﻟ ﻥﺅﺍﮈ", + "auto_generate_subaddresses": "آٹو سب ایڈریس تیار کرتا ہے" } diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 32d5904d3..ff43c68b8 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -686,5 +686,6 @@ "matrix_green_dark_theme": "Matrix Green Dark Akori", "monero_light_theme": "Monero Light Akori", "select_destination": "Jọwọ yan ibi ti o nlo fun faili afẹyinti.", - "save_to_downloads": "Fipamọ si Awọn igbasilẹ" + "save_to_downloads": "Fipamọ si Awọn igbasilẹ", + "auto_generate_subaddresses": "Aṣiṣe Ibi-Afọwọkọ" } diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 3012a7137..16dcd0a66 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -691,5 +691,6 @@ "matrix_green_dark_theme": "矩阵绿暗主题", "monero_light_theme": "门罗币浅色主题", "select_destination": "请选择备份文件的目的地。", - "save_to_downloads": "保存到下载" + "save_to_downloads": "保存到下载", + "auto_generate_subaddresses": "自动生成子辅助" } diff --git a/scripts/docker/.gitignore b/scripts/docker/.gitignore new file mode 100644 index 000000000..ea1472ec1 --- /dev/null +++ b/scripts/docker/.gitignore @@ -0,0 +1 @@ +output/ diff --git a/scripts/docker/Dockerfile b/scripts/docker/Dockerfile new file mode 100644 index 000000000..eef09a323 --- /dev/null +++ b/scripts/docker/Dockerfile @@ -0,0 +1,26 @@ +FROM ubuntu:20.04 +LABEL authors="konsti" + +ENV MONERO_BRANCH=release-v0.18.2.2-android +RUN apt-get update && \ + echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections && \ + apt-get install -y dialog apt-utils curl unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake clang + +RUN mkdir /opt/android/ + +COPY . /opt/android/cakewallet/ + +WORKDIR /opt/android/cakewallet/ + + +RUN ./install_ndk.sh + +RUN ./build_iconv.sh +RUN ./build_boost.sh +RUN ./build_openssl.sh +RUN ./build_sodium.sh +RUN ./build_unbound.sh +RUN ./build_zmq.sh + + +ENTRYPOINT ["./entrypoint.sh"] diff --git a/scripts/docker/build_all.sh b/scripts/docker/build_all.sh new file mode 100644 index 000000000..0acb7fcde --- /dev/null +++ b/scripts/docker/build_all.sh @@ -0,0 +1 @@ +#!/bin/sh if [ -z "$APP_ANDROID_TYPE" ]; then echo "Please set APP_ANDROID_TYPE" exit 1 fi DIR=$(dirname "$0") case $APP_ANDROID_TYPE in "monero.com") $DIR/build_monero_all.sh ;; "cakewallet") $DIR/build_monero_all.sh $DIR/build_haven.sh ;; "haven") $DIR/build_haven_all.sh ;; esac \ No newline at end of file diff --git a/scripts/docker/build_boost.sh b/scripts/docker/build_boost.sh new file mode 100644 index 000000000..2c98afab5 --- /dev/null +++ b/scripts/docker/build_boost.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +. ./config.sh +BOOST_SRC_DIR=$WORKDIR/boost_1_72_0 +BOOST_FILENAME=boost_1_72_0.tar.bz2 +BOOST_VERSION=1.72.0 + +for arch in "aarch" "aarch64" "i686" "x86_64" +do + +PREFIX=$WORKDIR/prefix_${arch} +PATH="${TOOLCHAIN_BASE_DIR}_${arch}/bin:${ORIGINAL_PATH}" +./init_boost.sh $arch $PREFIX $BOOST_SRC_DIR $BOOST_FILENAME $BOOST_VERSION +./finish_boost.sh $arch $PREFIX $BOOST_SRC_DIR $BOOST_SRC_DIR + +done diff --git a/scripts/docker/build_haven.sh b/scripts/docker/build_haven.sh new file mode 100644 index 000000000..7927c5102 --- /dev/null +++ b/scripts/docker/build_haven.sh @@ -0,0 +1 @@ +#!/bin/sh . ./config.sh HAVEN_VERSION=tags/v3.0.7 HAVEN_SRC_DIR=${WORKDIR}/haven git clone https://github.com/haven-protocol-org/haven-main.git ${HAVEN_SRC_DIR} git checkout ${HAVEN_VERSION} cd $HAVEN_SRC_DIR git submodule init git submodule update for arch in "aarch" "aarch64" "i686" "x86_64" do FLAGS="" PREFIX=${WORKDIR}/prefix_${arch} DEST_LIB_DIR=${PREFIX}/lib/haven DEST_INCLUDE_DIR=${PREFIX}/include/haven export CMAKE_INCLUDE_PATH="${PREFIX}/include" export CMAKE_LIBRARY_PATH="${PREFIX}/lib" ANDROID_STANDALONE_TOOLCHAIN_PATH="${TOOLCHAIN_BASE_DIR}_${arch}" PATH="${ANDROID_STANDALONE_TOOLCHAIN_PATH}/bin:${ORIGINAL_PATH}" mkdir -p $DEST_LIB_DIR mkdir -p $DEST_INCLUDE_DIR case $arch in "aarch" ) CLANG=arm-linux-androideabi-clang CXXLANG=arm-linux-androideabi-clang++ BUILD_64=OFF TAG="android-armv7" ARCH="armv7-a" ARCH_ABI="armeabi-v7a" FLAGS="-D CMAKE_ANDROID_ARM_MODE=ON -D NO_AES=true";; "aarch64" ) CLANG=aarch64-linux-androideabi-clang CXXLANG=aarch64-linux-androideabi-clang++ BUILD_64=ON TAG="android-armv8" ARCH="armv8-a" ARCH_ABI="arm64-v8a";; "i686" ) CLANG=i686-linux-androideabi-clang CXXLANG=i686-linux-androideabi-clang++ BUILD_64=OFF TAG="android-x86" ARCH="i686" ARCH_ABI="x86";; "x86_64" ) CLANG=x86_64-linux-androideabi-clang CXXLANG=x86_64-linux-androideabi-clang++ BUILD_64=ON TAG="android-x86_64" ARCH="x86-64" ARCH_ABI="x86_64";; esac cd $HAVEN_SRC_DIR rm -rf ./build/release mkdir -p ./build/release cd ./build/release CC=${CLANG} CXX=${CXXLANG} cmake -D USE_DEVICE_TREZOR=OFF -D BUILD_GUI_DEPS=1 -D BUILD_TESTS=OFF -D ARCH=${ARCH} -D STATIC=ON -D BUILD_64=${BUILD_64} -D CMAKE_BUILD_TYPE=release -D ANDROID=true -D INSTALL_VENDORED_LIBUNBOUND=ON -D BUILD_TAG=${TAG} -D CMAKE_SYSTEM_NAME="Android" -D CMAKE_ANDROID_STANDALONE_TOOLCHAIN="${ANDROID_STANDALONE_TOOLCHAIN_PATH}" -D CMAKE_ANDROID_ARCH_ABI=${ARCH_ABI} $FLAGS ../.. make wallet_api -j$THREADS find . -path ./lib -prune -o -name '*.a' -exec cp '{}' lib \; cp -r ./lib/* $DEST_LIB_DIR cp ../../src/wallet/api/wallet2_api.h $DEST_INCLUDE_DIR done \ No newline at end of file diff --git a/scripts/docker/build_haven_all.sh b/scripts/docker/build_haven_all.sh new file mode 100644 index 000000000..4b33ad077 --- /dev/null +++ b/scripts/docker/build_haven_all.sh @@ -0,0 +1 @@ +#!/bin/bash ./build_iconv.sh ./build_boost.sh ./build_openssl.sh ./build_sodium.sh ./build_zmq.sh ./build_haven.sh \ No newline at end of file diff --git a/scripts/docker/build_iconv.sh b/scripts/docker/build_iconv.sh new file mode 100644 index 000000000..9edac26b3 --- /dev/null +++ b/scripts/docker/build_iconv.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +. ./config.sh +export ICONV_FILENAME=libiconv-1.16.tar.gz +export ICONV_FILE_PATH=$WORKDIR/$ICONV_FILENAME +export ICONV_SRC_DIR=$WORKDIR/libiconv-1.16 +ICONV_SHA256="e6a1b1b589654277ee790cce3734f07876ac4ccfaecbee8afa0b649cf529cc04" + +curl http://ftp.gnu.org/pub/gnu/libiconv/$ICONV_FILENAME -o $ICONV_FILE_PATH +echo $ICONV_SHA256 $ICONV_FILE_PATH | sha256sum -c - || exit 1 + +for arch in aarch aarch64 i686 x86_64 +do + +PREFIX=${WORKDIR}/prefix_${arch} +PATH="${TOOLCHAIN_BASE_DIR}_${arch}/bin:${ORIGINAL_PATH}" + +case $arch in + "aarch" ) + CLANG=arm-linux-androideabi-clang + CXXLANG=arm-linux-androideabi-clang++ + HOST="arm-linux-android";; + * ) + CLANG=${arch}-linux-android-clang + CXXLANG=${arch}-linux-android-clang++ + HOST="${arch}-linux-android";; +esac + +cd $WORKDIR +rm -rf $ICONV_SRC_DIR +tar -xzf $ICONV_FILE_PATH -C $WORKDIR +cd $ICONV_SRC_DIR +CC=${CLANG} CXX=${CXXLANG} ./configure --build=x86_64-linux-gnu --host=${HOST} --prefix=${PREFIX} --disable-rpath +make -j$THREADS +make install + +done + diff --git a/scripts/docker/build_monero.sh b/scripts/docker/build_monero.sh new file mode 100644 index 000000000..d663f5288 --- /dev/null +++ b/scripts/docker/build_monero.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +. ./config.sh + +MONERO_SRC_DIR=${WORKDIR}/monero + +git clone https://github.com/cake-tech/monero.git ${MONERO_SRC_DIR} --branch ${MONERO_BRANCH} +cd $MONERO_SRC_DIR +git submodule init +git submodule update + +for arch in "aarch" "aarch64" "i686" "x86_64" +do +FLAGS="" +PREFIX=${WORKDIR}/prefix_${arch} +DEST_LIB_DIR=${PREFIX}/lib/monero +DEST_INCLUDE_DIR=${PREFIX}/include/monero +export CMAKE_INCLUDE_PATH="${PREFIX}/include" +export CMAKE_LIBRARY_PATH="${PREFIX}/lib" +ANDROID_STANDALONE_TOOLCHAIN_PATH="${TOOLCHAIN_BASE_DIR}_${arch}" +PATH="${ANDROID_STANDALONE_TOOLCHAIN_PATH}/bin:${ORIGINAL_PATH}" + +mkdir -p $DEST_LIB_DIR +mkdir -p $DEST_INCLUDE_DIR + +case $arch in + "aarch" ) + CLANG=arm-linux-androideabi-clang + CXXLANG=arm-linux-androideabi-clang++ + BUILD_64=OFF + TAG="android-armv7" + ARCH="armv7-a" + ARCH_ABI="armeabi-v7a" + FLAGS="-D CMAKE_ANDROID_ARM_MODE=ON -D NO_AES=true";; + "aarch64" ) + CLANG=aarch64-linux-androideabi-clang + CXXLANG=aarch64-linux-androideabi-clang++ + BUILD_64=ON + TAG="android-armv8" + ARCH="armv8-a" + ARCH_ABI="arm64-v8a";; + "i686" ) + CLANG=i686-linux-androideabi-clang + CXXLANG=i686-linux-androideabi-clang++ + BUILD_64=OFF + TAG="android-x86" + ARCH="i686" + ARCH_ABI="x86";; + "x86_64" ) + CLANG=x86_64-linux-androideabi-clang + CXXLANG=x86_64-linux-androideabi-clang++ + BUILD_64=ON + TAG="android-x86_64" + ARCH="x86-64" + ARCH_ABI="x86_64";; +esac + +cd $MONERO_SRC_DIR +rm -rf ./build/release +mkdir -p ./build/release +cd ./build/release +CC=${CLANG} CXX=${CXXLANG} cmake -D USE_DEVICE_TREZOR=OFF -D BUILD_GUI_DEPS=1 -D BUILD_TESTS=OFF -D ARCH=${ARCH} -D STATIC=ON -D BUILD_64=${BUILD_64} -D CMAKE_BUILD_TYPE=release -D ANDROID=true -D INSTALL_VENDORED_LIBUNBOUND=ON -D BUILD_TAG=${TAG} -D CMAKE_SYSTEM_NAME="Android" -D CMAKE_ANDROID_STANDALONE_TOOLCHAIN="${ANDROID_STANDALONE_TOOLCHAIN_PATH}" -D CMAKE_ANDROID_ARCH_ABI=${ARCH_ABI} $FLAGS ../.. + +make wallet_api -j$THREADS +find . -path ./lib -prune -o -name '*.a' -exec cp '{}' lib \; + +cp -r ./lib/* $DEST_LIB_DIR +cp ../../src/wallet/api/wallet2_api.h $DEST_INCLUDE_DIR +done diff --git a/scripts/docker/build_openssl.sh b/scripts/docker/build_openssl.sh new file mode 100644 index 000000000..685d0a1be --- /dev/null +++ b/scripts/docker/build_openssl.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +set -e + +. ./config.sh +OPENSSL_FILENAME=openssl-1.1.1q.tar.gz +OPENSSL_FILE_PATH=$WORKDIR/$OPENSSL_FILENAME +OPENSSL_SRC_DIR=$WORKDIR/openssl-1.1.1q +OPENSSL_SHA256="d7939ce614029cdff0b6c20f0e2e5703158a489a72b2507b8bd51bf8c8fd10ca" +ZLIB_DIR=$WORKDIR/zlib +ZLIB_TAG=v1.2.11 +ZLIB_COMMIT_HASH="cacf7f1d4e3d44d871b605da3b647f07d718623f" + +rm -rf $ZLIB_DIR +git clone -b $ZLIB_TAG --depth 1 https://github.com/madler/zlib $ZLIB_DIR +cd $ZLIB_DIR +git reset --hard $ZLIB_COMMIT_HASH +CC=clang CXX=clang++ ./configure --static +make + +curl https://www.openssl.org/source/$OPENSSL_FILENAME -o $OPENSSL_FILE_PATH +echo $OPENSSL_SHA256 $OPENSSL_FILE_PATH | sha256sum -c - || exit 1 + +for arch in "aarch" "aarch64" "i686" "x86_64" +do +PREFIX=$WORKDIR/prefix_${arch} +TOOLCHAIN=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64 +PATH="${TOOLCHAIN}/bin:${ORIGINAL_PATH}" + +case $arch in + "aarch") X_ARCH="android-arm";; + "aarch64") X_ARCH="android-arm64";; + "i686") X_ARCH="android-x86";; + "x86_64") X_ARCH="android-x86_64";; + *) X_ARCH="android-${arch}";; +esac + +cd $WORKDIR +rm -rf $OPENSSL_SRC_DIR +tar -xzf $OPENSSL_FILE_PATH -C $WORKDIR +cd $OPENSSL_SRC_DIR + +CC=clang ANDROID_NDK=$TOOLCHAIN \ + ./Configure ${X_ARCH} \ + no-shared no-tests \ + --with-zlib-include=${PREFIX}/include \ + --with-zlib-lib=${PREFIX}/lib \ + --prefix=${PREFIX} \ + --openssldir=${PREFIX} \ + -D__ANDROID_API__=$API +make -j$THREADS +make -j$THREADS install_sw + +done + diff --git a/scripts/docker/build_sodium.sh b/scripts/docker/build_sodium.sh new file mode 100644 index 000000000..a934d641b --- /dev/null +++ b/scripts/docker/build_sodium.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +. ./config.sh +SODIUM_SRC_DIR=${WORKDIR}/libsodium +SODIUM_BRANCH=1.0.16 + +for arch in "aarch" "aarch64" "i686" "x86_64" +do + +PREFIX=${WORKDIR}/prefix_${arch} +PATH="${TOOLCHAIN_BASE_DIR}_${arch}/bin:${ORIGINAL_PATH}" + +case $arch in + "aarch" ) TARGET="arm";; + "i686" ) TARGET="x86";; + * ) TARGET="${arch}";; +esac + +HOST="${TARGET}-linux-android" +cd $WORKDIR +rm -rf $SODIUM_SRC_DIR +git clone https://github.com/jedisct1/libsodium.git $SODIUM_SRC_DIR -b $SODIUM_BRANCH +cd $SODIUM_SRC_DIR +./autogen.sh +CC=clang CXX=clang++ ./configure --prefix=${PREFIX} --host=${HOST} --enable-static --disable-shared +make -j$THREADS +make install + +done + diff --git a/scripts/docker/build_unbound.sh b/scripts/docker/build_unbound.sh new file mode 100644 index 000000000..8786b0f2b --- /dev/null +++ b/scripts/docker/build_unbound.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +. ./config.sh + +EXPAT_VERSION=R_2_4_8 +EXPAT_HASH="3bab6c09bbe8bf42d84b81563ddbcf4cca4be838" +EXPAT_SRC_DIR=$WORKDIR/libexpat + +for arch in "aarch" "aarch64" "i686" "x86_64" +do +PREFIX=$WORKDIR/prefix_${arch} +TOOLCHAIN=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64 +PATH="${TOOLCHAIN_BASE_DIR}_${arch}/bin:${ORIGINAL_PATH}" + +cd $WORKDIR +rm -rf $EXPAT_SRC_DIR +git clone https://github.com/libexpat/libexpat.git -b ${EXPAT_VERSION} ${EXPAT_SRC_DIR} +cd $EXPAT_SRC_DIR +test `git rev-parse HEAD` = ${EXPAT_HASH} || exit 1 +cd $EXPAT_SRC_DIR/expat + +case $arch in + "aarch") HOST="arm-linux-androideabi";; + "i686") HOST="x86-linux-android";; + *) HOST="${arch}-linux-android";; +esac + +./buildconf.sh +CC=clang CXX=clang++ ./configure --enable-static --disable-shared --prefix=${PREFIX} --host=${HOST} +make -j$THREADS +make -j$THREADS install +done + +UNBOUND_VERSION=release-1.16.2 +UNBOUND_HASH="cbed768b8ff9bfcf11089a5f1699b7e5707f1ea5" +UNBOUND_SRC_DIR=$WORKDIR/unbound-1.16.2 + +for arch in "aarch" "aarch64" "i686" "x86_64" +do +PREFIX=$WORKDIR/prefix_${arch} +TOOLCHAIN=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64 + +case $arch in + "aarch") TOOLCHAIN_BIN_PATH=${TOOLCHAIN_BASE_DIR}_${arch}/arm-linux-androideabi/bin;; + *) TOOLCHAIN_BIN_PATH=${TOOLCHAIN_BASE_DIR}_${arch}/${arch}-linux-android/bin;; +esac + +PATH="${TOOLCHAIN_BIN_PATH}:${TOOLCHAIN_BASE_DIR}_${arch}/bin:${ORIGINAL_PATH}" +echo $PATH +cd $WORKDIR +rm -rf $UNBOUND_SRC_DIR +git clone https://github.com/NLnetLabs/unbound.git -b ${UNBOUND_VERSION} ${UNBOUND_SRC_DIR} +cd $UNBOUND_SRC_DIR +test `git rev-parse HEAD` = ${UNBOUND_HASH} || exit 1 + +case $arch in + "aarch") HOST="arm-linux-androideabi";; + "i686") HOST="x86-linux-android";; + *) HOST="${arch}-linux-android";; +esac + +CC=clang CXX=clang++ ./configure --prefix=${PREFIX} --host=${HOST} --enable-static --disable-shared --disable-flto --with-ssl=${PREFIX} --with-libexpat=${PREFIX} +make -j$THREADS +make -j$THREADS install +done diff --git a/scripts/docker/build_zmq.sh b/scripts/docker/build_zmq.sh new file mode 100644 index 000000000..bbff9e41b --- /dev/null +++ b/scripts/docker/build_zmq.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +. ./config.sh +ZMQ_SRC_DIR=$WORKDIR/libzmq +ZMQ_BRANCH=v4.3.3 +ZMQ_COMMIT_HASH=04f5bbedee58c538934374dc45182d8fc5926fa3 + +for arch in "aarch" "aarch64" "i686" "x86_64" +do + +PREFIX=$WORKDIR/prefix_${arch} +PATH="${TOOLCHAIN_BASE_DIR}_${arch}/bin:${ORIGINAL_PATH}" + +case $arch in + "aarch" ) TARGET="arm";; + "i686" ) TARGET="x86";; + * ) TARGET="${arch}";; +esac + + +HOST="${TARGET}-linux-android" +cd $WORKDIR +rm -rf $ZMQ_SRC_DIR +git clone https://github.com/zeromq/libzmq.git ${ZMQ_SRC_DIR} -b ${ZMQ_BRANCH} +cd $ZMQ_SRC_DIR +git checkout ${ZMQ_COMMIT_HASH} +./autogen.sh +CC=clang CXX=clang++ ./configure --prefix=${PREFIX} --host=${HOST} --enable-static --disable-shared +make -j$THREADS +make install + +done diff --git a/scripts/docker/config.sh b/scripts/docker/config.sh new file mode 100644 index 000000000..c5067f2c3 --- /dev/null +++ b/scripts/docker/config.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +export API=21 +export WORKDIR=/opt/android +export ANDROID_NDK_ZIP=${WORKDIR}/android-ndk-r17c.zip +export ANDROID_NDK_ROOT=${WORKDIR}/android-ndk-r17c +export ANDROID_NDK_HOME=$ANDROID_NDK_ROOT +export TOOLCHAIN_DIR="${WORKDIR}/toolchain" +export TOOLCHAIN_BASE_DIR=$TOOLCHAIN_DIR +export ORIGINAL_PATH=$PATH +export THREADS=4 diff --git a/scripts/docker/copy_haven_deps.sh b/scripts/docker/copy_haven_deps.sh new file mode 100644 index 000000000..d59e9d7f0 --- /dev/null +++ b/scripts/docker/copy_haven_deps.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +WORKDIR=/opt/android +CW_DIR=${WORKDIR}/cake_wallet +CW_EXRTERNAL_DIR=${CW_DIR}/cw_shared_external/ios/External/android +CW_HAVEN_EXTERNAL_DIR=${CW_DIR}/cw_haven/ios/External/android +CW_MONERO_EXTERNAL_DIR=${CW_DIR}/cw_monero/ios/External/android +for arch in "aarch" "aarch64" "i686" "x86_64" +do + +PREFIX=${WORKDIR}/prefix_${arch} +ABI="" + +case $arch in + "aarch" ) + ABI="armeabi-v7a";; + "aarch64" ) + ABI="arm64-v8a";; + "i686" ) + ABI="x86";; + "x86_64" ) + ABI="x86_64";; +esac + +LIB_DIR=${CW_EXRTERNAL_DIR}/${ABI}/lib +INCLUDE_DIR=${CW_EXRTERNAL_DIR}/${ABI}/include +LIBANBOUND_PATH=${PREFIX}/lib/libunbound.a + +mkdir -p $LIB_DIR +mkdir -p $INCLUDE_DIR + +cp -r ${PREFIX}/lib/* $LIB_DIR +cp -r ${PREFIX}/include/* $INCLUDE_DIR + +if [ -f "$LIBANBOUND_PATH" ]; then + cp $LIBANBOUND_PATH ${LIB_DIR}/monero +fi + +done + +mkdir -p ${CW_HAVEN_EXTERNAL_DIR}/include +mkdir -p ${CW_MONERO_EXTERNAL_DIR}/include + +cp $CW_EXRTERNAL_DIR/x86/include/monero/wallet2_api.h ${CW_MONERO_EXTERNAL_DIR}/include +cp $CW_EXRTERNAL_DIR/x86/include/haven/wallet2_api.h ${CW_HAVEN_EXTERNAL_DIR}/include diff --git a/scripts/docker/copy_monero_deps.sh b/scripts/docker/copy_monero_deps.sh new file mode 100644 index 000000000..e4392186c --- /dev/null +++ b/scripts/docker/copy_monero_deps.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +WORKDIR=/opt/android +CW_EXRTERNAL_DIR=${WORKDIR}/output/android + +for arch in "aarch" "aarch64" "i686" "x86_64" +do + +PREFIX=${WORKDIR}/prefix_${arch} +ABI="" + +case $arch in + "aarch" ) + ABI="armeabi-v7a";; + "aarch64" ) + ABI="arm64-v8a";; + "i686" ) + ABI="x86";; + "x86_64" ) + ABI="x86_64";; +esac + +LIB_DIR=${CW_EXRTERNAL_DIR}/${ABI}/lib +INCLUDE_DIR=${CW_EXRTERNAL_DIR}/${ABI}/include +LIBANBOUND_PATH=${PREFIX}/lib/libunbound.a + +mkdir -p $LIB_DIR +mkdir -p $INCLUDE_DIR + +cp -r ${PREFIX}/lib/* $LIB_DIR +cp -r ${PREFIX}/include/* $INCLUDE_DIR + +if [ -f "$LIBANBOUND_PATH" ]; then + cp $LIBANBOUND_PATH ${LIB_DIR}/monero +fi + +done diff --git a/scripts/docker/docker-compose.yml b/scripts/docker/docker-compose.yml new file mode 100644 index 000000000..eaeea0f5b --- /dev/null +++ b/scripts/docker/docker-compose.yml @@ -0,0 +1,9 @@ +version: '3.6' + +services: + build_deps: + image: build_monero_deps + environment: + MONERO_BRANCH: release-v0.18.2.2-android + volumes: + - ./output:/opt/android/output diff --git a/scripts/docker/entrypoint.sh b/scripts/docker/entrypoint.sh new file mode 100644 index 000000000..e4bdc017c --- /dev/null +++ b/scripts/docker/entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +./build_monero.sh +./copy_monero_deps.sh diff --git a/scripts/docker/finish_boost.sh b/scripts/docker/finish_boost.sh new file mode 100644 index 000000000..e3f195276 --- /dev/null +++ b/scripts/docker/finish_boost.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +ARCH=$1 +PREFIX=$2 +BOOST_SRC_DIR=$3 + +cd $BOOST_SRC_DIR + +./b2 --build-type=minimal link=static runtime-link=static --with-chrono --with-date_time --with-filesystem --with-program_options --with-regex --with-serialization --with-system --with-thread --with-locale --build-dir=android --stagedir=android toolset=clang threading=multi threadapi=pthread target-os=android -sICONV_PATH=${PREFIX} -j$THREADS install diff --git a/scripts/docker/init_boost.sh b/scripts/docker/init_boost.sh new file mode 100644 index 000000000..ffb7a1416 --- /dev/null +++ b/scripts/docker/init_boost.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +ARCH=$1 +PREFIX=$2 +BOOST_SRC_DIR=$3 +BOOST_FILENAME=$4 +BOOST_VERSION=$5 +BOOST_FILE_PATH=$WORKDIR/$BOOST_FILENAME +BOOST_SHA256="59c9b274bc451cf91a9ba1dd2c7fdcaf5d60b1b3aa83f2c9fa143417cc660722" + +if [ ! -e "$BOOST_FILE_PATH" ]; then + curl -L http://downloads.sourceforge.net/project/boost/boost/${BOOST_VERSION}/${BOOST_FILENAME} > $BOOST_FILE_PATH +fi + +echo $BOOST_SHA256 $BOOST_FILE_PATH | sha256sum -c - || exit 1 + +cd $WORKDIR +rm -rf $BOOST_SRC_DIR +rm -rf $PREFIX/include/boost +tar -xvf $BOOST_FILE_PATH -C $WORKDIR +cd $BOOST_SRC_DIR +./bootstrap.sh --prefix=${PREFIX} diff --git a/scripts/docker/install_ndk.sh b/scripts/docker/install_ndk.sh new file mode 100644 index 000000000..5f97751e3 --- /dev/null +++ b/scripts/docker/install_ndk.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +. ./config.sh +TOOLCHAIN_DIR=${WORKDIR}/toolchain +TOOLCHAIN_A32_DIR=${TOOLCHAIN_DIR}_aarch +TOOLCHAIN_A64_DIR=${TOOLCHAIN_DIR}_aarch64 +TOOLCHAIN_x86_DIR=${TOOLCHAIN_DIR}_i686 +TOOLCHAIN_x86_64_DIR=${TOOLCHAIN_DIR}_x86_64 +ANDROID_NDK_SHA256="3f541adbd0330a9205ba12697f6d04ec90752c53d6b622101a2a8a856e816589" + +curl https://dl.google.com/android/repository/android-ndk-r17c-linux-x86_64.zip -o ${ANDROID_NDK_ZIP} +echo $ANDROID_NDK_SHA256 $ANDROID_NDK_ZIP | sha256sum -c || exit 1 +unzip $ANDROID_NDK_ZIP -d $WORKDIR + +${ANDROID_NDK_ROOT}/build/tools/make_standalone_toolchain.py --arch arm64 --api $API --install-dir ${TOOLCHAIN_A64_DIR} --stl=libc++ +${ANDROID_NDK_ROOT}/build/tools/make_standalone_toolchain.py --arch arm --api $API --install-dir ${TOOLCHAIN_A32_DIR} --stl=libc++ +${ANDROID_NDK_ROOT}/build/tools/make_standalone_toolchain.py --arch x86 --api $API --install-dir ${TOOLCHAIN_x86_DIR} --stl=libc++ +${ANDROID_NDK_ROOT}/build/tools/make_standalone_toolchain.py --arch x86_64 --api $API --install-dir ${TOOLCHAIN_x86_64_DIR} --stl=libc++ diff --git a/scripts/ios/build_monero.sh b/scripts/ios/build_monero.sh index 9a892d8c5..54dda546f 100755 --- a/scripts/ios/build_monero.sh +++ b/scripts/ios/build_monero.sh @@ -59,4 +59,4 @@ cp -r ./lib/* $DEST_LIB_DIR cp ../../src/wallet/api/wallet2_api.h $DEST_INCLUDE_DIR popd -done \ No newline at end of file +done diff --git a/tool/append_translation.dart b/tool/append_translation.dart index 080b2c5e7..5c48aceab 100644 --- a/tool/append_translation.dart +++ b/tool/append_translation.dart @@ -1,66 +1,22 @@ -// import 'dart:convert'; -// import 'dart:io'; -// -// import 'package:translator/translator.dart'; -// -// const defaultLang = "en"; -// const langs = [ -// "ar", "bg", "cs", "de", "en", "es", "fr", "ha", "hi", "hr", "id", "it", -// "ja", "ko", "my", "nl", "pl", "pt", "ru", "th", "tr", "uk", "ur", "yo", -// "zh-cn" // zh, but Google Translate uses zh-cn for Chinese (Simplified) -// ]; -// final translator = GoogleTranslator(); -// -// void main(List<String> args) async { -// if (args.length != 2) { -// throw Exception( -// 'Insufficient arguments!\n\nTry to run `./append_translation.dart greetings "Hello World!"`'); -// } -// -// final name = args.first; -// final text = args.last; -// -// print('Appending "$name": "$text"'); -// -// for (var lang in langs) { -// final fileName = getFileName(lang); -// final translation = await getTranslation(text, lang); -// -// appendArbFile(fileName, name, translation); -// } -// } -// -// void appendArbFile(String fileName, String name, String text) { -// final file = File(fileName); -// final inputContent = file.readAsStringSync(); -// final arbObj = json.decode(inputContent) as Map<String, dynamic>; -// -// if (arbObj.containsKey(name)) { -// print("String $name already exists in $fileName!"); -// return; -// } -// -// arbObj.addAll({name: text}); -// -// final outputContent = json -// .encode(arbObj) -// .replaceAll('","', '",\n "') -// .replaceAll('{"', '{\n "') -// .replaceAll('"}', '"\n}') -// .replaceAll('":"', '": "'); -// -// file.writeAsStringSync(outputContent); -// } -// -// -// Future<String> getTranslation(String text, String lang) async { -// if (lang == defaultLang) return text; -// return (await translator.translate(text, from: defaultLang, to: lang)).text; -// } -// -// String getFileName(String lang) { -// final shortLang = lang -// .split("-") -// .first; -// return "./res/values/strings_$shortLang.arb"; -// } +import 'utils/translation/arb_file_utils.dart'; +import 'utils/translation/translation_constants.dart'; +import 'utils/translation/translation_utils.dart'; + +void main(List<String> args) async { + if (args.length != 2) { + throw Exception( + 'Insufficient arguments!\n\nTry to run `./append_translation.dart greetings "Hello World!"`'); + } + + final name = args.first; + final text = args.last; + + print('Appending "$name": "$text"'); + + for (var lang in langs) { + final fileName = getArbFileName(lang); + final translation = await getTranslation(text, lang); + + appendStringToArbFile(fileName, name, translation); + } +} diff --git a/tool/configure.dart b/tool/configure.dart index e47e9bd7b..037ee0403 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -46,6 +46,7 @@ Future<void> main(List<String> args) async { Future<void> generateBitcoin(bool hasImplementation) async { final outputFile = File(bitcoinOutputPath); const bitcoinCommonHeaders = """ +import 'package:cake_wallet/entities/unspent_transaction_output.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -69,24 +70,6 @@ import 'package:cw_bitcoin/litecoin_wallet_service.dart'; """; const bitcoinCwPart = "part 'cw_bitcoin.dart';"; const bitcoinContent = """ -class Unspent { - Unspent(this.address, this.hash, this.value, this.vout) - : isSending = true, - isFrozen = false, - note = ''; - - final String address; - final String hash; - final int value; - final int vout; - - bool isSending; - bool isFrozen; - String note; - - bool get isP2wpkh => address.startsWith('bc') || address.startsWith('ltc'); -} - abstract class Bitcoin { TransactionPriority getMediumTransactionPriority(); @@ -143,6 +126,9 @@ abstract class Bitcoin { Future<void> generateMonero(bool hasImplementation) async { final outputFile = File(moneroOutputPath); const moneroCommonHeaders = """ +import 'package:cake_wallet/entities/unspent_transaction_output.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_monero/monero_unspent.dart'; import 'package:mobx/mobx.dart'; import 'package:flutter/foundation.dart'; import 'package:cw_core/wallet_credentials.dart'; @@ -252,6 +238,9 @@ abstract class Monero { TransactionPriority deserializeMoneroTransactionPriority({required int raw}); List<TransactionPriority> getTransactionPriorities(); List<String> getMoneroWordList(String language); + + List<Unspent> getUnspents(Object wallet); + void updateUnspents(Object wallet); WalletCredentials createMoneroRestoreWalletFromKeysCredentials({ required String name, @@ -273,7 +262,7 @@ abstract class Monero { void setCurrentAccount(Object wallet, int id, String label, String? balance); void onStartup(); int getTransactionInfoAccountId(TransactionInfo tx); - WalletService createMoneroWalletService(Box<WalletInfo> walletInfoSource); + WalletService createMoneroWalletService(Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource); Map<String, String> pendingTransactionInfo(Object transaction); } diff --git a/tool/translation_consistence.dart b/tool/translation_consistence.dart new file mode 100644 index 000000000..04f64dfc8 --- /dev/null +++ b/tool/translation_consistence.dart @@ -0,0 +1,37 @@ +import 'dart:io'; + +import 'utils/translation/arb_file_utils.dart'; +import 'utils/translation/translation_constants.dart'; +import 'utils/translation/translation_utils.dart'; + +void main(List<String> args) async { + print('Checking Consistency of all arb-files. Default: $defaultLang'); + + final doFix = args.contains("--fix"); + + if (doFix) + print('Auto fixing enabled!\n'); + else + print('Auto fixing disabled!\nRun with arg "--fix" to enable autofix\n'); + + final fileName = getArbFileName(defaultLang); + final file = File(fileName); + final arbObj = readArbFile(file); + + for (var lang in langs) { + final fileName = getArbFileName(lang); + final missingKeys = getMissingKeysInArbFile(fileName, arbObj.keys); + if (missingKeys.isNotEmpty) { + final missingDefaults = <String, String>{}; + + missingKeys.forEach((key) { + print('Missing in "$lang": "$key"'); + if (doFix) + missingDefaults[key] = arbObj[key] as String; + }); + + if (missingDefaults.isNotEmpty) + await appendTranslations(lang, missingDefaults); + } + } +} diff --git a/tool/utils/translation/arb_file_utils.dart b/tool/utils/translation/arb_file_utils.dart new file mode 100644 index 000000000..693c5b93e --- /dev/null +++ b/tool/utils/translation/arb_file_utils.dart @@ -0,0 +1,66 @@ +import 'dart:convert'; +import 'dart:io'; + +void appendStringToArbFile(String fileName, String name, String text) { + final file = File(fileName); + final arbObj = readArbFile(file); + + if (arbObj.containsKey(name)) { + print("String $name already exists in $fileName!"); + return; + } + + arbObj.addAll({name: text}); + + final outputContent = json + .encode(arbObj) + .replaceAll('","', '",\n "') + .replaceAll('{"', '{\n "') + .replaceAll('"}', '"\n}') + .replaceAll('":"', '": "'); + + file.writeAsStringSync(outputContent); +} + +void appendStringsToArbFile(String fileName, Map<String, String> strings) { + final file = File(fileName); + final arbObj = readArbFile(file); + + arbObj.addAll(strings); + + final outputContent = json + .encode(arbObj) + .replaceAll('","', '",\n "') + .replaceAll('{"', '{\n "') + .replaceAll('"}', '"\n}') + .replaceAll('":"', '": "'); + + file.writeAsStringSync(outputContent); +} + +Map<String, dynamic> readArbFile(File file) { + final inputContent = file.readAsStringSync(); + + return json.decode(inputContent) as Map<String, dynamic>; +} + +String getArbFileName(String lang) { + final shortLang = lang + .split("-") + .first; + return "./res/values/strings_$shortLang.arb"; +} + +List<String> getMissingKeysInArbFile(String fileName, Iterable<String> langKeys) { + final file = File(fileName); + final arbObj = readArbFile(file); + final results = <String>[]; + + for (var langKey in langKeys) { + if (!arbObj.containsKey(langKey)) { + results.add(langKey); + } + } + + return results; +} diff --git a/tool/utils/translation/translation_constants.dart b/tool/utils/translation/translation_constants.dart new file mode 100644 index 000000000..6563feb32 --- /dev/null +++ b/tool/utils/translation/translation_constants.dart @@ -0,0 +1,6 @@ +const defaultLang = "en"; +const langs = [ + "ar", "bg", "cs", "de", "en", "es", "fr", "ha", "hi", "hr", "id", "it", + "ja", "ko", "my", "nl", "pl", "pt", "ru", "th", "tr", "uk", "ur", "yo", + "zh-cn" // zh, but Google Translate uses zh-cn for Chinese (Simplified) +]; diff --git a/tool/utils/translation/translation_utils.dart b/tool/utils/translation/translation_utils.dart new file mode 100644 index 000000000..a37838b91 --- /dev/null +++ b/tool/utils/translation/translation_utils.dart @@ -0,0 +1,37 @@ +import 'package:translator/translator.dart'; + +import 'arb_file_utils.dart'; +import 'translation_constants.dart'; + +final translator = GoogleTranslator(); + +Future<void> appendTranslation(String lang, String key, String text) async { + final fileName = getArbFileName(lang); + final translation = await getTranslation(text, lang); + + appendStringToArbFile(fileName, key, translation); +} + +Future<void> appendTranslations(String lang, Map<String, String> defaults) async { + final fileName = getArbFileName(lang); + final translations = <String, String>{}; + + for (var key in defaults.keys) { + final value = defaults[key]!; + + if (value.contains("{")) continue; + final translation = await getTranslation(value, lang); + + translations[key] = translation; + } + + print(translations); + + appendStringsToArbFile(fileName, translations); +} + +Future<String> getTranslation(String text, String lang) async { + if (lang == defaultLang) return text; + return (await translator.translate(text, from: defaultLang, to: lang)).text; +} +