mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2024-10-01 03:45:36 -04:00
lib: add full subaddress support
This commit is contained in:
parent
c12ae593ae
commit
147989ba3f
@ -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,
|
||||
|
@ -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(),
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -4,7 +4,5 @@ class PendingTransfer internal constructor(
|
||||
private val pendingTransfer: IPendingTransfer,
|
||||
) : AutoCloseable {
|
||||
|
||||
override fun close() {
|
||||
pendingTransfer.close()
|
||||
}
|
||||
override fun close() = pendingTransfer.close()
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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
2
vendor/monero
vendored
@ -1 +1 @@
|
||||
Subproject commit 6063fbeb1414eab1c027ee57b4f2834bb178af7e
|
||||
Subproject commit 36d6a9c4c68a9c787a14d419edc05384fe1c506d
|
Loading…
Reference in New Issue
Block a user