mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2024-12-25 07:29:23 -05:00
lib: add per-account balance and account listing to Ledger
This commit is contained in:
parent
6cc43d2502
commit
ada39f13e8
@ -6,5 +6,5 @@ import im.molly.monero.internal.TxInfo;
|
||||
oneway interface IBalanceListener {
|
||||
void onBalanceChanged(in List<TxInfo> txHistory, in String[] subAddresses, in BlockchainTime blockchainTime);
|
||||
void onRefresh(in BlockchainTime blockchainTime);
|
||||
void onAddressCreated(String subAddress);
|
||||
void onSubAddressListUpdated(in String[] subAddresses);
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ interface IWallet {
|
||||
oneway void addDetachedSubAddress(int accountIndex, int subAddressIndex, in IWalletCallbacks callback);
|
||||
oneway void createAccount(in IWalletCallbacks callback);
|
||||
oneway void createSubAddressForAccount(int accountIndex, in IWalletCallbacks callback);
|
||||
oneway void getAddressesForAccount(int accountIndex, in IWalletCallbacks callback);
|
||||
oneway void getAllAddresses(in IWalletCallbacks callback);
|
||||
oneway void resumeRefresh(boolean skipCoinbase, in IWalletCallbacks callback);
|
||||
oneway void cancelRefresh();
|
||||
|
@ -3,8 +3,9 @@ package im.molly.monero;
|
||||
import im.molly.monero.BlockchainTime;
|
||||
|
||||
oneway interface IWalletCallbacks {
|
||||
void onAddressReady(in String[] subAddresses);
|
||||
void onRefreshResult(in BlockchainTime blockchainTime, int status);
|
||||
void onCommitResult(boolean success);
|
||||
void onSubAddressReady(String subAddress);
|
||||
void onSubAddressListReceived(in String[] subAddresses);
|
||||
void onFeesReceived(in long[] fees);
|
||||
}
|
||||
|
@ -220,14 +220,16 @@ std::string Wallet::public_address() const {
|
||||
return account.get_public_address_str(m_wallet.nettype());
|
||||
}
|
||||
|
||||
std::vector<std::string> Wallet::formatted_subaddresses() {
|
||||
std::vector<std::string> Wallet::formatted_subaddresses(uint32_t index_major) {
|
||||
std::lock_guard<std::mutex> lock(m_subaddresses_mutex);
|
||||
|
||||
std::vector<std::string> ret;
|
||||
ret.reserve(m_subaddresses.size());
|
||||
|
||||
for (const auto& entry: m_subaddresses) {
|
||||
ret.push_back(FormatAccountAddress(entry));
|
||||
if (index_major == -1 || index_major == entry.first.major) {
|
||||
ret.push_back(FormatAccountAddress(entry));
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
@ -745,9 +747,15 @@ JNIEXPORT jobjectArray JNICALL
|
||||
Java_im_molly_monero_WalletNative_nativeGetSubAddresses(
|
||||
JNIEnv* env,
|
||||
jobject thiz,
|
||||
jint sub_address_major,
|
||||
jlong handle) {
|
||||
auto* wallet = reinterpret_cast<Wallet*>(handle);
|
||||
return NativeToJavaStringArray(env, wallet->formatted_subaddresses());
|
||||
try {
|
||||
auto subaddresses = wallet->formatted_subaddresses(sub_address_major);
|
||||
return NativeToJavaStringArray(env, subaddresses);
|
||||
} catch (error::account_index_outofbound& e) {
|
||||
return NativeToJavaStringArray(env, {});
|
||||
}
|
||||
}
|
||||
|
||||
extern "C"
|
||||
|
@ -112,7 +112,7 @@ class Wallet : i_wallet2_callback {
|
||||
std::vector<uint64_t> fetchBaseFeeEstimate();
|
||||
|
||||
std::string public_address() const;
|
||||
std::vector<std::string> formatted_subaddresses();
|
||||
std::vector<std::string> formatted_subaddresses(uint32_t index_major = -1);
|
||||
|
||||
uint32_t current_blockchain_height() const { return static_cast<uint32_t>(m_last_block_height); }
|
||||
uint64_t current_blockchain_timestamp() const { return m_last_block_timestamp; }
|
||||
|
@ -49,10 +49,3 @@ data class AccountAddress(
|
||||
|
||||
override fun toString(): String = "$accountIndex/$subAddressIndex/$publicAddress"
|
||||
}
|
||||
|
||||
fun Iterable<AccountAddress>.findByIndexes(
|
||||
accountIndex: Int,
|
||||
subAddressIndex: Int,
|
||||
): AccountAddress? {
|
||||
return find { it.accountIndex == accountIndex && it.subAddressIndex == subAddressIndex }
|
||||
}
|
||||
|
@ -1,29 +1,17 @@
|
||||
package im.molly.monero
|
||||
|
||||
//import im.molly.monero.proto.LedgerProto
|
||||
|
||||
data class Ledger(
|
||||
val publicAddress: PublicAddress,
|
||||
val accountAddresses: Set<AccountAddress>,
|
||||
val indexedAccounts: List<WalletAccount>,
|
||||
val transactionById: Map<String, Transaction>,
|
||||
val enotes: Set<TimeLocked<Enote>>,
|
||||
val enoteSet: Set<TimeLocked<Enote>>,
|
||||
val checkedAt: BlockchainTime,
|
||||
) {
|
||||
val transactions get() = transactionById.values
|
||||
val transactions: Collection<Transaction>
|
||||
get() = transactionById.values
|
||||
|
||||
val balance: Balance = enotes.calculateBalance()
|
||||
fun getBalance(): Balance = enoteSet.calculateBalance()
|
||||
|
||||
// companion object {
|
||||
// fun fromProto(proto: LedgerProto) = Ledger(
|
||||
// publicAddress = PublicAddress.base58(proto.publicAddress),
|
||||
// txOuts = proto.ownedTxOutsList.map { OwnedTxOut.fromProto(it) },
|
||||
// checkedAtBlockHeight = proto.blockHeight,
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// fun proto(): LedgerProto = LedgerProto.newBuilder()
|
||||
// .setPublicAddress(publicAddress.base58)
|
||||
// .addAllOwnedTxOuts(txOuts.map { it.proto() })
|
||||
// .setBlockHeight(checkedAtBlockHeight)
|
||||
// .build()
|
||||
fun getBalanceForAccount(accountIndex: Int): Balance =
|
||||
enoteSet.calculateBalance { it.accountIndex == accountIndex }
|
||||
}
|
||||
|
@ -27,54 +27,90 @@ class MoneroWallet internal constructor(
|
||||
|
||||
var dataStore by storageAdapter::dataStore
|
||||
|
||||
suspend fun addDetachedSubAddress(accountIndex: Int, subAddressIndex: Int): AccountAddress =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
wallet.addDetachedSubAddress(
|
||||
accountIndex,
|
||||
subAddressIndex,
|
||||
object : BaseWalletCallbacks() {
|
||||
override fun onAddressReady(subAddresses: Array<String>) {
|
||||
val accountAddress = AccountAddress.parseWithIndexes(subAddresses[0])
|
||||
continuation.resume(accountAddress) {}
|
||||
}
|
||||
})
|
||||
}
|
||||
// suspend fun addDetachedSubAddress(accountIndex: Int, subAddressIndex: Int): AccountAddress =
|
||||
// suspendCancellableCoroutine { continuation ->
|
||||
// wallet.addDetachedSubAddress(
|
||||
// accountIndex,
|
||||
// subAddressIndex,
|
||||
// object : BaseWalletCallbacks() {
|
||||
// override fun onSubAddressReady(subAddress: String) {
|
||||
// continuation.resume(AccountAddress.parseWithIndexes(subAddress)) {}
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
suspend fun createAccount(): AccountAddress =
|
||||
suspend fun createAccount(): WalletAccount =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
wallet.createAccount(object : BaseWalletCallbacks() {
|
||||
override fun onAddressReady(subAddresses: Array<String>) {
|
||||
val accountAddress = AccountAddress.parseWithIndexes(subAddresses[0])
|
||||
continuation.resume(accountAddress) {}
|
||||
override fun onSubAddressReady(subAddress: String) {
|
||||
val primaryAddress = AccountAddress.parseWithIndexes(subAddress)
|
||||
continuation.resume(
|
||||
WalletAccount(
|
||||
addresses = listOf(primaryAddress),
|
||||
accountIndex = primaryAddress.accountIndex,
|
||||
)
|
||||
) {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NoSuchAccountException
|
||||
*/
|
||||
suspend fun createSubAddressForAccount(accountIndex: Int = 0): AccountAddress =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
wallet.createSubAddressForAccount(accountIndex, object : BaseWalletCallbacks() {
|
||||
override fun onAddressReady(subAddresses: Array<String>) {
|
||||
if (subAddresses.isEmpty()) {
|
||||
throw NoSuchAccountException(accountIndex)
|
||||
}
|
||||
val accountAddress = AccountAddress.parseWithIndexes(subAddresses[0])
|
||||
continuation.resume(accountAddress) {}
|
||||
override fun onSubAddressReady(subAddress: String) {
|
||||
continuation.resume(AccountAddress.parseWithIndexes(subAddress)) {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
suspend fun getAllAddresses(): Set<AccountAddress> =
|
||||
/**
|
||||
* @throws NoSuchAccountException
|
||||
*/
|
||||
suspend fun findUnusedSubAddress(accountIndex: Int = 0): AccountAddress? {
|
||||
val ledger = ledger().first()
|
||||
val transactions = ledger.transactions
|
||||
val account = ledger.indexedAccounts.getOrNull(accountIndex)
|
||||
?: throw NoSuchAccountException(accountIndex)
|
||||
|
||||
return account.addresses.firstOrNull { !it.isAddressUsed(transactions) }
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NoSuchAccountException
|
||||
*/
|
||||
suspend fun getAccount(accountIndex: Int = 0): WalletAccount =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
wallet.getAddressesForAccount(accountIndex, object : BaseWalletCallbacks() {
|
||||
override fun onSubAddressListReceived(subAddresses: Array<String>) {
|
||||
val accounts = parseAndAggregateAddresses(subAddresses)
|
||||
continuation.resume(accounts.single()) {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
suspend fun getAllAccounts(): List<WalletAccount> =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
wallet.getAllAddresses(object : BaseWalletCallbacks() {
|
||||
override fun onAddressReady(subAddresses: Array<String>) {
|
||||
continuation.resume(subAddresses.toAccountAddresses()) {}
|
||||
override fun onSubAddressListReceived(subAddresses: Array<String>) {
|
||||
val accounts = parseAndAggregateAddresses(subAddresses)
|
||||
continuation.resume(accounts) {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun Array<String>.toAccountAddresses(): Set<AccountAddress> {
|
||||
return map { AccountAddress.parseWithIndexes(it) }.toSet()
|
||||
}
|
||||
private fun parseAndAggregateAddresses(subAddresses: Array<String>): List<WalletAccount> =
|
||||
subAddresses.map { AccountAddress.parseWithIndexes(it) }
|
||||
.groupBy { it.accountIndex }
|
||||
.map { (index, addresses) ->
|
||||
WalletAccount(
|
||||
addresses = addresses,
|
||||
accountIndex = index,
|
||||
)
|
||||
}
|
||||
.sortedBy { it.accountIndex }
|
||||
|
||||
/**
|
||||
* A [Flow] of ledger changes.
|
||||
@ -88,16 +124,16 @@ class MoneroWallet internal constructor(
|
||||
subAddresses: Array<String>,
|
||||
blockchainTime: BlockchainTime,
|
||||
) {
|
||||
val accountAddresses = subAddresses.toAccountAddresses()
|
||||
val indexedAccounts = parseAndAggregateAddresses(subAddresses)
|
||||
val (txById, enotes) = txHistory.consolidateTransactions(
|
||||
accountAddresses = accountAddresses,
|
||||
accounts = indexedAccounts,
|
||||
blockchainContext = blockchainTime,
|
||||
)
|
||||
val ledger = Ledger(
|
||||
publicAddress = publicAddress,
|
||||
accountAddresses = accountAddresses,
|
||||
indexedAccounts = indexedAccounts,
|
||||
transactionById = txById,
|
||||
enotes = enotes,
|
||||
enoteSet = enotes,
|
||||
checkedAt = blockchainTime,
|
||||
)
|
||||
sendLedger(ledger)
|
||||
@ -107,11 +143,10 @@ class MoneroWallet internal constructor(
|
||||
sendLedger(lastKnownLedger.copy(checkedAt = blockchainTime))
|
||||
}
|
||||
|
||||
override fun onAddressCreated(subAddress: String) {
|
||||
val addressSet = lastKnownLedger.accountAddresses.toMutableSet()
|
||||
val accountAddress = AccountAddress.parseWithIndexes(subAddress)
|
||||
if (addressSet.add(accountAddress)) {
|
||||
sendLedger(lastKnownLedger.copy(accountAddresses = addressSet))
|
||||
override fun onSubAddressListUpdated(subAddresses: Array<String>) {
|
||||
val accountsUpdated = parseAndAggregateAddresses(subAddresses)
|
||||
if (lastKnownLedger.indexedAccounts != accountsUpdated) {
|
||||
sendLedger(lastKnownLedger.copy(indexedAccounts = accountsUpdated))
|
||||
}
|
||||
}
|
||||
|
||||
@ -205,12 +240,14 @@ class NoSuchAccountException(private val accountIndex: Int) : NoSuchElementExcep
|
||||
}
|
||||
|
||||
private abstract class BaseWalletCallbacks : IWalletCallbacks.Stub() {
|
||||
override fun onAddressReady(subAddresses: Array<String>) = Unit
|
||||
|
||||
override fun onRefreshResult(blockchainTime: BlockchainTime, status: Int) = Unit
|
||||
|
||||
override fun onCommitResult(success: Boolean) = Unit
|
||||
|
||||
override fun onSubAddressReady(subAddress: String) = Unit
|
||||
|
||||
override fun onSubAddressListReceived(subAddresses: Array<String>) = Unit
|
||||
|
||||
override fun onFeesReceived(fees: LongArray?) = Unit
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,9 @@ sealed interface PublicAddress : Parcelable {
|
||||
fun isSubAddress(): Boolean
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* @throws InvalidAddress
|
||||
*/
|
||||
fun parse(addressString: String): PublicAddress {
|
||||
val decoded = try {
|
||||
addressString.decodeBase58()
|
||||
@ -39,7 +42,9 @@ sealed interface PublicAddress : Parcelable {
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidAddress(message: String, cause: Throwable? = null) : Exception(message, cause)
|
||||
// TODO: Extend ParseException
|
||||
class InvalidAddress(message: String, cause: Throwable? = null) :
|
||||
IllegalArgumentException(message, cause)
|
||||
|
||||
@Parcelize
|
||||
data class StandardAddress(
|
||||
|
15
lib/android/src/main/kotlin/im/molly/monero/WalletAccount.kt
Normal file
15
lib/android/src/main/kotlin/im/molly/monero/WalletAccount.kt
Normal file
@ -0,0 +1,15 @@
|
||||
package im.molly.monero
|
||||
|
||||
data class WalletAccount(
|
||||
val addresses: List<AccountAddress>,
|
||||
val accountIndex: Int,
|
||||
)
|
||||
|
||||
fun Iterable<WalletAccount>.findAddressByIndex(
|
||||
accountIndex: Int,
|
||||
subAddressIndex: Int = 0,
|
||||
): AccountAddress? {
|
||||
return flatMap { it.addresses }.find {
|
||||
it.accountIndex == accountIndex && it.subAddressIndex == subAddressIndex
|
||||
}
|
||||
}
|
@ -122,10 +122,13 @@ internal class WalletNative private constructor(
|
||||
val currentBalance: Balance
|
||||
get() = TODO() // txHistorySnapshot().consolidateTransactions().second.balance()
|
||||
|
||||
val subAddresses: Array<String>
|
||||
get() = nativeGetSubAddresses(handle)
|
||||
private fun getSubAddresses(accountIndex: Int? = null): Array<String> {
|
||||
return nativeGetSubAddresses(accountIndex ?: -1, handle)
|
||||
}
|
||||
|
||||
private fun txHistorySnapshot(): List<TxInfo> = nativeGetTxHistory(handle).toList()
|
||||
private fun getTxHistorySnapshot(): List<TxInfo> {
|
||||
return nativeGetTxHistory(handle).toList()
|
||||
}
|
||||
|
||||
@GuardedBy("listenersLock")
|
||||
private val balanceListeners = mutableSetOf<IBalanceListener>()
|
||||
@ -224,9 +227,12 @@ internal class WalletNative private constructor(
|
||||
* Also replays the last known balance whenever a new listener registers.
|
||||
*/
|
||||
override fun addBalanceListener(listener: IBalanceListener) {
|
||||
val txHistory = getTxHistorySnapshot()
|
||||
val subAddresses = getSubAddresses()
|
||||
|
||||
balanceListenersLock.withLock {
|
||||
balanceListeners.add(listener)
|
||||
listener.onBalanceChanged(txHistorySnapshot(), subAddresses, currentBlockchainTime)
|
||||
listener.onBalanceChanged(txHistory, subAddresses, currentBlockchainTime)
|
||||
}
|
||||
}
|
||||
|
||||
@ -249,8 +255,8 @@ internal class WalletNative private constructor(
|
||||
|
||||
override fun createAccount(callback: IWalletCallbacks?) {
|
||||
scope.launch(ioDispatcher) {
|
||||
val subAddress = nativeCreateSubAddressAccount(handle)
|
||||
notifyAddressCreation(subAddress, callback)
|
||||
val primaryAddress = nativeCreateSubAddressAccount(handle)
|
||||
notifyAddressCreation(primaryAddress, callback)
|
||||
}
|
||||
}
|
||||
|
||||
@ -260,23 +266,37 @@ internal class WalletNative private constructor(
|
||||
if (subAddress != null) {
|
||||
notifyAddressCreation(subAddress, callback)
|
||||
} else {
|
||||
callback?.onAddressReady(emptyArray())
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyAddressCreation(subAddress: String, callback: IWalletCallbacks?) {
|
||||
balanceListenersLock.withLock {
|
||||
balanceListeners.forEach { listener ->
|
||||
listener.onAddressCreated(subAddress)
|
||||
if (balanceListeners.isNotEmpty()) {
|
||||
val subAddresses = getSubAddresses()
|
||||
balanceListeners.forEach { listener ->
|
||||
listener.onSubAddressListUpdated(subAddresses)
|
||||
}
|
||||
}
|
||||
}
|
||||
callback?.onSubAddressReady(subAddress)
|
||||
}
|
||||
|
||||
override fun getAddressesForAccount(accountIndex: Int, callback: IWalletCallbacks) {
|
||||
scope.launch(ioDispatcher) {
|
||||
val accountSubAddresses = getSubAddresses(accountIndex)
|
||||
if (accountSubAddresses.isNotEmpty()) {
|
||||
callback.onSubAddressListReceived(accountSubAddresses)
|
||||
} else {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
callback?.onAddressReady(arrayOf(subAddress))
|
||||
}
|
||||
|
||||
override fun getAllAddresses(callback: IWalletCallbacks) {
|
||||
scope.launch(ioDispatcher) {
|
||||
callback.onAddressReady(subAddresses)
|
||||
callback.onSubAddressListReceived(getSubAddresses())
|
||||
}
|
||||
}
|
||||
|
||||
@ -286,7 +306,8 @@ internal class WalletNative private constructor(
|
||||
if (balanceListeners.isNotEmpty()) {
|
||||
val blockchainTime = network.blockchainTime(height, timestamp)
|
||||
val call = if (balanceChanged) {
|
||||
val txHistory = txHistorySnapshot()
|
||||
val txHistory = getTxHistorySnapshot()
|
||||
val subAddresses = getSubAddresses()
|
||||
fun(listener: IBalanceListener) {
|
||||
listener.onBalanceChanged(txHistory, subAddresses, blockchainTime)
|
||||
}
|
||||
@ -396,7 +417,7 @@ internal class WalletNative private constructor(
|
||||
private external fun nativeGetPublicAddress(handle: Long): String
|
||||
private external fun nativeGetCurrentBlockchainHeight(handle: Long): Int
|
||||
private external fun nativeGetCurrentBlockchainTimestamp(handle: Long): Long
|
||||
private external fun nativeGetSubAddresses(handle: Long): Array<String>
|
||||
private external fun nativeGetSubAddresses(subAddressMajor: Int, handle: Long): Array<String>
|
||||
private external fun nativeGetTxHistory(handle: Long): Array<TxInfo>
|
||||
private external fun nativeFetchBaseFeeEstimate(handle: Long): LongArray
|
||||
private external fun nativeLoad(handle: Long, fd: Int): Boolean
|
||||
|
@ -1,12 +1,12 @@
|
||||
package im.molly.monero.internal
|
||||
|
||||
import android.os.Parcelable
|
||||
import im.molly.monero.AccountAddress
|
||||
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.WalletAccount
|
||||
import im.molly.monero.MoneroAmount
|
||||
import im.molly.monero.PaymentDetail
|
||||
import im.molly.monero.PublicAddress
|
||||
@ -15,7 +15,7 @@ import im.molly.monero.TimeLocked
|
||||
import im.molly.monero.Transaction
|
||||
import im.molly.monero.TxState
|
||||
import im.molly.monero.UnlockTime
|
||||
import im.molly.monero.findByIndexes
|
||||
import im.molly.monero.findAddressByIndex
|
||||
import im.molly.monero.isBlockHeightInRange
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.time.Instant
|
||||
@ -62,12 +62,12 @@ internal data class TxInfo @CalledByNative constructor(
|
||||
}
|
||||
|
||||
internal fun List<TxInfo>.consolidateTransactions(
|
||||
accountAddresses: Set<AccountAddress>,
|
||||
accounts: List<WalletAccount>,
|
||||
blockchainContext: BlockchainTime,
|
||||
): Pair<Map<String, Transaction>, Set<TimeLocked<Enote>>> {
|
||||
// Extract enotes from incoming transactions
|
||||
val allEnotes =
|
||||
filter { it.incoming }.map { it.toEnote(blockchainContext.height, accountAddresses) }
|
||||
filter { it.incoming }.map { it.toEnote(blockchainContext.height, accounts) }
|
||||
|
||||
val enoteByTxId = allEnotes.groupBy { enote -> enote.sourceTxId!! }
|
||||
|
||||
@ -152,9 +152,9 @@ private fun List<TxInfo>.determineTxState(): TxState {
|
||||
}
|
||||
}
|
||||
|
||||
private fun TxInfo.toEnote(blockchainHeight: Int, accountAddresses: Set<AccountAddress>): Enote {
|
||||
val ownerAddress = accountAddresses.findByIndexes(subAddressMajor, subAddressMinor)
|
||||
?: error("Failed to find account address for: $subAddressMajor/$subAddressMinor")
|
||||
private fun TxInfo.toEnote(blockchainHeight: Int, accounts: List<WalletAccount>): Enote {
|
||||
val ownerAddress = accounts.findAddressByIndex(subAddressMajor, subAddressMinor)
|
||||
?: error("Failed to find subaddress: $subAddressMajor/$subAddressMinor")
|
||||
val calculatedAge = if (height > 0) (blockchainHeight - height + 1) else 0
|
||||
|
||||
return Enote(
|
||||
|
Loading…
Reference in New Issue
Block a user