mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2025-05-13 05:30:43 +01:00
demo: complete basic UI for sending payments
This commit is contained in:
parent
abb43563e3
commit
4b87d76643
16 changed files with 362 additions and 56 deletions
|
@ -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),
|
||||||
|
)
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue