mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2025-01-12 16:09:40 -05:00
lib: fix transaction timestamps
This commit is contained in:
parent
f5998ac369
commit
2bd227124a
@ -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(
|
||||||
|
@ -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))
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
package im.molly.monero;
|
||||||
|
|
||||||
|
parcelable BlockchainTime;
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
|
|
||||||
|
@ -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");
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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),
|
||||||
|
Loading…
Reference in New Issue
Block a user