diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index 7f70ce755..80d5d74aa 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/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_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 #include #include +#include #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 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(); + + 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 _preferred_inputs; + + for (int i = 0; i < preferred_inputs_size; i++) { + _preferred_inputs.insert(std::string(*preferred_inputs)); + preferred_inputs++; + } auto priority = static_cast(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(), m_wallet->defaultMixin(), priority, subaddr_account); + transaction = m_wallet->createTransaction(std::string(address), _payment_id, Monero::optional(), 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 _preferred_inputs; + + for (int i = 0; i < preferred_inputs_size; i++) { + _preferred_inputs.insert(std::string(*preferred_inputs)); + preferred_inputs++; + } + auto priority = static_cast(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::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 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 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 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>('refresh_coins') + .asFunction(); + +final coinsCountNative = moneroApi + .lookup>('coins_count') + .asFunction(); + +final coinNative = moneroApi + .lookup>('coin') + .asFunction(); + +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, Pointer, Pointer, Int32, Int64, Pointer); -typedef restore_wallet_from_keys = Int8 Function(Pointer, Pointer, - Pointer, Pointer, Pointer, Pointer, Int32, Int64, Pointer); +typedef restore_wallet_from_keys = Int8 Function(Pointer, Pointer, Pointer, + Pointer, Pointer, Pointer, Int32, Int64, Pointer); typedef is_wallet_exist = Int8 Function(Pointer); @@ -63,8 +64,7 @@ typedef subaddrress_refresh = Void Function(Int32); typedef subaddress_get_all = Pointer Function(); -typedef subaddress_add_new = Void Function( - Int32 accountIndex, Pointer label); +typedef subaddress_add_new = Void Function(Int32 accountIndex, Pointer label); typedef subaddress_set_label = Void Function( Int32 accountIndex, Int32 addressIndex, Pointer label); @@ -77,8 +77,7 @@ typedef account_get_all = Pointer Function(); typedef account_add_new = Void Function(Pointer label); -typedef account_set_label = Void Function( - Int32 accountIndex, Pointer label); +typedef account_set_label = Void Function(Int32 accountIndex, Pointer label); typedef transactions_refresh = Void Function(); @@ -94,6 +93,8 @@ typedef transaction_create = Int8 Function( Pointer amount, Int8 priorityRaw, Int32 subaddrAccount, + Pointer> preferredInputs, + Int32 preferredInputsSize, Pointer error, Pointer pendingTransaction); @@ -104,6 +105,8 @@ typedef transaction_create_mult_dest = Int8 Function( Int32 size, Int8 priorityRaw, Int32 subaddrAccount, + Pointer> preferredInputs, + Int32 preferredInputsSize, Pointer error, Pointer pendingTransaction); @@ -123,10 +126,16 @@ typedef on_startup = Void Function(); typedef rescan_blockchain = Void Function(); -typedef get_subaddress_label = Pointer Function( - Int32 accountIndex, - Int32 addressIndex); +typedef get_subaddress_label = Pointer 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 Function(Pointer txid); + +typedef coin = Pointer 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 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 address; + + external Pointer addressLabel; + + external Pointer keyImage; + + @Uint64() + external int unlockTime; + + @Int8() + external int unlocked; + + external Pointer pubKey; + + @Int8() + external int coinbase; + + external Pointer 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>('transaction_commit') .asFunction(); -final getTxKeyNative = moneroApi - .lookup>('get_tx_key') - .asFunction(); +final getTxKeyNative = + moneroApi.lookup>('get_tx_key').asFunction(); 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 preferredInputs = const []}) { final addressPointer = address.toNativeUtf8(); final paymentIdPointer = paymentId.toNativeUtf8(); final amountPointer = amount != null ? amount.toNativeUtf8() : nullptr; + + final int preferredInputsSize = preferredInputs.length; + final List> preferredInputsPointers = + preferredInputs.map((output) => output.toNativeUtf8()).toList(); + final Pointer> preferredInputsPointerPointer = calloc(preferredInputsSize); + + for (int i = 0; i < preferredInputsSize; i++) { + preferredInputsPointerPointer[i] = preferredInputsPointers[i]; + } + final errorMessagePointer = calloc(); final pendingTransactionRawPointer = calloc(); 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 outputs, - required String paymentId, - required int priorityRaw, - int accountIndex = 0}) { + required String paymentId, + required int priorityRaw, + int accountIndex = 0, + List preferredInputs = const []}) { final int size = outputs.length; - final List> addressesPointers = outputs.map((output) => - output.address.toNativeUtf8()).toList(); + final List> addressesPointers = + outputs.map((output) => output.address.toNativeUtf8()).toList(); final Pointer> addressesPointerPointer = calloc(size); - final List> amountsPointers = outputs.map((output) => - output.amount.toNativeUtf8()).toList(); + final List> amountsPointers = + outputs.map((output) => output.amount.toNativeUtf8()).toList(); final Pointer> 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> preferredInputsPointers = + preferredInputs.map((output) => output.toNativeUtf8()).toList(); + final Pointer> preferredInputsPointerPointer = calloc(preferredInputsSize); + + for (int i = 0; i < preferredInputsSize; i++) { + preferredInputsPointerPointer[i] = preferredInputsPointers[i]; + } + final paymentIdPointer = paymentId.toNativeUtf8(); final errorMessagePointer = calloc(); final pendingTransactionRawPointer = calloc(); 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.fromAddress(address)); +void commitTransactionFromPointerAddress({required int address}) => + commitTransaction(transactionPointer: Pointer.fromAddress(address)); void commitTransaction({required Pointer transactionPointer}) { final errorMessagePointer = calloc(); - 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; 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; return createTransactionMultDestSync( outputs: outputs, paymentId: paymentId, priorityRaw: priorityRaw, - accountIndex: accountIndex); + accountIndex: accountIndex, + preferredInputs: preferredInputs); } Future createTransaction( @@ -212,23 +245,27 @@ Future createTransaction( required int priorityRaw, String? amount, String paymentId = '', - int accountIndex = 0}) => + int accountIndex = 0, + List preferredInputs = const []}) => compute(_createTransactionSync, { 'address': address, 'paymentId': paymentId, 'amount': amount, 'priorityRaw': priorityRaw, - 'accountIndex': accountIndex + 'accountIndex': accountIndex, + 'preferredInputs': preferredInputs }); Future createTransactionMultDest( - {required List outputs, - required int priorityRaw, - String paymentId = '', - int accountIndex = 0}) => + {required List outputs, + required int priorityRaw, + String paymentId = '', + int accountIndex = 0, + List 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 amount, int priorityRaw, int subaddrAccount, + Pointer> preferredInputs, + int preferredInputsSize, Pointer error, Pointer pendingTransaction); @@ -102,6 +105,8 @@ typedef TransactionCreateMultDest = int Function( int size, int priorityRaw, int subaddrAccount, + Pointer> preferredInputs, + int preferredInputsSize, Pointer error, Pointer pendingTransaction); @@ -127,4 +132,10 @@ typedef GetSubaddressLabel = Pointer 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 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..53efc1251 100644 --- a/cw_monero/lib/monero_subaddress_list.dart +++ b/cw_monero/lib/monero_subaddress_list.dart @@ -1,6 +1,6 @@ -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'; @@ -22,6 +22,8 @@ abstract class MoneroSubaddressListBase with Store { bool _isUpdating; void update({required int accountIndex}) { + refreshCoins(accountIndex); + if (_isUpdating) { return; } 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..76563310e 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,37 +39,39 @@ class MoneroWallet = MoneroWalletBase with _$MoneroWallet; abstract class MoneroWalletBase extends WalletBase with Store { - MoneroWalletBase({required WalletInfo walletInfo}) + MoneroWalletBase({required WalletInfo walletInfo, + required Box unspentCoinsInfo}) : balance = ObservableMap.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), syncStatus = NotConnectedSyncStatus(), + unspentCoins = [], + this.unspentCoinsInfo = unspentCoinsInfo, super(walletInfo) { transactionHistory = MoneroTransactionHistory(); - _onAccountChangeReaction = reaction((_) => walletAddresses.account, - (Account? account) { + _onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) { if (account == null) { return; } - balance = ObservableMap.of( - { - currency: MoneroBalance( + balance = ObservableMap.of({ + currency: MoneroBalance( fullBalance: monero_wallet.getFullBalance(accountIndex: account.id), - unlockedBalance: - monero_wallet.getUnlockedBalance(accountIndex: account.id)) - }); + unlockedBalance: monero_wallet.getUnlockedBalance(accountIndex: account.id)) + }); walletAddresses.updateSubaddressList(accountIndex: account.id); }); } static const int _autoSaveInterval = 30; + Box unspentCoinsInfo; + @override MoneroWalletAddresses walletAddresses; @@ -89,11 +93,12 @@ abstract class MoneroWalletBase extends WalletBase unspentCoins; Future init() async { await walletAddresses.init(); @@ -170,10 +175,12 @@ abstract class MoneroWalletBase extends WalletBase createTransaction(Object credentials) async { final _credentials = credentials as MoneroTransactionCreationCredentials; + final inputs = []; 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 +188,21 @@ abstract class MoneroWalletBase extends WalletBase item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { @@ -208,7 +230,8 @@ abstract class MoneroWalletBase extends WalletBase 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 _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 _refreshUnspentCoinsInfo() async { + try { + final List keys = []; + 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 +463,7 @@ abstract class MoneroWalletBase extends WalletBase> fetchTransactions() async { - monero_transaction_history.refreshTransactions(); + transaction_history.refreshTransactions(); return _getAllTransactions(null).fold>( {}, (Map acc, MoneroTransactionInfo tx) { @@ -392,7 +494,7 @@ abstract class MoneroWalletBase extends WalletBase _getAllTransactions(dynamic _) => - monero_transaction_history + transaction_history .getAllTransations() .map((row) => MoneroTransactionInfo.fromRow(row)) .toList(); @@ -407,7 +509,7 @@ abstract class MoneroWalletBase extends WalletBase 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) { 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 walletInfoSource; + final Box 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 dfd3b1538..3164aacaa 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -128,7 +128,8 @@ class CWBitcoin extends Bitcoin { bitcoinUnspent.address.address, bitcoinUnspent.hash, bitcoinUnspent.value, - bitcoinUnspent.vout)) + bitcoinUnspent.vout, + null)) .toList(); } @@ -160,4 +161,4 @@ class CWBitcoin extends Bitcoin { @override TransactionPriority getLitecoinTransactionPrioritySlow() => LitecoinTransactionPriority.slow; -} \ No newline at end of file +} diff --git a/lib/di.dart b/lib/di.dart index c37575337..80a55e1c6 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -216,10 +216,10 @@ late Box