mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2024-10-01 03:45:36 -04:00
lib: make output public key optional in Enote
This commit is contained in:
parent
75b33a24f3
commit
7dba0c1b18
@ -178,7 +178,8 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
|
||||
for (const auto& td: tds) {
|
||||
snapshot.emplace_back(td.m_txid, TxInfo::INCOMING);
|
||||
TxInfo& recv = snapshot.back();
|
||||
recv.m_key = td.get_public_key();
|
||||
recv.m_public_key = td.get_public_key();
|
||||
recv.m_public_key_known = true;
|
||||
recv.m_key_image = td.m_key_image;
|
||||
recv.m_key_image_known = td.m_key_image_known;
|
||||
recv.m_subaddress_major = td.m_subaddr_index.major;
|
||||
@ -247,7 +248,6 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
|
||||
// Add pending transfers to our own wallet.
|
||||
snapshot.emplace_back(pair.first, TxInfo::INCOMING);
|
||||
TxInfo& recv = snapshot.back();
|
||||
// TODO: recv.m_key
|
||||
recv.m_recipient = m_wallet.get_subaddress_as_str(*dest_subaddr_idx);
|
||||
recv.m_subaddress_major = (*dest_subaddr_idx).major;
|
||||
recv.m_subaddress_minor = (*dest_subaddr_idx).minor;
|
||||
@ -273,7 +273,6 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
|
||||
if (utx.m_change > 0) {
|
||||
snapshot.emplace_back(pair.first, TxInfo::INCOMING);
|
||||
TxInfo& change = snapshot.back();
|
||||
// TODO: change.m_key
|
||||
change.m_recipient = m_wallet.get_subaddress_as_str({utx.m_subaddr_account, 0});
|
||||
change.m_subaddress_major = utx.m_subaddr_account;
|
||||
change.m_subaddress_minor = 0; // All changes go to 0-th subaddress
|
||||
@ -306,7 +305,6 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
|
||||
for (uint64_t amount: upd.m_amounts) {
|
||||
snapshot.emplace_back(upd.m_tx_hash, TxInfo::INCOMING);
|
||||
TxInfo& recv = snapshot.back();
|
||||
// TODO: recv.m_key
|
||||
recv.m_recipient = m_wallet.get_subaddress_as_str(upd.m_subaddr_index);
|
||||
recv.m_subaddress_major = upd.m_subaddr_index.major;
|
||||
recv.m_subaddress_minor = upd.m_subaddr_index.minor;
|
||||
@ -569,25 +567,26 @@ Java_im_molly_monero_WalletNative_nativeGetCurrentBlockchainHeight(
|
||||
}
|
||||
|
||||
ScopedJvmLocalRef<jobject> nativeToJvmTxInfo(JNIEnv* env,
|
||||
const TxInfo& info) {
|
||||
LOG_FATAL_IF(info.m_height >= CRYPTONOTE_MAX_BLOCK_NUMBER, "Blockchain max height reached");
|
||||
const TxInfo& tx) {
|
||||
LOG_FATAL_IF(tx.m_height >= CRYPTONOTE_MAX_BLOCK_NUMBER, "Blockchain max height reached");
|
||||
// TODO: Check amount overflow
|
||||
return {env, TxInfoClass.newObject(
|
||||
env, TxInfo_ctor,
|
||||
nativeToJvmString(env, pod_to_hex(info.m_tx_hash)).obj(),
|
||||
nativeToJvmString(env, pod_to_hex(info.m_key)).obj(),
|
||||
info.m_key_image_known ? nativeToJvmString(env, pod_to_hex(info.m_key_image)).obj(): nullptr,
|
||||
info.m_subaddress_major,
|
||||
info.m_subaddress_minor,
|
||||
(!info.m_recipient.empty()) ? nativeToJvmString(env, info.m_recipient).obj() : nullptr,
|
||||
info.m_amount,
|
||||
static_cast<jint>(info.m_height),
|
||||
info.m_state,
|
||||
info.m_unlock_time,
|
||||
info.m_timestamp,
|
||||
info.m_fee,
|
||||
info.m_coinbase,
|
||||
info.m_type == TxInfo::INCOMING)
|
||||
nativeToJvmString(env, pod_to_hex(tx.m_tx_hash)).obj(),
|
||||
tx.m_public_key_known ? nativeToJvmString(env, pod_to_hex(tx.m_public_key)).obj() : nullptr,
|
||||
tx.m_key_image_known ? nativeToJvmString(env, pod_to_hex(tx.m_key_image)).obj() : nullptr,
|
||||
tx.m_subaddress_major,
|
||||
tx.m_subaddress_minor,
|
||||
(!tx.m_recipient.empty()) ? nativeToJvmString(env, tx.m_recipient).obj() : nullptr,
|
||||
tx.m_amount,
|
||||
static_cast<jint>(tx.m_height),
|
||||
tx.m_state,
|
||||
tx.m_unlock_time,
|
||||
tx.m_timestamp,
|
||||
tx.m_fee,
|
||||
tx.m_change,
|
||||
tx.m_coinbase,
|
||||
tx.m_type == TxInfo::INCOMING)
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ using unconfirmed_transfer_details = tools::wallet2::unconfirmed_transfer_detail
|
||||
// Basic structure combining transaction details with input or output info.
|
||||
struct TxInfo {
|
||||
crypto::hash m_tx_hash;
|
||||
crypto::public_key m_key;
|
||||
crypto::public_key m_public_key;
|
||||
crypto::key_image m_key_image;
|
||||
uint32_t m_subaddress_major;
|
||||
uint32_t m_subaddress_minor;
|
||||
@ -31,6 +31,7 @@ struct TxInfo {
|
||||
uint64_t m_fee;
|
||||
uint64_t m_change;
|
||||
bool m_coinbase;
|
||||
bool m_public_key_known;
|
||||
bool m_key_image_known;
|
||||
|
||||
enum TxType {
|
||||
@ -47,7 +48,7 @@ struct TxInfo {
|
||||
|
||||
TxInfo(crypto::hash tx_hash, TxType type):
|
||||
m_tx_hash(tx_hash),
|
||||
m_key(crypto::public_key{}),
|
||||
m_public_key(crypto::public_key{}),
|
||||
m_key_image(crypto::key_image{}),
|
||||
m_subaddress_major(-1),
|
||||
m_subaddress_minor(-1),
|
||||
@ -59,6 +60,7 @@ struct TxInfo {
|
||||
m_fee(0),
|
||||
m_change(0),
|
||||
m_coinbase(false),
|
||||
m_public_key_known(false),
|
||||
m_key_image_known(false),
|
||||
m_type(type),
|
||||
m_state(OFF_CHAIN) {}
|
||||
|
@ -13,7 +13,7 @@ data class Block(
|
||||
|
||||
data class BlockHeader(
|
||||
val height: Int,
|
||||
val timestamp: Long,
|
||||
val epochSecond: Long,
|
||||
) {
|
||||
companion object {
|
||||
const val MAX_HEIGHT = CRYPTONOTE_MAX_BLOCK_NUMBER - 1
|
||||
|
@ -25,6 +25,8 @@ open class BlockchainTime(
|
||||
override fun compareTo(other: BlockchainTime): Int =
|
||||
this.height.compareTo(other.height)
|
||||
|
||||
override fun toString(): String = "Block #$height | $timestamp"
|
||||
|
||||
data object Genesis : BlockchainTime(0, Instant.ofEpochSecond(1397818193))
|
||||
|
||||
class Block(height: Int, referencePoint: BlockchainTime = Genesis) :
|
||||
@ -42,17 +44,20 @@ open class BlockchainTime(
|
||||
}
|
||||
|
||||
companion object {
|
||||
val AVERAGE_BLOCK_TIME = Duration.ofSeconds(DIFFICULTY_TARGET_V2)
|
||||
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" }
|
||||
require(targetHeight >= 0) {
|
||||
"Block height $targetHeight must not be negative"
|
||||
}
|
||||
|
||||
return if (targetHeight == 0) {
|
||||
Genesis.timestamp
|
||||
} else {
|
||||
val heightDiff = targetHeight - referencePoint.height
|
||||
val estTimeDiff = AVERAGE_BLOCK_TIME.multipliedBy(heightDiff.toLong())
|
||||
referencePoint.timestamp.plus(estTimeDiff)
|
||||
return when (targetHeight) {
|
||||
0 -> Genesis.timestamp
|
||||
else -> {
|
||||
val heightDiff = targetHeight - referencePoint.height
|
||||
val estTimeDiff = AVERAGE_BLOCK_TIME.multipliedBy(heightDiff.toLong())
|
||||
referencePoint.timestamp.plus(estTimeDiff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,6 +91,10 @@ open class BlockchainTime(
|
||||
operator fun minus(other: BlockchainTime): BlockchainTimeSpan = other.until(this)
|
||||
}
|
||||
|
||||
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) {
|
||||
companion object {
|
||||
val ZERO = BlockchainTimeSpan(duration = Duration.ZERO, blocks = 0)
|
||||
|
@ -1,15 +1,25 @@
|
||||
package im.molly.monero
|
||||
|
||||
data class Enote(
|
||||
class Enote(
|
||||
val amount: MoneroAmount,
|
||||
val owner: AccountAddress,
|
||||
val key: PublicKey,
|
||||
val key: PublicKey?,
|
||||
val keyImage: HashDigest?,
|
||||
val age: Int,
|
||||
val sourceTxId: String?,
|
||||
) {
|
||||
var spent: Boolean = false
|
||||
|
||||
init {
|
||||
require(age >= 0) { "Enote age $age must not be negative" }
|
||||
}
|
||||
|
||||
var spent: Boolean = false
|
||||
private set
|
||||
|
||||
fun markAsSpent() {
|
||||
spent = true
|
||||
}
|
||||
|
||||
override fun hashCode() = System.identityHashCode(this)
|
||||
|
||||
override fun equals(other: Any?) = this === other
|
||||
}
|
||||
|
@ -4,18 +4,21 @@ data class Transaction(
|
||||
val hash: HashDigest,
|
||||
// TODO: val version: ProtocolInfo,
|
||||
val state: TxState,
|
||||
val timeLock: BlockchainTime,
|
||||
val timeLock: BlockchainTime?,
|
||||
val sent: Set<Enote>,
|
||||
val received: Set<Enote>,
|
||||
val payments: List<PaymentDetail>,
|
||||
val fee: MoneroAmount,
|
||||
val change: MoneroAmount,
|
||||
) {
|
||||
val txId: String get() = hash.toString()
|
||||
val txId: String
|
||||
get() = hash.toString()
|
||||
|
||||
val blockHeight: Int?
|
||||
get() = (state as? TxState.OnChain)?.blockHeader?.height
|
||||
}
|
||||
|
||||
sealed interface TxState {
|
||||
|
||||
data class OnChain(
|
||||
val blockHeader: BlockHeader,
|
||||
) : TxState
|
||||
|
@ -2,18 +2,20 @@ package im.molly.monero.internal
|
||||
|
||||
import android.os.Parcelable
|
||||
import im.molly.monero.AccountAddress
|
||||
import im.molly.monero.MoneroAmount
|
||||
import im.molly.monero.BlockHeader
|
||||
import im.molly.monero.BlockchainTime
|
||||
import im.molly.monero.CalledByNative
|
||||
import im.molly.monero.Enote
|
||||
import im.molly.monero.HashDigest
|
||||
import im.molly.monero.MoneroAmount
|
||||
import im.molly.monero.PaymentDetail
|
||||
import im.molly.monero.PublicAddress
|
||||
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 kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
@ -27,7 +29,7 @@ import kotlinx.parcelize.Parcelize
|
||||
internal data class TxInfo
|
||||
@CalledByNative("wallet.cc") constructor(
|
||||
val txHash: String,
|
||||
val key: String,
|
||||
val publicKey: String?,
|
||||
val keyImage: String?,
|
||||
val subAddressMajor: Int,
|
||||
val subAddressMinor: Int,
|
||||
@ -61,85 +63,87 @@ internal data class TxInfo
|
||||
internal fun List<TxInfo>.consolidateTransactions(
|
||||
blockchainContext: BlockchainTime,
|
||||
): Pair<Map<String, Transaction>, Set<TimeLocked<Enote>>> {
|
||||
val (enoteByKey, enoteByKeyImage) = extractEnotesFromIncomingTxs(blockchainContext)
|
||||
// Extract enotes from incoming transactions
|
||||
val allEnotes = filter { it.incoming }.map { it.toEnote(blockchainContext.height) }
|
||||
|
||||
val timeLockedEnotes = HashSet<TimeLocked<Enote>>(enoteByKey.size)
|
||||
val enoteByTxId = allEnotes.groupBy { enote -> enote.sourceTxId!! }
|
||||
|
||||
// Group transactions by their hash and then map each group to a Transaction
|
||||
val groupedByTxId = groupBy { it.txHash }
|
||||
val enoteByKeyImage = allEnotes.mapNotNull { enote ->
|
||||
enote.keyImage?.let { keyImage -> keyImage.toString() to enote }
|
||||
}.toMap()
|
||||
|
||||
val validEnotes = HashSet<TimeLocked<Enote>>(allEnotes.size)
|
||||
|
||||
// Group transaction info by their hash and then map each group to a Transaction
|
||||
val groupedByTxId = groupBy { txInfo -> txInfo.txHash }
|
||||
val txById = groupedByTxId.mapValues { (_, infoList) ->
|
||||
createTransaction(blockchainContext, infoList, enoteByKey, enoteByKeyImage)
|
||||
.also { tx ->
|
||||
if (tx.state !is TxState.Failed) {
|
||||
val lockedEnotesToAdd =
|
||||
tx.received.map { enote -> TimeLocked(enote, tx.timeLock) }
|
||||
timeLockedEnotes.addAll(lockedEnotesToAdd)
|
||||
tx.sent.forEach { enote -> enote.spent = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
val tx = infoList.createTransaction(blockchainContext, enoteByTxId, enoteByKeyImage)
|
||||
|
||||
return txById to timeLockedEnotes
|
||||
}
|
||||
// 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)
|
||||
|
||||
private fun List<TxInfo>.extractEnotesFromIncomingTxs(
|
||||
blockchainContext: BlockchainTime,
|
||||
): Pair<Map<String, Enote>, Map<String, Enote>> {
|
||||
val enoteByKey = mutableMapOf<String, Enote>()
|
||||
val enoteByKeyImage = mutableMapOf<String, Enote>()
|
||||
|
||||
for (txInfo in filter { it.incoming }) {
|
||||
enoteByKey.computeIfAbsent(txInfo.key) {
|
||||
val enote = txInfo.toEnote(blockchainContext.height)
|
||||
txInfo.keyImage?.let { keyImage ->
|
||||
enoteByKeyImage[keyImage] = enote
|
||||
}
|
||||
enote
|
||||
val lockedEnotesToAdd = tx.received.map { enote -> TimeLocked(enote, maxUnlockTime) }
|
||||
validEnotes.addAll(lockedEnotesToAdd)
|
||||
}
|
||||
|
||||
// Mark the sent enotes as spent
|
||||
tx.sent.forEach { enote -> enote.markAsSpent() }
|
||||
|
||||
tx
|
||||
}
|
||||
|
||||
return enoteByKey to enoteByKeyImage
|
||||
return txById to validEnotes
|
||||
}
|
||||
|
||||
private fun createTransaction(
|
||||
private fun List<TxInfo>.createTransaction(
|
||||
blockchainContext: BlockchainTime,
|
||||
infoList: List<TxInfo>,
|
||||
enoteMap: Map<String, Enote>,
|
||||
keyImageMap: Map<String, Enote>,
|
||||
enoteByTxId: Map<String, List<Enote>>,
|
||||
enoteByKeyImage: Map<String, Enote>,
|
||||
): Transaction {
|
||||
val txHash = infoList.first().txHash
|
||||
val unlockTime = infoList.maxOf { it.unlockTime }
|
||||
val fee = infoList.maxOf { it.fee }
|
||||
val change = infoList.maxOf { it.change }
|
||||
val txHash = first().txHash
|
||||
|
||||
val (ins, outs) = infoList.partition { it.incoming }
|
||||
val fee = maxOf { it.fee }
|
||||
val change = maxOf { it.change }
|
||||
|
||||
val receivedEnotes = ins.map { enoteMap.getValue(it.key) }
|
||||
val spentKeyImages = outs.mapNotNull { it.keyImage }.toSet()
|
||||
val sentEnotes = keyImageMap.filterKeys { spentKeyImages.contains(it) }.values
|
||||
val payments = outs.map { it.toPaymentDetail() }
|
||||
val timeLock = maxOf { it.unlockTime }.let { unlockTime ->
|
||||
if (unlockTime == 0L) null else blockchainContext.resolveUnlockTime(unlockTime)
|
||||
}
|
||||
|
||||
val receivedEnotes = enoteByTxId.getValue(txHash).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() }
|
||||
|
||||
return Transaction(
|
||||
hash = HashDigest(txHash),
|
||||
state = determineTxState(infoList),
|
||||
timeLock = blockchainContext.resolveUnlockTime(unlockTime),
|
||||
sent = sentEnotes.toSet(),
|
||||
received = receivedEnotes.toSet(),
|
||||
state = determineTxState(),
|
||||
timeLock = timeLock,
|
||||
sent = sentEnotes,
|
||||
received = receivedEnotes,
|
||||
payments = payments,
|
||||
fee = MoneroAmount(fee),
|
||||
change = MoneroAmount(change),
|
||||
fee = MoneroAmount(atomicUnits = fee),
|
||||
change = MoneroAmount(atomicUnits = change),
|
||||
)
|
||||
}
|
||||
|
||||
private fun determineTxState(infoList: List<TxInfo>): TxState {
|
||||
val txInfo = infoList.distinctBy { it.state }.single()
|
||||
private fun List<TxInfo>.determineTxState(): TxState {
|
||||
val uniqueTx = distinctBy { it.state }.single()
|
||||
|
||||
return when (txInfo.state) {
|
||||
return when (uniqueTx.state) {
|
||||
TxInfo.OFF_CHAIN -> TxState.OffChain
|
||||
TxInfo.PENDING -> TxState.InMemoryPool
|
||||
TxInfo.FAILED -> TxState.Failed
|
||||
TxInfo.ON_CHAIN -> TxState.OnChain(BlockHeader(txInfo.height, txInfo.timestamp))
|
||||
else -> error("Invalid tx state value: ${txInfo.state}")
|
||||
TxInfo.ON_CHAIN -> TxState.OnChain(BlockHeader(uniqueTx.height, uniqueTx.timestamp))
|
||||
else -> error("Invalid tx state value: ${uniqueTx.state}")
|
||||
}
|
||||
}
|
||||
|
||||
@ -153,15 +157,16 @@ private fun TxInfo.toEnote(blockchainHeight: Int): Enote {
|
||||
val calculatedAge = if (height == 0) 0 else blockchainHeight - height + 1
|
||||
|
||||
return Enote(
|
||||
amount = MoneroAmount(amount),
|
||||
amount = MoneroAmount(atomicUnits = amount),
|
||||
owner = ownerAddress,
|
||||
key = PublicKey(key),
|
||||
key = publicKey?.let { PublicKey(it) },
|
||||
keyImage = keyImage?.let { HashDigest(it) },
|
||||
age = calculatedAge,
|
||||
sourceTxId = txHash,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TxInfo.toPaymentDetail() = PaymentDetail(
|
||||
amount = MoneroAmount(amount),
|
||||
amount = MoneroAmount(atomicUnits = amount),
|
||||
recipient = PublicAddress.parse(recipient!!),
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user