demo: complete basic UI for sending payments

This commit is contained in:
Oscar Mira 2024-03-03 20:44:59 +01:00
parent abb43563e3
commit 4b87d76643
16 changed files with 362 additions and 56 deletions

View file

@ -0,0 +1,41 @@
package im.molly.monero.demo.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import im.molly.monero.PendingTransfer
import im.molly.monero.toFormattedString
@Composable
fun PendingTransferView(
spendingAccountIndex: Int,
pendingTransfer: PendingTransfer,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
Text(
text = "Confirm transfer",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(bottom = 16.dp)
)
with(pendingTransfer) {
TextRow("Sending account", "#$spendingAccountIndex")
TextRow("Amount", amount.toFormattedString())
TextRow("Fee", fee.toFormattedString())
TextRow("Transactions", txCount.toString())
}
}
}
@Composable
fun TextRow(label: String, text: String, modifier: Modifier = Modifier) {
Text(
text = "$label: $text",
style = MaterialTheme.typography.bodyMedium,
modifier = modifier.padding(bottom = 8.dp),
)
}

View file

@ -15,6 +15,7 @@ import im.molly.monero.demo.AppModule
import im.molly.monero.demo.data.WalletRepository import im.molly.monero.demo.data.WalletRepository
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -107,8 +108,41 @@ class SendTabViewModel(
} }
} }
fun confirmPayment() { fun confirmPendingTransfer() {
TODO() val savedState = viewModelState.getAndUpdate {
it.copy(status = TransferStatus.Sending)
}
check(savedState.status is TransferStatus.ReadyForApproval)
viewModelScope.launch {
val result = runCatching {
savedState.status.pendingTransfer.use {
it.commit()
}
}
viewModelState.update {
val updatedState = result.fold(
onSuccess = { success ->
it.copy(
recipients = emptyList(),
status = TransferStatus.Sent,
)
},
onFailure = { error ->
it.copy(status = TransferStatus.Error(error.message))
},
)
updatedState
}
}
}
fun cancelPendingTransfer() {
val savedState = viewModelState.getAndUpdate {
it.copy(status = TransferStatus.Idle)
}
if (savedState.status is TransferStatus.ReadyForApproval) {
savedState.status.pendingTransfer.close()
}
} }
companion object { companion object {
@ -127,12 +161,16 @@ data class SendTabUiState(
val status: TransferStatus = TransferStatus.Idle, val status: TransferStatus = TransferStatus.Idle,
) { ) {
val isInProgress: Boolean val isInProgress: Boolean
get() = !(status == TransferStatus.Idle || status is TransferStatus.Error) get() = status == TransferStatus.Preparing ||
status == TransferStatus.Sending ||
status is TransferStatus.ReadyForApproval
} }
sealed interface TransferStatus { sealed interface TransferStatus {
data object Idle : TransferStatus data object Idle : TransferStatus
data object Preparing : TransferStatus data object Preparing : TransferStatus
data object Sending : TransferStatus
data object Sent : TransferStatus
data class ReadyForApproval( data class ReadyForApproval(
val pendingTransfer: PendingTransfer val pendingTransfer: PendingTransfer
) : TransferStatus ) : TransferStatus

View file

@ -1,5 +1,6 @@
package im.molly.monero.demo.ui package im.molly.monero.demo.ui
import android.widget.Toast
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
@ -12,6 +13,7 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -26,6 +28,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import im.molly.monero.FeePriority import im.molly.monero.FeePriority
import im.molly.monero.Ledger import im.molly.monero.Ledger
import im.molly.monero.MoneroCurrency import im.molly.monero.MoneroCurrency
import im.molly.monero.PendingTransfer
import im.molly.monero.demo.data.model.WalletConfig import im.molly.monero.demo.data.model.WalletConfig
import im.molly.monero.demo.ui.component.SelectListBox import im.molly.monero.demo.ui.component.SelectListBox
import im.molly.monero.demo.ui.component.Toolbar import im.molly.monero.demo.ui.component.Toolbar
@ -69,6 +72,8 @@ fun WalletRoute(
onTransferPrioritySelect = { sendTabViewModel.updatePriority(it) }, onTransferPrioritySelect = { sendTabViewModel.updatePriority(it) },
onTransferRecipientChange = { sendTabViewModel.updateRecipients(it) }, onTransferRecipientChange = { sendTabViewModel.updateRecipients(it) },
onTransferSendClick = { sendTabViewModel.createPayment() }, onTransferSendClick = { sendTabViewModel.createPayment() },
onTransferConfirmClick = { sendTabViewModel.confirmPendingTransfer() },
onTransferCancelClick = { sendTabViewModel.cancelPendingTransfer() },
onBackClick = onBackClick, onBackClick = onBackClick,
modifier = modifier, modifier = modifier,
) )
@ -87,6 +92,8 @@ private fun WalletScreen(
onTransferPrioritySelect: (FeePriority) -> Unit = {}, onTransferPrioritySelect: (FeePriority) -> Unit = {},
onTransferRecipientChange: (List<Pair<String, String>>) -> Unit = {}, onTransferRecipientChange: (List<Pair<String, String>>) -> Unit = {},
onTransferSendClick: () -> Unit = {}, onTransferSendClick: () -> Unit = {},
onTransferConfirmClick: () -> Unit = {},
onTransferCancelClick: () -> Unit = {},
onBackClick: () -> Unit = {}, onBackClick: () -> Unit = {},
) { ) {
when (walletUiState) { when (walletUiState) {
@ -101,6 +108,8 @@ private fun WalletScreen(
onTransferPrioritySelect = onTransferPrioritySelect, onTransferPrioritySelect = onTransferPrioritySelect,
onTransferRecipientChange = onTransferRecipientChange, onTransferRecipientChange = onTransferRecipientChange,
onTransferSendClick = onTransferSendClick, onTransferSendClick = onTransferSendClick,
onTransferConfirmClick = onTransferConfirmClick,
onTransferCancelClick = onTransferCancelClick,
onBackClick = onBackClick, onBackClick = onBackClick,
modifier = modifier, modifier = modifier,
) )
@ -138,11 +147,14 @@ private fun WalletScreenLoaded(
onTransferPrioritySelect: (FeePriority) -> Unit, onTransferPrioritySelect: (FeePriority) -> Unit,
onTransferRecipientChange: (List<Pair<String, String>>) -> Unit, onTransferRecipientChange: (List<Pair<String, String>>) -> Unit,
onTransferSendClick: () -> Unit, onTransferSendClick: () -> Unit,
onTransferConfirmClick: () -> Unit,
onTransferCancelClick: () -> Unit,
onBackClick: () -> Unit, onBackClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
var showRenameDialog by remember { mutableStateOf(false) } val context = LocalContext.current
var showRenameDialog by remember { mutableStateOf(false) }
var selectedTabIndex by rememberSaveable { mutableStateOf(0) } var selectedTabIndex by rememberSaveable { mutableStateOf(0) }
Scaffold( Scaffold(
@ -266,6 +278,26 @@ private fun WalletScreenLoaded(
}, },
) )
} }
if (sendTabUiState.status is TransferStatus.ReadyForApproval) {
SendConfirmationDialog(
spendingAccountIndex = sendTabUiState.accountIndex,
pendingTransfer = sendTabUiState.status.pendingTransfer,
onConfirmRequest = {
onTransferConfirmClick()
},
onDismissRequest = {
Toast.makeText(context, "Transfer canceled", Toast.LENGTH_LONG).show()
onTransferCancelClick()
},
)
}
LaunchedEffect(sendTabUiState.status) {
if (sendTabUiState.status == TransferStatus.Sent) {
Toast.makeText(context, "Transfer submitted", Toast.LENGTH_LONG).show()
}
}
} }
} }
@ -336,18 +368,62 @@ private fun WalletSendTab(
} }
} }
ElevatedButton( FilledTonalButton(
modifier = Modifier.padding(vertical = 24.dp), modifier = Modifier.padding(vertical = 24.dp),
onClick = onSendClick, onClick = onSendClick,
enabled = !sendTabUiState.isInProgress, enabled = !sendTabUiState.isInProgress,
) { ) {
val text = when (sendTabUiState.status) { val text = when (sendTabUiState.status) {
TransferStatus.Preparing -> "Preparing..." TransferStatus.Preparing -> "Processing..."
is TransferStatus.ReadyForApproval -> "Processing..."
TransferStatus.Sending -> "Sending..."
else -> "Send" else -> "Send"
} }
Text(text = text) Text(text = text)
} }
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SendConfirmationDialog(
spendingAccountIndex: Int,
pendingTransfer: PendingTransfer,
onConfirmRequest: () -> Unit = {},
onDismissRequest: () -> Unit = {},
) {
val sheetState = rememberModalBottomSheetState()
ModalBottomSheet(
onDismissRequest = onDismissRequest,
sheetState = sheetState,
) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
PendingTransferView(
spendingAccountIndex = spendingAccountIndex,
pendingTransfer = pendingTransfer,
)
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.fillMaxWidth()
) {
OutlinedButton(
onClick = onDismissRequest,
modifier = Modifier.padding(end = 8.dp)
) {
Text("Cancel")
}
FilledTonalButton(
onClick = onConfirmRequest,
) {
Text("Confirm")
}
}
}
} }
} }

View file

@ -1,8 +1,11 @@
package im.molly.monero; package im.molly.monero;
//import im.molly.monero.ITransactionCallback; import im.molly.monero.ITransferCallback;
interface IPendingTransfer { interface IPendingTransfer {
// oneway void submit(in ITransactionCallback callback); long getAmount();
void close(); long getFee();
int getTxCount();
oneway void commitAndClose(in ITransferCallback callback);
oneway void close();
} }

View file

@ -2,8 +2,9 @@ package im.molly.monero;
import im.molly.monero.IPendingTransfer; import im.molly.monero.IPendingTransfer;
oneway interface ITransferRequestCallback { oneway interface ITransferCallback {
void onTransferCreated(in IPendingTransfer pendingTransfer); void onTransferCreated(in IPendingTransfer pendingTransfer);
void onTransferCommitted();
// void onDaemonBusy(); // void onDaemonBusy();
// void onNoConnectionToDaemon(); // void onNoConnectionToDaemon();
// void onRPCError(String errorMessage); // void onRPCError(String errorMessage);

View file

@ -1,7 +1,7 @@
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.ITransferCallback;
import im.molly.monero.IWalletCallbacks; import im.molly.monero.IWalletCallbacks;
import im.molly.monero.PaymentRequest; import im.molly.monero.PaymentRequest;
import im.molly.monero.SweepRequest; import im.molly.monero.SweepRequest;
@ -19,8 +19,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 createPayment(in PaymentRequest request, in ITransferCallback callback);
oneway void createSweep(in SweepRequest request, in ITransferRequestCallback callback); oneway void createSweep(in SweepRequest request, in ITransferCallback callback);
oneway void requestFees(in IWalletCallbacks callback); oneway void requestFees(in IWalletCallbacks callback);
void close(); void close();
} }

View file

@ -60,6 +60,7 @@ set(WALLET_SOURCES
wallet/jni_cache.cc wallet/jni_cache.cc
wallet/jni_loader.cc wallet/jni_loader.cc
wallet/logging.cc wallet/logging.cc
wallet/transfer.cc
wallet/wallet.cc wallet/wallet.cc
) )

View file

@ -6,8 +6,9 @@ namespace monero {
jmethodID HttpResponse_getBody; jmethodID HttpResponse_getBody;
jmethodID HttpResponse_getCode; jmethodID HttpResponse_getCode;
jmethodID HttpResponse_getContentType; jmethodID HttpResponse_getContentType;
jmethodID ITransferRequestCb_onTransferCreated; jmethodID ITransferCallback_onTransferCreated;
jmethodID ITransferRequestCb_onUnexpectedError; jmethodID ITransferCallback_onTransferCommitted;
jmethodID ITransferCallback_onUnexpectedError;
jmethodID Logger_logFromNative; jmethodID Logger_logFromNative;
jmethodID TxInfo_ctor; jmethodID TxInfo_ctor;
jmethodID WalletNative_createPendingTransfer; jmethodID WalletNative_createPendingTransfer;
@ -24,7 +25,7 @@ ScopedJavaGlobalRef<jclass> StringClass;
void InitializeJniCache(JNIEnv* env) { void InitializeJniCache(JNIEnv* env) {
jclass httpResponse = GetClass(env, "im/molly/monero/HttpResponse"); jclass httpResponse = GetClass(env, "im/molly/monero/HttpResponse");
jclass iTransferRequestCb = GetClass(env, "im/molly/monero/ITransferRequestCallback"); jclass iTransferCallback = GetClass(env, "im/molly/monero/ITransferCallback");
jclass logger = GetClass(env, "im/molly/monero/Logger"); jclass logger = GetClass(env, "im/molly/monero/Logger");
jclass txInfo = GetClass(env, "im/molly/monero/internal/TxInfo"); jclass txInfo = GetClass(env, "im/molly/monero/internal/TxInfo");
jclass walletNative = GetClass(env, "im/molly/monero/WalletNative"); jclass walletNative = GetClass(env, "im/molly/monero/WalletNative");
@ -39,11 +40,14 @@ void InitializeJniCache(JNIEnv* env) {
HttpResponse_getContentType = GetMethodId( HttpResponse_getContentType = GetMethodId(
env, httpResponse, env, httpResponse,
"getContentType", "()Ljava/lang/String;"); "getContentType", "()Ljava/lang/String;");
ITransferRequestCb_onTransferCreated = GetMethodId( ITransferCallback_onTransferCreated = GetMethodId(
env, iTransferRequestCb, env, iTransferCallback,
"onTransferCreated", "(Lim/molly/monero/IPendingTransfer;)V"); "onTransferCreated", "(Lim/molly/monero/IPendingTransfer;)V");
ITransferRequestCb_onUnexpectedError = GetMethodId( ITransferCallback_onTransferCommitted = GetMethodId(
env, iTransferRequestCb, env, iTransferCallback,
"onTransferCommitted", "()V");
ITransferCallback_onUnexpectedError = GetMethodId(
env, iTransferCallback,
"onUnexpectedError", "(Ljava/lang/String;)V"); "onUnexpectedError", "(Ljava/lang/String;)V");
Logger_logFromNative = GetMethodId( Logger_logFromNative = GetMethodId(
env, logger, env, logger,
@ -55,7 +59,7 @@ void InitializeJniCache(JNIEnv* env) {
WalletNative_createPendingTransfer = GetMethodId( WalletNative_createPendingTransfer = GetMethodId(
env, walletNative, env, walletNative,
"createPendingTransfer", "createPendingTransfer",
"(J)Lim/molly/monero/WalletNative$NativePendingTransfer;"); "(JJJI)Lim/molly/monero/IPendingTransfer;");
WalletNative_callRemoteNode = GetMethodId( WalletNative_callRemoteNode = GetMethodId(
env, walletNative, env, walletNative,
"callRemoteNode", "callRemoteNode",

View file

@ -13,8 +13,9 @@ 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 ITransferCallback_onTransferCreated;
extern jmethodID ITransferRequestCb_onUnexpectedError; extern jmethodID ITransferCallback_onTransferCommitted;
extern jmethodID ITransferCallback_onUnexpectedError;
extern jmethodID Logger_logFromNative; extern jmethodID Logger_logFromNative;
extern jmethodID TxInfo_ctor; extern jmethodID TxInfo_ctor;
extern jmethodID WalletNative_callRemoteNode; extern jmethodID WalletNative_callRemoteNode;

View file

@ -1,5 +1,18 @@
#include "transfer.h" #include "transfer.h"
#include "common/debug.h"
#include "jni_cache.h"
namespace monero { namespace monero {
extern "C"
JNIEXPORT void JNICALL
Java_im_molly_monero_WalletNative_00024NativePendingTransfer_nativeDispose(
JNIEnv* env,
jobject thiz,
jlong transfer_handle) {
delete reinterpret_cast<PendingTransfer*>(transfer_handle);
}
} // namespace monero } // namespace monero

View file

@ -7,13 +7,33 @@ namespace monero {
using wallet2 = tools::wallet2; using wallet2 = tools::wallet2;
class PendingTransfer { struct PendingTransfer {
public: std::vector<wallet2::pending_tx> m_ptxs;
uint64_t fee() const {
uint64_t n = 0;
for (const auto& ptx: m_ptxs) {
n += ptx.fee;
}
return n;
}
uint64_t amount() const {
uint64_t n = 0;
for (const auto& ptx: m_ptxs) {
for (const auto& dest: ptx.dests) {
n += dest.amount;
}
}
return n;
}
int txCount() const {
return m_ptxs.size();
}
PendingTransfer(const std::vector<wallet2::pending_tx>& ptxs) PendingTransfer(const std::vector<wallet2::pending_tx>& ptxs)
: m_ptxs(ptxs) {} : m_ptxs(ptxs) {}
private:
std::vector<wallet2::pending_tx> m_ptxs;
}; };
} // namespace monero } // namespace monero

View file

@ -195,7 +195,7 @@ std::unique_ptr<PendingTransfer> Wallet::createPayment(
auto ptxs = m_wallet.create_transactions_2( auto ptxs = m_wallet.create_transactions_2(
dsts, dsts,
m_wallet.default_mixin(), m_wallet.get_min_ring_size() - 1,
time_lock, time_lock,
priority, priority,
{}, /* extra */ {}, /* extra */
@ -205,6 +205,20 @@ std::unique_ptr<PendingTransfer> Wallet::createPayment(
return std::make_unique<PendingTransfer>(ptxs); return std::make_unique<PendingTransfer>(ptxs);
} }
void Wallet::commit_transfer(PendingTransfer& pending_transfer) {
std::unique_lock<std::mutex> wallet_lock(m_wallet_mutex);
while (!pending_transfer.m_ptxs.empty()) {
m_wallet.commit_tx(pending_transfer.m_ptxs.back());
m_balance_changed = true;
pending_transfer.m_ptxs.pop_back();
}
if (m_balance_changed) {
processBalanceChanges(false);
}
}
template<typename Consumer> template<typename Consumer>
void Wallet::withTxHistory(Consumer consumer) { void Wallet::withTxHistory(Consumer consumer) {
std::lock_guard<std::mutex> lock(m_tx_history_mutex); std::lock_guard<std::mutex> lock(m_tx_history_mutex);
@ -356,7 +370,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 == wallet2::unconfirmed_transfer_details::pending) auto state = (utx.m_state != wallet2::unconfirmed_transfer_details::failed)
? TxInfo::PENDING ? TxInfo::PENDING
: TxInfo::FAILED; : TxInfo::FAILED;
@ -594,15 +608,6 @@ Java_im_molly_monero_WalletNative_nativeDispose(
delete reinterpret_cast<Wallet*>(handle); delete reinterpret_cast<Wallet*>(handle);
} }
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"
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_im_molly_monero_WalletNative_nativeRestoreAccount( Java_im_molly_monero_WalletNative_nativeRestoreAccount(
@ -848,10 +853,10 @@ Java_im_molly_monero_WalletNative_nativeCreatePayment(
const auto& amounts = JavaToNativeLongArray(env, j_amounts); const auto& amounts = JavaToNativeLongArray(env, j_amounts);
const auto& subaddr_indexes = JavaToNativeIntArray(env, j_subaddr_indexes); const auto& subaddr_indexes = JavaToNativeIntArray(env, j_subaddr_indexes);
std::unique_ptr<PendingTransfer> pendingTransfer; std::unique_ptr<PendingTransfer> pending_transfer;
try { try {
pendingTransfer = wallet->createPayment( pending_transfer = wallet->createPayment(
addresses, addresses,
{amounts.begin(), amounts.end()}, {amounts.begin(), amounts.end()},
time_lock, priority, time_lock, priority,
@ -877,18 +882,47 @@ Java_im_molly_monero_WalletNative_nativeCreatePayment(
} catch (const std::exception& e) { } catch (const std::exception& e) {
LOGW("Caught unhandled exception: %s", e.what()); LOGW("Caught unhandled exception: %s", e.what());
CallVoidMethod(env, j_callback, CallVoidMethod(env, j_callback,
ITransferRequestCb_onUnexpectedError, ITransferCallback_onUnexpectedError,
NativeToJavaString(env, e.what())); NativeToJavaString(env, e.what()));
return; return;
} }
PendingTransfer* ptr = pending_transfer.release();
jobject j_pending_transfer = CallObjectMethod( jobject j_pending_transfer = CallObjectMethod(
env, thiz, WalletNative_createPendingTransfer, env, thiz,
NativeToJavaPointer(pendingTransfer.get())); WalletNative_createPendingTransfer,
NativeToJavaPointer(ptr),
ptr->amount(),
ptr->fee(),
ptr->txCount());
CallVoidMethod(env, j_callback, CallVoidMethod(env, j_callback,
ITransferRequestCb_onTransferCreated, ITransferCallback_onTransferCreated, j_pending_transfer);
j_pending_transfer); }
extern "C"
JNIEXPORT void JNICALL
Java_im_molly_monero_WalletNative_nativeCommitPendingTransfer(
JNIEnv* env,
jobject thiz,
jlong handle,
jlong transfer_handle,
jobject j_callback) {
auto* wallet = reinterpret_cast<Wallet*>(handle);
auto* pending_transfer = reinterpret_cast<PendingTransfer*>(transfer_handle);
try {
wallet->commit_transfer(*pending_transfer);
} catch (const std::exception& e) {
LOGW("Caught unhandled exception: %s", e.what());
CallVoidMethod(env, j_callback,
ITransferCallback_onUnexpectedError,
NativeToJavaString(env, e.what()));
return;
}
CallVoidMethod(env, j_callback, ITransferCallback_onTransferCommitted);
} }
extern "C" extern "C"

View file

@ -106,6 +106,8 @@ class Wallet : i_wallet2_callback {
uint32_t account_index, uint32_t account_index,
const std::set<uint32_t>& subaddr_indexes); const std::set<uint32_t>& subaddr_indexes);
void commit_transfer(PendingTransfer& pending_transfer);
template<typename Consumer> template<typename Consumer>
void withTxHistory(Consumer consumer); void withTxHistory(Consumer consumer);

View file

@ -186,13 +186,15 @@ class MoneroWallet internal constructor(
suspend fun createTransfer(transferRequest: TransferRequest): PendingTransfer = suspend fun createTransfer(transferRequest: TransferRequest): PendingTransfer =
suspendCancellableCoroutine { continuation -> suspendCancellableCoroutine { continuation ->
val callback = object : ITransferRequestCallback.Stub() { val callback = object : ITransferCallback.Stub() {
override fun onTransferCreated(pendingTransfer: IPendingTransfer) { override fun onTransferCreated(pendingTransfer: IPendingTransfer) {
continuation.resume(PendingTransfer(pendingTransfer)) { continuation.resume(PendingTransfer(pendingTransfer)) {
pendingTransfer.close() pendingTransfer.close()
} }
} }
override fun onTransferCommitted() = Unit
override fun onUnexpectedError(message: String) { override fun onUnexpectedError(message: String) {
continuation.resumeWithException( continuation.resumeWithException(
IllegalStateException(message) IllegalStateException(message)

View file

@ -1,8 +1,39 @@
package im.molly.monero package im.molly.monero
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resumeWithException
@OptIn(ExperimentalCoroutinesApi::class)
class PendingTransfer internal constructor( class PendingTransfer internal constructor(
private val pendingTransfer: IPendingTransfer, private val pendingTransfer: IPendingTransfer,
) : AutoCloseable { ) : AutoCloseable {
val fee: MoneroAmount
get() = pendingTransfer.fee.toAtomicUnits()
val amount: MoneroAmount
get() = pendingTransfer.amount.toAtomicUnits()
val txCount: Int
get() = pendingTransfer.txCount
suspend fun commit(): Boolean = suspendCancellableCoroutine { continuation ->
val callback = object : ITransferCallback.Stub() {
override fun onTransferCreated(pendingTransfer: IPendingTransfer) = Unit
override fun onTransferCommitted() {
continuation.resume(true) {}
}
override fun onUnexpectedError(message: String) {
continuation.resumeWithException(
IllegalStateException(message)
)
}
}
pendingTransfer.commitAndClose(callback)
}
override fun close() = pendingTransfer.close() override fun close() = pendingTransfer.close()
} }

View file

@ -175,7 +175,7 @@ internal class WalletNative private constructor(
} }
} }
override fun createPayment(request: PaymentRequest, callback: ITransferRequestCallback) { override fun createPayment(request: PaymentRequest, callback: ITransferCallback) {
scope.launch(singleThreadedDispatcher) { scope.launch(singleThreadedDispatcher) {
val (amounts, addresses) = request.paymentDetails.map { val (amounts, addresses) = request.paymentDetails.map {
it.amount.atomicUnits to it.recipientAddress.address it.amount.atomicUnits to it.recipientAddress.address
@ -194,26 +194,56 @@ internal class WalletNative private constructor(
} }
} }
override fun createSweep(request: SweepRequest, callback: ITransferRequestCallback) { override fun createSweep(request: SweepRequest, callback: ITransferCallback) {
TODO() TODO()
} }
@CalledByNative @CalledByNative
private fun createPendingTransfer(handle: Long) = NativePendingTransfer(handle) private fun createPendingTransfer(
transferHandle: Long,
amount: Long,
fee: Long,
txCount: Int,
): IPendingTransfer =
NativePendingTransfer(transferHandle, amount, fee, txCount)
inner class NativePendingTransfer(private val handle: Long) : Closeable, inner class NativePendingTransfer(
private val transferHandle: Long,
private val amount: Long,
private val fee: Long,
private val txCount: Int,
) : Closeable,
IPendingTransfer.Stub() { IPendingTransfer.Stub() {
private val closed = AtomicBoolean() private val closed = AtomicBoolean()
override fun getAmount() = amount
override fun getFee() = fee
override fun getTxCount() = txCount
override fun commitAndClose(callback: ITransferCallback) {
scope.launch(singleThreadedDispatcher) {
if (closed.compareAndSet(false, true)) {
nativeCommitPendingTransfer(handle, transferHandle, callback)
nativeDispose(transferHandle)
} else {
callback.onUnexpectedError("PendingTransfer is closed")
}
}
}
override fun close() { override fun close() {
if (closed.getAndSet(true)) return if (closed.getAndSet(true)) return
nativeDisposePendingTransfer(handle) nativeDispose(transferHandle)
} }
protected fun finalize() { protected fun finalize() {
nativeDisposePendingTransfer(handle) close()
} }
private external fun nativeDispose(transferHandle: Long)
} }
override fun requestFees(callback: IWalletCallbacks?) { override fun requestFees(callback: IWalletCallbacks?) {
@ -407,17 +437,26 @@ internal class WalletNative private constructor(
priority: Int, priority: Int,
accountIndex: Int, accountIndex: Int,
subAddressIndexes: IntArray, subAddressIndexes: IntArray,
callback: ITransferRequestCallback, callback: ITransferCallback,
)
private external fun nativeCommitPendingTransfer(
handle: Long,
transferHandle: Long,
callback: ITransferCallback,
) )
private external fun nativeCreateSubAddressAccount(handle: Long): String private external fun nativeCreateSubAddressAccount(handle: Long): String
private external fun nativeCreateSubAddress(handle: Long, subAddressMajor: Int): String? private external fun nativeCreateSubAddress(handle: Long, subAddressMajor: Int): String?
private external fun nativeDispose(handle: Long) private external fun nativeDispose(handle: Long)
private external fun nativeDisposePendingTransfer(handle: Long)
private external fun nativeGetPublicAddress(handle: Long): String private external fun nativeGetPublicAddress(handle: Long): String
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 nativeGetSubAddresses(subAddressMajor: Int, handle: Long): Array<String> private external fun nativeGetSubAddresses(
subAddressMajor: Int,
handle: Long,
): Array<String>
private external fun nativeGetTxHistory(handle: Long): Array<TxInfo> private external fun nativeGetTxHistory(handle: Long): Array<TxInfo>
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