lib: add basic transfer funcionality

This commit is contained in:
Oscar Mira 2024-02-23 11:49:59 +01:00
parent 5c4cfd9d92
commit c12ae593ae
30 changed files with 368 additions and 62 deletions

View File

@ -0,0 +1,8 @@
package im.molly.monero;
//import im.molly.monero.ITransactionCallback;
interface IPendingTransfer {
// oneway void submit(in ITransactionCallback callback);
void close();
}

View File

@ -0,0 +1,23 @@
package im.molly.monero;
import im.molly.monero.IPendingTransfer;
oneway interface ITransferRequestCallback {
void onTransferCreated(in IPendingTransfer pendingTransfer);
// void onDaemonBusy();
// void onNoConnectionToDaemon();
// void onRPCError(String errorMessage);
// void onFailedToGetOutputs();
// void onNotEnoughUnlockedMoney(long available, long sentAmount);
// void onNotEnoughMoney(long available, long sentAmount);
// void onTransactionNotPossible(long available, long transactionAmount, long fee);
// void onNotEnoughOutsToMix(int mixinCount, Map<Long, Long> scantyOuts);
// void onTransactionNotConstructed();
// void onTransactionRejected(String transactionHash, int status);
// void onTransactionSumOverflow(String errorMessage);
// void onZeroDestination();
// void onTransactionTooBig();
// void onTransferError(String errorMessage);
// void onWalletInternalError(String errorMessage);
// void onUnexpectedError(String errorMessage);
}

View File

@ -1,7 +1,10 @@
package im.molly.monero; package im.molly.monero;
import im.molly.monero.IBalanceListener; import im.molly.monero.IBalanceListener;
import im.molly.monero.ITransferRequestCallback;
import im.molly.monero.IWalletCallbacks; import im.molly.monero.IWalletCallbacks;
import im.molly.monero.PaymentRequest;
import im.molly.monero.SweepRequest;
interface IWallet { interface IWallet {
String getAccountPrimaryAddress(); String getAccountPrimaryAddress();
@ -11,6 +14,8 @@ interface IWallet {
oneway void cancelRefresh(); oneway void cancelRefresh();
oneway void setRefreshSince(long heightOrTimestamp); oneway void setRefreshSince(long heightOrTimestamp);
oneway void commit(in IWalletCallbacks callback); oneway void commit(in IWalletCallbacks callback);
oneway void createPayment(in PaymentRequest request, in ITransferRequestCallback callback);
oneway void createSweep(in SweepRequest request, in ITransferRequestCallback callback);
oneway void requestFees(in IWalletCallbacks callback); oneway void requestFees(in IWalletCallbacks callback);
void close(); void close();
} }

View File

@ -0,0 +1,3 @@
package im.molly.monero;
parcelable PaymentRequest;

View File

@ -0,0 +1,3 @@
package im.molly.monero;
parcelable SweepRequest;

View File

@ -69,6 +69,8 @@ set(WALLET2_SOURCES
src/hardforks/hardforks.cpp src/hardforks/hardforks.cpp
src/mnemonics/electrum-words.cpp src/mnemonics/electrum-words.cpp
src/multisig/multisig.cpp src/multisig/multisig.cpp
src/multisig/multisig_clsag_context.cpp
src/multisig/multisig_tx_builder_ringct.cpp
src/net/error.cpp src/net/error.cpp
src/net/http.cpp src/net/http.cpp
src/net/i2p_address.cpp src/net/i2p_address.cpp
@ -77,6 +79,7 @@ set(WALLET2_SOURCES
src/net/socks_connect.cpp src/net/socks_connect.cpp
src/net/tor_address.cpp src/net/tor_address.cpp
src/ringct/bulletproofs.cc src/ringct/bulletproofs.cc
src/ringct/bulletproofs_plus.cc
src/ringct/multiexp.cc src/ringct/multiexp.cc
src/ringct/rctCryptoOps.c src/ringct/rctCryptoOps.c
src/ringct/rctOps.cpp src/ringct/rctOps.cpp

View File

@ -13,9 +13,11 @@ void InitializeJniCache(JNIEnv* env);
extern jmethodID HttpResponse_getBody; extern jmethodID HttpResponse_getBody;
extern jmethodID HttpResponse_getCode; extern jmethodID HttpResponse_getCode;
extern jmethodID HttpResponse_getContentType; extern jmethodID HttpResponse_getContentType;
extern jmethodID ITransferRequestCb_onTransferCreated;
extern jmethodID Logger_logFromNative; extern jmethodID Logger_logFromNative;
extern jmethodID TxInfo_ctor; extern jmethodID TxInfo_ctor;
extern jmethodID WalletNative_callRemoteNode; extern jmethodID WalletNative_callRemoteNode;
extern jmethodID WalletNative_createPendingTransfer;
extern jmethodID WalletNative_onRefresh; extern jmethodID WalletNative_onRefresh;
extern jmethodID WalletNative_onSuspendRefresh; extern jmethodID WalletNative_onSuspendRefresh;
extern ScopedJavaGlobalRef<jclass> TxInfoClass; extern ScopedJavaGlobalRef<jclass> TxInfoClass;

View File

@ -0,0 +1,5 @@
#include "transfer.h"
namespace monero {
} // namespace monero

View File

@ -0,0 +1,21 @@
#ifndef WALLET_TRANSFER_H_
#define WALLET_TRANSFER_H_
#include "wallet2.h"
namespace monero {
using wallet2 = tools::wallet2;
class PendingTransfer {
public:
PendingTransfer(const std::vector<wallet2::pending_tx>& ptxs)
: m_ptxs(ptxs) {}
private:
std::vector<wallet2::pending_tx> m_ptxs;
};
} // namespace monero
#endif // WALLET_TRANSFER_H_

View File

@ -132,6 +132,44 @@ void Wallet::withTxHistory(Consumer consumer) {
consumer(m_tx_history); consumer(m_tx_history);
} }
std::unique_ptr<PendingTransfer> Wallet::createPayment(
const std::vector<std::string>& addresses,
const std::vector<uint64_t>& amounts,
uint64_t time_lock,
int priority,
uint32_t account_index,
const std::set<uint32_t>& subaddr_indexes) {
std::vector<cryptonote::tx_destination_entry> dsts;
dsts.reserve(addresses.size());
for (size_t i = 0; i < addresses.size(); ++i) {
const std::string& address = addresses[i];
cryptonote::address_parse_info info;
if (!cryptonote::get_account_address_from_str(info, m_wallet.nettype(), address)) {
LOG_FATAL("Failed to parse recipient address: %s", address.c_str());
}
LOG_FATAL_IF(info.has_payment_id);
cryptonote::tx_destination_entry de;
de.original = address;
de.addr = info.address;
de.amount = amounts.at(i);
de.is_subaddress = info.is_subaddress;
de.is_integrated = false;
dsts.push_back(de);
}
auto ptxs = m_wallet.create_transactions_2(
dsts,
m_wallet.default_mixin(),
time_lock,
priority,
{}, /* extra */
account_index,
subaddr_indexes);
return std::make_unique<PendingTransfer>(ptxs);
}
std::vector<uint64_t> Wallet::fetchBaseFeeEstimate() { std::vector<uint64_t> Wallet::fetchBaseFeeEstimate() {
return m_wallet.get_dynamic_base_fee_scaling_estimate(); return m_wallet.get_dynamic_base_fee_scaling_estimate();
} }
@ -146,8 +184,8 @@ cryptonote::account_base& Wallet::require_account() {
return m_wallet.get_account(); return m_wallet.get_account();
} }
const payment_details* find_payment_by_txid( const wallet2::payment_details* Find_payment_by_txid(
const std::list<std::pair<crypto::hash, payment_details>>& pds, const std::list<std::pair<crypto::hash, wallet2::payment_details>>& pds,
const crypto::hash& txid) { const crypto::hash& txid) {
if (txid == crypto::null_hash) { if (txid == crypto::null_hash) {
return nullptr; return nullptr;
@ -161,8 +199,8 @@ const payment_details* find_payment_by_txid(
return nullptr; return nullptr;
} }
const confirmed_transfer_details* find_transfer_by_txid( const wallet2::confirmed_transfer_details* Find_transfer_by_txid(
const std::list<std::pair<crypto::hash, confirmed_transfer_details>>& txs, const std::list<std::pair<crypto::hash, wallet2::confirmed_transfer_details>>& txs,
const crypto::hash& txid) { const crypto::hash& txid) {
if (txid == crypto::null_hash) { if (txid == crypto::null_hash) {
return nullptr; return nullptr;
@ -182,15 +220,15 @@ const confirmed_transfer_details* find_transfer_by_txid(
void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) { void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
snapshot.clear(); snapshot.clear();
std::vector<transfer_details> tds; std::vector<wallet2::transfer_details> tds;
m_wallet.get_transfers(tds); m_wallet.get_transfers(tds);
uint64_t min_height = 0; uint64_t min_height = 0;
std::list<std::pair<crypto::hash, payment_details>> pds; std::list<std::pair<crypto::hash, wallet2::payment_details>> pds;
std::list<std::pair<crypto::hash, pool_payment_details>> upds; std::list<std::pair<crypto::hash, wallet2::pool_payment_details>> upds;
std::list<std::pair<crypto::hash, confirmed_transfer_details>> txs; std::list<std::pair<crypto::hash, wallet2::confirmed_transfer_details>> txs;
std::list<std::pair<crypto::hash, unconfirmed_transfer_details>> utxs; std::list<std::pair<crypto::hash, wallet2::unconfirmed_transfer_details>> utxs;
m_wallet.get_payments(pds, min_height); m_wallet.get_payments(pds, min_height);
m_wallet.get_unconfirmed_payments(upds, min_height); m_wallet.get_unconfirmed_payments(upds, min_height);
m_wallet.get_payments_out(txs, min_height); m_wallet.get_payments_out(txs, min_height);
@ -211,13 +249,13 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
recv.m_unlock_time = td.m_tx.unlock_time; recv.m_unlock_time = td.m_tx.unlock_time;
// Check if the payment or transfer exists and update metadata if found. // Check if the payment or transfer exists and update metadata if found.
if (const auto* pd = find_payment_by_txid(pds, td.m_txid)) { if (const auto* pd = Find_payment_by_txid(pds, td.m_txid)) {
recv.m_height = pd->m_block_height; recv.m_height = pd->m_block_height;
recv.m_timestamp = pd->m_timestamp; recv.m_timestamp = pd->m_timestamp;
recv.m_fee = pd->m_fee; recv.m_fee = pd->m_fee;
recv.m_coinbase = pd->m_coinbase; recv.m_coinbase = pd->m_coinbase;
recv.m_state = TxInfo::ON_CHAIN; recv.m_state = TxInfo::ON_CHAIN;
} else if (const auto* tx = find_transfer_by_txid(txs, td.m_txid)) { } else if (const auto* tx = Find_transfer_by_txid(txs, td.m_txid)) {
recv.m_height = tx->m_block_height; recv.m_height = tx->m_block_height;
recv.m_timestamp = tx->m_timestamp; recv.m_timestamp = tx->m_timestamp;
recv.m_fee = tx->m_amount_in - tx->m_amount_out; recv.m_fee = tx->m_amount_in - tx->m_amount_out;
@ -263,7 +301,7 @@ void Wallet::captureTxHistorySnapshot(std::vector<TxInfo>& snapshot) {
for (const auto& pair: utxs) { for (const auto& pair: utxs) {
const auto& utx = pair.second; const auto& utx = pair.second;
uint64_t fee = utx.m_amount_in - utx.m_amount_out; uint64_t fee = utx.m_amount_in - utx.m_amount_out;
auto state = (utx.m_state == unconfirmed_transfer_details::pending) auto state = (utx.m_state == wallet2::unconfirmed_transfer_details::pending)
? TxInfo::PENDING ? TxInfo::PENDING
: TxInfo::FAILED; : TxInfo::FAILED;
@ -394,8 +432,8 @@ Wallet::Status Wallet::nonReentrantRefresh(bool skip_coinbase) {
"Refresh should not be called concurrently"); "Refresh should not be called concurrently");
Status ret; Status ret;
std::unique_lock<std::mutex> wallet_lock(m_wallet_mutex); std::unique_lock<std::mutex> wallet_lock(m_wallet_mutex);
m_wallet.set_refresh_type(skip_coinbase ? tools::wallet2::RefreshType::RefreshNoCoinbase m_wallet.set_refresh_type(skip_coinbase ? wallet2::RefreshType::RefreshNoCoinbase
: tools::wallet2::RefreshType::RefreshDefault); : wallet2::RefreshType::RefreshDefault);
while (!m_refresh_canceled) { while (!m_refresh_canceled) {
m_wallet.set_refresh_from_block_height(m_restore_height); m_wallet.set_refresh_from_block_height(m_restore_height);
try { try {
@ -406,10 +444,10 @@ Wallet::Status Wallet::nonReentrantRefresh(bool skip_coinbase) {
ret = Status::OK; ret = Status::OK;
break; break;
} }
} catch (const tools::error::no_connection_to_daemon&) { } catch (const error::no_connection_to_daemon&) {
ret = Status::NO_NETWORK_CONNECTIVITY; ret = Status::NO_NETWORK_CONNECTIVITY;
break; break;
} catch (const tools::error::refresh_error&) { } catch (const error::refresh_error&) {
ret = Status::REFRESH_ERROR; ret = Status::REFRESH_ERROR;
break; break;
} }
@ -481,8 +519,16 @@ Java_im_molly_monero_WalletNative_nativeDispose(
JNIEnv* env, JNIEnv* env,
jobject thiz, jobject thiz,
jlong handle) { jlong handle) {
auto* wallet = reinterpret_cast<Wallet*>(handle); delete reinterpret_cast<Wallet*>(handle);
delete wallet; }
extern "C"
JNIEXPORT void JNICALL
Java_im_molly_monero_WalletNative_nativeDisposePendingTransfer(
JNIEnv* env,
jobject thiz,
jlong handle) {
delete reinterpret_cast<PendingTransfer*>(handle);
} }
extern "C" extern "C"
@ -656,6 +702,66 @@ Java_im_molly_monero_WalletNative_nativeGetTxHistory(
return j_array; return j_array;
} }
extern "C"
JNIEXPORT void JNICALL
Java_im_molly_monero_WalletNative_nativeCreatePayment(
JNIEnv* env,
jobject thiz,
jlong handle,
jobjectArray j_addresses,
jlongArray j_amounts,
jlong time_lock,
jint priority,
jint account_index,
jintArray j_subaddr_indexes,
jobject j_callback) {
auto* wallet = reinterpret_cast<Wallet*>(handle);
const auto& addresses = JavaToNativeVector<std::string, jstring>(
env, j_addresses, &JavaToNativeString);
const auto& amounts = JavaToNativeLongArray(env, j_amounts);
const auto& subaddr_indexes = JavaToNativeIntArray(env, j_subaddr_indexes);
std::unique_ptr<PendingTransfer> pendingTransfer;
try {
pendingTransfer = wallet->createPayment(
addresses,
{amounts.begin(), amounts.end()},
time_lock, priority,
account_index,
{subaddr_indexes.begin(), subaddr_indexes.end()});
// } catch (error::daemon_busy& e) {
// } catch (error::no_connection_to_daemon& e) {
// } catch (error::wallet_rpc_error& e) {
// } catch (error::get_outs_error& e) {
// } catch (error::not_enough_unlocked_money& e) {
// } catch (error::not_enough_money& e) {
// } catch (error::tx_not_possible& e) {
// } catch (error::not_enough_outs_to_mix& e) {
// } catch (error::tx_not_constructed& e) {
// } catch (error::tx_rejected& e) {
// } catch (error::tx_sum_overflow& e) {
// } catch (error::zero_amount& e) {
// } catch (error::zero_destination& e) {
// } catch (error::tx_too_big& e) {
// } catch (error::transfer_error& e) {
// } catch (error::wallet_internal_error& e) {
// } catch (error::wallet_logic_error& e) {
// } catch (const std::exception& e) {
} catch (...) {
LOG_FATAL("Caught unknown exception");
}
jobject j_pending_transfer = CallObjectMethod(
env, thiz, WalletNative_createPendingTransfer,
NativeToJavaPointer(pendingTransfer.get()));
CallVoidMethod(env, j_callback,
ITransferRequestCb_onTransferCreated,
j_pending_transfer);
}
extern "C" extern "C"
JNIEXPORT jlongArray JNICALL JNIEXPORT jlongArray JNICALL
Java_im_molly_monero_WalletNative_nativeFetchBaseFeeEstimate( Java_im_molly_monero_WalletNative_nativeFetchBaseFeeEstimate(

View File

@ -5,17 +5,17 @@
#include "common/jvm.h" #include "common/jvm.h"
#include "transfer.h"
#include "http_client.h" #include "http_client.h"
#include "wallet2.h" #include "wallet2.h"
namespace monero { namespace monero {
using transfer_details = tools::wallet2::transfer_details; namespace error = tools::error;
using payment_details = tools::wallet2::payment_details;
using pool_payment_details = tools::wallet2::pool_payment_details; using wallet2 = tools::wallet2;
using confirmed_transfer_details = tools::wallet2::confirmed_transfer_details; using i_wallet2_callback = tools::i_wallet2_callback;
using unconfirmed_transfer_details = tools::wallet2::unconfirmed_transfer_details;
// Basic structure combining transaction details with input or output info. // Basic structure combining transaction details with input or output info.
struct TxInfo { struct TxInfo {
@ -70,7 +70,7 @@ struct TxInfo {
}; };
// Wrapper for wallet2.h core API. // Wrapper for wallet2.h core API.
class Wallet : tools::i_wallet2_callback { class Wallet : i_wallet2_callback {
public: public:
enum Status : int { enum Status : int {
OK = 0, OK = 0,
@ -97,6 +97,14 @@ class Wallet : tools::i_wallet2_callback {
template<typename Consumer> template<typename Consumer>
void withTxHistory(Consumer consumer); void withTxHistory(Consumer consumer);
std::unique_ptr<PendingTransfer> createPayment(
const std::vector<std::string>& addresses,
const std::vector<uint64_t>& amounts,
uint64_t time_lock,
int priority,
uint32_t account_index,
const std::set<uint32_t>& subaddr_indexes);
std::vector<uint64_t> fetchBaseFeeEstimate(); std::vector<uint64_t> fetchBaseFeeEstimate();
std::string public_address() const; std::string public_address() const;
@ -115,7 +123,7 @@ class Wallet : tools::i_wallet2_callback {
private: private:
cryptonote::account_base& require_account(); cryptonote::account_base& require_account();
tools::wallet2 m_wallet; wallet2 m_wallet;
bool m_account_ready; bool m_account_ready;
uint64_t m_restore_height; uint64_t m_restore_height;

View File

@ -1,5 +1,8 @@
package im.molly.monero package im.molly.monero
import android.os.Parcel
import android.os.Parcelable
data class AccountAddress( data class AccountAddress(
val publicAddress: PublicAddress, val publicAddress: PublicAddress,
val accountIndex: Int = 0, val accountIndex: Int = 0,
@ -12,11 +15,13 @@ data class AccountAddress(
init { init {
when (publicAddress) { when (publicAddress) {
is StandardAddress -> require(isPrimaryAddress) { is StandardAddress -> require(isPrimaryAddress) {
"Standard addresses must have subaddress indices set to zero" "Only the primary address is a standard address"
} }
is SubAddress -> require(accountIndex != -1 && subAddressIndex != -1) { is SubAddress -> require(accountIndex != -1 && subAddressIndex != -1) {
"Invalid subaddress indices" "Invalid subaddress indices"
} }
else -> throw IllegalArgumentException("Unsupported address type") else -> throw IllegalArgumentException("Unsupported address type")
} }
} }

View File

@ -1,7 +1,5 @@
package im.molly.monero package im.molly.monero
import java.time.Instant
data class Balance( data class Balance(
val pendingAmount: MoneroAmount, val pendingAmount: MoneroAmount,
val timeLockedAmounts: List<TimeLocked<MoneroAmount>>, val timeLockedAmounts: List<TimeLocked<MoneroAmount>>,

View File

@ -15,6 +15,7 @@ data class Block(
data class BlockHeader( data class BlockHeader(
val height: Int, val height: Int,
val epochSecond: Long, val epochSecond: Long,
// val version: ProtocolInfo,
) { ) {
val timestamp: Instant val timestamp: Instant
get() = Instant.ofEpochSecond(epochSecond) get() = Instant.ofEpochSecond(epochSecond)

View File

@ -6,4 +6,4 @@ package im.molly.monero
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR) @Target(AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR)
@Retention(AnnotationRetention.SOURCE) @Retention(AnnotationRetention.SOURCE)
@MustBeDocumented @MustBeDocumented
annotation class CalledByNative(val fileName: String) annotation class CalledByNative

View File

@ -7,7 +7,7 @@ data class DynamicFeeRate(val feePerByte: Map<FeePriority, MoneroAmount>) {
val quantizationMask: MoneroAmount = BigDecimal.TEN.pow(PER_KB_FEE_QUANTIZATION_DECIMALS).xmr val quantizationMask: MoneroAmount = BigDecimal.TEN.pow(PER_KB_FEE_QUANTIZATION_DECIMALS).xmr
fun estimateFee(tx: PendingTransaction): Map<FeePriority, MoneroAmount> { fun estimateFee(pendingTransfer: PendingTransfer): Map<FeePriority, MoneroAmount> {
TODO() TODO()
} }
} }

View File

@ -1,7 +1,11 @@
package im.molly.monero package im.molly.monero
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@JvmInline @JvmInline
value class HashDigest(private val hashDigest: String) { @Parcelize
value class HashDigest(private val hashDigest: String) : Parcelable {
init { init {
require(hashDigest.length == 64) { "Hash length must be 64 hex chars" } require(hashDigest.length == 64) { "Hash length must be 64 hex chars" }
} }

View File

@ -71,7 +71,7 @@ internal open class Logger(val tag: String) : LogAdapter {
/** /**
* Log method called from native code. * Log method called from native code.
*/ */
@CalledByNative("logging.cc") @CalledByNative
fun logFromNative(priority: Int, tag: String, msg: String?) { fun logFromNative(priority: Int, tag: String, msg: String?) {
val pri = if (priority in Log.VERBOSE.rangeTo(Log.ASSERT)) priority else Log.ASSERT val pri = if (priority in Log.VERBOSE.rangeTo(Log.ASSERT)) priority else Log.ASSERT
val jniTag = "MoneroJNI.$tag" val jniTag = "MoneroJNI.$tag"

View File

@ -15,7 +15,7 @@ enum class MoneroNetwork(val id: Int, val epoch: Long, val epochV2: Pair<Int, Lo
Stagenet(2, 1518932025, (32000 to 1520937818)); Stagenet(2, 1518932025, (32000 to 1520937818));
companion object { companion object {
fun fromId(value: Int) = values().first { it.id == value } fun fromId(value: Int) = entries.first { it.id == value }
fun of(publicAddress: String) = PublicAddress.parse(publicAddress).network fun of(publicAddress: String) = PublicAddress.parse(publicAddress).network
} }

View File

@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@OptIn(ExperimentalCoroutinesApi::class)
class MoneroWallet internal constructor( class MoneroWallet internal constructor(
private val wallet: IWallet, private val wallet: IWallet,
private val storageAdapter: StorageAdapter, private val storageAdapter: StorageAdapter,
@ -53,7 +54,6 @@ class MoneroWallet internal constructor(
awaitClose { wallet.removeBalanceListener(listener) } awaitClose { wallet.removeBalanceListener(listener) }
} }
@OptIn(ExperimentalCoroutinesApi::class)
suspend fun awaitRefresh( suspend fun awaitRefresh(
ignoreMiningRewards: Boolean = true, ignoreMiningRewards: Boolean = true,
): RefreshResult = suspendCancellableCoroutine { continuation -> ): RefreshResult = suspendCancellableCoroutine { continuation ->
@ -67,7 +67,6 @@ class MoneroWallet internal constructor(
continuation.invokeOnCancellation { wallet.cancelRefresh() } continuation.invokeOnCancellation { wallet.cancelRefresh() }
} }
@OptIn(ExperimentalCoroutinesApi::class)
suspend fun commit(): Boolean = suspendCancellableCoroutine { continuation -> suspend fun commit(): Boolean = suspendCancellableCoroutine { continuation ->
wallet.commit(object : BaseWalletCallbacks() { wallet.commit(object : BaseWalletCallbacks() {
override fun onCommitResult(success: Boolean) { override fun onCommitResult(success: Boolean) {
@ -76,6 +75,21 @@ class MoneroWallet internal constructor(
}) })
} }
suspend fun createTransfer(transferRequest: TransferRequest): PendingTransfer =
suspendCancellableCoroutine { continuation ->
val callback = object : ITransferRequestCallback.Stub() {
override fun onTransferCreated(pendingTransfer: IPendingTransfer) {
continuation.resume(PendingTransfer(pendingTransfer)) {
pendingTransfer.close()
}
}
}
when (transferRequest) {
is PaymentRequest -> wallet.createPayment(transferRequest, callback)
is SweepRequest -> wallet.createSweep(transferRequest, callback)
}
}
fun dynamicFeeRate(): Flow<DynamicFeeRate> = flow { fun dynamicFeeRate(): Flow<DynamicFeeRate> = flow {
while (true) { while (true) {
val fees = requestFees() ?: emptyList() val fees = requestFees() ?: emptyList()
@ -99,7 +113,6 @@ class MoneroWallet internal constructor(
} }
} }
@OptIn(ExperimentalCoroutinesApi::class)
private suspend fun requestFees(): List<MoneroAmount>? = private suspend fun requestFees(): List<MoneroAmount>? =
suspendCancellableCoroutine { continuation -> suspendCancellableCoroutine { continuation ->
wallet.requestFees(object : BaseWalletCallbacks() { wallet.requestFees(object : BaseWalletCallbacks() {

View File

@ -0,0 +1,14 @@
package im.molly.monero
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class PaymentDetail(
val amount: MoneroAmount,
val recipientAddress: PublicAddress,
) : Parcelable {
init {
require(amount >= 0) { "Payment amount cannot be negative" }
}
}

View File

@ -1,3 +0,0 @@
package im.molly.monero
class PendingTransaction

View File

@ -0,0 +1,10 @@
package im.molly.monero
class PendingTransfer internal constructor(
private val pendingTransfer: IPendingTransfer,
) : AutoCloseable {
override fun close() {
pendingTransfer.close()
}
}

View File

@ -1,14 +1,17 @@
package im.molly.monero package im.molly.monero
import android.os.Parcelable
import im.molly.monero.util.decodeBase58 import im.molly.monero.util.decodeBase58
import kotlinx.parcelize.Parcelize
sealed interface PublicAddress { sealed interface PublicAddress : Parcelable {
val address: String val address: String
val network: MoneroNetwork val network: MoneroNetwork
val subAddress: Boolean
// viewPublicKey: ByteArray // viewPublicKey: ByteArray
// spendPublicKey: ByteArray // spendPublicKey: ByteArray
fun isSubAddress(): Boolean
companion object { companion object {
fun parse(publicAddress: String): PublicAddress { fun parse(publicAddress: String): PublicAddress {
val decoded = try { val decoded = try {
@ -38,11 +41,11 @@ sealed interface PublicAddress {
class InvalidAddress(message: String, cause: Throwable? = null) : Exception(message, cause) class InvalidAddress(message: String, cause: Throwable? = null) : Exception(message, cause)
@Parcelize
data class StandardAddress( data class StandardAddress(
override val address: String, override val address: String,
override val network: MoneroNetwork, override val network: MoneroNetwork,
) : PublicAddress { ) : PublicAddress {
override val subAddress = false
companion object { companion object {
val prefixes = mapOf( val prefixes = mapOf(
@ -52,14 +55,16 @@ data class StandardAddress(
) )
} }
override fun isSubAddress() = false
override fun toString(): String = address override fun toString(): String = address
} }
@Parcelize
data class SubAddress( data class SubAddress(
override val address: String, override val address: String,
override val network: MoneroNetwork, override val network: MoneroNetwork,
) : PublicAddress { ) : PublicAddress {
override val subAddress = true
companion object { companion object {
val prefixes = mapOf( val prefixes = mapOf(
@ -69,15 +74,17 @@ data class SubAddress(
) )
} }
override fun isSubAddress() = true
override fun toString(): String = address override fun toString(): String = address
} }
@Parcelize
data class IntegratedAddress( data class IntegratedAddress(
override val address: String, override val address: String,
override val network: MoneroNetwork, override val network: MoneroNetwork,
val paymentId: Long, val paymentId: Long,
) : PublicAddress { ) : PublicAddress {
override val subAddress = false
companion object { companion object {
val prefixes = mapOf( val prefixes = mapOf(
@ -87,5 +94,7 @@ data class IntegratedAddress(
) )
} }
override fun isSubAddress() = false
override fun toString(): String = address override fun toString(): String = address
} }

View File

@ -56,9 +56,7 @@ class SecretKey : Destroyable, Closeable, Parcelable {
parcel.writeByteArray(secret) parcel.writeByteArray(secret)
} }
override fun describeContents(): Int { override fun describeContents(): Int = 0
return 0
}
companion object CREATOR : Parcelable.Creator<SecretKey> { companion object CREATOR : Parcelable.Creator<SecretKey> {
override fun createFromParcel(parcel: Parcel): SecretKey { override fun createFromParcel(parcel: Parcel): SecretKey {

View File

@ -2,7 +2,6 @@ package im.molly.monero
data class Transaction( data class Transaction(
val hash: HashDigest, val hash: HashDigest,
// TODO: val version: ProtocolInfo,
val state: TxState, val state: TxState,
val network: MoneroNetwork, val network: MoneroNetwork,
val timeLock: UnlockTime?, val timeLock: UnlockTime?,
@ -37,7 +36,3 @@ sealed interface TxState {
data object OffChain : TxState data object OffChain : TxState
} }
data class PaymentDetail(
val amount: MoneroAmount,
val recipient: PublicAddress,
)

View File

@ -0,0 +1,23 @@
package im.molly.monero
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
sealed interface TransferRequest : Parcelable
@Parcelize
data class PaymentRequest(
val paymentDetails: List<PaymentDetail>,
val sourceAccounts: Set<AccountAddress>,
val feePriority: FeePriority? = null,
val timeLock: UnlockTime? = null,
) : TransferRequest
@Parcelize
data class SweepRequest(
val recipientAddress: PublicAddress,
val splitCount: Int = 1,
val keyImageHashes: List<HashDigest>,
val feePriority: FeePriority? = null,
val timeLock: UnlockTime? = null,
) : TransferRequest

View File

@ -7,11 +7,12 @@ import kotlinx.coroutines.*
import java.io.Closeable import java.io.Closeable
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
class WalletNative private constructor( internal class WalletNative private constructor(
private val network: MoneroNetwork, private val network: MoneroNetwork,
private val storageAdapter: IStorageAdapter, private val storageAdapter: IStorageAdapter,
private val remoteNodeClient: IRemoteNodeClient?, private val remoteNodeClient: IRemoteNodeClient?,
@ -168,6 +169,45 @@ 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()
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) {
TODO()
}
@CalledByNative
private fun createPendingTransfer(handle: Long) = NativePendingTransfer(handle)
inner class NativePendingTransfer(private val handle: Long) : Closeable,
IPendingTransfer.Stub() {
private val closed = AtomicBoolean()
override fun close() {
if (closed.getAndSet(true)) return
nativeDisposePendingTransfer(handle)
}
protected fun finalize() {
nativeDisposePendingTransfer(handle)
}
}
override fun requestFees(callback: IWalletCallbacks?) { override fun requestFees(callback: IWalletCallbacks?) {
scope.launch(ioDispatcher) { scope.launch(ioDispatcher) {
val fees = nativeFetchBaseFeeEstimate(handle) val fees = nativeFetchBaseFeeEstimate(handle)
@ -191,7 +231,7 @@ class WalletNative private constructor(
} }
} }
@CalledByNative("wallet.cc") @CalledByNative
private fun onRefresh(height: Int, timestamp: Long, balanceChanged: Boolean) { private fun onRefresh(height: Int, timestamp: Long, balanceChanged: Boolean) {
balanceListenersLock.withLock { balanceListenersLock.withLock {
if (balanceListeners.isNotEmpty()) { if (balanceListeners.isNotEmpty()) {
@ -208,7 +248,7 @@ class WalletNative private constructor(
} }
} }
@CalledByNative("wallet.cc") @CalledByNative
private fun onSuspendRefresh(suspending: Boolean) { private fun onSuspendRefresh(suspending: Boolean) {
if (suspending) { if (suspending) {
pendingRequestLock.withLock { pendingRequestLock.withLock {
@ -232,7 +272,7 @@ class WalletNative private constructor(
* *
* Caller must close [HttpResponse.body] upon completion of processing the response. * Caller must close [HttpResponse.body] upon completion of processing the response.
*/ */
@CalledByNative("http_client.cc") @CalledByNative
private fun callRemoteNode( private fun callRemoteNode(
method: String?, method: String?,
path: String?, path: String?,
@ -280,12 +320,25 @@ class WalletNative private constructor(
private external fun nativeCancelRefresh(handle: Long) private external fun nativeCancelRefresh(handle: Long)
private external fun nativeCreate(networkId: Int): Long private external fun nativeCreate(networkId: Int): Long
private external fun nativeCreatePayment(
handle: Long,
addresses: Array<String>,
amounts: LongArray,
timeLock: Long,
priority: Int,
accountIndex: Int,
subAddressIndexes: IntArray,
callback: ITransferRequestCallback,
)
private external fun nativeDispose(handle: Long) private external fun nativeDispose(handle: Long)
private external fun nativeDisposePendingTransfer(handle: Long)
private external fun nativeGetCurrentBlockchainHeight(handle: Long): Int private external fun nativeGetCurrentBlockchainHeight(handle: Long): Int
private external fun nativeGetCurrentBlockchainTimestamp(handle: Long): Long private external fun nativeGetCurrentBlockchainTimestamp(handle: Long): Long
private external fun nativeGetTxHistory(handle: Long): Array<TxInfo> private external fun nativeGetTxHistory(handle: Long): Array<TxInfo>
private external fun nativeGetAccountPrimaryAddress(handle: Long): String private external fun nativeGetAccountPrimaryAddress(handle: Long): String
// private external fun nativeGetAccountSubAddress(handle: Long, accountIndex: Int, subAddressIndex: Int): String
// private external fun nativeGetAccountSubAddress(handle: Long, accountIndex: Int, subAddressIndex: Int): String
private external fun nativeFetchBaseFeeEstimate(handle: Long): LongArray private external fun nativeFetchBaseFeeEstimate(handle: Long): LongArray
private external fun nativeLoad(handle: Long, fd: Int): Boolean private external fun nativeLoad(handle: Long, fd: Int): Boolean
private external fun nativeNonReentrantRefresh(handle: Long, skipCoinbase: Boolean): Int private external fun nativeNonReentrantRefresh(handle: Long, skipCoinbase: Boolean): Int

View File

@ -27,8 +27,7 @@ import java.time.Instant
* transaction history data. * transaction history data.
*/ */
@Parcelize @Parcelize
internal data class TxInfo internal data class TxInfo @CalledByNative constructor(
@CalledByNative("wallet.cc") constructor(
val txHash: String, val txHash: String,
val publicKey: String?, val publicKey: String?,
val keyImage: String?, val keyImage: String?,
@ -170,10 +169,10 @@ private fun TxInfo.toEnote(blockchainHeight: Int): Enote {
} }
private fun TxInfo.toPaymentDetail(): PaymentDetail? { private fun TxInfo.toPaymentDetail(): PaymentDetail? {
val recipient = PublicAddress.parse(recipient ?: return null) val recipientAddress = PublicAddress.parse(recipient ?: return null)
return PaymentDetail( return PaymentDetail(
amount = MoneroAmount(atomicUnits = amount), amount = MoneroAmount(atomicUnits = amount),
recipient = recipient, recipientAddress = recipientAddress,
) )
} }

View File

@ -61,7 +61,7 @@ object MoneroMnemonic {
} }
} }
@CalledByNative("mnemonics/mnemonics.cc") @CalledByNative
@JvmStatic @JvmStatic
private fun buildMnemonicFromJNI( private fun buildMnemonicFromJNI(
entropy: ByteArray, entropy: ByteArray,