mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2024-10-01 03:45:36 -04:00
demo: display account addresses in receive tab
This commit is contained in:
parent
2a26407258
commit
ec27aa42d4
@ -0,0 +1,9 @@
|
||||
package im.molly.monero.demo.data.model
|
||||
|
||||
import im.molly.monero.AccountAddress
|
||||
|
||||
data class WalletAddress(
|
||||
val address: AccountAddress,
|
||||
val used: Boolean,
|
||||
val isLastForAccount: Boolean,
|
||||
)
|
@ -0,0 +1,64 @@
|
||||
package im.molly.monero.demo.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.unit.dp
|
||||
import im.molly.monero.demo.data.model.WalletAddress
|
||||
import im.molly.monero.demo.ui.component.CopyableText
|
||||
import im.molly.monero.demo.ui.theme.Blue40
|
||||
import im.molly.monero.demo.ui.theme.Red40
|
||||
|
||||
@Composable
|
||||
fun AddressCardExpanded(
|
||||
walletAddress: WalletAddress,
|
||||
onClick: () -> Unit,
|
||||
onCreateSubAddressClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
with(walletAddress.address) {
|
||||
val used = walletAddress.used || isPrimaryAddress
|
||||
if (isPrimaryAddress) {
|
||||
Text(
|
||||
text = "Account #$accountIndex Primary address",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = "Account #$accountIndex Subaddress #$subAddressIndex",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
}
|
||||
CopyableText(
|
||||
text = address,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = if (used) Modifier.alpha(0.5f) else Modifier,
|
||||
)
|
||||
if (walletAddress.isLastForAccount) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
TextButton(onClick = onCreateSubAddressClick) {
|
||||
Text(
|
||||
text = "Add subaddress",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package im.molly.monero.demo.ui
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.ui.Modifier
|
||||
import im.molly.monero.demo.data.model.WalletAddress
|
||||
|
||||
fun LazyListScope.addressCardItems(
|
||||
items: List<WalletAddress>,
|
||||
onCreateSubAddressClick: (accountIndex: Int) -> Unit,
|
||||
itemModifier: Modifier = Modifier,
|
||||
) = items(
|
||||
items = items,
|
||||
key = { it.address },
|
||||
itemContent = {
|
||||
AddressCardExpanded(
|
||||
walletAddress = it,
|
||||
onClick = { },
|
||||
onCreateSubAddressClick = {
|
||||
onCreateSubAddressClick(it.address.accountIndex)
|
||||
},
|
||||
modifier = itemModifier,
|
||||
)
|
||||
},
|
||||
)
|
@ -9,7 +9,6 @@ import im.molly.monero.demo.AppModule
|
||||
import im.molly.monero.demo.common.Result
|
||||
import im.molly.monero.demo.common.asResult
|
||||
import im.molly.monero.demo.data.WalletRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
@ -44,6 +44,10 @@ fun WalletRoute(
|
||||
uiState = uiState,
|
||||
onWalletConfigChange = { config -> viewModel.updateConfig(config) },
|
||||
onTransactionClick = onTransactionClick,
|
||||
onCreateAccountClick = { viewModel.createAccount() },
|
||||
onCreateSubAddressClick = { accountIndex ->
|
||||
viewModel.createSubAddress(accountIndex)
|
||||
},
|
||||
onBackClick = onBackClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
@ -54,6 +58,8 @@ private fun WalletScreen(
|
||||
uiState: WalletUiState,
|
||||
onWalletConfigChange: (WalletConfig) -> Unit,
|
||||
onTransactionClick: (String, Long) -> Unit,
|
||||
onCreateAccountClick: () -> Unit,
|
||||
onCreateSubAddressClick: (Int) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@ -62,6 +68,8 @@ private fun WalletScreen(
|
||||
uiState = uiState,
|
||||
onWalletConfigChange = onWalletConfigChange,
|
||||
onTransactionClick = onTransactionClick,
|
||||
onCreateAccountClick = onCreateAccountClick,
|
||||
onCreateSubAddressClick = onCreateSubAddressClick,
|
||||
onBackClick = onBackClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
@ -77,12 +85,17 @@ private fun WalletScreenLoaded(
|
||||
uiState: WalletUiState.Loaded,
|
||||
onWalletConfigChange: (WalletConfig) -> Unit,
|
||||
onTransactionClick: (String, Long) -> Unit,
|
||||
onCreateAccountClick: () -> Unit,
|
||||
onCreateSubAddressClick: (Int) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var showRenameDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(topBar = {
|
||||
var selectedTabIndex by rememberSaveable { mutableStateOf(0) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
Toolbar(navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
@ -96,7 +109,8 @@ private fun WalletScreenLoaded(
|
||||
onDeleteClick = { },
|
||||
)
|
||||
})
|
||||
}) { padding ->
|
||||
},
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
@ -107,14 +121,13 @@ private fun WalletScreenLoaded(
|
||||
append(MoneroCurrency.SYMBOL + " ")
|
||||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(
|
||||
MoneroCurrency.Format(precision = 5).format(uiState.balance.confirmedAmount)
|
||||
MoneroCurrency.Format(precision = 5)
|
||||
.format(uiState.balance.confirmedAmount)
|
||||
)
|
||||
}
|
||||
})
|
||||
Text(text = uiState.config.name, style = MaterialTheme.typography.headlineSmall)
|
||||
|
||||
var selectedTabIndex by rememberSaveable { mutableStateOf(0) }
|
||||
|
||||
WalletHeaderTabs(
|
||||
titles = listOf("Balance", "Send", "Receive", "History"),
|
||||
selectedTabIndex = selectedTabIndex,
|
||||
@ -132,21 +145,33 @@ private fun WalletScreenLoaded(
|
||||
1 -> {} // TODO
|
||||
|
||||
2 -> {
|
||||
val scrollState = rememberLazyListState()
|
||||
|
||||
LazyColumn(
|
||||
state = scrollState,
|
||||
) {
|
||||
addressCardItems(
|
||||
items = uiState.addresses,
|
||||
onCreateSubAddressClick = onCreateSubAddressClick,
|
||||
)
|
||||
item {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onCreateAccountClick,
|
||||
modifier = modifier.padding(bottom = 16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Primary address",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
CopyableText(
|
||||
text = uiState.config.publicAddress,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
text = "Create new account",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3 -> {
|
||||
val scrollState = rememberLazyListState()
|
||||
@ -286,15 +311,19 @@ private fun WalletScreenPopulated(
|
||||
network = ledger.publicAddress.network,
|
||||
balance = ledger.balance,
|
||||
blockchainTime = ledger.checkedAt,
|
||||
addresses = emptyList(),
|
||||
transactions = emptyList(),
|
||||
),
|
||||
onWalletConfigChange = {},
|
||||
onTransactionClick = { _: String, _: Long -> },
|
||||
onCreateAccountClick = {},
|
||||
onCreateSubAddressClick = {},
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class WalletScreenPreviewParameterProvider : PreviewParameterProvider<Ledger> {
|
||||
private class WalletScreenPreviewParameterProvider :
|
||||
PreviewParameterProvider<Ledger> {
|
||||
override val values = sequenceOf(PreviewParameterData.ledger)
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import im.molly.monero.demo.AppModule
|
||||
import im.molly.monero.demo.common.Result
|
||||
import im.molly.monero.demo.common.asResult
|
||||
import im.molly.monero.demo.data.WalletRepository
|
||||
import im.molly.monero.demo.data.model.WalletAddress
|
||||
import im.molly.monero.demo.data.model.WalletConfig
|
||||
import im.molly.monero.demo.data.model.WalletTransaction
|
||||
import kotlinx.coroutines.flow.*
|
||||
@ -18,7 +19,7 @@ import kotlinx.coroutines.launch
|
||||
import java.time.Instant
|
||||
|
||||
class WalletViewModel(
|
||||
walletId: Long,
|
||||
private val walletId: Long,
|
||||
private val walletRepository: WalletRepository = AppModule.walletRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
@ -46,6 +47,18 @@ class WalletViewModel(
|
||||
walletRepository.updateWalletConfig(config)
|
||||
}
|
||||
}
|
||||
|
||||
fun createAccount() {
|
||||
viewModelScope.launch {
|
||||
walletRepository.getWallet(walletId).createAccount()
|
||||
}
|
||||
}
|
||||
|
||||
fun createSubAddress(accountIndex: Int) {
|
||||
viewModelScope.launch {
|
||||
walletRepository.getWallet(walletId).createSubAddressForAccount(accountIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun walletUiState(
|
||||
@ -61,14 +74,27 @@ private fun walletUiState(
|
||||
is Result.Success -> {
|
||||
val config = result.data.first
|
||||
val ledger = result.data.second
|
||||
val balance = ledger.balance
|
||||
val blockchainTime = ledger.checkedAt
|
||||
val addresses =
|
||||
ledger.accountAddresses.groupBy { it.accountIndex }.flatMap { (_, group) ->
|
||||
group.sortedBy { it.subAddressIndex }.mapIndexed { index, address ->
|
||||
WalletAddress(
|
||||
address = address,
|
||||
used = address.isAddressUsed(ledger.transactions.values),
|
||||
isLastForAccount = index == group.size - 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
val transactions =
|
||||
ledger.transactions
|
||||
.map { WalletTransaction(config.id, it.value) }
|
||||
ledger.transactions.map { WalletTransaction(config.id, it.value) }
|
||||
.sortedByDescending { it.transaction.blockTimestamp ?: Instant.MAX }
|
||||
val network = ledger.publicAddress.network
|
||||
WalletUiState.Loaded(config, network, blockchainTime, balance, transactions)
|
||||
WalletUiState.Loaded(
|
||||
config = config,
|
||||
network = ledger.publicAddress.network,
|
||||
blockchainTime = ledger.checkedAt,
|
||||
balance = ledger.balance,
|
||||
addresses = addresses,
|
||||
transactions = transactions,
|
||||
)
|
||||
}
|
||||
|
||||
is Result.Loading -> {
|
||||
@ -88,6 +114,7 @@ sealed interface WalletUiState {
|
||||
val network: MoneroNetwork,
|
||||
val blockchainTime: BlockchainTime,
|
||||
val balance: Balance,
|
||||
val addresses: List<WalletAddress>,
|
||||
val transactions: List<WalletTransaction>,
|
||||
) : WalletUiState
|
||||
|
||||
|
@ -16,6 +16,7 @@ import androidx.compose.ui.text.TextStyle
|
||||
fun CopyableText(
|
||||
text: String,
|
||||
style: TextStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
@ -23,7 +24,7 @@ fun CopyableText(
|
||||
Text(
|
||||
text = text,
|
||||
style = style,
|
||||
modifier = Modifier.combinedClickable(
|
||||
modifier = modifier.combinedClickable(
|
||||
onClick = {},
|
||||
onLongClick = {
|
||||
clipboardManager.setText(AnnotatedString(text))
|
||||
|
@ -10,7 +10,7 @@ interface IWallet {
|
||||
String getPublicAddress();
|
||||
void addBalanceListener(in IBalanceListener listener);
|
||||
void removeBalanceListener(in IBalanceListener listener);
|
||||
oneway void getOrCreateAddress(int accountIndex, int subAddressIndex, in IWalletCallbacks callback);
|
||||
oneway void addDetachedSubAddress(int accountIndex, int subAddressIndex, in IWalletCallbacks callback);
|
||||
oneway void createAccount(in IWalletCallbacks callback);
|
||||
oneway void createSubAddressForAccount(int accountIndex, in IWalletCallbacks callback);
|
||||
oneway void getAllAddresses(in IWalletCallbacks callback);
|
||||
|
@ -134,6 +134,14 @@ std::string FormatAccountAddress(
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string Wallet::addDetachedSubAddress(uint32_t index_major, uint32_t index_minor) {
|
||||
return suspendRefreshAndRunLocked([&]() {
|
||||
cryptonote::subaddress_index index = {index_major, index_minor};
|
||||
m_wallet.create_one_off_subaddress(index);
|
||||
return addSubaddressInternal(index);
|
||||
});
|
||||
}
|
||||
|
||||
std::string Wallet::createSubAddressAccount() {
|
||||
return suspendRefreshAndRunLocked([&]() {
|
||||
uint32_t index_major = m_wallet.get_num_subaddress_accounts();
|
||||
@ -150,14 +158,6 @@ std::string Wallet::createSubAddress(uint32_t index_major) {
|
||||
});
|
||||
}
|
||||
|
||||
std::string Wallet::addSubAddress(uint32_t index_major, uint32_t index_minor) {
|
||||
return suspendRefreshAndRunLocked([&]() {
|
||||
cryptonote::subaddress_index index = {index_major, index_minor};
|
||||
m_wallet.create_one_off_subaddress(index);
|
||||
return addSubaddressInternal(index);
|
||||
});
|
||||
}
|
||||
|
||||
std::string Wallet::addSubaddressInternal(const cryptonote::subaddress_index& index) {
|
||||
std::string subaddress = m_wallet.get_subaddress_as_str(index);
|
||||
std::unique_lock<std::mutex> lock(m_subaddresses_mutex);
|
||||
@ -704,7 +704,7 @@ Java_im_molly_monero_WalletNative_nativeGetPublicAddress(
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_im_molly_monero_WalletNative_nativeAddSubAddress(
|
||||
Java_im_molly_monero_WalletNative_nativeAddDetachedSubAddress(
|
||||
JNIEnv* env,
|
||||
jobject thiz,
|
||||
jlong handle,
|
||||
@ -712,7 +712,7 @@ Java_im_molly_monero_WalletNative_nativeAddSubAddress(
|
||||
jint sub_address_minor) {
|
||||
auto* wallet = reinterpret_cast<Wallet*>(handle);
|
||||
return NativeToJavaString(
|
||||
env, wallet->addSubAddress(sub_address_major, sub_address_minor));
|
||||
env, wallet->addDetachedSubAddress(sub_address_major, sub_address_minor));
|
||||
}
|
||||
|
||||
extern "C"
|
||||
|
@ -94,9 +94,9 @@ class Wallet : i_wallet2_callback {
|
||||
void cancelRefresh();
|
||||
void setRefreshSince(long height_or_timestamp);
|
||||
|
||||
std::string addDetachedSubAddress(uint32_t index_major, uint32_t index_minor);
|
||||
std::string createSubAddressAccount();
|
||||
std::string createSubAddress(uint32_t index_major);
|
||||
std::string addSubAddress(uint32_t index_major, uint32_t index_minor);
|
||||
|
||||
std::unique_ptr<PendingTransfer> createPayment(
|
||||
const std::vector<std::string>& addresses,
|
||||
|
@ -26,6 +26,16 @@ data class AccountAddress(
|
||||
}
|
||||
}
|
||||
|
||||
fun isAddressUsed(transactions: Iterable<Transaction>): Boolean {
|
||||
return transactions.any { tx ->
|
||||
tx.sent.any { enote ->
|
||||
enote.owner == this
|
||||
} || tx.received.any { enote ->
|
||||
enote.owner == this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun parseWithIndexes(addressString: String): AccountAddress {
|
||||
val parts = addressString.split("/")
|
||||
|
@ -5,9 +5,11 @@ import im.molly.monero.internal.consolidateTransactions
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.onFailure
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
@ -25,9 +27,9 @@ class MoneroWallet internal constructor(
|
||||
|
||||
var dataStore by storageAdapter::dataStore
|
||||
|
||||
suspend fun getOrCreateAddress(accountIndex: Int, subAddressIndex: Int): AccountAddress =
|
||||
suspend fun addDetachedSubAddress(accountIndex: Int, subAddressIndex: Int): AccountAddress =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
wallet.getOrCreateAddress(
|
||||
wallet.addDetachedSubAddress(
|
||||
accountIndex,
|
||||
subAddressIndex,
|
||||
object : BaseWalletCallbacks() {
|
||||
@ -91,14 +93,14 @@ class MoneroWallet internal constructor(
|
||||
accountAddresses = accountAddresses,
|
||||
blockchainContext = blockchainTime,
|
||||
)
|
||||
lastKnownLedger = Ledger(
|
||||
val ledger = Ledger(
|
||||
publicAddress = publicAddress,
|
||||
accountAddresses = accountAddresses,
|
||||
transactions = txById,
|
||||
enotes = enotes,
|
||||
checkedAt = blockchainTime,
|
||||
)
|
||||
sendLedger(lastKnownLedger)
|
||||
sendLedger(ledger)
|
||||
}
|
||||
|
||||
override fun onRefresh(blockchainTime: BlockchainTime) {
|
||||
@ -114,16 +116,16 @@ class MoneroWallet internal constructor(
|
||||
}
|
||||
|
||||
private fun sendLedger(ledger: Ledger) {
|
||||
trySend(ledger).onFailure {
|
||||
logger.e("Too many ledger updates, channel capacity exceeded", it)
|
||||
}
|
||||
lastKnownLedger = ledger
|
||||
// Shouldn't block as we conflate the flow.
|
||||
trySendBlocking(ledger)
|
||||
}
|
||||
}
|
||||
|
||||
wallet.addBalanceListener(listener)
|
||||
|
||||
awaitClose { wallet.removeBalanceListener(listener) }
|
||||
}
|
||||
}.conflate()
|
||||
|
||||
suspend fun awaitRefresh(
|
||||
ignoreMiningRewards: Boolean = true,
|
||||
|
@ -236,13 +236,13 @@ internal class WalletNative private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getOrCreateAddress(
|
||||
override fun addDetachedSubAddress(
|
||||
accountIndex: Int,
|
||||
subAddressIndex: Int,
|
||||
callback: IWalletCallbacks?,
|
||||
) {
|
||||
scope.launch(ioDispatcher) {
|
||||
val subAddress = nativeAddSubAddress(handle, accountIndex, subAddressIndex)
|
||||
val subAddress = nativeAddDetachedSubAddress(handle, accountIndex, subAddressIndex)
|
||||
notifyAddressCreation(subAddress, callback)
|
||||
}
|
||||
}
|
||||
@ -370,7 +370,7 @@ internal class WalletNative private constructor(
|
||||
const val REFRESH_ERROR: Int = 3
|
||||
}
|
||||
|
||||
private external fun nativeAddSubAddress(
|
||||
private external fun nativeAddDetachedSubAddress(
|
||||
handle: Long,
|
||||
subAddressMajor: Int,
|
||||
subAddressMinor: Int,
|
||||
|
Loading…
Reference in New Issue
Block a user