lib: fix transaction timestamps

This commit is contained in:
Oscar Mira 2023-10-23 12:12:11 +02:00
parent f5998ac369
commit 2bd227124a
13 changed files with 126 additions and 86 deletions

View File

@ -49,7 +49,7 @@ fun TransactionCardExpanded(
localDateTime.format(formatter) localDateTime.format(formatter)
} }
Text( Text(
text = timestamp ?: "", text = timestamp ?: "Pending",
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
) )
Text( Text(
@ -64,7 +64,7 @@ fun TransactionCardExpanded(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
) { ) {
Text( Text(
text = "#${transaction.blockHeight}", text = transaction.blockHeight?.toString() ?: "",
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
) )
Text( Text(

View File

@ -54,7 +54,7 @@ fun WalletBalanceView(
) { ) {
Text( Text(
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
text = "Balance at Block #${blockchainTime.height}", text = "Balance at ${blockchainTime}",
) )
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))

View File

@ -15,6 +15,7 @@ import im.molly.monero.demo.data.model.WalletConfig
import im.molly.monero.demo.data.model.WalletTransaction import im.molly.monero.demo.data.model.WalletTransaction
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.Instant
class WalletViewModel( class WalletViewModel(
walletId: Long, walletId: Long,
@ -65,7 +66,7 @@ private fun walletUiState(
val transactions = val transactions =
ledger.transactions ledger.transactions
.map { WalletTransaction(config.id, it.value) } .map { WalletTransaction(config.id, it.value) }
.sortedByDescending { it.transaction.timestamp } .sortedByDescending { it.transaction.timestamp ?: Instant.MAX }
val network = ledger.primaryAddress.network val network = ledger.primaryAddress.network
WalletUiState.Loaded(config, network, blockchainTime, balance, transactions) WalletUiState.Loaded(config, network, blockchainTime, balance, transactions)
} }

View File

@ -0,0 +1,3 @@
package im.molly.monero;
parcelable BlockchainTime;

View File

@ -1,8 +1,9 @@
package im.molly.monero; package im.molly.monero;
import im.molly.monero.BlockchainTime;
import im.molly.monero.internal.TxInfo; import im.molly.monero.internal.TxInfo;
oneway interface IBalanceListener { oneway interface IBalanceListener {
void onBalanceChanged(in List<TxInfo> txHistory, int blockchainHeight); void onBalanceChanged(in List<TxInfo> txHistory, in BlockchainTime blockchainTime);
void onRefresh(int blockchainHeight); void onRefresh(in BlockchainTime blockchainTime);
} }

View File

@ -1,6 +1,8 @@
package im.molly.monero; package im.molly.monero;
import im.molly.monero.BlockchainTime;
oneway interface IWalletCallbacks { oneway interface IWalletCallbacks {
void onRefreshResult(int blockHeight, int status); void onRefreshResult(in BlockchainTime blockchainTime, int status);
void onCommitResult(boolean success); void onCommitResult(boolean success);
} }

View File

@ -40,7 +40,7 @@ void initializeJniCache(JNIEnv* env) {
"callRemoteNode", "callRemoteNode",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[B)Lim/molly/monero/HttpResponse;"); "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[B)Lim/molly/monero/HttpResponse;");
WalletNative_onRefresh = walletNative WalletNative_onRefresh = walletNative
.getMethodId(env, "onRefresh", "(IZ)V"); .getMethodId(env, "onRefresh", "(IJZ)V");
WalletNative_onSuspendRefresh = walletNative WalletNative_onSuspendRefresh = walletNative
.getMethodId(env, "onSuspendRefresh", "(Z)V"); .getMethodId(env, "onSuspendRefresh", "(Z)V");

View File

@ -36,7 +36,8 @@ Wallet::Wallet(
std::make_unique<RemoteNodeClientFactory>(env, wallet_native)), std::make_unique<RemoteNodeClientFactory>(env, wallet_native)),
m_callback(env, wallet_native), m_callback(env, wallet_native),
m_account_ready(false), m_account_ready(false),
m_blockchain_height(1), m_last_block_height(1),
m_last_block_timestamp(1397818193),
m_restore_height(0), m_restore_height(0),
m_refresh_running(false), m_refresh_running(false),
m_refresh_canceled(false) { m_refresh_canceled(false) {
@ -97,7 +98,6 @@ bool Wallet::parseFrom(std::istream& input) {
return false; return false;
if (!serialization::serialize(ar, m_wallet)) if (!serialization::serialize(ar, m_wallet))
return false; return false;
set_current_blockchain_height(m_wallet.get_blockchain_current_height());
captureTxHistorySnapshot(m_tx_history); captureTxHistorySnapshot(m_tx_history);
m_account_ready = true; m_account_ready = true;
return true; return true;
@ -127,49 +127,40 @@ std::string Wallet::public_address() const {
return account.get_public_address_str(m_wallet.nettype()); return account.get_public_address_str(m_wallet.nettype());
} }
void Wallet::set_current_blockchain_height(uint64_t height) {
LOG_FATAL_IF(height >= CRYPTONOTE_MAX_BLOCK_NUMBER, "Blockchain max height reached");
m_blockchain_height = height;
}
cryptonote::account_base& Wallet::require_account() { cryptonote::account_base& Wallet::require_account() {
LOG_FATAL_IF(!m_account_ready, "Account is not initialized"); LOG_FATAL_IF(!m_account_ready, "Account is not initialized");
return m_wallet.get_account(); return m_wallet.get_account();
} }
const payment_details* find_matching_payment( const payment_details* find_payment_by_txid(
const std::list<std::pair<crypto::hash, payment_details>> pds, const std::list<std::pair<crypto::hash, payment_details>>& pds,
const transfer_details& td) { const crypto::hash& txid) {
if (td.m_txid == crypto::null_hash) { if (txid == crypto::null_hash) {
return nullptr; return nullptr;
} }
for (const auto& p: pds) { for (auto it = pds.begin(); it != pds.end(); ++it) {
const auto& pd = p.second; const auto pd = &it->second;
if (td.m_amount == pd.m_amount if (txid == pd->m_tx_hash) {
&& td.m_subaddr_index == pd.m_subaddr_index return pd;
&& td.m_txid == pd.m_tx_hash) {
return &pd;
} }
} }
return nullptr; return nullptr;
}; }
const confirmed_transfer_details* find_matching_transfer_for_change( const confirmed_transfer_details* find_transfer_by_txid(
const std::list<std::pair<crypto::hash, confirmed_transfer_details>> txs, const std::list<std::pair<crypto::hash, confirmed_transfer_details>>& txs,
const transfer_details& td) { const crypto::hash& txid) {
if (td.m_txid == crypto::null_hash || td.m_subaddr_index.minor != 0) { if (txid == crypto::null_hash) {
return nullptr; return nullptr;
} }
for (const auto& p: txs) { for (auto it = txs.begin(); it != txs.end(); ++it) {
const auto& tx = p.second; const auto tx = &it->second;
if (td.m_amount == tx.m_change if (txid == it->first) {
&& td.m_subaddr_index.major == tx.m_subaddr_account return tx;
&& td.m_txid == p.first) {
return &tx;
} }
} }
return nullptr; return nullptr;
}; }
// Only call this function from the callback thread or during initialization, // Only call this function from the callback thread or during initialization,
// as there is no locking mechanism to safeguard reading transaction history // as there is no locking mechanism to safeguard reading transaction history
@ -205,14 +196,14 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
recv.m_amount = td.m_amount; recv.m_amount = td.m_amount;
recv.m_unlock_time = td.m_tx.unlock_time; recv.m_unlock_time = td.m_tx.unlock_time;
// Check if the payment or change exists and update metadata if found. // Check if the payment or transfer exists and update metadata if found.
if (const auto* pd = find_matching_payment(pds, td)) { if (const auto* pd = find_payment_by_txid(pds, td.m_txid)) {
recv.m_height = pd->m_block_height; recv.m_height = pd->m_block_height;
recv.m_timestamp = pd->m_timestamp; recv.m_timestamp = pd->m_timestamp;
recv.m_fee = pd->m_fee; recv.m_fee = pd->m_fee;
recv.m_coinbase = pd->m_coinbase; recv.m_coinbase = pd->m_coinbase;
recv.m_state = TxInfo::ON_CHAIN; recv.m_state = TxInfo::ON_CHAIN;
} else if (const auto tx = find_matching_transfer_for_change(txs, td)) { } else if (const auto* tx = find_transfer_by_txid(txs, td.m_txid)) {
recv.m_height = tx->m_block_height; recv.m_height = tx->m_block_height;
recv.m_timestamp = tx->m_timestamp; recv.m_timestamp = tx->m_timestamp;
recv.m_fee = tx->m_amount_in - tx->m_amount_out; recv.m_fee = tx->m_amount_in - tx->m_amount_out;
@ -336,15 +327,11 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
} }
} }
void Wallet::handleNewBlock(uint64_t height, bool refresh_running) { void Wallet::handleNewBlock(uint64_t height, uint64_t timestamp) {
set_current_blockchain_height(height); LOG_FATAL_IF(height >= CRYPTONOTE_MAX_BLOCK_NUMBER, "Blockchain max height reached");
if (m_balance_changed) { m_last_block_height = height;
m_tx_history_mutex.lock(); m_last_block_timestamp = timestamp;
captureTxHistorySnapshot(m_tx_history); processBalanceChanges(true);
m_tx_history_mutex.unlock();
}
notifyRefresh(!m_balance_changed && refresh_running);
m_balance_changed = false;
} }
void Wallet::handleReorgEvent(uint64_t at_block_height) { void Wallet::handleReorgEvent(uint64_t at_block_height) {
@ -355,11 +342,22 @@ void Wallet::handleMoneyEvent(uint64_t at_block_height) {
m_balance_changed = true; m_balance_changed = true;
} }
void Wallet::notifyRefresh(bool debounce) { void Wallet::processBalanceChanges(bool refresh_running) {
if (m_balance_changed) {
m_tx_history_mutex.lock();
captureTxHistorySnapshot(m_tx_history);
m_tx_history_mutex.unlock();
}
notifyRefreshState(!m_balance_changed && refresh_running);
m_balance_changed = false;
}
void Wallet::notifyRefreshState(bool debounce) {
static std::chrono::steady_clock::time_point last_time; static std::chrono::steady_clock::time_point last_time;
// If debouncing is requested and the blockchain height is a multiple of 100, it limits // If debouncing is requested and the blockchain height is a multiple of 100, it limits
// the notifications to once every 200 ms. // the notifications to once every 200 ms.
uint32_t height = current_blockchain_height(); uint32_t height = current_blockchain_height();
uint64_t ts = current_blockchain_timestamp();
if (debounce) { if (debounce) {
if (height % 100 == 0) { if (height % 100 == 0) {
auto now = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now();
@ -373,7 +371,7 @@ void Wallet::notifyRefresh(bool debounce) {
} }
if (!debounce) { if (!debounce) {
m_callback.callVoidMethod(getJniEnv(), WalletNative_onRefresh, m_callback.callVoidMethod(getJniEnv(), WalletNative_onRefresh,
height, m_balance_changed); height, ts, m_balance_changed);
} }
} }
@ -409,7 +407,7 @@ Wallet::Status Wallet::nonReentrantRefresh(bool skip_coinbase) {
} }
m_refresh_running.store(false); m_refresh_running.store(false);
// Ensure the latest block and pool state are consistently processed. // Ensure the latest block and pool state are consistently processed.
handleNewBlock(m_wallet.get_blockchain_current_height(), false); processBalanceChanges(false);
return ret; return ret;
} }
@ -583,6 +581,16 @@ Java_im_molly_monero_WalletNative_nativeGetCurrentBlockchainHeight(
return wallet->current_blockchain_height(); return wallet->current_blockchain_height();
} }
extern "C"
JNIEXPORT jlong JNICALL
Java_im_molly_monero_WalletNative_nativeGetCurrentBlockchainTimestamp(
JNIEnv* env,
jobject thiz,
jlong handle) {
auto* wallet = reinterpret_cast<Wallet*>(handle);
return wallet->current_blockchain_timestamp();
}
ScopedJvmLocalRef<jobject> nativeToJvmTxInfo(JNIEnv* env, ScopedJvmLocalRef<jobject> nativeToJvmTxInfo(JNIEnv* env,
const TxInfo& tx) { const TxInfo& tx) {
LOG_FATAL_IF(tx.m_height >= CRYPTONOTE_MAX_BLOCK_NUMBER, "Blockchain max height reached"); LOG_FATAL_IF(tx.m_height >= CRYPTONOTE_MAX_BLOCK_NUMBER, "Blockchain max height reached");

View File

@ -97,13 +97,15 @@ class Wallet : tools::i_wallet2_callback {
std::string public_address() const; std::string public_address() const;
void set_current_blockchain_height(uint64_t height); uint32_t current_blockchain_height() const { return static_cast<uint32_t>(m_last_block_height); }
uint32_t current_blockchain_height() const { return static_cast<uint32_t>(m_blockchain_height); } uint64_t current_blockchain_timestamp() const { return m_last_block_timestamp; }
// Extra state that must be persistent but isn't restored by wallet2's serializer. // Extra state that must be persistent but isn't restored by wallet2's serializer.
BEGIN_SERIALIZE_OBJECT() BEGIN_SERIALIZE_OBJECT()
VERSION_FIELD(0) VERSION_FIELD(0)
FIELD(m_restore_height) FIELD(m_restore_height)
FIELD(m_last_block_height)
FIELD(m_last_block_timestamp)
END_SERIALIZE() END_SERIALIZE()
private: private:
@ -113,7 +115,8 @@ class Wallet : tools::i_wallet2_callback {
bool m_account_ready; bool m_account_ready;
uint64_t m_restore_height; uint64_t m_restore_height;
uint64_t m_blockchain_height; uint64_t m_last_block_height;
uint64_t m_last_block_timestamp;
// Saved transaction history. // Saved transaction history.
std::vector<TxInfo> m_tx_history; std::vector<TxInfo> m_tx_history;
@ -131,20 +134,22 @@ class Wallet : tools::i_wallet2_callback {
bool m_refresh_canceled; bool m_refresh_canceled;
bool m_balance_changed; bool m_balance_changed;
void notifyRefresh(bool debounce); void processBalanceChanges(bool refresh_running);
void notifyRefreshState(bool debounce);
template<typename T> template<typename T>
auto suspendRefreshAndRunLocked(T block) -> decltype(block()); auto suspendRefreshAndRunLocked(T block) -> decltype(block());
void captureTxHistorySnapshot(std::vector<TxInfo>& snapshot); void captureTxHistorySnapshot(std::vector<TxInfo>& snapshot);
void handleNewBlock(uint64_t height, bool refresh_running); void handleNewBlock(uint64_t height, uint64_t timestmap);
void handleReorgEvent(uint64_t at_block_height); void handleReorgEvent(uint64_t at_block_height);
void handleMoneyEvent(uint64_t at_block_height); void handleMoneyEvent(uint64_t at_block_height);
// Implementation of i_wallet2_callback follows. // Implementation of i_wallet2_callback follows.
private: private:
void on_new_block(uint64_t height, const cryptonote::block& block) override { void on_new_block(uint64_t height, const cryptonote::block& block) override {
handleNewBlock(height, true); // Block could be empty during a fast refresh.
handleNewBlock(height, block.timestamp);
} }
void on_reorg(uint64_t height) override { void on_reorg(uint64_t height) override {

View File

@ -1,6 +1,8 @@
package im.molly.monero package im.molly.monero
import android.os.Parcelable
import im.molly.monero.internal.constants.DIFFICULTY_TARGET_V2 import im.molly.monero.internal.constants.DIFFICULTY_TARGET_V2
import kotlinx.parcelize.Parcelize
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
@ -9,10 +11,11 @@ import java.time.LocalDate
/** /**
* A point in the blockchain timeline, which could be either a block height or a timestamp. * A point in the blockchain timeline, which could be either a block height or a timestamp.
*/ */
@Parcelize
open class BlockchainTime( open class BlockchainTime(
val height: Int, val height: Int,
val timestamp: Instant, val timestamp: Instant,
) : Comparable<BlockchainTime> { ) : Comparable<BlockchainTime>, Parcelable {
init { init {
require(isBlockHeightInRange(height)) { require(isBlockHeightInRange(height)) {
@ -25,12 +28,15 @@ open class BlockchainTime(
override fun compareTo(other: BlockchainTime): Int = override fun compareTo(other: BlockchainTime): Int =
this.height.compareTo(other.height) this.height.compareTo(other.height)
override fun toString(): String = "Block #$height | $timestamp" override fun toString(): String = "Block $height | Time $timestamp"
data object Genesis : BlockchainTime(0, Instant.ofEpochSecond(1397818193)) data object Genesis : BlockchainTime(0, Instant.ofEpochSecond(1397818193))
class Block(height: Int, referencePoint: BlockchainTime = Genesis) : class Block(height: Int, referencePoint: BlockchainTime = Genesis) :
BlockchainTime(height, estimateTimestamp(height, referencePoint)) {} BlockchainTime(height, estimateTimestamp(height, referencePoint)) {
override fun toString(): String = "Block $height | Time $timestamp (Estimated)"
}
class Timestamp(timestamp: Instant, referencePoint: BlockchainTime = Genesis) : class Timestamp(timestamp: Instant, referencePoint: BlockchainTime = Genesis) :
BlockchainTime(estimateBlockHeight(timestamp, referencePoint), timestamp) { BlockchainTime(estimateBlockHeight(timestamp, referencePoint), timestamp) {
@ -41,6 +47,8 @@ open class BlockchainTime(
override fun compareTo(other: BlockchainTime): Int = override fun compareTo(other: BlockchainTime): Int =
this.timestamp.compareTo(other.timestamp) this.timestamp.compareTo(other.timestamp)
override fun toString(): String = "Block $height (Estimated) | Time $timestamp"
} }
companion object { companion object {

View File

@ -29,17 +29,14 @@ class MoneroWallet internal constructor(
val listener = object : IBalanceListener.Stub() { val listener = object : IBalanceListener.Stub() {
lateinit var lastKnownLedger: Ledger lateinit var lastKnownLedger: Ledger
override fun onBalanceChanged(txHistory: List<TxInfo>, blockchainHeight: Int) { override fun onBalanceChanged(txHistory: List<TxInfo>, blockchainTime: BlockchainTime) {
val now = Instant.now() val (txs, spendableEnotes) = txHistory.consolidateTransactions(blockchainTime)
val checkedAt = BlockchainTime(blockchainHeight, now) lastKnownLedger = Ledger(primaryAddress, txs, spendableEnotes, blockchainTime)
val (txs, spendableEnotes) = txHistory.consolidateTransactions(checkedAt)
lastKnownLedger = Ledger(primaryAddress, txs, spendableEnotes, checkedAt)
sendLedger(lastKnownLedger) sendLedger(lastKnownLedger)
} }
override fun onRefresh(blockHeight: Int) { override fun onRefresh(blockchainTime: BlockchainTime) {
val checkedAt = BlockchainTime.Block(blockHeight) sendLedger(lastKnownLedger.copy(checkedAt = blockchainTime))
sendLedger(lastKnownLedger.copy(checkedAt = checkedAt))
} }
private fun sendLedger(ledger: Ledger) { private fun sendLedger(ledger: Ledger) {
@ -59,8 +56,8 @@ class MoneroWallet internal constructor(
skipCoinbaseOutputs: Boolean = false, skipCoinbaseOutputs: Boolean = false,
): RefreshResult = suspendCancellableCoroutine { continuation -> ): RefreshResult = suspendCancellableCoroutine { continuation ->
wallet.resumeRefresh(skipCoinbaseOutputs, object : BaseWalletCallbacks() { wallet.resumeRefresh(skipCoinbaseOutputs, object : BaseWalletCallbacks() {
override fun onRefreshResult(blockHeight: Int, status: Int) { override fun onRefreshResult(blockchainTime: BlockchainTime, status: Int) {
val result = RefreshResult(blockHeight, status) val result = RefreshResult(blockchainTime, status)
continuation.resume(result) {} continuation.resume(result) {}
} }
}) })
@ -81,11 +78,11 @@ class MoneroWallet internal constructor(
} }
private abstract class BaseWalletCallbacks : IWalletCallbacks.Stub() { private abstract class BaseWalletCallbacks : IWalletCallbacks.Stub() {
override fun onRefreshResult(blockHeight: Int, status: Int) = Unit override fun onRefreshResult(blockchainTime: BlockchainTime, status: Int) = Unit
override fun onCommitResult(success: Boolean) = Unit override fun onCommitResult(success: Boolean) = Unit
} }
class RefreshResult(val blockHeight: Int, private val status: Int) { class RefreshResult(val blockchainTime: BlockchainTime, private val status: Int) {
fun isError() = status != WalletNative.Status.OK fun isError() = status != WalletNative.Status.OK
} }

View File

@ -6,6 +6,7 @@ import im.molly.monero.internal.TxInfo
import im.molly.monero.internal.consolidateTransactions import im.molly.monero.internal.consolidateTransactions
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.Closeable import java.io.Closeable
import java.time.Instant
import java.util.* import java.util.*
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
@ -103,8 +104,19 @@ class WalletNative private constructor(
override fun getAccountPrimaryAddress() = nativeGetAccountPrimaryAddress(handle) override fun getAccountPrimaryAddress() = nativeGetAccountPrimaryAddress(handle)
val currentBlockchainHeight: Int private fun createBlockchainTime(height: Int, epochSeconds: Long): BlockchainTime {
get() = nativeGetCurrentBlockchainHeight(handle) return if (epochSeconds == 0L) {
BlockchainTime.Block(height)
} else {
BlockchainTime(height, Instant.ofEpochSecond(epochSeconds))
}
}
val currentBlockchainTime: BlockchainTime
get() = createBlockchainTime(
nativeGetCurrentBlockchainHeight(handle),
nativeGetCurrentBlockchainTimestamp(handle),
)
val currentBalance: Balance val currentBalance: Balance
get() = TODO() // txHistorySnapshot().consolidateTransactions().second.balance() get() = TODO() // txHistorySnapshot().consolidateTransactions().second.balance()
@ -133,7 +145,7 @@ class WalletNative private constructor(
nativeCancelRefresh(handle) nativeCancelRefresh(handle)
} }
} }
callback?.onRefreshResult(currentBlockchainHeight, status) callback?.onRefreshResult(currentBlockchainTime, status)
} }
} }
@ -163,7 +175,7 @@ class WalletNative private constructor(
requireNotNull(listener) requireNotNull(listener)
balanceListenersLock.withLock { balanceListenersLock.withLock {
balanceListeners.add(listener) balanceListeners.add(listener)
listener.onBalanceChanged(txHistorySnapshot(), currentBlockchainHeight) listener.onBalanceChanged(txHistorySnapshot(), currentBlockchainTime)
} }
} }
@ -175,14 +187,15 @@ class WalletNative private constructor(
} }
@CalledByNative("wallet.cc") @CalledByNative("wallet.cc")
private fun onRefresh(blockchainHeight: Int, balanceChanged: Boolean) { private fun onRefresh(height: Int, timestamp: Long, balanceChanged: Boolean) {
balanceListenersLock.withLock { balanceListenersLock.withLock {
if (balanceListeners.isNotEmpty()) { if (balanceListeners.isNotEmpty()) {
val call = fun(listener: IBalanceListener) { val call = fun(listener: IBalanceListener) {
val blockchainTime = createBlockchainTime(height, timestamp)
if (balanceChanged) { if (balanceChanged) {
listener.onBalanceChanged(txHistorySnapshot(), blockchainHeight) listener.onBalanceChanged(txHistorySnapshot(), blockchainTime)
} else { } else {
listener.onRefresh(blockchainHeight) listener.onRefresh(blockchainTime)
} }
} }
balanceListeners.forEach { call(it) } balanceListeners.forEach { call(it) }
@ -264,6 +277,7 @@ class WalletNative private constructor(
private external fun nativeCreate(networkId: Int): Long private external fun nativeCreate(networkId: Int): Long
private external fun nativeDispose(handle: Long) private external fun nativeDispose(handle: Long)
private external fun nativeGetCurrentBlockchainHeight(handle: Long): Int private external fun nativeGetCurrentBlockchainHeight(handle: Long): Int
private external fun nativeGetCurrentBlockchainTimestamp(handle: Long): Long
private external fun nativeGetTxHistory(handle: Long): Array<TxInfo> private external fun nativeGetTxHistory(handle: Long): Array<TxInfo>
private external fun nativeGetAccountPrimaryAddress(handle: Long): String private external fun nativeGetAccountPrimaryAddress(handle: Long): String
// private external fun nativeGetAccountSubAddress(handle: Long, accountIndex: Int, subAddressIndex: Int): String // private external fun nativeGetAccountSubAddress(handle: Long, accountIndex: Int, subAddressIndex: Int): String

View File

@ -83,7 +83,7 @@ internal fun List<TxInfo>.consolidateTransactions(
if (tx.state !is TxState.Failed) { if (tx.state !is TxState.Failed) {
val maxUnlockTime = tx.blockHeight?.let { height -> val maxUnlockTime = tx.blockHeight?.let { height ->
val defaultUnlockTime = BlockchainTime.Block( val defaultUnlockTime = BlockchainTime.Block(
height = height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE, height = height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE - 1,
referencePoint = blockchainContext, referencePoint = blockchainContext,
) )
max(defaultUnlockTime, tx.timeLock ?: BlockchainTime.Genesis) max(defaultUnlockTime, tx.timeLock ?: BlockchainTime.Genesis)
@ -139,14 +139,15 @@ private fun List<TxInfo>.createTransaction(
} }
private fun List<TxInfo>.determineTxState(): TxState { private fun List<TxInfo>.determineTxState(): TxState {
val uniqueTx = distinctBy { it.state }.single() val height = maxOf { it.height }
val timestamp = maxOf { it.timestamp }
return when (uniqueTx.state) { return when (val state = first().state) {
TxInfo.OFF_CHAIN -> TxState.OffChain TxInfo.OFF_CHAIN -> TxState.OffChain
TxInfo.PENDING -> TxState.InMemoryPool TxInfo.PENDING -> TxState.InMemoryPool
TxInfo.FAILED -> TxState.Failed TxInfo.FAILED -> TxState.Failed
TxInfo.ON_CHAIN -> TxState.OnChain(BlockHeader(uniqueTx.height, uniqueTx.timestamp)) TxInfo.ON_CHAIN -> TxState.OnChain(BlockHeader(height, timestamp))
else -> error("Invalid tx state value: ${uniqueTx.state}") else -> error("Invalid tx state value: $state")
} }
} }
@ -157,7 +158,7 @@ private fun TxInfo.toEnote(blockchainHeight: Int): Enote {
subAddressIndex = subAddressMinor subAddressIndex = subAddressMinor
) )
val calculatedAge = if (height == 0) 0 else blockchainHeight - height + 1 val calculatedAge = if (height > 0) (blockchainHeight - height + 1) else 0
return Enote( return Enote(
amount = MoneroAmount(atomicUnits = amount), amount = MoneroAmount(atomicUnits = amount),