diff --git a/lib/android/src/main/aidl/im/molly/monero/IPendingTransfer.aidl b/lib/android/src/main/aidl/im/molly/monero/IPendingTransfer.aidl new file mode 100644 index 0000000..04dd90c --- /dev/null +++ b/lib/android/src/main/aidl/im/molly/monero/IPendingTransfer.aidl @@ -0,0 +1,8 @@ +package im.molly.monero; + +//import im.molly.monero.ITransactionCallback; + +interface IPendingTransfer { +// oneway void submit(in ITransactionCallback callback); + void close(); +} diff --git a/lib/android/src/main/aidl/im/molly/monero/ITransferRequestCallback.aidl b/lib/android/src/main/aidl/im/molly/monero/ITransferRequestCallback.aidl new file mode 100644 index 0000000..f4898f0 --- /dev/null +++ b/lib/android/src/main/aidl/im/molly/monero/ITransferRequestCallback.aidl @@ -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 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); +} diff --git a/lib/android/src/main/aidl/im/molly/monero/IWallet.aidl b/lib/android/src/main/aidl/im/molly/monero/IWallet.aidl index 8237485..a487dc0 100644 --- a/lib/android/src/main/aidl/im/molly/monero/IWallet.aidl +++ b/lib/android/src/main/aidl/im/molly/monero/IWallet.aidl @@ -1,7 +1,10 @@ package im.molly.monero; import im.molly.monero.IBalanceListener; +import im.molly.monero.ITransferRequestCallback; import im.molly.monero.IWalletCallbacks; +import im.molly.monero.PaymentRequest; +import im.molly.monero.SweepRequest; interface IWallet { String getAccountPrimaryAddress(); @@ -11,6 +14,8 @@ interface IWallet { oneway void cancelRefresh(); oneway void setRefreshSince(long heightOrTimestamp); 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); void close(); } diff --git a/lib/android/src/main/aidl/im/molly/monero/PaymentRequest.aidl b/lib/android/src/main/aidl/im/molly/monero/PaymentRequest.aidl new file mode 100644 index 0000000..91139c7 --- /dev/null +++ b/lib/android/src/main/aidl/im/molly/monero/PaymentRequest.aidl @@ -0,0 +1,3 @@ +package im.molly.monero; + +parcelable PaymentRequest; diff --git a/lib/android/src/main/aidl/im/molly/monero/SweepRequest.aidl b/lib/android/src/main/aidl/im/molly/monero/SweepRequest.aidl new file mode 100644 index 0000000..a7dd19f --- /dev/null +++ b/lib/android/src/main/aidl/im/molly/monero/SweepRequest.aidl @@ -0,0 +1,3 @@ +package im.molly.monero; + +parcelable SweepRequest; diff --git a/lib/android/src/main/cpp/monero/wallet2/CMakeLists.txt b/lib/android/src/main/cpp/monero/wallet2/CMakeLists.txt index ae49275..8659a6d 100644 --- a/lib/android/src/main/cpp/monero/wallet2/CMakeLists.txt +++ b/lib/android/src/main/cpp/monero/wallet2/CMakeLists.txt @@ -69,6 +69,8 @@ set(WALLET2_SOURCES src/hardforks/hardforks.cpp src/mnemonics/electrum-words.cpp src/multisig/multisig.cpp + src/multisig/multisig_clsag_context.cpp + src/multisig/multisig_tx_builder_ringct.cpp src/net/error.cpp src/net/http.cpp src/net/i2p_address.cpp @@ -77,6 +79,7 @@ set(WALLET2_SOURCES src/net/socks_connect.cpp src/net/tor_address.cpp src/ringct/bulletproofs.cc + src/ringct/bulletproofs_plus.cc src/ringct/multiexp.cc src/ringct/rctCryptoOps.c src/ringct/rctOps.cpp diff --git a/lib/android/src/main/cpp/wallet/jni_cache.h b/lib/android/src/main/cpp/wallet/jni_cache.h index a8e119d..0629423 100644 --- a/lib/android/src/main/cpp/wallet/jni_cache.h +++ b/lib/android/src/main/cpp/wallet/jni_cache.h @@ -13,9 +13,11 @@ void InitializeJniCache(JNIEnv* env); extern jmethodID HttpResponse_getBody; extern jmethodID HttpResponse_getCode; extern jmethodID HttpResponse_getContentType; +extern jmethodID ITransferRequestCb_onTransferCreated; extern jmethodID Logger_logFromNative; extern jmethodID TxInfo_ctor; extern jmethodID WalletNative_callRemoteNode; +extern jmethodID WalletNative_createPendingTransfer; extern jmethodID WalletNative_onRefresh; extern jmethodID WalletNative_onSuspendRefresh; extern ScopedJavaGlobalRef TxInfoClass; diff --git a/lib/android/src/main/cpp/wallet/transfer.cc b/lib/android/src/main/cpp/wallet/transfer.cc new file mode 100644 index 0000000..ccc55a5 --- /dev/null +++ b/lib/android/src/main/cpp/wallet/transfer.cc @@ -0,0 +1,5 @@ +#include "transfer.h" + +namespace monero { + +} // namespace monero diff --git a/lib/android/src/main/cpp/wallet/transfer.h b/lib/android/src/main/cpp/wallet/transfer.h new file mode 100644 index 0000000..1cda57b --- /dev/null +++ b/lib/android/src/main/cpp/wallet/transfer.h @@ -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& ptxs) + : m_ptxs(ptxs) {} + + private: + std::vector m_ptxs; +}; + +} // namespace monero + +#endif // WALLET_TRANSFER_H_ diff --git a/lib/android/src/main/cpp/wallet/wallet.cc b/lib/android/src/main/cpp/wallet/wallet.cc index bab4743..63c924c 100644 --- a/lib/android/src/main/cpp/wallet/wallet.cc +++ b/lib/android/src/main/cpp/wallet/wallet.cc @@ -132,6 +132,44 @@ void Wallet::withTxHistory(Consumer consumer) { consumer(m_tx_history); } +std::unique_ptr Wallet::createPayment( + const std::vector& addresses, + const std::vector& amounts, + uint64_t time_lock, + int priority, + uint32_t account_index, + const std::set& subaddr_indexes) { + std::vector 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(ptxs); +} + std::vector Wallet::fetchBaseFeeEstimate() { return m_wallet.get_dynamic_base_fee_scaling_estimate(); } @@ -146,8 +184,8 @@ cryptonote::account_base& Wallet::require_account() { return m_wallet.get_account(); } -const payment_details* find_payment_by_txid( - const std::list>& pds, +const wallet2::payment_details* Find_payment_by_txid( + const std::list>& pds, const crypto::hash& txid) { if (txid == crypto::null_hash) { return nullptr; @@ -161,8 +199,8 @@ const payment_details* find_payment_by_txid( return nullptr; } -const confirmed_transfer_details* find_transfer_by_txid( - const std::list>& txs, +const wallet2::confirmed_transfer_details* Find_transfer_by_txid( + const std::list>& txs, const crypto::hash& txid) { if (txid == crypto::null_hash) { return nullptr; @@ -182,15 +220,15 @@ const confirmed_transfer_details* find_transfer_by_txid( void Wallet::captureTxHistorySnapshot(std::vector& snapshot) { snapshot.clear(); - std::vector tds; + std::vector tds; m_wallet.get_transfers(tds); uint64_t min_height = 0; - std::list> pds; - std::list> upds; - std::list> txs; - std::list> utxs; + std::list> pds; + std::list> upds; + std::list> txs; + std::list> utxs; m_wallet.get_payments(pds, min_height); m_wallet.get_unconfirmed_payments(upds, min_height); m_wallet.get_payments_out(txs, min_height); @@ -211,13 +249,13 @@ void Wallet::captureTxHistorySnapshot(std::vector& snapshot) { recv.m_unlock_time = td.m_tx.unlock_time; // 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_timestamp = pd->m_timestamp; recv.m_fee = pd->m_fee; recv.m_coinbase = pd->m_coinbase; 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_timestamp = tx->m_timestamp; recv.m_fee = tx->m_amount_in - tx->m_amount_out; @@ -263,7 +301,7 @@ void Wallet::captureTxHistorySnapshot(std::vector& snapshot) { for (const auto& pair: utxs) { const auto& utx = pair.second; 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::FAILED; @@ -394,8 +432,8 @@ Wallet::Status Wallet::nonReentrantRefresh(bool skip_coinbase) { "Refresh should not be called concurrently"); Status ret; std::unique_lock wallet_lock(m_wallet_mutex); - m_wallet.set_refresh_type(skip_coinbase ? tools::wallet2::RefreshType::RefreshNoCoinbase - : tools::wallet2::RefreshType::RefreshDefault); + m_wallet.set_refresh_type(skip_coinbase ? wallet2::RefreshType::RefreshNoCoinbase + : wallet2::RefreshType::RefreshDefault); while (!m_refresh_canceled) { m_wallet.set_refresh_from_block_height(m_restore_height); try { @@ -406,10 +444,10 @@ Wallet::Status Wallet::nonReentrantRefresh(bool skip_coinbase) { ret = Status::OK; break; } - } catch (const tools::error::no_connection_to_daemon&) { + } catch (const error::no_connection_to_daemon&) { ret = Status::NO_NETWORK_CONNECTIVITY; break; - } catch (const tools::error::refresh_error&) { + } catch (const error::refresh_error&) { ret = Status::REFRESH_ERROR; break; } @@ -481,8 +519,16 @@ Java_im_molly_monero_WalletNative_nativeDispose( JNIEnv* env, jobject thiz, jlong handle) { - auto* wallet = reinterpret_cast(handle); - delete wallet; + delete reinterpret_cast(handle); +} + +extern "C" +JNIEXPORT void JNICALL +Java_im_molly_monero_WalletNative_nativeDisposePendingTransfer( + JNIEnv* env, + jobject thiz, + jlong handle) { + delete reinterpret_cast(handle); } extern "C" @@ -656,6 +702,66 @@ Java_im_molly_monero_WalletNative_nativeGetTxHistory( 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(handle); + + const auto& addresses = JavaToNativeVector( + env, j_addresses, &JavaToNativeString); + const auto& amounts = JavaToNativeLongArray(env, j_amounts); + const auto& subaddr_indexes = JavaToNativeIntArray(env, j_subaddr_indexes); + + std::unique_ptr 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" JNIEXPORT jlongArray JNICALL Java_im_molly_monero_WalletNative_nativeFetchBaseFeeEstimate( diff --git a/lib/android/src/main/cpp/wallet/wallet.h b/lib/android/src/main/cpp/wallet/wallet.h index 7e98ddb..eaeef0e 100644 --- a/lib/android/src/main/cpp/wallet/wallet.h +++ b/lib/android/src/main/cpp/wallet/wallet.h @@ -5,17 +5,17 @@ #include "common/jvm.h" +#include "transfer.h" #include "http_client.h" #include "wallet2.h" namespace monero { -using transfer_details = tools::wallet2::transfer_details; -using payment_details = tools::wallet2::payment_details; -using pool_payment_details = tools::wallet2::pool_payment_details; -using confirmed_transfer_details = tools::wallet2::confirmed_transfer_details; -using unconfirmed_transfer_details = tools::wallet2::unconfirmed_transfer_details; +namespace error = tools::error; + +using wallet2 = tools::wallet2; +using i_wallet2_callback = tools::i_wallet2_callback; // Basic structure combining transaction details with input or output info. struct TxInfo { @@ -70,7 +70,7 @@ struct TxInfo { }; // Wrapper for wallet2.h core API. -class Wallet : tools::i_wallet2_callback { +class Wallet : i_wallet2_callback { public: enum Status : int { OK = 0, @@ -97,6 +97,14 @@ class Wallet : tools::i_wallet2_callback { template void withTxHistory(Consumer consumer); + std::unique_ptr createPayment( + const std::vector& addresses, + const std::vector& amounts, + uint64_t time_lock, + int priority, + uint32_t account_index, + const std::set& subaddr_indexes); + std::vector fetchBaseFeeEstimate(); std::string public_address() const; @@ -115,7 +123,7 @@ class Wallet : tools::i_wallet2_callback { private: cryptonote::account_base& require_account(); - tools::wallet2 m_wallet; + wallet2 m_wallet; bool m_account_ready; uint64_t m_restore_height; diff --git a/lib/android/src/main/kotlin/im/molly/monero/AccountAddress.kt b/lib/android/src/main/kotlin/im/molly/monero/AccountAddress.kt index 2026e78..555a86e 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/AccountAddress.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/AccountAddress.kt @@ -1,5 +1,8 @@ package im.molly.monero +import android.os.Parcel +import android.os.Parcelable + data class AccountAddress( val publicAddress: PublicAddress, val accountIndex: Int = 0, @@ -12,11 +15,13 @@ data class AccountAddress( init { when (publicAddress) { 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) { "Invalid subaddress indices" } + else -> throw IllegalArgumentException("Unsupported address type") } } diff --git a/lib/android/src/main/kotlin/im/molly/monero/Balance.kt b/lib/android/src/main/kotlin/im/molly/monero/Balance.kt index c815270..cc51ccf 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/Balance.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/Balance.kt @@ -1,7 +1,5 @@ package im.molly.monero -import java.time.Instant - data class Balance( val pendingAmount: MoneroAmount, val timeLockedAmounts: List>, diff --git a/lib/android/src/main/kotlin/im/molly/monero/Block.kt b/lib/android/src/main/kotlin/im/molly/monero/Block.kt index 7b08026..5554789 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/Block.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/Block.kt @@ -15,6 +15,7 @@ data class Block( data class BlockHeader( val height: Int, val epochSecond: Long, +// val version: ProtocolInfo, ) { val timestamp: Instant get() = Instant.ofEpochSecond(epochSecond) diff --git a/lib/android/src/main/kotlin/im/molly/monero/CalledByNative.kt b/lib/android/src/main/kotlin/im/molly/monero/CalledByNative.kt index e9e9214..e361cbd 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/CalledByNative.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/CalledByNative.kt @@ -6,4 +6,4 @@ package im.molly.monero @Target(AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR) @Retention(AnnotationRetention.SOURCE) @MustBeDocumented -annotation class CalledByNative(val fileName: String) +annotation class CalledByNative diff --git a/lib/android/src/main/kotlin/im/molly/monero/DynamicFeeRate.kt b/lib/android/src/main/kotlin/im/molly/monero/DynamicFeeRate.kt index 01acc63..dcdd894 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/DynamicFeeRate.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/DynamicFeeRate.kt @@ -7,7 +7,7 @@ data class DynamicFeeRate(val feePerByte: Map) { val quantizationMask: MoneroAmount = BigDecimal.TEN.pow(PER_KB_FEE_QUANTIZATION_DECIMALS).xmr - fun estimateFee(tx: PendingTransaction): Map { + fun estimateFee(pendingTransfer: PendingTransfer): Map { TODO() } } diff --git a/lib/android/src/main/kotlin/im/molly/monero/HashDigest.kt b/lib/android/src/main/kotlin/im/molly/monero/HashDigest.kt index 9c3d333..f7cd759 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/HashDigest.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/HashDigest.kt @@ -1,7 +1,11 @@ package im.molly.monero +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + @JvmInline -value class HashDigest(private val hashDigest: String) { +@Parcelize +value class HashDigest(private val hashDigest: String) : Parcelable { init { require(hashDigest.length == 64) { "Hash length must be 64 hex chars" } } diff --git a/lib/android/src/main/kotlin/im/molly/monero/Logging.kt b/lib/android/src/main/kotlin/im/molly/monero/Logging.kt index 609c60c..7fd7f92 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/Logging.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/Logging.kt @@ -71,7 +71,7 @@ internal open class Logger(val tag: String) : LogAdapter { /** * Log method called from native code. */ - @CalledByNative("logging.cc") + @CalledByNative fun logFromNative(priority: Int, tag: String, msg: String?) { val pri = if (priority in Log.VERBOSE.rangeTo(Log.ASSERT)) priority else Log.ASSERT val jniTag = "MoneroJNI.$tag" diff --git a/lib/android/src/main/kotlin/im/molly/monero/MoneroNetwork.kt b/lib/android/src/main/kotlin/im/molly/monero/MoneroNetwork.kt index e10f47b..b1cc2b0 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/MoneroNetwork.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/MoneroNetwork.kt @@ -15,7 +15,7 @@ enum class MoneroNetwork(val id: Int, val epoch: Long, val epochV2: Pair @@ -67,7 +67,6 @@ class MoneroWallet internal constructor( continuation.invokeOnCancellation { wallet.cancelRefresh() } } - @OptIn(ExperimentalCoroutinesApi::class) suspend fun commit(): Boolean = suspendCancellableCoroutine { continuation -> wallet.commit(object : BaseWalletCallbacks() { 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 = flow { while (true) { val fees = requestFees() ?: emptyList() @@ -99,7 +113,6 @@ class MoneroWallet internal constructor( } } - @OptIn(ExperimentalCoroutinesApi::class) private suspend fun requestFees(): List? = suspendCancellableCoroutine { continuation -> wallet.requestFees(object : BaseWalletCallbacks() { diff --git a/lib/android/src/main/kotlin/im/molly/monero/PaymentDetail.kt b/lib/android/src/main/kotlin/im/molly/monero/PaymentDetail.kt new file mode 100644 index 0000000..609d918 --- /dev/null +++ b/lib/android/src/main/kotlin/im/molly/monero/PaymentDetail.kt @@ -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" } + } +} diff --git a/lib/android/src/main/kotlin/im/molly/monero/PendingTransaction.kt b/lib/android/src/main/kotlin/im/molly/monero/PendingTransaction.kt deleted file mode 100644 index adf958e..0000000 --- a/lib/android/src/main/kotlin/im/molly/monero/PendingTransaction.kt +++ /dev/null @@ -1,3 +0,0 @@ -package im.molly.monero - -class PendingTransaction diff --git a/lib/android/src/main/kotlin/im/molly/monero/PendingTransfer.kt b/lib/android/src/main/kotlin/im/molly/monero/PendingTransfer.kt new file mode 100644 index 0000000..e9c464f --- /dev/null +++ b/lib/android/src/main/kotlin/im/molly/monero/PendingTransfer.kt @@ -0,0 +1,10 @@ +package im.molly.monero + +class PendingTransfer internal constructor( + private val pendingTransfer: IPendingTransfer, +) : AutoCloseable { + + override fun close() { + pendingTransfer.close() + } +} diff --git a/lib/android/src/main/kotlin/im/molly/monero/PublicAddress.kt b/lib/android/src/main/kotlin/im/molly/monero/PublicAddress.kt index 3f62eb3..6ac7e74 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/PublicAddress.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/PublicAddress.kt @@ -1,14 +1,17 @@ package im.molly.monero +import android.os.Parcelable import im.molly.monero.util.decodeBase58 +import kotlinx.parcelize.Parcelize -sealed interface PublicAddress { +sealed interface PublicAddress : Parcelable { val address: String val network: MoneroNetwork - val subAddress: Boolean // viewPublicKey: ByteArray // spendPublicKey: ByteArray + fun isSubAddress(): Boolean + companion object { fun parse(publicAddress: String): PublicAddress { val decoded = try { @@ -38,11 +41,11 @@ sealed interface PublicAddress { class InvalidAddress(message: String, cause: Throwable? = null) : Exception(message, cause) +@Parcelize data class StandardAddress( override val address: String, override val network: MoneroNetwork, ) : PublicAddress { - override val subAddress = false companion object { val prefixes = mapOf( @@ -52,14 +55,16 @@ data class StandardAddress( ) } + override fun isSubAddress() = false + override fun toString(): String = address } +@Parcelize data class SubAddress( override val address: String, override val network: MoneroNetwork, ) : PublicAddress { - override val subAddress = true companion object { val prefixes = mapOf( @@ -69,15 +74,17 @@ data class SubAddress( ) } + override fun isSubAddress() = true + override fun toString(): String = address } +@Parcelize data class IntegratedAddress( override val address: String, override val network: MoneroNetwork, val paymentId: Long, ) : PublicAddress { - override val subAddress = false companion object { val prefixes = mapOf( @@ -87,5 +94,7 @@ data class IntegratedAddress( ) } + override fun isSubAddress() = false + override fun toString(): String = address } diff --git a/lib/android/src/main/kotlin/im/molly/monero/SecretKey.kt b/lib/android/src/main/kotlin/im/molly/monero/SecretKey.kt index 1c02adf..4c46ae5 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/SecretKey.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/SecretKey.kt @@ -56,9 +56,7 @@ class SecretKey : Destroyable, Closeable, Parcelable { parcel.writeByteArray(secret) } - override fun describeContents(): Int { - return 0 - } + override fun describeContents(): Int = 0 companion object CREATOR : Parcelable.Creator { override fun createFromParcel(parcel: Parcel): SecretKey { diff --git a/lib/android/src/main/kotlin/im/molly/monero/Transaction.kt b/lib/android/src/main/kotlin/im/molly/monero/Transaction.kt index 8b2b32e..7f47661 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/Transaction.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/Transaction.kt @@ -2,7 +2,6 @@ package im.molly.monero data class Transaction( val hash: HashDigest, - // TODO: val version: ProtocolInfo, val state: TxState, val network: MoneroNetwork, val timeLock: UnlockTime?, @@ -37,7 +36,3 @@ sealed interface TxState { data object OffChain : TxState } -data class PaymentDetail( - val amount: MoneroAmount, - val recipient: PublicAddress, -) diff --git a/lib/android/src/main/kotlin/im/molly/monero/TransferRequest.kt b/lib/android/src/main/kotlin/im/molly/monero/TransferRequest.kt new file mode 100644 index 0000000..d16f63b --- /dev/null +++ b/lib/android/src/main/kotlin/im/molly/monero/TransferRequest.kt @@ -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, + val sourceAccounts: Set, + val feePriority: FeePriority? = null, + val timeLock: UnlockTime? = null, +) : TransferRequest + +@Parcelize +data class SweepRequest( + val recipientAddress: PublicAddress, + val splitCount: Int = 1, + val keyImageHashes: List, + val feePriority: FeePriority? = null, + val timeLock: UnlockTime? = null, +) : TransferRequest diff --git a/lib/android/src/main/kotlin/im/molly/monero/WalletNative.kt b/lib/android/src/main/kotlin/im/molly/monero/WalletNative.kt index 79d4768..3aff2ee 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/WalletNative.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/WalletNative.kt @@ -7,11 +7,12 @@ import kotlinx.coroutines.* import java.io.Closeable import java.time.Instant import java.util.* +import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock import kotlin.coroutines.CoroutineContext -class WalletNative private constructor( +internal class WalletNative private constructor( private val network: MoneroNetwork, private val storageAdapter: IStorageAdapter, 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?) { scope.launch(ioDispatcher) { 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) { balanceListenersLock.withLock { if (balanceListeners.isNotEmpty()) { @@ -208,7 +248,7 @@ class WalletNative private constructor( } } - @CalledByNative("wallet.cc") + @CalledByNative private fun onSuspendRefresh(suspending: Boolean) { if (suspending) { pendingRequestLock.withLock { @@ -232,7 +272,7 @@ class WalletNative private constructor( * * Caller must close [HttpResponse.body] upon completion of processing the response. */ - @CalledByNative("http_client.cc") + @CalledByNative private fun callRemoteNode( method: String?, path: String?, @@ -280,12 +320,25 @@ class WalletNative private constructor( private external fun nativeCancelRefresh(handle: Long) private external fun nativeCreate(networkId: Int): Long + private external fun nativeCreatePayment( + handle: Long, + addresses: Array, + amounts: LongArray, + timeLock: Long, + priority: Int, + accountIndex: Int, + subAddressIndexes: IntArray, + callback: ITransferRequestCallback, + ) + private external fun nativeDispose(handle: Long) + private external fun nativeDisposePendingTransfer(handle: Long) private external fun nativeGetCurrentBlockchainHeight(handle: Long): Int private external fun nativeGetCurrentBlockchainTimestamp(handle: Long): Long private external fun nativeGetTxHistory(handle: Long): Array 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 nativeLoad(handle: Long, fd: Int): Boolean private external fun nativeNonReentrantRefresh(handle: Long, skipCoinbase: Boolean): Int diff --git a/lib/android/src/main/kotlin/im/molly/monero/internal/TxInfo.kt b/lib/android/src/main/kotlin/im/molly/monero/internal/TxInfo.kt index 323990c..aa93df6 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/internal/TxInfo.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/internal/TxInfo.kt @@ -27,8 +27,7 @@ import java.time.Instant * transaction history data. */ @Parcelize -internal data class TxInfo -@CalledByNative("wallet.cc") constructor( +internal data class TxInfo @CalledByNative constructor( val txHash: String, val publicKey: String?, val keyImage: String?, @@ -170,10 +169,10 @@ private fun TxInfo.toEnote(blockchainHeight: Int): Enote { } private fun TxInfo.toPaymentDetail(): PaymentDetail? { - val recipient = PublicAddress.parse(recipient ?: return null) + val recipientAddress = PublicAddress.parse(recipient ?: return null) return PaymentDetail( amount = MoneroAmount(atomicUnits = amount), - recipient = recipient, + recipientAddress = recipientAddress, ) } diff --git a/lib/android/src/main/kotlin/im/molly/monero/mnemonics/MoneroMnemonic.kt b/lib/android/src/main/kotlin/im/molly/monero/mnemonics/MoneroMnemonic.kt index a985093..09569bf 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/mnemonics/MoneroMnemonic.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/mnemonics/MoneroMnemonic.kt @@ -61,7 +61,7 @@ object MoneroMnemonic { } } - @CalledByNative("mnemonics/mnemonics.cc") + @CalledByNative @JvmStatic private fun buildMnemonicFromJNI( entropy: ByteArray,