From 44223c962b2b1f6bf63a6fc482c5912cc4ab1433 Mon Sep 17 00:00:00 2001 From: Oscar Mira Date: Wed, 7 Feb 2024 02:18:03 +0100 Subject: [PATCH] lib: add functions for retrieving base fees --- .../main/aidl/im/molly/monero/IWallet.aidl | 1 + .../im/molly/monero/IWalletCallbacks.aidl | 1 + lib/android/src/main/cpp/common/jvm.cc | 24 +++++++---- lib/android/src/main/cpp/common/jvm.h | 10 ++--- lib/android/src/main/cpp/wallet/wallet.cc | 39 +++++++++++++----- lib/android/src/main/cpp/wallet/wallet.h | 4 +- .../kotlin/im/molly/monero/DynamicFeeRate.kt | 13 ++++++ .../kotlin/im/molly/monero/FeePriority.kt | 8 ++++ .../kotlin/im/molly/monero/MoneroWallet.kt | 40 ++++++++++++++++++- .../im/molly/monero/PendingTransaction.kt | 3 ++ .../kotlin/im/molly/monero/ProtocolInfo.kt | 11 ++++- .../kotlin/im/molly/monero/WalletNative.kt | 8 ++++ .../monero/internal/constants/Constants.kt | 2 + vendor/monero | 2 +- 14 files changed, 139 insertions(+), 27 deletions(-) create mode 100644 lib/android/src/main/kotlin/im/molly/monero/DynamicFeeRate.kt create mode 100644 lib/android/src/main/kotlin/im/molly/monero/FeePriority.kt create mode 100644 lib/android/src/main/kotlin/im/molly/monero/PendingTransaction.kt diff --git a/lib/android/src/main/aidl/im/molly/monero/IWallet.aidl b/lib/android/src/main/aidl/im/molly/monero/IWallet.aidl index aa25728..8a8b5cf 100644 --- a/lib/android/src/main/aidl/im/molly/monero/IWallet.aidl +++ b/lib/android/src/main/aidl/im/molly/monero/IWallet.aidl @@ -11,5 +11,6 @@ interface IWallet { oneway void cancelRefresh(); oneway void setRefreshSince(long heightOrTimestamp); oneway void commit(in IWalletCallbacks callback); + oneway void requestFees(in IWalletCallbacks callback); void close(); } diff --git a/lib/android/src/main/aidl/im/molly/monero/IWalletCallbacks.aidl b/lib/android/src/main/aidl/im/molly/monero/IWalletCallbacks.aidl index a106a81..d8fb71a 100644 --- a/lib/android/src/main/aidl/im/molly/monero/IWalletCallbacks.aidl +++ b/lib/android/src/main/aidl/im/molly/monero/IWalletCallbacks.aidl @@ -5,4 +5,5 @@ import im.molly.monero.BlockchainTime; oneway interface IWalletCallbacks { void onRefreshResult(in BlockchainTime blockchainTime, int status); void onCommitResult(boolean success); + void onFeesReceived(in long[] fees); } diff --git a/lib/android/src/main/cpp/common/jvm.cc b/lib/android/src/main/cpp/common/jvm.cc index 49969b1..cc8dd16 100644 --- a/lib/android/src/main/cpp/common/jvm.cc +++ b/lib/android/src/main/cpp/common/jvm.cc @@ -213,10 +213,22 @@ ScopedJvmLocalRef nativeToJvmByteArray(JNIEnv* env, return {env, j_array}; } -ScopedJvmLocalRef nativeToJvmByteArray( - JNIEnv* env, - const std::vector& bytes) { - return nativeToJvmByteArray(env, bytes.data(), bytes.size()); +ScopedJvmLocalRef nativeToJvmLongArray(JNIEnv* env, + const int64_t* longs, + size_t len) { + LOG_FATAL_IF(len > INT_MAX); + auto j_len = (jsize) len; + jlongArray j_array = env->NewLongArray(j_len); + LOG_FATAL_IF(checkException(env)); + env->SetLongArrayRegion(j_array, 0, j_len, longs); + LOG_FATAL_IF(checkException(env)); + return {env, j_array}; +} + +ScopedJvmLocalRef nativeToJvmLongArray(JNIEnv* env, + const uint64_t* longs, + size_t len) { + return nativeToJvmLongArray(env, reinterpret_cast(longs), len); } jlong nativeToJvmPointer(void* ptr) { @@ -225,7 +237,7 @@ jlong nativeToJvmPointer(void* ptr) { } std::string jvmToStdString(JNIEnv* env, const JvmRef& j_string) { - const char* chars = env->GetStringUTFChars(j_string.obj(), nullptr); + const char* chars = env->GetStringUTFChars(j_string.obj(), /*isCopy=*/nullptr); LOG_FATAL_IF(checkException(env)); const jsize len = env->GetStringUTFLength(j_string.obj()); LOG_FATAL_IF(checkException(env)); @@ -250,6 +262,4 @@ std::vector jvmToNativeByteArray(JNIEnv* env, return v; } - - } // namespace monero diff --git a/lib/android/src/main/cpp/common/jvm.h b/lib/android/src/main/cpp/common/jvm.h index 490698f..55636a8 100644 --- a/lib/android/src/main/cpp/common/jvm.h +++ b/lib/android/src/main/cpp/common/jvm.h @@ -270,13 +270,9 @@ ScopedJvmLocalRef findClass(JNIEnv* env, const char* name); // Methods for converting native types to Java types. ScopedJvmLocalRef nativeToJvmString(JNIEnv* env, const char* str); ScopedJvmLocalRef nativeToJvmString(JNIEnv* env, const std::string& str); -ScopedJvmLocalRef nativeToJvmByteArray( - JNIEnv* env, - const char* bytes, - size_t len); -ScopedJvmLocalRef nativeToJvmByteArray( - JNIEnv* env, - const std::vector& bytes); +ScopedJvmLocalRef nativeToJvmByteArray(JNIEnv* env, const char* bytes, size_t len); +ScopedJvmLocalRef nativeToJvmLongArray(JNIEnv* env, const int64_t* longs, size_t len); +ScopedJvmLocalRef nativeToJvmLongArray(JNIEnv* env, const uint64_t* longs, size_t len); jlong nativeToJvmPointer(void* ptr); // Helper function for converting std::vector into a Java array. diff --git a/lib/android/src/main/cpp/wallet/wallet.cc b/lib/android/src/main/cpp/wallet/wallet.cc index 1629733..62ac70b 100644 --- a/lib/android/src/main/cpp/wallet/wallet.cc +++ b/lib/android/src/main/cpp/wallet/wallet.cc @@ -21,11 +21,17 @@ namespace monero { using namespace std::chrono_literals; using namespace epee::string_tools; -static_assert(COIN == 1e12, "Monero atomic unit must be 1e-12 XMR"); +// Ensure constant values match the expected values in Kotlin. +static_assert(COIN == 1e12, + "Monero atomic unit must be 1e-12 XMR"); static_assert(CRYPTONOTE_MAX_BLOCK_NUMBER == 500000000, "Min timestamp must be higher than max block height"); -static_assert(CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE == 10, ""); // TODO -static_assert(DIFFICULTY_TARGET_V2 == 120, ""); +static_assert(CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE == 10, + "CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE mismatch"); +static_assert(DIFFICULTY_TARGET_V2 == 120, + "DIFFICULTY_TARGET_V2 mismatch"); +static_assert(PER_KB_FEE_QUANTIZATION_DECIMALS == 8, + "PER_KB_FEE_QUANTIZATION_DECIMALS mismatch"); Wallet::Wallet( JNIEnv* env, @@ -109,7 +115,7 @@ bool Wallet::parseFrom(std::istream& input) { bool Wallet::writeTo(std::ostream& output) { return suspendRefreshAndRunLocked([&]() -> bool { - binary_archive < true > ar(output); + binary_archive ar(output); if (!serialization::serialize_noeof(ar, *this)) return false; if (!serialization::serialize_noeof(ar, require_account())) @@ -126,6 +132,10 @@ void Wallet::withTxHistory(Consumer consumer) { consumer(m_tx_history); } +std::vector Wallet::fetchBaseFeeEstimate() { + return m_wallet.get_dynamic_base_fee_scaling_estimate(); +} + std::string Wallet::public_address() const { auto account = const_cast(this)->require_account(); return account.get_public_address_str(m_wallet.nettype()); @@ -191,7 +201,7 @@ void Wallet::captureTxHistorySnapshot(std::vector& snapshot) { snapshot.emplace_back(td.m_txid, TxInfo::INCOMING); TxInfo& recv = snapshot.back(); recv.m_public_key = td.get_public_key(); - recv.m_public_key_known = true; + recv.m_public_key_known = true; recv.m_key_image = td.m_key_image; recv.m_key_image_known = td.m_key_image_known; recv.m_subaddress_major = td.m_subaddr_index.major; @@ -235,7 +245,7 @@ void Wallet::captureTxHistorySnapshot(std::vector& snapshot) { spent.m_state = TxInfo::ON_CHAIN; } - for (const auto& ring : tx.m_rings) { + for (const auto& ring: tx.m_rings) { snapshot.emplace_back(pair.first, TxInfo::OUTGOING); TxInfo& spent = snapshot.back(); spent.m_key_image = ring.first; @@ -254,8 +264,8 @@ void Wallet::captureTxHistorySnapshot(std::vector& snapshot) { const auto& utx = pair.second; uint64_t fee = utx.m_amount_in - utx.m_amount_out; auto state = (utx.m_state == unconfirmed_transfer_details::pending) - ? TxInfo::PENDING - : TxInfo::FAILED; + ? TxInfo::PENDING + : TxInfo::FAILED; for (const auto& dest: utx.m_dests) { if (const auto dest_subaddr_idx = m_wallet.get_subaddress_index(dest.addr)) { @@ -297,7 +307,7 @@ void Wallet::captureTxHistorySnapshot(std::vector& snapshot) { change.m_state = state; } - for (const auto& ring : utx.m_rings) { + for (const auto& ring: utx.m_rings) { snapshot.emplace_back(pair.first, TxInfo::OUTGOING); TxInfo& spent = snapshot.back(); spent.m_key_image = ring.first; @@ -636,4 +646,15 @@ Java_im_molly_monero_WalletNative_nativeGetTxHistory( return j_array.Release(); } +extern "C" +JNIEXPORT jlongArray JNICALL +Java_im_molly_monero_WalletNative_nativeFetchBaseFeeEstimate( + JNIEnv* env, + jobject thiz, + jlong handle) { + auto* wallet = reinterpret_cast(handle); + std::vector fees = wallet->fetchBaseFeeEstimate(); + return nativeToJvmLongArray(env, fees.data(), fees.size()).Release(); +} + } // namespace monero diff --git a/lib/android/src/main/cpp/wallet/wallet.h b/lib/android/src/main/cpp/wallet/wallet.h index 2b21c42..a146903 100644 --- a/lib/android/src/main/cpp/wallet/wallet.h +++ b/lib/android/src/main/cpp/wallet/wallet.h @@ -47,7 +47,7 @@ struct TxInfo { ON_CHAIN = 3, } m_state; - TxInfo(crypto::hash tx_hash, TxType type): + TxInfo(crypto::hash tx_hash, TxType type) : m_tx_hash(tx_hash), m_public_key(crypto::public_key{}), m_key_image(crypto::key_image{}), @@ -96,6 +96,8 @@ class Wallet : tools::i_wallet2_callback { template void withTxHistory(Consumer consumer); + std::vector fetchBaseFeeEstimate(); + std::string public_address() const; uint32_t current_blockchain_height() const { return static_cast(m_last_block_height); } diff --git a/lib/android/src/main/kotlin/im/molly/monero/DynamicFeeRate.kt b/lib/android/src/main/kotlin/im/molly/monero/DynamicFeeRate.kt new file mode 100644 index 0000000..01acc63 --- /dev/null +++ b/lib/android/src/main/kotlin/im/molly/monero/DynamicFeeRate.kt @@ -0,0 +1,13 @@ +package im.molly.monero + +import im.molly.monero.internal.constants.PER_KB_FEE_QUANTIZATION_DECIMALS +import java.math.BigDecimal + +data class DynamicFeeRate(val feePerByte: Map) { + + val quantizationMask: MoneroAmount = BigDecimal.TEN.pow(PER_KB_FEE_QUANTIZATION_DECIMALS).xmr + + fun estimateFee(tx: PendingTransaction): Map { + TODO() + } +} diff --git a/lib/android/src/main/kotlin/im/molly/monero/FeePriority.kt b/lib/android/src/main/kotlin/im/molly/monero/FeePriority.kt new file mode 100644 index 0000000..493fb10 --- /dev/null +++ b/lib/android/src/main/kotlin/im/molly/monero/FeePriority.kt @@ -0,0 +1,8 @@ +package im.molly.monero + +enum class FeePriority(val priority: Int) { + LOW(1), + MEDIUM(2), + HIGH(3), + URGENT(4), +} diff --git a/lib/android/src/main/kotlin/im/molly/monero/MoneroWallet.kt b/lib/android/src/main/kotlin/im/molly/monero/MoneroWallet.kt index fb1743b..99d4e6f 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/MoneroWallet.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/MoneroWallet.kt @@ -5,10 +5,12 @@ import im.molly.monero.internal.consolidateTransactions import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.onFailure +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.suspendCancellableCoroutine -import java.time.Instant +import kotlin.time.Duration.Companion.seconds class MoneroWallet internal constructor( private val wallet: IWallet, @@ -74,6 +76,40 @@ class MoneroWallet internal constructor( }) } + fun dynamicFeeRate(): Flow = flow { + while (true) { + val fees = requestFees() ?: emptyList() + val feePerByte = when (fees.size) { + 1 -> mapOf(FeePriority.MEDIUM to fees[0]) + 4 -> mapOf( + FeePriority.LOW to fees[0], + FeePriority.MEDIUM to fees[1], + FeePriority.HIGH to fees[2], + FeePriority.URGENT to fees[3], + ) + + else -> { + logger.e("Unexpected number of fees received: ${fees.size}") + null + } + } + feePerByte?.let { emit(DynamicFeeRate(it)) } + // RPC client caches fees for 30 secs, wait before re-requesting fees + delay(30.seconds) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + private suspend fun requestFees(): List? = + suspendCancellableCoroutine { continuation -> + wallet.requestFees(object : BaseWalletCallbacks() { + override fun onFeesReceived(fees: LongArray?) { + val feeAmounts = fees?.map { MoneroAmount(atomicUnits = it) } + continuation.resume(feeAmounts) {} + } + }) + } + override fun close() = wallet.close() } @@ -81,6 +117,8 @@ private abstract class BaseWalletCallbacks : IWalletCallbacks.Stub() { override fun onRefreshResult(blockchainTime: BlockchainTime, status: Int) = Unit override fun onCommitResult(success: Boolean) = Unit + + override fun onFeesReceived(fees: LongArray?) = Unit } class RefreshResult(val blockchainTime: BlockchainTime, private val status: Int) { diff --git a/lib/android/src/main/kotlin/im/molly/monero/PendingTransaction.kt b/lib/android/src/main/kotlin/im/molly/monero/PendingTransaction.kt new file mode 100644 index 0000000..adf958e --- /dev/null +++ b/lib/android/src/main/kotlin/im/molly/monero/PendingTransaction.kt @@ -0,0 +1,3 @@ +package im.molly.monero + +class PendingTransaction diff --git a/lib/android/src/main/kotlin/im/molly/monero/ProtocolInfo.kt b/lib/android/src/main/kotlin/im/molly/monero/ProtocolInfo.kt index ef50c29..a08b768 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/ProtocolInfo.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/ProtocolInfo.kt @@ -1,3 +1,12 @@ package im.molly.monero -enum class ProtocolInfo +interface ProtocolInfo { + val version: Int + val perByteFee: Boolean + val feeScaling2021: Boolean +} + +data class MoneroReleaseInfo(override val version: Int) : ProtocolInfo { + override val perByteFee = version >= 8 + override val feeScaling2021 = version >= 15 +} diff --git a/lib/android/src/main/kotlin/im/molly/monero/WalletNative.kt b/lib/android/src/main/kotlin/im/molly/monero/WalletNative.kt index 6bc7205..b86f410 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/WalletNative.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/WalletNative.kt @@ -168,6 +168,13 @@ class WalletNative private constructor( } } + override fun requestFees(callback: IWalletCallbacks?) { + scope.launch(ioDispatcher) { + val fees = nativeFetchBaseFeeEstimate(handle) + callback?.onFeesReceived(fees) + } + } + /** * Also replays the last known balance whenever a new listener registers. */ @@ -281,6 +288,7 @@ class WalletNative private constructor( private external fun nativeGetTxHistory(handle: Long): Array private external fun nativeGetAccountPrimaryAddress(handle: Long): String // private external fun nativeGetAccountSubAddress(handle: Long, accountIndex: Int, subAddressIndex: Int): String + private external fun nativeFetchBaseFeeEstimate(handle: Long): LongArray private external fun nativeLoad(handle: Long, fd: Int): Boolean private external fun nativeNonReentrantRefresh(handle: Long, skipCoinbase: Boolean): Int private external fun nativeRestoreAccount( diff --git a/lib/android/src/main/kotlin/im/molly/monero/internal/constants/Constants.kt b/lib/android/src/main/kotlin/im/molly/monero/internal/constants/Constants.kt index 04037c7..e6bd448 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/internal/constants/Constants.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/internal/constants/Constants.kt @@ -5,3 +5,5 @@ internal const val CRYPTONOTE_MAX_BLOCK_NUMBER: Int = 500_000_000 internal const val CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE: Int = 10 internal const val DIFFICULTY_TARGET_V1: Long = 60 internal const val DIFFICULTY_TARGET_V2: Long = 120 +internal const val PER_KB_FEE_QUANTIZATION_DECIMALS: Int = 8 +internal const val BULLETPROOF_PLUS_MAX_OUTPUTS: Int = 16 diff --git a/vendor/monero b/vendor/monero index 04dc1bf..6063fbe 160000 --- a/vendor/monero +++ b/vendor/monero @@ -1 +1 @@ -Subproject commit 04dc1bf8f16e6c6345cabaff15ecc5f55ff648d9 +Subproject commit 6063fbeb1414eab1c027ee57b4f2834bb178af7e