lib: add functions for retrieving base fees

This commit is contained in:
Oscar Mira 2024-02-07 02:18:03 +01:00
parent f91af7aabd
commit 44223c962b
14 changed files with 139 additions and 27 deletions

View File

@ -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();
}

View File

@ -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);
}

View File

@ -213,10 +213,22 @@ ScopedJvmLocalRef<jbyteArray> nativeToJvmByteArray(JNIEnv* env,
return {env, j_array};
}
ScopedJvmLocalRef<jbyteArray> nativeToJvmByteArray(
JNIEnv* env,
const std::vector<char>& bytes) {
return nativeToJvmByteArray(env, bytes.data(), bytes.size());
ScopedJvmLocalRef<jlongArray> 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<jlongArray> nativeToJvmLongArray(JNIEnv* env,
const uint64_t* longs,
size_t len) {
return nativeToJvmLongArray(env, reinterpret_cast<const int64_t*>(longs), len);
}
jlong nativeToJvmPointer(void* ptr) {
@ -225,7 +237,7 @@ jlong nativeToJvmPointer(void* ptr) {
}
std::string jvmToStdString(JNIEnv* env, const JvmRef<jstring>& 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<char> jvmToNativeByteArray(JNIEnv* env,
return v;
}
} // namespace monero

View File

@ -270,13 +270,9 @@ ScopedJvmLocalRef<jclass> findClass(JNIEnv* env, const char* name);
// Methods for converting native types to Java types.
ScopedJvmLocalRef<jstring> nativeToJvmString(JNIEnv* env, const char* str);
ScopedJvmLocalRef<jstring> nativeToJvmString(JNIEnv* env, const std::string& str);
ScopedJvmLocalRef<jbyteArray> nativeToJvmByteArray(
JNIEnv* env,
const char* bytes,
size_t len);
ScopedJvmLocalRef<jbyteArray> nativeToJvmByteArray(
JNIEnv* env,
const std::vector<char>& bytes);
ScopedJvmLocalRef<jbyteArray> nativeToJvmByteArray(JNIEnv* env, const char* bytes, size_t len);
ScopedJvmLocalRef<jlongArray> nativeToJvmLongArray(JNIEnv* env, const int64_t* longs, size_t len);
ScopedJvmLocalRef<jlongArray> nativeToJvmLongArray(JNIEnv* env, const uint64_t* longs, size_t len);
jlong nativeToJvmPointer(void* ptr);
// Helper function for converting std::vector<T> into a Java array.

View File

@ -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<true> 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<uint64_t> Wallet::fetchBaseFeeEstimate() {
return m_wallet.get_dynamic_base_fee_scaling_estimate();
}
std::string Wallet::public_address() const {
auto account = const_cast<Wallet*>(this)->require_account();
return account.get_public_address_str(m_wallet.nettype());
@ -191,7 +201,7 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& 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<TxInfo>& 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<TxInfo>& 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<TxInfo>& 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<Wallet*>(handle);
std::vector<uint64_t> fees = wallet->fetchBaseFeeEstimate();
return nativeToJvmLongArray(env, fees.data(), fees.size()).Release();
}
} // namespace monero

View File

@ -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<typename Consumer>
void withTxHistory(Consumer consumer);
std::vector<uint64_t> fetchBaseFeeEstimate();
std::string public_address() const;
uint32_t current_blockchain_height() const { return static_cast<uint32_t>(m_last_block_height); }

View File

@ -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<FeePriority, MoneroAmount>) {
val quantizationMask: MoneroAmount = BigDecimal.TEN.pow(PER_KB_FEE_QUANTIZATION_DECIMALS).xmr
fun estimateFee(tx: PendingTransaction): Map<FeePriority, MoneroAmount> {
TODO()
}
}

View File

@ -0,0 +1,8 @@
package im.molly.monero
enum class FeePriority(val priority: Int) {
LOW(1),
MEDIUM(2),
HIGH(3),
URGENT(4),
}

View File

@ -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<DynamicFeeRate> = 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<MoneroAmount>? =
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) {

View File

@ -0,0 +1,3 @@
package im.molly.monero
class PendingTransaction

View File

@ -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
}

View File

@ -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<TxInfo>
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(

View File

@ -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

2
vendor/monero vendored

@ -1 +1 @@
Subproject commit 04dc1bf8f16e6c6345cabaff15ecc5f55ff648d9
Subproject commit 6063fbeb1414eab1c027ee57b4f2834bb178af7e