lib: refactor blockchain time classes

This commit is contained in:
Oscar Mira 2023-10-25 17:46:54 +02:00
parent 02f8575f82
commit 5852add98e
19 changed files with 246 additions and 152 deletions

View File

@ -32,7 +32,7 @@ class MoneroSdkClient(private val context: Context) {
network: MoneroNetwork,
filename: String,
secretSpendKey: SecretKey,
restorePoint: BlockchainTime,
restorePoint: RestorePoint,
): MoneroWallet {
val provider = providerDeferred.await()
return provider.restoreWallet(

View File

@ -84,7 +84,7 @@ class WalletRepository(
name: String,
remoteNodeIds: List<Long>,
secretSpendKey: SecretKey,
restorePoint: BlockchainTime,
restorePoint: RestorePoint,
): Pair<Long, MoneroWallet> {
val uniqueFilename = UUID.randomUUID().toString()
val wallet = moneroSdkClient.restoreWallet(

View File

@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import im.molly.monero.MoneroNetwork
import im.molly.monero.BlockchainTime
import im.molly.monero.RestorePoint
import im.molly.monero.SecretKey
import im.molly.monero.demo.AppModule
import im.molly.monero.demo.data.RemoteNodeRepository
@ -93,10 +94,10 @@ class AddWalletViewModel(
secretSpendKeyHex.length == 64 && runCatching { secretSpendKeyHex.parseHex() }.isSuccess
fun validateCreationDate(): Boolean =
creationDate.isEmpty() || runCatching { LocalDate.parse(creationDate) }.isSuccess
creationDate.isEmpty() || runCatching { RestorePoint.creationTime(LocalDate.parse(creationDate)) }.isSuccess
fun validateRestoreHeight(): Boolean =
restoreHeight.isEmpty() || runCatching { BlockchainTime.Block(restoreHeight.toInt()) }.isSuccess
restoreHeight.isEmpty() || runCatching { RestorePoint.blockHeight(restoreHeight.toInt()) }.isSuccess
fun createWallet() = viewModelScope.launch {
walletRepository.addWallet(network, walletName, getSelectedRemoteNodeIds())
@ -104,9 +105,9 @@ class AddWalletViewModel(
fun restoreWallet() = viewModelScope.launch {
val restorePoint = when {
creationDate.isNotEmpty() -> BlockchainTime.Timestamp(LocalDate.parse(creationDate))
restoreHeight.isNotEmpty() -> BlockchainTime.Block(restoreHeight.toInt())
else -> BlockchainTime.Genesis
creationDate.isNotEmpty() -> RestorePoint.creationTime(LocalDate.parse(creationDate))
restoreHeight.isNotEmpty() -> RestorePoint.blockHeight(restoreHeight.toInt())
else -> RestorePoint.Genesis
}
SecretKey(secretSpendKeyHex.parseHex()).use { secretSpendKey ->
walletRepository.restoreWallet(

View File

@ -44,7 +44,7 @@ fun TransactionCardExpanded(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = modifier.fillMaxWidth(),
) {
val timestamp = transaction.timestamp?.let {
val timestamp = transaction.blockTimestamp?.let {
val localDateTime = it.atZone(ZoneId.systemDefault()).toLocalDateTime()
localDateTime.format(formatter)
}

View File

@ -23,8 +23,10 @@ import im.molly.monero.MoneroAmount
import im.molly.monero.Balance
import im.molly.monero.BlockchainTime
import im.molly.monero.MoneroCurrency
import im.molly.monero.MoneroNetwork
import im.molly.monero.TimeLocked
import im.molly.monero.demo.ui.theme.AppTheme
import im.molly.monero.genesisTime
import im.molly.monero.xmr
import kotlinx.coroutines.delay
import java.math.BigDecimal
@ -63,8 +65,10 @@ fun WalletBalanceView(
Divider()
BalanceRow("Total", balance.totalAmount)
BalanceRow("Unlocked", balance.unlockedAmountAt(blockchainTime.height, now))
balance.lockedAmountsAt(blockchainTime.height, now).forEach { (timeSpan, amount) ->
val currentTime = blockchainTime.copy(timestamp = now)
BalanceRow("Unlocked", balance.unlockedAmountAt(currentTime))
balance.lockedAmountsAt(currentTime).forEach { (timeSpan, amount) ->
LockedBalanceRow("Locked", amount, timeSpan.blocks, timeSpan.timeRemaining)
}
}
@ -117,12 +121,12 @@ fun WalletBalanceDetailsPreview() {
balance = Balance(
pendingAmount = 5.xmr,
timeLockedAmounts = listOf(
TimeLocked(10.xmr, BlockchainTime.Genesis),
TimeLocked(BigDecimal("0.000000000001").xmr, BlockchainTime.Block(10)),
TimeLocked(30.xmr, BlockchainTime.Block(500)),
TimeLocked(10.xmr, null),
TimeLocked(BigDecimal("0.000000000001").xmr, null),
TimeLocked(30.xmr, null)
),
),
blockchainTime = BlockchainTime.Genesis,
blockchainTime = MoneroNetwork.Mainnet.genesisTime,
)
}
}

View File

@ -66,7 +66,7 @@ private fun walletUiState(
val transactions =
ledger.transactions
.map { WalletTransaction(config.id, it.value) }
.sortedByDescending { it.transaction.timestamp ?: Instant.MAX }
.sortedByDescending { it.transaction.blockTimestamp ?: Instant.MAX }
val network = ledger.primaryAddress.network
WalletUiState.Loaded(config, network, blockchainTime, balance, transactions)
}

View File

@ -5,21 +5,29 @@ import im.molly.monero.BlockchainTime
import im.molly.monero.HashDigest
import im.molly.monero.Ledger
import im.molly.monero.MoneroAmount
import im.molly.monero.MoneroNetwork
import im.molly.monero.PaymentDetail
import im.molly.monero.PublicAddress
import im.molly.monero.Transaction
import im.molly.monero.TxState
import im.molly.monero.UnlockTime
import im.molly.monero.xmr
import java.time.Instant
object PreviewParameterData {
val network = MoneroNetwork.Mainnet
val blockHeader = BlockHeader(height = 2999840, epochSecond = 1697792826)
val recipients =
listOf(PublicAddress.parse("888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H"))
val transactions = listOf(
Transaction(
hash = HashDigest("e7a60483591378d536792d070f2bf6ccb7d0666df03b57f485ddaf66899a294b"),
state = TxState.OnChain(BlockHeader(height = 2999840, epochSecond = 1697792826)),
timeLock = BlockchainTime.Block(2999850),
state = TxState.OnChain(blockHeader),
network = network,
timeLock = UnlockTime.Block(BlockchainTime(2999850, Instant.ofEpochSecond(1697792826), network)),
sent = emptySet(),
received = emptySet(),
payments = listOf(PaymentDetail((0.10).xmr, recipients.first())),
@ -30,7 +38,7 @@ object PreviewParameterData {
val ledger = Ledger(
primaryAddress = PublicAddress.parse("4AYjQM9HoAFNUeC3cvSfgeAN89oMMpMqiByvunzSzhn97cj726rJj3x8hCbH58UnMqQJShczCxbpWRiCJQ3HCUDHLiKuo4T"),
checkedAt = BlockchainTime.Block(2999840),
checkedAt = BlockchainTime(blockHeader = blockHeader, network = network),
enotes = emptySet(),
transactions = transactions.associateBy { it.txId },
)

View File

@ -37,7 +37,7 @@ Wallet::Wallet(
m_callback(env, wallet_native),
m_account_ready(false),
m_last_block_height(1),
m_last_block_timestamp(1397818193),
m_last_block_timestamp(0),
m_restore_height(0),
m_refresh_running(false),
m_refresh_canceled(false) {
@ -67,12 +67,15 @@ void Wallet::restoreAccount(const std::vector<char>& secret_scalar, uint64_t res
generateAccountKeys(account, secret_scalar);
if (restore_point < CRYPTONOTE_MAX_BLOCK_NUMBER) {
m_restore_height = restore_point;
m_last_block_timestamp = 0;
} else {
if (restore_point > account.get_createtime()) {
account.set_createtime(restore_point);
}
m_restore_height = estimateRestoreHeight(account.get_createtime());
m_last_block_timestamp = account.get_createtime();
}
m_last_block_height = (m_restore_height == 0) ? 1 : m_restore_height;
m_wallet.rescan_blockchain(true, false, false);
m_account_ready = true;
}

View File

@ -9,19 +9,13 @@ data class Balance(
val confirmedAmount: MoneroAmount = timeLockedAmounts.sumOf { it.value }
val totalAmount: MoneroAmount = confirmedAmount + pendingAmount
fun unlockedAmountAt(
blockHeight: Int,
currentInstant: Instant = Instant.now(),
): MoneroAmount {
val targetTime = BlockchainTime(blockHeight, currentInstant)
return timeLockedAmounts.filter { it.isUnlocked(targetTime) }.sumOf { it.value }
fun unlockedAmountAt(targetTime: BlockchainTime): MoneroAmount {
return timeLockedAmounts
.filter { it.isUnlocked(targetTime) }
.sumOf { it.value }
}
fun lockedAmountsAt(
blockHeight: Int,
currentInstant: Instant = Instant.now(),
): Map<BlockchainTimeSpan, MoneroAmount> {
val targetTime = BlockchainTime(blockHeight, currentInstant)
fun lockedAmountsAt(targetTime: BlockchainTime): Map<BlockchainTimeSpan, MoneroAmount> {
return timeLockedAmounts
.filter { it.isLocked(targetTime) }
.groupBy({ it.timeUntilUnlock(targetTime) }, { it.value })

View File

@ -1,6 +1,7 @@
package im.molly.monero
import im.molly.monero.internal.constants.CRYPTONOTE_MAX_BLOCK_NUMBER
import java.time.Instant
data class Block(
// TODO: val hash: HashDigest,
@ -15,6 +16,9 @@ data class BlockHeader(
val height: Int,
val epochSecond: Long,
) {
val timestamp: Instant
get() = Instant.ofEpochSecond(epochSecond)
companion object {
const val MAX_HEIGHT = CRYPTONOTE_MAX_BLOCK_NUMBER - 1
}
@ -23,3 +27,5 @@ data class BlockHeader(
fun isBlockHeightInRange(height: Long) = !(height < 0 || height > BlockHeader.MAX_HEIGHT)
fun isBlockHeightInRange(height: Int) = isBlockHeightInRange(height.toLong())
fun isBlockEpochInRange(epochSecond: Long) = epochSecond > BlockHeader.MAX_HEIGHT

View File

@ -1,107 +1,100 @@
package im.molly.monero
import android.os.Parcelable
import im.molly.monero.internal.constants.DIFFICULTY_TARGET_V2
import im.molly.monero.internal.constants.CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE
import kotlinx.parcelize.Parcelize
import java.time.Duration
import java.time.Instant
import java.time.LocalDate
/**
* A point in the blockchain timeline, which could be either a block height or a timestamp.
*/
@Parcelize
open class BlockchainTime(
data class BlockchainTime(
val height: Int,
val timestamp: Instant,
) : Comparable<BlockchainTime>, Parcelable {
val network: MoneroNetwork,
) : RestorePoint, Parcelable {
constructor(blockHeader: BlockHeader, network: MoneroNetwork) : this(
blockHeader.height, blockHeader.timestamp, network
)
init {
require(isBlockHeightInRange(height)) {
"Block height $height out of range"
}
require(isBlockEpochInRange(timestamp.epochSecond)) {
"Block timestamp $timestamp out of range"
}
}
open fun toLong(): Long = height.toLong()
fun estimateHeight(targetTimestamp: Instant): Int {
val timeDiff = Duration.between(timestamp, targetTimestamp)
val estHeight = timeDiff.seconds / network.avgBlockTime(height).seconds + height
val validHeight = estHeight.coerceIn(0, BlockHeader.MAX_HEIGHT.toLong())
return validHeight.toInt()
}
override fun compareTo(other: BlockchainTime): Int =
this.height.compareTo(other.height)
fun estimateTimestamp(targetHeight: Int): Instant {
require(targetHeight >= 0) {
"Block height $targetHeight must not be negative"
}
val heightDiff = targetHeight - height
val estTimeDiff = network.avgBlockTime(height).multipliedBy(heightDiff.toLong())
return timestamp.plus(estTimeDiff)
}
fun effectiveUnlockTime(targetHeight: Int, txTimeLock: UnlockTime?): UnlockTime {
val spendableHeight = targetHeight + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE - 1
val spendableTime = BlockchainTime(
height = spendableHeight,
timestamp = estimateTimestamp(spendableHeight),
network = network,
)
return txTimeLock?.takeIf { it > spendableTime } ?: spendableTime.toUnlockTime()
}
private fun toUnlockTime(): UnlockTime {
return UnlockTime.Block(blockchainTime = this)
}
fun until(endTime: BlockchainTime): BlockchainTimeSpan = BlockchainTimeSpan(
duration = Duration.between(timestamp, endTime.timestamp),
blocks = endTime.height - height,
)
operator fun minus(other: BlockchainTime): BlockchainTimeSpan = other.until(this)
override fun toString(): String = "Block $height | Time $timestamp"
data object Genesis : BlockchainTime(0, Instant.ofEpochSecond(1397818193))
class Block(height: Int, referencePoint: BlockchainTime = Genesis) :
BlockchainTime(height, estimateTimestamp(height, referencePoint)) {
override fun toString(): String = "Block $height | Time $timestamp (Estimated)"
}
class Timestamp(timestamp: Instant, referencePoint: BlockchainTime = Genesis) :
BlockchainTime(estimateBlockHeight(timestamp, referencePoint), timestamp) {
constructor(date: LocalDate) : this(Instant.ofEpochSecond(date.toEpochDay()))
override fun toLong() = timestamp.epochSecond.coerceAtLeast(BlockHeader.MAX_HEIGHT + 1L)
override fun compareTo(other: BlockchainTime): Int =
this.timestamp.compareTo(other.timestamp)
override fun toString(): String = "Block $height (Estimated) | Time $timestamp"
}
companion object {
val AVERAGE_BLOCK_TIME: Duration = Duration.ofSeconds(DIFFICULTY_TARGET_V2)
fun estimateTimestamp(targetHeight: Int, referencePoint: BlockchainTime): Instant {
require(targetHeight >= 0) {
"Block height $targetHeight must not be negative"
}
return when (targetHeight) {
0 -> Genesis.timestamp
else -> {
val heightDiff = targetHeight - referencePoint.height
val estTimeDiff = AVERAGE_BLOCK_TIME.multipliedBy(heightDiff.toLong())
referencePoint.timestamp.plus(estTimeDiff)
}
}
}
fun estimateBlockHeight(targetTime: Instant, referencePoint: BlockchainTime): Int {
val timeDiff = Duration.between(referencePoint.timestamp, targetTime)
val estHeight = timeDiff.seconds / AVERAGE_BLOCK_TIME.seconds + referencePoint.height
val clampedHeight = estHeight.coerceIn(0, BlockHeader.MAX_HEIGHT.toLong())
return clampedHeight.toInt()
}
}
fun resolveUnlockTime(heightOrTimestamp: Long): BlockchainTime {
return if (isBlockHeightInRange(heightOrTimestamp)) {
val height = heightOrTimestamp.toInt()
Block(height, referencePoint = this)
} else {
val clampedTs =
if (heightOrTimestamp < 0 || heightOrTimestamp > Instant.MAX.epochSecond) Instant.MAX
else Instant.ofEpochSecond(heightOrTimestamp)
Timestamp(clampedTs, referencePoint = this)
}
}
fun until(endTime: BlockchainTime): BlockchainTimeSpan {
return BlockchainTimeSpan(
duration = Duration.between(timestamp, endTime.timestamp),
blocks = endTime.height - height,
)
}
operator fun minus(other: BlockchainTime): BlockchainTimeSpan = other.until(this)
override fun toLong() = height.toLong()
}
fun max(a: BlockchainTime, b: BlockchainTime) = if (a >= b) a else b
val MoneroNetwork.genesisTime: BlockchainTime
get() = BlockchainTime(1, Instant.ofEpochSecond(epoch), this)
fun min(a: BlockchainTime, b: BlockchainTime) = if (a <= b) a else b
val MoneroNetwork.v2forkTime: BlockchainTime
get() = BlockchainTime(epochV2.first, Instant.ofEpochSecond(epochV2.second), this)
fun MoneroNetwork.estimateTimestamp(targetHeight: Int): Instant {
return if (targetHeight < v2forkTime.height) {
genesisTime.estimateTimestamp(targetHeight)
} else {
v2forkTime.estimateTimestamp(targetHeight)
}
}
fun MoneroNetwork.estimateHeight(targetTimestamp: Instant): Int {
return if (targetTimestamp < v2forkTime.timestamp) {
genesisTime.estimateHeight(targetTimestamp)
} else {
v2forkTime.estimateHeight(targetTimestamp)
}
}
data class BlockchainTimeSpan(val duration: Duration, val blocks: Int) {
val timeRemaining: Duration
@ -112,18 +105,20 @@ data class BlockchainTimeSpan(val duration: Duration, val blocks: Int) {
}
}
class TimeLocked<T>(val value: T, val unlockTime: BlockchainTime?) {
fun isLocked(currentTime: BlockchainTime): Boolean {
return currentTime < (unlockTime ?: return false)
sealed interface UnlockTime : Comparable<BlockchainTime>, Parcelable {
val blockchainTime: BlockchainTime
@Parcelize
data class Block(override val blockchainTime: BlockchainTime) : UnlockTime {
override operator fun compareTo(other: BlockchainTime): Int {
return blockchainTime.height.compareTo(other.height)
}
}
fun isUnlocked(currentTime: BlockchainTime) = !isLocked(currentTime)
fun timeUntilUnlock(currentTime: BlockchainTime): BlockchainTimeSpan {
return if (isLocked(currentTime)) {
unlockTime!!.minus(currentTime)
} else {
BlockchainTimeSpan.ZERO
@Parcelize
data class Timestamp(override val blockchainTime: BlockchainTime) : UnlockTime {
override operator fun compareTo(other: BlockchainTime): Int {
return blockchainTime.timestamp.compareTo(other.timestamp)
}
}
}

View File

@ -1,18 +1,30 @@
package im.molly.monero
import im.molly.monero.internal.constants.DIFFICULTY_TARGET_V1
import im.molly.monero.internal.constants.DIFFICULTY_TARGET_V2
import java.time.Duration
/**
* Monero network environments.
*
* Defined in cryptonote_config.h
*/
enum class MoneroNetwork(val id: Int) {
Mainnet(0),
Testnet(1),
Stagenet(2);
enum class MoneroNetwork(val id: Int, val epoch: Long, val epochV2: Pair<Int, Long>) {
Mainnet(0, 1397818193, (1009827 to 1458748658)),
Testnet(1, 1410295020, (624634 to 1448285909)),
Stagenet(2, 1518932025, (32000 to 1520937818));
companion object {
fun fromId(value: Int) = values().first { it.id == value }
fun of(publicAddress: String) = PublicAddress.parse(publicAddress).network
}
fun avgBlockTime(height: Int): Duration {
return if (height < epochV2.second) {
Duration.ofSeconds(DIFFICULTY_TARGET_V1)
} else {
Duration.ofSeconds(DIFFICULTY_TARGET_V2)
}
}
}

View File

@ -0,0 +1,38 @@
package im.molly.monero
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
interface RestorePoint {
fun toLong(): Long
companion object {
val Genesis: RestorePoint = RestorePointValue(heightOrTimestamp = 0)
fun blockHeight(height: Int): RestorePoint {
require(isBlockHeightInRange(height))
return RestorePointValue(heightOrTimestamp = height.toLong())
}
fun creationTime(localDate: LocalDate): RestorePoint = creationTime(
epochSecond = localDate.atStartOfDay(ZoneId.systemDefault()).toEpochSecond()
)
fun creationTime(instant: Instant): RestorePoint = creationTime(
epochSecond = instant.epochSecond
)
fun creationTime(epochSecond: Long): RestorePoint {
require(epochSecond >= 1402185600) {
"Monero accounts cannot be restored before June 8, 2014"
}
return RestorePointValue(heightOrTimestamp = epochSecond)
}
}
}
@JvmInline
value class RestorePointValue(val heightOrTimestamp: Long) : RestorePoint {
override fun toLong() = heightOrTimestamp
}

View File

@ -0,0 +1,16 @@
package im.molly.monero
class TimeLocked<T>(val value: T, val unlockTime: UnlockTime?) {
fun isLocked(currentTime: BlockchainTime): Boolean {
return unlockTime != null && unlockTime > currentTime
}
fun isUnlocked(currentTime: BlockchainTime) = !isLocked(currentTime)
fun timeUntilUnlock(currentTime: BlockchainTime): BlockchainTimeSpan {
if (unlockTime == null || isUnlocked(currentTime)) {
return BlockchainTimeSpan.ZERO
}
return unlockTime.blockchainTime - currentTime
}
}

View File

@ -1,12 +1,11 @@
package im.molly.monero
import java.time.Instant
data class Transaction(
val hash: HashDigest,
// TODO: val version: ProtocolInfo,
val state: TxState,
val timeLock: BlockchainTime?,
val network: MoneroNetwork,
val timeLock: UnlockTime?,
val sent: Set<Enote>,
val received: Set<Enote>,
val payments: List<PaymentDetail>,
@ -15,14 +14,13 @@ data class Transaction(
) {
val amount: MoneroAmount = received.sumOf { it.amount } - sent.sumOf { it.amount }
val txId: String
get() = hash.toString()
val txId: String = hash.toString()
val blockHeight: Int?
get() = (state as? TxState.OnChain)?.blockHeader?.height
private val blockHeader = (state as? TxState.OnChain)?.blockHeader
val timestamp: Instant?
get() = (state as? TxState.OnChain)?.let { Instant.ofEpochSecond(it.blockHeader.epochSecond) }
val blockHeight = blockHeader?.height
val blockTimestamp = blockHeader?.timestamp
}
sealed interface TxState {

View File

@ -3,7 +3,6 @@ package im.molly.monero
import android.os.ParcelFileDescriptor
import androidx.annotation.GuardedBy
import im.molly.monero.internal.TxInfo
import im.molly.monero.internal.consolidateTransactions
import kotlinx.coroutines.*
import java.io.Closeable
import java.time.Instant
@ -13,7 +12,7 @@ import kotlin.concurrent.withLock
import kotlin.coroutines.CoroutineContext
class WalletNative private constructor(
networkId: Int,
private val network: MoneroNetwork,
private val storageAdapter: IStorageAdapter,
private val remoteNodeClient: IRemoteNodeClient?,
private val scope: CoroutineScope,
@ -31,7 +30,7 @@ class WalletNative private constructor(
coroutineContext: CoroutineContext = Dispatchers.Default + SupervisorJob(),
ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) = WalletNative(
networkId = networkId,
network = MoneroNetwork.fromId(networkId),
storageAdapter = storageAdapter,
remoteNodeClient = remoteNodeClient,
scope = CoroutineScope(coroutineContext),
@ -59,7 +58,7 @@ class WalletNative private constructor(
MoneroJni.loadLibrary(logger = logger)
}
private val handle: Long = nativeCreate(networkId)
private val handle: Long = nativeCreate(network.id)
private suspend fun tryWriteState(): Boolean {
return withContext(ioDispatcher) {
@ -104,16 +103,17 @@ class WalletNative private constructor(
override fun getAccountPrimaryAddress() = nativeGetAccountPrimaryAddress(handle)
private fun createBlockchainTime(height: Int, epochSeconds: Long): BlockchainTime {
return if (epochSeconds == 0L) {
BlockchainTime.Block(height)
} else {
BlockchainTime(height, Instant.ofEpochSecond(epochSeconds))
private fun MoneroNetwork.blockchainTime(height: Int, epochSecond: Long): BlockchainTime {
// Block timestamp could be zero during a fast refresh.
val timestamp = when (epochSecond) {
0L -> estimateTimestamp(height)
else -> Instant.ofEpochSecond(epochSecond)
}
return BlockchainTime(height = height, timestamp = timestamp, network = this)
}
val currentBlockchainTime: BlockchainTime
get() = createBlockchainTime(
get() = network.blockchainTime(
nativeGetCurrentBlockchainHeight(handle),
nativeGetCurrentBlockchainTimestamp(handle),
)
@ -191,7 +191,7 @@ class WalletNative private constructor(
balanceListenersLock.withLock {
if (balanceListeners.isNotEmpty()) {
val call = fun(listener: IBalanceListener) {
val blockchainTime = createBlockchainTime(height, timestamp)
val blockchainTime = network.blockchainTime(height, timestamp)
if (balanceChanged) {
listener.onBalanceChanged(txHistorySnapshot(), blockchainTime)
} else {

View File

@ -69,9 +69,12 @@ class WalletProvider private constructor(
dataStore: WalletDataStore? = null,
client: RemoteNodeClient? = null,
secretSpendKey: SecretKey,
restorePoint: BlockchainTime,
restorePoint: RestorePoint,
): MoneroWallet {
require(client == null || client.network == network)
if (restorePoint is BlockchainTime) {
require(restorePoint.network == network)
}
val storageAdapter = StorageAdapter(dataStore)
val wallet = suspendCancellableCoroutine { continuation ->
service.restoreWallet(

View File

@ -14,9 +14,10 @@ import im.molly.monero.PublicKey
import im.molly.monero.TimeLocked
import im.molly.monero.Transaction
import im.molly.monero.TxState
import im.molly.monero.internal.constants.CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE
import im.molly.monero.max
import im.molly.monero.UnlockTime
import im.molly.monero.isBlockHeightInRange
import kotlinx.parcelize.Parcelize
import java.time.Instant
/**
* TxInfo represents transaction information in a compact and easily serializable format.
@ -81,15 +82,11 @@ internal fun List<TxInfo>.consolidateTransactions(
// If transaction isn't failed, calculate unlock time and save enotes
if (tx.state !is TxState.Failed) {
val maxUnlockTime = tx.blockHeight?.let { height ->
val defaultUnlockTime = BlockchainTime.Block(
height = height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE - 1,
referencePoint = blockchainContext,
)
max(defaultUnlockTime, tx.timeLock ?: BlockchainTime.Genesis)
val unlockTime = tx.blockHeight?.let { height ->
blockchainContext.effectiveUnlockTime(height, tx.timeLock)
}
val lockedEnotesToAdd = tx.received.map { enote ->
TimeLocked(enote, maxUnlockTime)
TimeLocked(enote, unlockTime)
}
validEnotes.addAll(lockedEnotesToAdd)
}
@ -114,7 +111,8 @@ private fun List<TxInfo>.createTransaction(
val change = maxOf { it.change }
val timeLock = maxOf { it.unlockTime }.let { unlockTime ->
if (unlockTime == 0L) null else blockchainContext.resolveUnlockTime(unlockTime)
if (unlockTime == 0L) null
else blockchainContext.resolveUnlockTime(unlockTime)
}
val receivedEnotes = enoteByTxId.getOrDefault(txHash, emptyList()).toSet()
@ -129,6 +127,7 @@ private fun List<TxInfo>.createTransaction(
return Transaction(
hash = HashDigest(txHash),
state = determineTxState(),
network = blockchainContext.network,
timeLock = timeLock,
sent = sentEnotes,
received = receivedEnotes,
@ -177,3 +176,19 @@ private fun TxInfo.toPaymentDetail(): PaymentDetail? {
recipient = recipient,
)
}
private fun BlockchainTime.resolveUnlockTime(heightOrTimestamp: Long): UnlockTime {
return if (isBlockHeightInRange(heightOrTimestamp)) {
val height = heightOrTimestamp.toInt()
UnlockTime.Block(
BlockchainTime(height, estimateTimestamp(height), network)
)
} else {
val clampedTs = if (heightOrTimestamp in network.epoch..Instant.MAX.epochSecond) {
Instant.ofEpochSecond(heightOrTimestamp)
} else Instant.MAX
UnlockTime.Timestamp(
BlockchainTime(estimateHeight(clampedTs), clampedTs, network)
)
}
}

View File

@ -3,4 +3,5 @@ package im.molly.monero.internal.constants
internal const val CRYPTONOTE_DISPLAY_DECIMAL_POINT: Int = 12
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