mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2025-01-12 07:59:30 -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 payment_details* find_matching_payment(
|
||||||
const std::list<std::pair<crypto::hash, payment_details>> pds,
|
const std::list<std::pair<crypto::hash, payment_details>> pds,
|
||||||
uint64_t amount,
|
const transfer_details& td) {
|
||||||
const crypto::hash& txid,
|
if (td.m_txid == crypto::null_hash) {
|
||||||
const cryptonote::subaddress_index& subaddr_index) {
|
|
||||||
if (txid == crypto::null_hash) {
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
for (const auto& p: pds) {
|
for (const auto& p: pds) {
|
||||||
const auto& pd = p.second;
|
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 &pd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nullptr;
|
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,
|
// 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
|
||||||
// from wallet2.
|
// from wallet2.
|
||||||
@ -174,7 +191,7 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
|
|||||||
m_wallet.get_payments_out(txs, min_height);
|
m_wallet.get_payments_out(txs, min_height);
|
||||||
m_wallet.get_unconfirmed_payments_out(utxs);
|
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) {
|
for (const auto& td: tds) {
|
||||||
snapshot.emplace_back(td.m_txid, TxInfo::INCOMING);
|
snapshot.emplace_back(td.m_txid, TxInfo::INCOMING);
|
||||||
TxInfo& recv = snapshot.back();
|
TxInfo& recv = snapshot.back();
|
||||||
@ -188,14 +205,18 @@ 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 exists and update metadata if found.
|
// Check if the payment or change exists and update metadata if found.
|
||||||
const auto* pd = find_matching_payment(pds, td.m_amount, td.m_txid, td.m_subaddr_index);
|
if (const auto* pd = find_matching_payment(pds, td)) {
|
||||||
if (pd) {
|
|
||||||
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)) {
|
||||||
|
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 {
|
} else {
|
||||||
recv.m_state = TxInfo::OFF_CHAIN;
|
recv.m_state = TxInfo::OFF_CHAIN;
|
||||||
}
|
}
|
||||||
@ -219,18 +240,16 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
|
|||||||
spent.m_state = TxInfo::ON_CHAIN;
|
spent.m_state = TxInfo::ON_CHAIN;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& in: tx.m_tx.vin) {
|
for (const auto& ring : tx.m_rings) {
|
||||||
if (in.type() != typeid(cryptonote::txin_to_key)) continue;
|
|
||||||
const auto& txin = boost::get<cryptonote::txin_to_key>(in);
|
|
||||||
snapshot.emplace_back(pair.first, TxInfo::OUTGOING);
|
snapshot.emplace_back(pair.first, TxInfo::OUTGOING);
|
||||||
TxInfo& spent = snapshot.back();
|
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_key_image_known = true;
|
||||||
spent.m_amount = txin.amount;
|
|
||||||
spent.m_height = tx.m_block_height;
|
spent.m_height = tx.m_block_height;
|
||||||
spent.m_unlock_time = tx.m_unlock_time;
|
spent.m_unlock_time = tx.m_unlock_time;
|
||||||
spent.m_timestamp = tx.m_timestamp;
|
spent.m_timestamp = tx.m_timestamp;
|
||||||
spent.m_fee = fee;
|
spent.m_fee = fee;
|
||||||
|
spent.m_change = tx.m_change;
|
||||||
spent.m_state = TxInfo::ON_CHAIN;
|
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) {
|
if (utx.m_change > 0) {
|
||||||
snapshot.emplace_back(pair.first, TxInfo::INCOMING);
|
snapshot.emplace_back(pair.first, TxInfo::INCOMING);
|
||||||
TxInfo& change = snapshot.back();
|
TxInfo& change = snapshot.back();
|
||||||
@ -283,16 +302,14 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
|
|||||||
change.m_state = state;
|
change.m_state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& in: utx.m_tx.vin) {
|
for (const auto& ring : utx.m_rings) {
|
||||||
if (in.type() != typeid(cryptonote::txin_to_key)) continue;
|
|
||||||
const auto& txin = boost::get<cryptonote::txin_to_key>(in);
|
|
||||||
snapshot.emplace_back(pair.first, TxInfo::OUTGOING);
|
snapshot.emplace_back(pair.first, TxInfo::OUTGOING);
|
||||||
TxInfo& spent = snapshot.back();
|
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_key_image_known = true;
|
||||||
spent.m_amount = txin.amount;
|
|
||||||
spent.m_timestamp = utx.m_timestamp;
|
spent.m_timestamp = utx.m_timestamp;
|
||||||
spent.m_fee = fee;
|
spent.m_fee = fee;
|
||||||
|
spent.m_change = utx.m_change;
|
||||||
spent.m_state = state;
|
spent.m_state = state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package im.molly.monero
|
package im.molly.monero
|
||||||
|
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
data class Balance(
|
data class Balance(
|
||||||
val pendingAmount: MoneroAmount,
|
val pendingAmount: MoneroAmount,
|
||||||
val timeLockedAmounts: List<TimeLocked<MoneroAmount>>,
|
val timeLockedAmounts: List<TimeLocked<MoneroAmount>>,
|
||||||
@ -7,18 +9,26 @@ data class Balance(
|
|||||||
val confirmedAmount: MoneroAmount = timeLockedAmounts.sumOf { it.value }
|
val confirmedAmount: MoneroAmount = timeLockedAmounts.sumOf { it.value }
|
||||||
val totalAmount: MoneroAmount = confirmedAmount + pendingAmount
|
val totalAmount: MoneroAmount = confirmedAmount + pendingAmount
|
||||||
|
|
||||||
fun unlockedAmountAt(targetTime: BlockchainTime): MoneroAmount =
|
fun unlockedAmountAt(
|
||||||
timeLockedAmounts
|
blockHeight: Int,
|
||||||
.filter { it.isUnlocked(targetTime) }
|
currentInstant: Instant = Instant.now(),
|
||||||
.sumOf { it.value }
|
): MoneroAmount {
|
||||||
|
val targetTime = BlockchainTime(blockHeight, currentInstant)
|
||||||
|
return timeLockedAmounts.filter { it.isUnlocked(targetTime) }.sumOf { it.value }
|
||||||
|
}
|
||||||
|
|
||||||
fun lockedAmountsAt(targetTime: BlockchainTime): Map<BlockchainTimeSpan, MoneroAmount> =
|
fun lockedAmountsAt(
|
||||||
timeLockedAmounts
|
blockHeight: Int,
|
||||||
|
currentInstant: Instant = Instant.now(),
|
||||||
|
): Map<BlockchainTimeSpan, MoneroAmount> {
|
||||||
|
val targetTime = BlockchainTime(blockHeight, currentInstant)
|
||||||
|
return timeLockedAmounts
|
||||||
.filter { it.isLocked(targetTime) }
|
.filter { it.isLocked(targetTime) }
|
||||||
.groupBy({ it.timeUntilUnlock(targetTime) }, { it.value })
|
.groupBy({ it.timeUntilUnlock(targetTime) }, { it.value })
|
||||||
.mapValues { (_, amounts) ->
|
.mapValues { (_, amounts) ->
|
||||||
amounts.sum()
|
amounts.sum()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Iterable<TimeLocked<Enote>>.calculateBalance(): Balance {
|
fun Iterable<TimeLocked<Enote>>.calculateBalance(): Balance {
|
||||||
@ -26,7 +36,7 @@ fun Iterable<TimeLocked<Enote>>.calculateBalance(): Balance {
|
|||||||
|
|
||||||
val lockedAmounts = mutableListOf<TimeLocked<MoneroAmount>>()
|
val lockedAmounts = mutableListOf<TimeLocked<MoneroAmount>>()
|
||||||
|
|
||||||
for (timeLocked in filter { it.value.spent }) {
|
for (timeLocked in filter { !it.value.spent }) {
|
||||||
if (timeLocked.value.age == 0) {
|
if (timeLocked.value.age == 0) {
|
||||||
pendingAmount += timeLocked.value.amount
|
pendingAmount += timeLocked.value.amount
|
||||||
} else {
|
} 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
|
fun min(a: BlockchainTime, b: BlockchainTime) = if (a <= b) a else b
|
||||||
|
|
||||||
data class BlockchainTimeSpan(val duration: Duration, val blocks: Int) {
|
data class BlockchainTimeSpan(val duration: Duration, val blocks: Int) {
|
||||||
|
val timeRemaining: Duration
|
||||||
|
get() = maxOf(Duration.ZERO, duration)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val ZERO = BlockchainTimeSpan(duration = Duration.ZERO, blocks = 0)
|
val ZERO = BlockchainTimeSpan(duration = Duration.ZERO, blocks = 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TimeLocked<T>(val value: T, val unlockTime: BlockchainTime) {
|
class TimeLocked<T>(val value: T, val unlockTime: BlockchainTime?) {
|
||||||
fun isLocked(currentTime: BlockchainTime): Boolean = currentTime < unlockTime
|
fun isLocked(currentTime: BlockchainTime): Boolean {
|
||||||
|
return currentTime < (unlockTime ?: return false)
|
||||||
|
}
|
||||||
|
|
||||||
fun isUnlocked(currentTime: BlockchainTime) = !isLocked(currentTime)
|
fun isUnlocked(currentTime: BlockchainTime) = !isLocked(currentTime)
|
||||||
|
|
||||||
fun timeUntilUnlock(currentTime: BlockchainTime): BlockchainTimeSpan {
|
fun timeUntilUnlock(currentTime: BlockchainTime): BlockchainTimeSpan {
|
||||||
return if (isLocked(currentTime)) {
|
return if (isLocked(currentTime)) {
|
||||||
unlockTime.minus(currentTime)
|
unlockTime!!.minus(currentTime)
|
||||||
} else {
|
} else {
|
||||||
BlockchainTimeSpan.ZERO
|
BlockchainTimeSpan.ZERO
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ package im.molly.monero
|
|||||||
@JvmInline
|
@JvmInline
|
||||||
value class HashDigest(private val hashDigest: String) {
|
value class HashDigest(private val hashDigest: String) {
|
||||||
init {
|
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)
|
return StandardAddress(network)
|
||||||
}
|
}
|
||||||
SubAddress.prefixes[prefix]?.let { network ->
|
SubAddress.prefixes[prefix]?.let { network ->
|
||||||
TODO()
|
return SubAddress(network)
|
||||||
}
|
}
|
||||||
IntegratedAddress.prefixes[prefix]?.let { network ->
|
IntegratedAddress.prefixes[prefix]?.let { network ->
|
||||||
TODO()
|
TODO()
|
||||||
@ -60,7 +60,7 @@ data class SubAddress(
|
|||||||
companion object {
|
companion object {
|
||||||
val prefixes = mapOf(
|
val prefixes = mapOf(
|
||||||
42L to MoneroNetwork.Mainnet,
|
42L to MoneroNetwork.Mainnet,
|
||||||
64L to MoneroNetwork.Testnet,
|
63L to MoneroNetwork.Testnet,
|
||||||
36L to MoneroNetwork.Stagenet,
|
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 transaction isn't failed, calculate unlock time and save enotes
|
||||||
if (tx.state !is TxState.Failed) {
|
if (tx.state !is TxState.Failed) {
|
||||||
|
val maxUnlockTime = tx.blockHeight?.let { height ->
|
||||||
val defaultUnlockTime = BlockchainTime.Block(
|
val defaultUnlockTime = BlockchainTime.Block(
|
||||||
height = tx.blockHeight!! + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE,
|
height = height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE,
|
||||||
referencePoint = blockchainContext,
|
referencePoint = blockchainContext,
|
||||||
)
|
)
|
||||||
val maxUnlockTime = max(defaultUnlockTime, tx.timeLock ?: BlockchainTime.Genesis)
|
max(defaultUnlockTime, tx.timeLock ?: BlockchainTime.Genesis)
|
||||||
|
}
|
||||||
val lockedEnotesToAdd = tx.received.map { enote -> TimeLocked(enote, maxUnlockTime) }
|
val lockedEnotesToAdd = tx.received.map { enote ->
|
||||||
|
TimeLocked(enote, maxUnlockTime)
|
||||||
|
}
|
||||||
validEnotes.addAll(lockedEnotesToAdd)
|
validEnotes.addAll(lockedEnotesToAdd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,14 +117,14 @@ private fun List<TxInfo>.createTransaction(
|
|||||||
if (unlockTime == 0L) null else blockchainContext.resolveUnlockTime(unlockTime)
|
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 outTxs = filter { !it.incoming }
|
||||||
|
|
||||||
val spentKeyImages = outTxs.mapNotNull { it.keyImage }
|
val spentKeyImages = outTxs.mapNotNull { it.keyImage }
|
||||||
val sentEnotes = enoteByKeyImage.filterKeys { ki -> ki in spentKeyImages }.values.toSet()
|
val sentEnotes = enoteByKeyImage.filterKeys { ki -> ki in spentKeyImages }.values.toSet()
|
||||||
|
|
||||||
val payments = outTxs.map { it.toPaymentDetail() }
|
val payments = outTxs.mapNotNull { it.toPaymentDetail() }
|
||||||
|
|
||||||
return Transaction(
|
return Transaction(
|
||||||
hash = HashDigest(txHash),
|
hash = HashDigest(txHash),
|
||||||
@ -166,7 +169,10 @@ private fun TxInfo.toEnote(blockchainHeight: Int): Enote {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TxInfo.toPaymentDetail() = PaymentDetail(
|
private fun TxInfo.toPaymentDetail(): PaymentDetail? {
|
||||||
|
val recipient = PublicAddress.parse(recipient ?: return null)
|
||||||
|
return PaymentDetail(
|
||||||
amount = MoneroAmount(atomicUnits = amount),
|
amount = MoneroAmount(atomicUnits = amount),
|
||||||
recipient = PublicAddress.parse(recipient!!),
|
recipient = recipient,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user