lib: add full subaddress support

This commit is contained in:
Oscar Mira 2024-02-26 17:17:06 +01:00
parent c12ae593ae
commit 147989ba3f
20 changed files with 373 additions and 83 deletions

View File

@ -41,7 +41,7 @@ class WalletRepository(
},
httpClient = httpClient,
)
check(config.publicAddress == wallet.primaryAddress.address) {
check(config.publicAddress == wallet.publicAddress.address) {
"primary address mismatch"
}
wallet
@ -71,7 +71,7 @@ class WalletRepository(
val uniqueFilename = UUID.randomUUID().toString()
val wallet = moneroSdkClient.createWallet(moneroNetwork, uniqueFilename)
val walletId = walletDataSource.createWalletConfig(
publicAddress = wallet.primaryAddress.address,
publicAddress = wallet.publicAddress.address,
filename = uniqueFilename,
name = name,
remoteNodeIds = remoteNodeIds,
@ -94,7 +94,7 @@ class WalletRepository(
restorePoint,
)
val walletId = walletDataSource.createWalletConfig(
publicAddress = wallet.primaryAddress.address,
publicAddress = wallet.publicAddress.address,
filename = uniqueFilename,
name = name,
remoteNodeIds = remoteNodeIds,

View File

@ -21,7 +21,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import im.molly.monero.Ledger
import im.molly.monero.MoneroCurrency
import im.molly.monero.PublicAddress
import im.molly.monero.demo.data.model.WalletConfig
import im.molly.monero.demo.ui.component.CopyableText
import im.molly.monero.demo.ui.component.Toolbar
@ -279,12 +278,12 @@ private fun WalletScreenPopulated(
uiState = WalletUiState.Loaded(
config = WalletConfig(
id = 0,
publicAddress = ledger.primaryAddress.address,
publicAddress = ledger.publicAddress.address,
filename = "",
name = "Personal",
remoteNodes = emptySet(),
),
network = ledger.primaryAddress.network,
network = ledger.publicAddress.network,
balance = ledger.balance,
blockchainTime = ledger.checkedAt,
transactions = emptyList(),

View File

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

View File

@ -27,7 +27,9 @@ object PreviewParameterData {
hash = HashDigest("e7a60483591378d536792d070f2bf6ccb7d0666df03b57f485ddaf66899a294b"),
state = TxState.OnChain(blockHeader),
network = network,
timeLock = UnlockTime.Block(BlockchainTime(2999850, Instant.ofEpochSecond(1697792826), network)),
timeLock = UnlockTime.Block(
BlockchainTime(2999850, Instant.ofEpochSecond(1697792826), network)
),
sent = emptySet(),
received = emptySet(),
payments = listOf(PaymentDetail((0.10).xmr, recipients.first())),
@ -37,9 +39,10 @@ object PreviewParameterData {
)
val ledger = Ledger(
primaryAddress = PublicAddress.parse("4AYjQM9HoAFNUeC3cvSfgeAN89oMMpMqiByvunzSzhn97cj726rJj3x8hCbH58UnMqQJShczCxbpWRiCJQ3HCUDHLiKuo4T"),
checkedAt = BlockchainTime(blockHeader = blockHeader, network = network),
enotes = emptySet(),
publicAddress = PublicAddress.parse("4AYjQM9HoAFNUeC3cvSfgeAN89oMMpMqiByvunzSzhn97cj726rJj3x8hCbH58UnMqQJShczCxbpWRiCJQ3HCUDHLiKuo4T"),
accountAddresses = emptySet(),
transactions = transactions.associateBy { it.txId },
enotes = emptySet(),
checkedAt = BlockchainTime(blockHeader = blockHeader, network = network),
)
}

View File

@ -4,6 +4,7 @@ import im.molly.monero.BlockchainTime;
import im.molly.monero.internal.TxInfo;
oneway interface IBalanceListener {
void onBalanceChanged(in List<TxInfo> txHistory, in BlockchainTime blockchainTime);
void onBalanceChanged(in List<TxInfo> txHistory, in String[] subAddresses, in BlockchainTime blockchainTime);
void onRefresh(in BlockchainTime blockchainTime);
void onAddressCreated(String subAddress);
}

View File

@ -7,9 +7,13 @@ import im.molly.monero.PaymentRequest;
import im.molly.monero.SweepRequest;
interface IWallet {
String getAccountPrimaryAddress();
String getPublicAddress();
void addBalanceListener(in IBalanceListener listener);
void removeBalanceListener(in IBalanceListener listener);
oneway void getOrCreateAddress(int accountIndex, int subAddressIndex, in IWalletCallbacks callback);
oneway void createAccount(in IWalletCallbacks callback);
oneway void createSubAddressForAccount(int accountIndex, in IWalletCallbacks callback);
oneway void getAllAddresses(in IWalletCallbacks callback);
oneway void resumeRefresh(boolean skipCoinbase, in IWalletCallbacks callback);
oneway void cancelRefresh();
oneway void setRefreshSince(long heightOrTimestamp);

View File

@ -3,6 +3,7 @@ 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 onFeesReceived(in long[] fees);

View File

@ -108,6 +108,7 @@ bool Wallet::parseFrom(std::istream& input) {
return false;
if (!serialization::serialize(ar, m_wallet))
return false;
updateSubaddressMap(m_subaddresses);
captureTxHistorySnapshot(m_tx_history);
m_account_ready = true;
return true;
@ -126,10 +127,42 @@ bool Wallet::writeTo(std::ostream& output) {
});
}
template<typename Consumer>
void Wallet::withTxHistory(Consumer consumer) {
std::lock_guard<std::mutex> lock(m_tx_history_mutex);
consumer(m_tx_history);
std::string FormatAccountAddress(
const std::pair<cryptonote::subaddress_index, std::string>& pair) {
std::stringstream ss;
ss << pair.first.major << "/" << pair.first.minor << "/" << pair.second;
return ss.str();
}
std::string Wallet::createSubAddressAccount() {
return suspendRefreshAndRunLocked([&]() {
uint32_t index_major = m_wallet.get_num_subaddress_accounts();
m_wallet.add_subaddress_account("");
return addSubaddressInternal({index_major, 0});
});
}
std::string Wallet::createSubAddress(uint32_t index_major) {
return suspendRefreshAndRunLocked([&]() {
uint32_t index_minor = m_wallet.get_num_subaddresses(index_major);
m_wallet.add_subaddress(index_major, "");
return addSubaddressInternal({index_major, index_minor});
});
}
std::string Wallet::addSubAddress(uint32_t index_major, uint32_t index_minor) {
return suspendRefreshAndRunLocked([&]() {
cryptonote::subaddress_index index = {index_major, index_minor};
m_wallet.create_one_off_subaddress(index);
return addSubaddressInternal(index);
});
}
std::string Wallet::addSubaddressInternal(const cryptonote::subaddress_index& index) {
std::string subaddress = m_wallet.get_subaddress_as_str(index);
std::unique_lock<std::mutex> lock(m_subaddresses_mutex);
auto ret = m_subaddresses.insert({index, subaddress});
return FormatAccountAddress(*ret.first);
}
std::unique_ptr<PendingTransfer> Wallet::createPayment(
@ -139,6 +172,8 @@ std::unique_ptr<PendingTransfer> Wallet::createPayment(
int priority,
uint32_t account_index,
const std::set<uint32_t>& subaddr_indexes) {
std::unique_lock<std::mutex> wallet_lock(m_wallet_mutex);
std::vector<cryptonote::tx_destination_entry> dsts;
dsts.reserve(addresses.size());
@ -170,6 +205,12 @@ std::unique_ptr<PendingTransfer> Wallet::createPayment(
return std::make_unique<PendingTransfer>(ptxs);
}
template<typename Consumer>
void Wallet::withTxHistory(Consumer consumer) {
std::lock_guard<std::mutex> lock(m_tx_history_mutex);
consumer(m_tx_history);
}
std::vector<uint64_t> Wallet::fetchBaseFeeEstimate() {
return m_wallet.get_dynamic_base_fee_scaling_estimate();
}
@ -179,6 +220,19 @@ std::string Wallet::public_address() const {
return account.get_public_address_str(m_wallet.nettype());
}
std::vector<std::string> Wallet::formatted_subaddresses() {
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));
}
return ret;
}
cryptonote::account_base& Wallet::require_account() {
LOG_FATAL_IF(!m_account_ready, "Account is not initialized");
return m_wallet.get_account();
@ -244,7 +298,6 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
recv.m_key_image_known = td.m_key_image_known;
recv.m_subaddress_major = td.m_subaddr_index.major;
recv.m_subaddress_minor = td.m_subaddr_index.minor;
recv.m_recipient = m_wallet.get_subaddress_as_str(td.m_subaddr_index);
recv.m_amount = td.m_amount;
recv.m_unlock_time = td.m_tx.unlock_time;
@ -310,7 +363,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();
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;
recv.m_amount = dest.amount;
@ -335,7 +387,6 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
if (utx.m_change > 0) {
snapshot.emplace_back(pair.first, TxInfo::INCOMING);
TxInfo& change = snapshot.back();
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
change.m_amount = utx.m_change;
@ -365,7 +416,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();
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;
recv.m_amount = amount;
@ -379,6 +429,23 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
}
}
// Only call this function from the callback thread or during initialization.
void Wallet::updateSubaddressMap(std::map<cryptonote::subaddress_index, std::string>& map) {
uint32_t num_accounts = m_wallet.get_num_subaddress_accounts();
for (uint32_t index_major = 0; index_major < num_accounts; ++index_major) {
uint32_t num_subaddresses = m_wallet.get_num_subaddresses(index_major);
for (uint32_t index_minor = 0; index_minor < num_subaddresses; ++index_minor) {
cryptonote::subaddress_index index = {index_major, index_minor};
if (map.find(index) == map.end()) {
map[index] = m_wallet.get_subaddress_as_str(index);
}
}
}
}
void Wallet::handleNewBlock(uint64_t height, uint64_t timestamp) {
LOG_FATAL_IF(height >= CRYPTONOTE_MAX_BLOCK_NUMBER, "Blockchain max height reached");
m_last_block_height = height;
@ -396,6 +463,9 @@ void Wallet::handleMoneyEvent(uint64_t at_block_height) {
void Wallet::processBalanceChanges(bool refresh_running) {
if (m_balance_changed) {
m_subaddresses_mutex.lock();
updateSubaddressMap(m_subaddresses);
m_subaddresses_mutex.unlock();
m_tx_history_mutex.lock();
captureTxHistorySnapshot(m_tx_history);
m_tx_history_mutex.unlock();
@ -624,7 +694,7 @@ Java_im_molly_monero_WalletNative_nativeSetRefreshSince(
extern "C"
JNIEXPORT jstring JNICALL
Java_im_molly_monero_WalletNative_nativeGetAccountPrimaryAddress(
Java_im_molly_monero_WalletNative_nativeGetPublicAddress(
JNIEnv* env,
jobject thiz,
jlong handle) {
@ -632,6 +702,54 @@ Java_im_molly_monero_WalletNative_nativeGetAccountPrimaryAddress(
return NativeToJavaString(env, wallet->public_address());
}
extern "C"
JNIEXPORT jstring JNICALL
Java_im_molly_monero_WalletNative_nativeAddSubAddress(
JNIEnv* env,
jobject thiz,
jlong handle,
jint sub_address_major,
jint sub_address_minor) {
auto* wallet = reinterpret_cast<Wallet*>(handle);
return NativeToJavaString(
env, wallet->addSubAddress(sub_address_major, sub_address_minor));
}
extern "C"
JNIEXPORT jstring JNICALL
Java_im_molly_monero_WalletNative_nativeCreateSubAddressAccount(
JNIEnv* env,
jobject thiz,
jlong handle) {
auto* wallet = reinterpret_cast<Wallet*>(handle);
return NativeToJavaString(env, wallet->createSubAddressAccount());
}
extern "C"
JNIEXPORT jstring JNICALL
Java_im_molly_monero_WalletNative_nativeCreateSubAddress(
JNIEnv* env,
jobject thiz,
jlong handle,
jint sub_address_major) {
auto* wallet = reinterpret_cast<Wallet*>(handle);
try {
return NativeToJavaString(env, wallet->createSubAddress(sub_address_major));
} catch (error::account_index_outofbound& e) {
return nullptr;
}
}
extern "C"
JNIEXPORT jobjectArray JNICALL
Java_im_molly_monero_WalletNative_nativeGetSubAddresses(
JNIEnv* env,
jobject thiz,
jlong handle) {
auto* wallet = reinterpret_cast<Wallet*>(handle);
return NativeToJavaStringArray(env, wallet->formatted_subaddresses());
}
extern "C"
JNIEXPORT jint JNICALL
Java_im_molly_monero_WalletNative_nativeGetCurrentBlockchainHeight(

View File

@ -94,8 +94,9 @@ class Wallet : i_wallet2_callback {
void cancelRefresh();
void setRefreshSince(long height_or_timestamp);
template<typename Consumer>
void withTxHistory(Consumer consumer);
std::string createSubAddressAccount();
std::string createSubAddress(uint32_t index_major);
std::string addSubAddress(uint32_t index_major, uint32_t index_minor);
std::unique_ptr<PendingTransfer> createPayment(
const std::vector<std::string>& addresses,
@ -105,9 +106,13 @@ class Wallet : i_wallet2_callback {
uint32_t account_index,
const std::set<uint32_t>& subaddr_indexes);
template<typename Consumer>
void withTxHistory(Consumer consumer);
std::vector<uint64_t> fetchBaseFeeEstimate();
std::string public_address() const;
std::vector<std::string> formatted_subaddresses();
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; }
@ -130,13 +135,15 @@ class Wallet : i_wallet2_callback {
uint64_t m_last_block_height;
uint64_t m_last_block_timestamp;
std::map<cryptonote::subaddress_index, std::string> m_subaddresses;
// Saved transaction history.
std::vector<TxInfo> m_tx_history;
// Protects access to m_wallet instance and state fields.
std::mutex m_wallet_mutex;
std::mutex m_tx_history_mutex;
std::mutex m_refresh_mutex;
std::mutex m_subaddresses_mutex;
// Reference to Kotlin wallet instance.
const ScopedJavaGlobalRef<jobject> m_callback;
@ -153,7 +160,9 @@ class Wallet : i_wallet2_callback {
auto suspendRefreshAndRunLocked(T block) -> decltype(block());
void captureTxHistorySnapshot(std::vector<TxInfo>& snapshot);
void handleNewBlock(uint64_t height, uint64_t timestmap);
void updateSubaddressMap(std::map<cryptonote::subaddress_index, std::string>& map);
std::string addSubaddressInternal(const cryptonote::subaddress_index& index);
void handleNewBlock(uint64_t height, uint64_t timestamp);
void handleReorgEvent(uint64_t at_block_height);
void handleMoneyEvent(uint64_t at_block_height);

View File

@ -1,8 +1,8 @@
package im.molly.monero
import android.os.Parcel
import android.os.Parcelable
import android.annotation.SuppressLint
@SuppressLint("ParcelCreator")
data class AccountAddress(
val publicAddress: PublicAddress,
val accountIndex: Int = 0,
@ -10,12 +10,12 @@ data class AccountAddress(
) : PublicAddress by publicAddress {
val isPrimaryAddress: Boolean
get() = accountIndex == 0 && subAddressIndex == 0
get() = subAddressIndex == 0
init {
when (publicAddress) {
is StandardAddress -> require(isPrimaryAddress) {
"Only the primary address is a standard address"
is StandardAddress -> require(accountIndex == 0 && subAddressIndex == 0) {
"Only the account address 0/0 is a standard address"
}
is SubAddress -> require(accountIndex != -1 && subAddressIndex != -1) {
@ -25,4 +25,24 @@ data class AccountAddress(
else -> throw IllegalArgumentException("Unsupported address type")
}
}
companion object {
fun parseWithIndexes(addressString: String): AccountAddress {
val parts = addressString.split("/")
require(parts.size == 3) { "Invalid account address format" }
val accountIndex = parts[0].toInt()
val subAddressIndex = parts[1].toInt()
val publicAddress = PublicAddress.parse(parts[2])
return AccountAddress(publicAddress, accountIndex, subAddressIndex)
}
}
override fun toString(): String = "$accountIndex/$subAddressIndex/$publicAddress"
}
fun Iterable<AccountAddress>.findByIndexes(
accountIndex: Int,
subAddressIndex: Int,
): AccountAddress? {
return find { it.accountIndex == accountIndex && it.subAddressIndex == subAddressIndex }
}

View File

@ -24,10 +24,10 @@ data class Balance(
}
fun Iterable<TimeLocked<Enote>>.calculateBalance(): Balance {
var pendingAmount = MoneroAmount.ZERO
val lockedAmounts = mutableListOf<TimeLocked<MoneroAmount>>()
var pendingAmount = MoneroAmount.ZERO
for (timeLocked in filter { !it.value.spent }) {
if (timeLocked.value.age == 0) {
pendingAmount += timeLocked.value.amount

View File

@ -3,7 +3,8 @@ package im.molly.monero
//import im.molly.monero.proto.LedgerProto
data class Ledger(
val primaryAddress: PublicAddress,
val publicAddress: PublicAddress,
val accountAddresses: Set<AccountAddress>,
val transactions: Map<String, Transaction>,
val enotes: Set<TimeLocked<Enote>>,
val checkedAt: BlockchainTime,

View File

@ -21,10 +21,59 @@ class MoneroWallet internal constructor(
private val logger = loggerFor<MoneroWallet>()
val primaryAddress: PublicAddress = PublicAddress.parse(wallet.accountPrimaryAddress)
val publicAddress: PublicAddress = PublicAddress.parse(wallet.publicAddress)
var dataStore by storageAdapter::dataStore
suspend fun getOrCreateAddress(accountIndex: Int, subAddressIndex: Int): AccountAddress =
suspendCancellableCoroutine { continuation ->
wallet.getOrCreateAddress(
accountIndex,
subAddressIndex,
object : BaseWalletCallbacks() {
override fun onAddressReady(subAddresses: Array<String>) {
val accountAddress = AccountAddress.parseWithIndexes(subAddresses[0])
continuation.resume(accountAddress) {}
}
})
}
suspend fun createAccount(): AccountAddress =
suspendCancellableCoroutine { continuation ->
wallet.createAccount(object : BaseWalletCallbacks() {
override fun onAddressReady(subAddresses: Array<String>) {
val accountAddress = AccountAddress.parseWithIndexes(subAddresses[0])
continuation.resume(accountAddress) {}
}
})
}
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) {}
}
})
}
suspend fun getAllAddresses(): Set<AccountAddress> =
suspendCancellableCoroutine { continuation ->
wallet.getAllAddresses(object : BaseWalletCallbacks() {
override fun onAddressReady(subAddresses: Array<String>) {
continuation.resume(subAddresses.toAccountAddresses()) {}
}
})
}
private fun Array<String>.toAccountAddresses(): Set<AccountAddress> {
return map { AccountAddress.parseWithIndexes(it) }.toSet()
}
/**
* A [Flow] of ledger changes.
*/
@ -32,9 +81,23 @@ class MoneroWallet internal constructor(
val listener = object : IBalanceListener.Stub() {
lateinit var lastKnownLedger: Ledger
override fun onBalanceChanged(txHistory: List<TxInfo>, blockchainTime: BlockchainTime) {
val (txs, spendableEnotes) = txHistory.consolidateTransactions(blockchainTime)
lastKnownLedger = Ledger(primaryAddress, txs, spendableEnotes, blockchainTime)
override fun onBalanceChanged(
txHistory: MutableList<TxInfo>,
subAddresses: Array<String>,
blockchainTime: BlockchainTime,
) {
val accountAddresses = subAddresses.toAccountAddresses()
val (txById, enotes) = txHistory.consolidateTransactions(
accountAddresses = accountAddresses,
blockchainContext = blockchainTime,
)
lastKnownLedger = Ledger(
publicAddress = publicAddress,
accountAddresses = accountAddresses,
transactions = txById,
enotes = enotes,
checkedAt = blockchainTime,
)
sendLedger(lastKnownLedger)
}
@ -42,6 +105,14 @@ 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))
}
}
private fun sendLedger(ledger: Ledger) {
trySend(ledger).onFailure {
logger.e("Too many ledger updates, channel capacity exceeded", it)
@ -126,7 +197,14 @@ class MoneroWallet internal constructor(
override fun close() = wallet.close()
}
class NoSuchAccountException(private val accountIndex: Int) : NoSuchElementException() {
override val message: String
get() = "No account was found with the specified index: $accountIndex"
}
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

View File

@ -4,7 +4,5 @@ class PendingTransfer internal constructor(
private val pendingTransfer: IPendingTransfer,
) : AutoCloseable {
override fun close() {
pendingTransfer.close()
}
override fun close() = pendingTransfer.close()
}

View File

@ -13,9 +13,9 @@ sealed interface PublicAddress : Parcelable {
fun isSubAddress(): Boolean
companion object {
fun parse(publicAddress: String): PublicAddress {
fun parse(addressString: String): PublicAddress {
val decoded = try {
publicAddress.decodeBase58()
addressString.decodeBase58()
} catch (t: IllegalArgumentException) {
throw InvalidAddress("Base58 decoding error", t)
}
@ -25,10 +25,10 @@ sealed interface PublicAddress : Parcelable {
return when (val prefix = decoded[0].toLong()) {
in StandardAddress.prefixes -> {
StandardAddress(publicAddress, StandardAddress.prefixes[prefix]!!)
StandardAddress(addressString, StandardAddress.prefixes[prefix]!!)
}
in SubAddress.prefixes -> {
SubAddress(publicAddress, SubAddress.prefixes[prefix]!!)
SubAddress(addressString, SubAddress.prefixes[prefix]!!)
}
in IntegratedAddress.prefixes -> {
TODO()

View File

@ -16,7 +16,7 @@ import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
// TODO: Hide IRemoteNodeClient methods and rename to HttpRpcClient
// TODO: Hide IRemoteNodeClient methods and rename to HttpRpcClient or MoneroNodeClient
class RemoteNodeClient private constructor(
val network: MoneroNetwork,
private val loadBalancer: LoadBalancer,

View File

@ -102,7 +102,7 @@ internal class WalletNative private constructor(
}
}
override fun getAccountPrimaryAddress() = nativeGetAccountPrimaryAddress(handle)
override fun getPublicAddress() = nativeGetPublicAddress(handle)
private fun MoneroNetwork.blockchainTime(height: Int, epochSecond: Long): BlockchainTime {
// Block timestamp could be zero during a fast refresh.
@ -122,6 +122,9 @@ internal class WalletNative private constructor(
val currentBalance: Balance
get() = TODO() // txHistorySnapshot().consolidateTransactions().second.balance()
val subAddresses: Array<String>
get() = nativeGetSubAddresses(handle)
private fun txHistorySnapshot(): List<TxInfo> = nativeGetTxHistory(handle).toList()
@GuardedBy("listenersLock")
@ -170,20 +173,22 @@ internal class WalletNative private constructor(
}
override fun createPayment(request: PaymentRequest, callback: ITransferRequestCallback) {
val (amounts, addresses) = request.paymentDetails.map {
it.amount.atomicUnits to it.recipientAddress.address
}.unzip()
scope.launch(singleThreadedDispatcher) {
val (amounts, addresses) = request.paymentDetails.map {
it.amount.atomicUnits to it.recipientAddress.address
}.unzip()
nativeCreatePayment(
handle = handle,
addresses = addresses.toTypedArray(),
amounts = amounts.toLongArray(),
timeLock = request.timeLock?.blockchainTime?.toLong() ?: 0,
priority = request.feePriority?.priority ?: 0,
accountIndex = 0,
subAddressIndexes = IntArray(0),
callback = callback,
)
nativeCreatePayment(
handle = handle,
addresses = addresses.toTypedArray(),
amounts = amounts.toLongArray(),
timeLock = request.timeLock?.blockchainTime?.toLong() ?: 0,
priority = request.feePriority?.priority ?: 0,
accountIndex = 0,
subAddressIndexes = IntArray(0),
callback = callback,
)
}
}
override fun createSweep(request: SweepRequest, callback: ITransferRequestCallback) {
@ -221,7 +226,7 @@ internal class WalletNative private constructor(
override fun addBalanceListener(listener: IBalanceListener) {
balanceListenersLock.withLock {
balanceListeners.add(listener)
listener.onBalanceChanged(txHistorySnapshot(), currentBlockchainTime)
listener.onBalanceChanged(txHistorySnapshot(), subAddresses, currentBlockchainTime)
}
}
@ -231,15 +236,62 @@ internal class WalletNative private constructor(
}
}
override fun getOrCreateAddress(
accountIndex: Int,
subAddressIndex: Int,
callback: IWalletCallbacks?,
) {
scope.launch(ioDispatcher) {
val subAddress = nativeAddSubAddress(handle, accountIndex, subAddressIndex)
notifyAddressCreation(subAddress, callback)
}
}
override fun createAccount(callback: IWalletCallbacks?) {
scope.launch(ioDispatcher) {
val subAddress = nativeCreateSubAddressAccount(handle)
notifyAddressCreation(subAddress, callback)
}
}
override fun createSubAddressForAccount(accountIndex: Int, callback: IWalletCallbacks?) {
scope.launch(ioDispatcher) {
val subAddress = nativeCreateSubAddress(handle, accountIndex)
if (subAddress != null) {
notifyAddressCreation(subAddress, callback)
} else {
callback?.onAddressReady(emptyArray())
}
}
}
private fun notifyAddressCreation(subAddress: String, callback: IWalletCallbacks?) {
balanceListenersLock.withLock {
balanceListeners.forEach { listener ->
listener.onAddressCreated(subAddress)
}
}
callback?.onAddressReady(arrayOf(subAddress))
}
override fun getAllAddresses(callback: IWalletCallbacks) {
scope.launch(ioDispatcher) {
callback.onAddressReady(subAddresses)
}
}
@CalledByNative
private fun onRefresh(height: Int, timestamp: Long, balanceChanged: Boolean) {
balanceListenersLock.withLock {
if (balanceListeners.isNotEmpty()) {
val call = fun(listener: IBalanceListener) {
val blockchainTime = network.blockchainTime(height, timestamp)
if (balanceChanged) {
listener.onBalanceChanged(txHistorySnapshot(), blockchainTime)
} else {
val blockchainTime = network.blockchainTime(height, timestamp)
val call = if (balanceChanged) {
val txHistory = txHistorySnapshot()
fun(listener: IBalanceListener) {
listener.onBalanceChanged(txHistory, subAddresses, blockchainTime)
}
} else {
fun(listener: IBalanceListener) {
listener.onRefresh(blockchainTime)
}
}
@ -318,6 +370,12 @@ internal class WalletNative private constructor(
const val REFRESH_ERROR: Int = 3
}
private external fun nativeAddSubAddress(
handle: Long,
subAddressMajor: Int,
subAddressMinor: Int,
): String
private external fun nativeCancelRefresh(handle: Long)
private external fun nativeCreate(networkId: Int): Long
private external fun nativeCreatePayment(
@ -331,14 +389,15 @@ internal class WalletNative private constructor(
callback: ITransferRequestCallback,
)
private external fun nativeCreateSubAddressAccount(handle: Long): String
private external fun nativeCreateSubAddress(handle: Long, subAddressMajor: Int): String?
private external fun nativeDispose(handle: Long)
private external fun nativeDisposePendingTransfer(handle: Long)
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 nativeGetTxHistory(handle: Long): Array<TxInfo>
private external fun nativeGetAccountPrimaryAddress(handle: Long): String
// private external fun nativeGetAccountSubAddress(handle: Long, accountIndex: Int, subAddressIndex: Int): String
private external fun nativeFetchBaseFeeEstimate(handle: Long): LongArray
private external fun nativeLoad(handle: Long, fd: Int): Boolean
private external fun nativeNonReentrantRefresh(handle: Long, skipCoinbase: Boolean): Int

View File

@ -15,6 +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.isBlockHeightInRange
import kotlinx.parcelize.Parcelize
import java.time.Instant
@ -61,10 +62,12 @@ internal data class TxInfo @CalledByNative constructor(
}
internal fun List<TxInfo>.consolidateTransactions(
accountAddresses: Set<AccountAddress>,
blockchainContext: BlockchainTime,
): Pair<Map<String, Transaction>, Set<TimeLocked<Enote>>> {
// Extract enotes from incoming transactions
val allEnotes = filter { it.incoming }.map { it.toEnote(blockchainContext.height) }
val allEnotes =
filter { it.incoming }.map { it.toEnote(blockchainContext.height, accountAddresses) }
val enoteByTxId = allEnotes.groupBy { enote -> enote.sourceTxId!! }
@ -149,13 +152,9 @@ private fun List<TxInfo>.determineTxState(): TxState {
}
}
private fun TxInfo.toEnote(blockchainHeight: Int): Enote {
val ownerAddress = AccountAddress(
publicAddress = PublicAddress.parse(recipient!!),
accountIndex = subAddressMajor,
subAddressIndex = subAddressMinor
)
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")
val calculatedAge = if (height > 0) (blockchainHeight - height + 1) else 0
return Enote(

View File

@ -30,24 +30,24 @@ class LoadBalancer(
}
}
sealed interface RemoteNodeState {
sealed interface ConnectionState {
/**
* The remote node is currently online and able to handle requests.
*/
data class Online(val responseTime: Duration) : RemoteNodeState
data class Online(val responseTime: Duration) : ConnectionState
/**
* The client's request has timed out and no response has been received.
*/
data class Timeout(val cause: Throwable?)
data class Timeout(val cause: Throwable?) : ConnectionState
/**
* Indicates that an error occurred while processing the client's request to the remote node.
*/
// open data class Error(val message: String?) : RemoteNodeState {
sealed class Error(val message: String?) : ConnectionState
/**
* Indicates that the client is unauthorized to access the remote node, i.e. the client's credentials were invalid.
*/
// data class Unauthorized(override val message: String?) : Error
data object Unauthorized : Error("Unauthorized")
}

2
vendor/monero vendored

@ -1 +1 @@
Subproject commit 6063fbeb1414eab1c027ee57b4f2834bb178af7e
Subproject commit 36d6a9c4c68a9c787a14d419edc05384fe1c506d