mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2025-01-11 23:49:31 -05:00
lib: fix multiple bugs in balance recovery
This commit is contained in:
parent
7dba0c1b18
commit
471f22777c
@ -139,21 +139,38 @@ cryptonote::account_base& Wallet::require_account() {
|
||||
|
||||
const payment_details* find_matching_payment(
|
||||
const std::list<std::pair<crypto::hash, payment_details>> pds,
|
||||
uint64_t amount,
|
||||
const crypto::hash& txid,
|
||||
const cryptonote::subaddress_index& subaddr_index) {
|
||||
if (txid == crypto::null_hash) {
|
||||
const transfer_details& td) {
|
||||
if (td.m_txid == crypto::null_hash) {
|
||||
return nullptr;
|
||||
}
|
||||
for (const auto& p: pds) {
|
||||
const auto& pd = p.second;
|
||||
if (pd.m_amount == amount && pd.m_tx_hash == txid && pd.m_subaddr_index == subaddr_index) {
|
||||
if (td.m_amount == pd.m_amount
|
||||
&& td.m_subaddr_index == pd.m_subaddr_index
|
||||
&& td.m_txid == pd.m_tx_hash) {
|
||||
return &pd;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
const confirmed_transfer_details* find_matching_transfer_for_change(
|
||||
const std::list<std::pair<crypto::hash, confirmed_transfer_details>> txs,
|
||||
const transfer_details& td) {
|
||||
if (td.m_txid == crypto::null_hash || td.m_subaddr_index.minor != 0) {
|
||||
return nullptr;
|
||||
}
|
||||
for (const auto& p: txs) {
|
||||
const auto& tx = p.second;
|
||||
if (td.m_amount == tx.m_change
|
||||
&& td.m_subaddr_index.major == tx.m_subaddr_account
|
||||
&& td.m_txid == p.first) {
|
||||
return &tx;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
// Only call this function from the callback thread or during initialization,
|
||||
// as there is no locking mechanism to safeguard reading transaction history
|
||||
// from wallet2.
|
||||
@ -174,7 +191,7 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
|
||||
m_wallet.get_payments_out(txs, min_height);
|
||||
m_wallet.get_unconfirmed_payments_out(utxs);
|
||||
|
||||
// Iterate through the known owned outputs (incoming transactions).
|
||||
// Iterate through the known owned outputs.
|
||||
for (const auto& td: tds) {
|
||||
snapshot.emplace_back(td.m_txid, TxInfo::INCOMING);
|
||||
TxInfo& recv = snapshot.back();
|
||||
@ -188,14 +205,18 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
|
||||
recv.m_amount = td.m_amount;
|
||||
recv.m_unlock_time = td.m_tx.unlock_time;
|
||||
|
||||
// Check if the payment exists and update metadata if found.
|
||||
const auto* pd = find_matching_payment(pds, td.m_amount, td.m_txid, td.m_subaddr_index);
|
||||
if (pd) {
|
||||
// Check if the payment or change exists and update metadata if found.
|
||||
if (const auto* pd = find_matching_payment(pds, td)) {
|
||||
recv.m_height = pd->m_block_height;
|
||||
recv.m_timestamp = pd->m_timestamp;
|
||||
recv.m_fee = pd->m_fee;
|
||||
recv.m_coinbase = pd->m_coinbase;
|
||||
recv.m_state = TxInfo::ON_CHAIN;
|
||||
} else if (const auto tx = find_matching_transfer_for_change(txs, td)) {
|
||||
recv.m_height = tx->m_block_height;
|
||||
recv.m_timestamp = tx->m_timestamp;
|
||||
recv.m_fee = tx->m_amount_in - tx->m_amount_out;
|
||||
recv.m_state = TxInfo::ON_CHAIN;
|
||||
} else {
|
||||
recv.m_state = TxInfo::OFF_CHAIN;
|
||||
}
|
||||
@ -219,18 +240,16 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
|
||||
spent.m_state = TxInfo::ON_CHAIN;
|
||||
}
|
||||
|
||||
for (const auto& in: tx.m_tx.vin) {
|
||||
if (in.type() != typeid(cryptonote::txin_to_key)) continue;
|
||||
const auto& txin = boost::get<cryptonote::txin_to_key>(in);
|
||||
for (const auto& ring : tx.m_rings) {
|
||||
snapshot.emplace_back(pair.first, TxInfo::OUTGOING);
|
||||
TxInfo& spent = snapshot.back();
|
||||
spent.m_key_image = txin.k_image;
|
||||
spent.m_key_image = ring.first;
|
||||
spent.m_key_image_known = true;
|
||||
spent.m_amount = txin.amount;
|
||||
spent.m_height = tx.m_block_height;
|
||||
spent.m_unlock_time = tx.m_unlock_time;
|
||||
spent.m_timestamp = tx.m_timestamp;
|
||||
spent.m_fee = fee;
|
||||
spent.m_change = tx.m_change;
|
||||
spent.m_state = TxInfo::ON_CHAIN;
|
||||
}
|
||||
}
|
||||
@ -269,7 +288,7 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
|
||||
}
|
||||
}
|
||||
|
||||
// Change is ours too.
|
||||
// Change is ours too, but the output is not yet in transfer_details
|
||||
if (utx.m_change > 0) {
|
||||
snapshot.emplace_back(pair.first, TxInfo::INCOMING);
|
||||
TxInfo& change = snapshot.back();
|
||||
@ -283,16 +302,14 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
|
||||
change.m_state = state;
|
||||
}
|
||||
|
||||
for (const auto& in: utx.m_tx.vin) {
|
||||
if (in.type() != typeid(cryptonote::txin_to_key)) continue;
|
||||
const auto& txin = boost::get<cryptonote::txin_to_key>(in);
|
||||
for (const auto& ring : utx.m_rings) {
|
||||
snapshot.emplace_back(pair.first, TxInfo::OUTGOING);
|
||||
TxInfo& spent = snapshot.back();
|
||||
spent.m_key_image = txin.k_image;
|
||||
spent.m_key_image = ring.first;
|
||||
spent.m_key_image_known = true;
|
||||
spent.m_amount = txin.amount;
|
||||
spent.m_timestamp = utx.m_timestamp;
|
||||
spent.m_fee = fee;
|
||||
spent.m_change = utx.m_change;
|
||||
spent.m_state = state;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package im.molly.monero
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
data class Balance(
|
||||
val pendingAmount: MoneroAmount,
|
||||
val timeLockedAmounts: List<TimeLocked<MoneroAmount>>,
|
||||
@ -7,18 +9,26 @@ data class Balance(
|
||||
val confirmedAmount: MoneroAmount = timeLockedAmounts.sumOf { it.value }
|
||||
val totalAmount: MoneroAmount = confirmedAmount + pendingAmount
|
||||
|
||||
fun unlockedAmountAt(targetTime: BlockchainTime): MoneroAmount =
|
||||
timeLockedAmounts
|
||||
.filter { it.isUnlocked(targetTime) }
|
||||
.sumOf { it.value }
|
||||
fun unlockedAmountAt(
|
||||
blockHeight: Int,
|
||||
currentInstant: Instant = Instant.now(),
|
||||
): MoneroAmount {
|
||||
val targetTime = BlockchainTime(blockHeight, currentInstant)
|
||||
return timeLockedAmounts.filter { it.isUnlocked(targetTime) }.sumOf { it.value }
|
||||
}
|
||||
|
||||
fun lockedAmountsAt(targetTime: BlockchainTime): Map<BlockchainTimeSpan, MoneroAmount> =
|
||||
timeLockedAmounts
|
||||
fun lockedAmountsAt(
|
||||
blockHeight: Int,
|
||||
currentInstant: Instant = Instant.now(),
|
||||
): Map<BlockchainTimeSpan, MoneroAmount> {
|
||||
val targetTime = BlockchainTime(blockHeight, currentInstant)
|
||||
return timeLockedAmounts
|
||||
.filter { it.isLocked(targetTime) }
|
||||
.groupBy({ it.timeUntilUnlock(targetTime) }, { it.value })
|
||||
.mapValues { (_, amounts) ->
|
||||
amounts.sum()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Iterable<TimeLocked<Enote>>.calculateBalance(): Balance {
|
||||
@ -26,7 +36,7 @@ fun Iterable<TimeLocked<Enote>>.calculateBalance(): Balance {
|
||||
|
||||
val lockedAmounts = mutableListOf<TimeLocked<MoneroAmount>>()
|
||||
|
||||
for (timeLocked in filter { it.value.spent }) {
|
||||
for (timeLocked in filter { !it.value.spent }) {
|
||||
if (timeLocked.value.age == 0) {
|
||||
pendingAmount += timeLocked.value.amount
|
||||
} else {
|
||||
|
@ -96,19 +96,24 @@ fun max(a: BlockchainTime, b: BlockchainTime) = if (a >= b) a else b
|
||||
fun min(a: BlockchainTime, b: BlockchainTime) = if (a <= b) a else b
|
||||
|
||||
data class BlockchainTimeSpan(val duration: Duration, val blocks: Int) {
|
||||
val timeRemaining: Duration
|
||||
get() = maxOf(Duration.ZERO, duration)
|
||||
|
||||
companion object {
|
||||
val ZERO = BlockchainTimeSpan(duration = Duration.ZERO, blocks = 0)
|
||||
}
|
||||
}
|
||||
|
||||
class TimeLocked<T>(val value: T, val unlockTime: BlockchainTime) {
|
||||
fun isLocked(currentTime: BlockchainTime): Boolean = currentTime < unlockTime
|
||||
class TimeLocked<T>(val value: T, val unlockTime: BlockchainTime?) {
|
||||
fun isLocked(currentTime: BlockchainTime): Boolean {
|
||||
return currentTime < (unlockTime ?: return false)
|
||||
}
|
||||
|
||||
fun isUnlocked(currentTime: BlockchainTime) = !isLocked(currentTime)
|
||||
|
||||
fun timeUntilUnlock(currentTime: BlockchainTime): BlockchainTimeSpan {
|
||||
return if (isLocked(currentTime)) {
|
||||
unlockTime.minus(currentTime)
|
||||
unlockTime!!.minus(currentTime)
|
||||
} else {
|
||||
BlockchainTimeSpan.ZERO
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ package im.molly.monero
|
||||
@JvmInline
|
||||
value class HashDigest(private val hashDigest: String) {
|
||||
init {
|
||||
require(hashDigest.length == 32) { "Hash length must be 32 bytes" }
|
||||
require(hashDigest.length == 64) { "Hash length must be 64 hex chars" }
|
||||
}
|
||||
|
||||
override fun toString(): String = hashDigest
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ interface PublicAddress {
|
||||
return StandardAddress(network)
|
||||
}
|
||||
SubAddress.prefixes[prefix]?.let { network ->
|
||||
TODO()
|
||||
return SubAddress(network)
|
||||
}
|
||||
IntegratedAddress.prefixes[prefix]?.let { network ->
|
||||
TODO()
|
||||
@ -60,7 +60,7 @@ data class SubAddress(
|
||||
companion object {
|
||||
val prefixes = mapOf(
|
||||
42L to MoneroNetwork.Mainnet,
|
||||
64L to MoneroNetwork.Testnet,
|
||||
63L to MoneroNetwork.Testnet,
|
||||
36L to MoneroNetwork.Stagenet,
|
||||
)
|
||||
}
|
||||
|
@ -81,13 +81,16 @@ internal fun List<TxInfo>.consolidateTransactions(
|
||||
|
||||
// If transaction isn't failed, calculate unlock time and save enotes
|
||||
if (tx.state !is TxState.Failed) {
|
||||
val defaultUnlockTime = BlockchainTime.Block(
|
||||
height = tx.blockHeight!! + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE,
|
||||
referencePoint = blockchainContext,
|
||||
)
|
||||
val maxUnlockTime = max(defaultUnlockTime, tx.timeLock ?: BlockchainTime.Genesis)
|
||||
|
||||
val lockedEnotesToAdd = tx.received.map { enote -> TimeLocked(enote, maxUnlockTime) }
|
||||
val maxUnlockTime = tx.blockHeight?.let { height ->
|
||||
val defaultUnlockTime = BlockchainTime.Block(
|
||||
height = height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE,
|
||||
referencePoint = blockchainContext,
|
||||
)
|
||||
max(defaultUnlockTime, tx.timeLock ?: BlockchainTime.Genesis)
|
||||
}
|
||||
val lockedEnotesToAdd = tx.received.map { enote ->
|
||||
TimeLocked(enote, maxUnlockTime)
|
||||
}
|
||||
validEnotes.addAll(lockedEnotesToAdd)
|
||||
}
|
||||
|
||||
@ -114,14 +117,14 @@ private fun List<TxInfo>.createTransaction(
|
||||
if (unlockTime == 0L) null else blockchainContext.resolveUnlockTime(unlockTime)
|
||||
}
|
||||
|
||||
val receivedEnotes = enoteByTxId.getValue(txHash).toSet()
|
||||
val receivedEnotes = enoteByTxId.getOrDefault(txHash, emptyList()).toSet()
|
||||
|
||||
val outTxs = filter { !it.incoming }
|
||||
|
||||
val spentKeyImages = outTxs.mapNotNull { it.keyImage }
|
||||
val sentEnotes = enoteByKeyImage.filterKeys { ki -> ki in spentKeyImages }.values.toSet()
|
||||
|
||||
val payments = outTxs.map { it.toPaymentDetail() }
|
||||
val payments = outTxs.mapNotNull { it.toPaymentDetail() }
|
||||
|
||||
return Transaction(
|
||||
hash = HashDigest(txHash),
|
||||
@ -166,7 +169,10 @@ private fun TxInfo.toEnote(blockchainHeight: Int): Enote {
|
||||
)
|
||||
}
|
||||
|
||||
private fun TxInfo.toPaymentDetail() = PaymentDetail(
|
||||
amount = MoneroAmount(atomicUnits = amount),
|
||||
recipient = PublicAddress.parse(recipient!!),
|
||||
)
|
||||
private fun TxInfo.toPaymentDetail(): PaymentDetail? {
|
||||
val recipient = PublicAddress.parse(recipient ?: return null)
|
||||
return PaymentDetail(
|
||||
amount = MoneroAmount(atomicUnits = amount),
|
||||
recipient = recipient,
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user