diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/data/WalletRepository.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/data/WalletRepository.kt index f39e139..413cdbe 100644 --- a/demo/android/src/main/kotlin/im/molly/monero/demo/data/WalletRepository.kt +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/data/WalletRepository.kt @@ -58,6 +58,9 @@ class WalletRepository( emitAll(getWallet(walletId).ledger()) } + fun getTransaction(walletId: Long, txId: String): Flow = + getLedger(walletId).map { it.transactions[txId] } + suspend fun addWallet( moneroNetwork: MoneroNetwork, name: String, diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/data/model/Transaction.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/data/model/Transaction.kt deleted file mode 100644 index adc746a..0000000 --- a/demo/android/src/main/kotlin/im/molly/monero/demo/data/model/Transaction.kt +++ /dev/null @@ -1,9 +0,0 @@ -package im.molly.monero.demo.data.model - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity -data class Transaction( - @PrimaryKey val uid: Int, -) diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/data/model/WalletTransaction.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/data/model/WalletTransaction.kt new file mode 100644 index 0000000..6428e70 --- /dev/null +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/data/model/WalletTransaction.kt @@ -0,0 +1,8 @@ +package im.molly.monero.demo.data.model + +import im.molly.monero.Transaction + +data class WalletTransaction( + val walletId: Long, + val transaction: Transaction, +) diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/HistoryScreen.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/HistoryScreen.kt index e0f240e..e5c1ea0 100644 --- a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/HistoryScreen.kt +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/HistoryScreen.kt @@ -1,20 +1,37 @@ package im.molly.monero.demo.ui +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import im.molly.monero.demo.ui.component.Toolbar @Composable fun HistoryRoute( - modifier: Modifier = Modifier, + navigateToTransaction: (String, Long) -> Unit, ) { HistoryScreen( - modifier = modifier, + onTransactionClick = navigateToTransaction, ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun HistoryScreen( - modifier: Modifier = Modifier, + onTransactionClick: (String, Long) -> Unit, ) { - + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + Toolbar( + title = "Transaction history", + scrollBehavior = scrollBehavior, + ) + }, + ) { padding -> + // TODO + } } diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/HomeScreen.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/HomeScreen.kt index 4e0601a..5bff446 100644 --- a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/HomeScreen.kt +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/HomeScreen.kt @@ -18,9 +18,9 @@ import im.molly.monero.demo.ui.component.Toolbar fun HomeRoute( navigateToAddWalletWizard: () -> Unit, navigateToWallet: (Long) -> Unit, - viewModel: HomeViewModel = viewModel(), + walletListViewModel: WalletListViewModel = viewModel(), ) { - val walletListUiState: WalletListUiState by viewModel.walletListUiState.collectAsStateWithLifecycle() + val walletListUiState by walletListViewModel.uiState.collectAsStateWithLifecycle() HomeScreen( walletListUiState = walletListUiState, @@ -56,8 +56,8 @@ private fun HomeScreen( state = listState, modifier = Modifier .fillMaxSize() - .padding(padding)) - { + .padding(padding), + ) { walletCards(walletListUiState, onWalletClick) } } @@ -71,8 +71,11 @@ private fun LazyListScope.walletCards( WalletListUiState.Loading -> item { Text(text = "Loading wallet list...") // TODO } - is WalletListUiState.Success -> { - walletCardsItems(walletListUiState.ids, onWalletClick) + + is WalletListUiState.Loaded -> { + walletCardItems(walletListUiState.walletIds, onWalletClick) } + + is WalletListUiState.Empty -> Unit // TODO } } diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/HomeViewModel.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/HomeViewModel.kt deleted file mode 100644 index d7ff589..0000000 --- a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/HomeViewModel.kt +++ /dev/null @@ -1,36 +0,0 @@ -package im.molly.monero.demo.ui - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import im.molly.monero.demo.AppModule -import im.molly.monero.demo.data.WalletRepository -import kotlinx.coroutines.flow.* - -class HomeViewModel( - private val walletRepository: WalletRepository = AppModule.walletRepository, -) : ViewModel() { - - val walletListUiState: StateFlow = walletRepository.getWalletIdList() - .map { WalletListUiState.Success(it) } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = WalletListUiState.Loading, - ) - -} - -sealed interface HomeUiState { - data object Loading : HomeUiState - -// data class Ready( -// val wallets: List -// ) : HomeUiState - - data object Empty : HomeUiState -} - -sealed interface WalletListUiState { - data class Success(val ids: List) : WalletListUiState - data object Loading : WalletListUiState -} diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/TransactionCard.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/TransactionCard.kt new file mode 100644 index 0000000..0358e57 --- /dev/null +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/TransactionCard.kt @@ -0,0 +1,87 @@ +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import im.molly.monero.MoneroCurrency +import im.molly.monero.Transaction +import im.molly.monero.demo.ui.preview.PreviewParameterData +import im.molly.monero.demo.ui.theme.AppTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TransactionCardExpanded( + transaction: Transaction, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Card( + onClick = onClick, + modifier = modifier.padding(top = 8.dp, start = 8.dp, end = 8.dp) + ) { + Column( + modifier = Modifier.padding(12.dp), + ) { + TransactionDetail("State", transaction.state.toString()) + Spacer(modifier = Modifier.height(12.dp)) + TransactionDetail("Sent", transaction.sent.toString()) + Spacer(modifier = Modifier.height(12.dp)) + TransactionDetail("Received", transaction.received.toString()) + Spacer(modifier = Modifier.height(12.dp)) + TransactionDetail("Payments", transaction.payments.toString()) + Spacer(modifier = Modifier.height(12.dp)) + TransactionDetail("Time lock", transaction.timeLock.toString()) + Spacer(modifier = Modifier.height(12.dp)) + TransactionDetail("Change", MoneroCurrency.format(transaction.change)) + Spacer(modifier = Modifier.height(12.dp)) + TransactionDetail("Fee", MoneroCurrency.format(transaction.fee)) + Spacer(modifier = Modifier.height(12.dp)) + TransactionDetail("Transaction ID", transaction.txId) + Spacer(modifier = Modifier.height(12.dp)) + } + } +} + +@Composable +private fun TransactionDetail( + label: String, + value: String, + modifier: Modifier = Modifier, +) { + Column { + Text(label, style = MaterialTheme.typography.labelMedium, modifier = modifier) + Text(value, style = MaterialTheme.typography.bodyMedium, modifier = modifier) + } +} + +@Preview +@Composable +private fun TransactionCardExpandedPreview( + @PreviewParameter( + TransactionCardPreviewParameterProvider::class, + limit = 1, + ) transactions: List, +) { + AppTheme { + Surface { + TransactionCardExpanded( + transaction = transactions.first(), + onClick = {}, + ) + } + } +} + +private class TransactionCardPreviewParameterProvider : PreviewParameterProvider> { + override val values = sequenceOf(PreviewParameterData.transactions) +} diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/TransactionCardList.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/TransactionCardList.kt new file mode 100644 index 0000000..9623719 --- /dev/null +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/TransactionCardList.kt @@ -0,0 +1,23 @@ +package im.molly.monero.demo.ui + +import TransactionCardExpanded +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.ui.Modifier +import im.molly.monero.demo.data.model.WalletTransaction + +fun LazyListScope.transactionCardItems( + items: List, + onTransactionClick: (txId: String, walletId: Long) -> Unit, + itemModifier: Modifier = Modifier, +) = items( + items = items, + key = { it.transaction.txId }, + itemContent = { + TransactionCardExpanded( + transaction = it.transaction, + onClick = { onTransactionClick(it.transaction.txId, it.walletId) }, + modifier = itemModifier, + ) + }, +) diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/TransactionScreen.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/TransactionScreen.kt new file mode 100644 index 0000000..35fdf18 --- /dev/null +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/TransactionScreen.kt @@ -0,0 +1,158 @@ +package im.molly.monero.demo.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import im.molly.monero.MoneroCurrency +import im.molly.monero.Transaction +import im.molly.monero.demo.ui.component.Toolbar +import im.molly.monero.demo.ui.preview.PreviewParameterData +import im.molly.monero.demo.ui.theme.AppIcons +import im.molly.monero.demo.ui.theme.AppTheme + + +@Composable +fun TransactionRoute( + txId: String, + walletId: Long, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + viewModel: TransactionViewModel = viewModel( + factory = TransactionViewModel.factory(txId, walletId), + key = TransactionViewModel.key(txId, walletId), + ) +) { + val uiState: TxUiState by viewModel.uiState.collectAsStateWithLifecycle() + TransactionScreen( + uiState = uiState, + onBackClick = onBackClick, + modifier = modifier, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TransactionScreen( + uiState: TxUiState, + modifier: Modifier = Modifier, + onBackClick: () -> Unit, +) { + Scaffold(topBar = { + Toolbar( + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + imageVector = AppIcons.ArrowBack, + contentDescription = "Back", + ) + } + }, + title = "Transaction details", + ) + }) { padding -> + Box( + modifier = modifier + .padding(padding) + .verticalScroll(rememberScrollState()), + ) { + Column(Modifier.padding(10.dp)) { + when (uiState) { + is TxUiState.Loaded -> { + val tx = uiState.transaction + TransactionDetail("State", tx.state.toString()) + Spacer(modifier = Modifier.height(12.dp)) + TransactionDetail("Sent", tx.sent.toString()) + Spacer(modifier = Modifier.height(12.dp)) + TransactionDetail("Received", tx.received.toString()) + Spacer(modifier = Modifier.height(12.dp)) + TransactionDetail("Payments", tx.payments.toString()) + Spacer(modifier = Modifier.height(12.dp)) + TransactionDetail("Time lock", tx.timeLock.toString()) + Spacer(modifier = Modifier.height(12.dp)) + TransactionDetail("Change", MoneroCurrency.format(tx.change)) + Spacer(modifier = Modifier.height(12.dp)) + TransactionDetail("Fee", MoneroCurrency.format(tx.fee)) + Spacer(modifier = Modifier.height(12.dp)) + TransactionDetail("Transaction ID", tx.txId) + Spacer(modifier = Modifier.height(12.dp)) + } + + else -> Unit + } + } + } + } +} + +@Composable +private fun TransactionDetail( + label: String, + value: String, + modifier: Modifier = Modifier, +) { + Column() { + Text(label, style = MaterialTheme.typography.labelMedium, modifier = modifier) + Text(value, style = MaterialTheme.typography.bodyMedium, modifier = modifier) + } +} + +@Preview +@Composable +private fun TransactionScreenPopulated( + @PreviewParameter( + TransactionScreenPreviewParameterProvider::class, + limit = 1, + ) transactions: List, +) { + AppTheme { + TransactionScreen( + uiState = TxUiState.Loaded(transactions.first()), + onBackClick = {}, + ) + } +} + +@Preview +@Composable +private fun TransactionScreenError() { + AppTheme { + TransactionScreen( + uiState = TxUiState.Error, + onBackClick = {}, + ) + } +} + +@Preview +@Composable +private fun TransactionScreenNotFound() { + AppTheme { + TransactionScreen( + uiState = TxUiState.NotFound, + onBackClick = {}, + ) + } +} + +class TransactionScreenPreviewParameterProvider : PreviewParameterProvider> { + override val values = sequenceOf(PreviewParameterData.transactions) +} diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/TransactionViewModel.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/TransactionViewModel.kt new file mode 100644 index 0000000..729aaca --- /dev/null +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/TransactionViewModel.kt @@ -0,0 +1,58 @@ +package im.molly.monero.demo.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import im.molly.monero.Transaction +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 +import kotlinx.coroutines.flow.stateIn + +class TransactionViewModel( + txId: String, + walletId: Long, + walletRepository: WalletRepository = AppModule.walletRepository, +) : ViewModel() { + + val uiState: StateFlow = + walletRepository.getTransaction(walletId, txId) + .asResult() + .map { result -> + when (result) { + is Result.Success -> result.data?.let { tx -> + TxUiState.Loaded(tx) + } ?: TxUiState.NotFound + + is Result.Error -> TxUiState.Error + is Result.Loading -> TxUiState.Loading + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = TxUiState.Loading + ) + + companion object { + fun factory(txId: String, walletId: Long) = viewModelFactory { + initializer { + TransactionViewModel(txId, walletId) + } + } + + fun key(txId: String, walletId: Long): String = "tx_$txId:$walletId" + } +} + +sealed interface TxUiState { + data class Loaded(val transaction: Transaction) : TxUiState + data object Error : TxUiState + data object Loading : TxUiState + data object NotFound : TxUiState +} diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletBalanceView.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletBalanceView.kt index 5de7936..6b7b88e 100644 --- a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletBalanceView.kt +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletBalanceView.kt @@ -3,7 +3,9 @@ package im.molly.monero.demo.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.Divider import androidx.compose.material3.MaterialTheme @@ -51,10 +53,12 @@ fun WalletBalanceView( .padding(16.dp) ) { Text( - style = MaterialTheme.typography.headlineSmall, + style = MaterialTheme.typography.bodyLarge, text = "Balance at Block #${blockchainTime.height}", ) + Spacer(modifier = Modifier.height(12.dp)) + BalanceRow("Confirmed", balance.confirmedAmount) BalanceRow("Pending", balance.pendingAmount) Divider() @@ -71,7 +75,6 @@ fun WalletBalanceView( fun BalanceRow( label: String, amount: MoneroAmount, - format: MoneroCurrency.Format = MoneroCurrency.Format(), ) { Row( modifier = Modifier @@ -79,8 +82,8 @@ fun BalanceRow( .padding(vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, ) { - Text(text = label) - Text(text = format.format(amount)) + Text(text = label, style = MaterialTheme.typography.bodyMedium) + Text(text = MoneroCurrency.format(amount), style = MaterialTheme.typography.bodyMedium) } } @@ -90,7 +93,6 @@ fun LockedBalanceRow( amount: MoneroAmount, blockCount: Int, timeRemaining: Duration, - moneroFormat: MoneroCurrency.Format = MoneroCurrency.DefaultFormat, ) { Row( modifier = Modifier @@ -101,10 +103,10 @@ fun LockedBalanceRow( val durationText = "${timeRemaining.toKotlinDuration()} ($blockCount blocks)" Column { - Text(text = label) - Text(text = durationText, fontSize = 12.sp) + Text(text = label, style = MaterialTheme.typography.bodyMedium) + Text(text = durationText, style = MaterialTheme.typography.bodySmall) } - Text(text = moneroFormat.format(amount)) + Text(text = MoneroCurrency.format(amount), style = MaterialTheme.typography.bodyMedium) } } diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletCard.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletCard.kt index 2a18eba..3c4b051 100644 --- a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletCard.kt +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletCard.kt @@ -5,50 +5,81 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import im.molly.monero.Balance +import im.molly.monero.Ledger +import im.molly.monero.demo.data.model.WalletConfig +import im.molly.monero.demo.ui.theme.AppTheme @OptIn(ExperimentalMaterial3Api::class) @Composable fun WalletCard( walletId: Long, + onClick: () -> Unit, modifier: Modifier = Modifier, - onClick: (Long) -> Unit, viewModel: WalletViewModel = viewModel( factory = WalletViewModel.factory(walletId), - key = walletId.toString(), + key = WalletViewModel.key(walletId), ), ) { - val walletUiState: WalletUiState by viewModel.walletUiState.collectAsStateWithLifecycle() + val uiState: WalletUiState by viewModel.uiState.collectAsStateWithLifecycle() Card( - onClick = { onClick(walletId) }, - modifier = modifier - .padding(8.dp) + onClick = onClick, modifier = modifier.padding(8.dp) ) { Column { - when (walletUiState) { - WalletUiState.Error -> { - Text(text = "Error") // TODO - } - WalletUiState.Loading -> { - Text(text = "Loading wallet...") // TODO - } - is WalletUiState.Success -> { - val state = walletUiState as WalletUiState.Success - Row { - Text(text = "Name: ${state.config.name}") - } - Row { - Text(text = "Ledger: ${state.ledger}") - } + when (uiState) { + is WalletUiState.Loaded -> { + WalletCardExpanded( + (uiState as WalletUiState.Loaded).config, + (uiState as WalletUiState.Loaded).balance, + ) } + + WalletUiState.Error -> WalletCardError() + WalletUiState.Loading -> WalletCardLoading() } } } } + +@Composable +fun WalletCardExpanded( + config: WalletConfig, + balance: Balance, +) { + Row { + Text(text = "Name: ${config.name}") + } + Row { + Text(text = "Ledger: $balance") + } +} + +@Composable +fun WalletCardError() { + Text(text = "Error") // TODO +} + +@Composable +fun WalletCardLoading() { + Text(text = "Loading wallet...") // TODO +} + +//@Preview +//@Composable +//private fun WalletCardExpandedPreview() { +// AppTheme { +// Surface { +// WalletCardExpanded() +// } +// } +//} diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletCardList.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletCardList.kt index 07920c4..333ec1a 100644 --- a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletCardList.kt +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletCardList.kt @@ -4,9 +4,9 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.ui.Modifier -fun LazyListScope.walletCardsItems( +fun LazyListScope.walletCardItems( items: List, - onItemClick: (Long) -> Unit, + onItemClick: (walletId: Long) -> Unit, itemModifier: Modifier = Modifier, ) = items( items = items, @@ -14,7 +14,7 @@ fun LazyListScope.walletCardsItems( itemContent = { WalletCard( walletId = it, - onClick = onItemClick, + onClick = { onItemClick(it) }, modifier = itemModifier, ) }, diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletListViewModel.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletListViewModel.kt new file mode 100644 index 0000000..176bb41 --- /dev/null +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletListViewModel.kt @@ -0,0 +1,31 @@ +package im.molly.monero.demo.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import im.molly.monero.demo.AppModule +import im.molly.monero.demo.data.WalletRepository +import kotlinx.coroutines.flow.* + +class WalletListViewModel( + walletRepository: WalletRepository = AppModule.walletRepository, +) : ViewModel() { + + val uiState: StateFlow = + walletRepository.getWalletIdList().map { walletIds -> + if (walletIds.isNotEmpty()) { + WalletListUiState.Loaded(walletIds) + } else { + WalletListUiState.Empty + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = WalletListUiState.Loading, + ) +} + +sealed interface WalletListUiState { + data class Loaded(val walletIds: List) : WalletListUiState + data object Loading : WalletListUiState + data object Empty : WalletListUiState +} diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletScreen.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletScreen.kt index cc0a2d6..1fdcec6 100644 --- a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletScreen.kt +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletScreen.kt @@ -1,39 +1,46 @@ package im.molly.monero.demo.ui import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import im.molly.monero.BlockchainTime import im.molly.monero.Ledger import im.molly.monero.MoneroCurrency import im.molly.monero.demo.data.model.WalletConfig import im.molly.monero.demo.ui.component.Toolbar +import im.molly.monero.demo.ui.preview.PreviewParameterData import im.molly.monero.demo.ui.theme.AppIcons import im.molly.monero.demo.ui.theme.AppTheme @Composable fun WalletRoute( walletId: Long, + onTransactionClick: (String, Long) -> Unit, onBackClick: () -> Unit, modifier: Modifier = Modifier, viewModel: WalletViewModel = viewModel( factory = WalletViewModel.factory(walletId), - key = walletId.toString(), + key = WalletViewModel.key(walletId), ) ) { - val walletUiState: WalletUiState by viewModel.walletUiState.collectAsStateWithLifecycle() + val uiState: WalletUiState by viewModel.uiState.collectAsStateWithLifecycle() WalletScreen( - uiState = walletUiState, + uiState = uiState, onWalletConfigChange = { config -> viewModel.updateConfig(config) }, + onTransactionClick = onTransactionClick, onBackClick = onBackClick, modifier = modifier, ) @@ -42,20 +49,113 @@ fun WalletRoute( @Composable private fun WalletScreen( uiState: WalletUiState, + onWalletConfigChange: (WalletConfig) -> Unit, + onTransactionClick: (String, Long) -> Unit, + onBackClick: () -> Unit, modifier: Modifier = Modifier, - onWalletConfigChange: (WalletConfig) -> Unit = {}, - onBackClick: () -> Unit = {}, ) { when (uiState) { - WalletUiState.Error -> WalletScreenError(onBackClick = onBackClick) - WalletUiState.Loading -> WalletScreenLoading(onBackClick = onBackClick) - is WalletUiState.Success -> WalletScreenPopulated( - walletConfig = uiState.config, - ledger = uiState.ledger, + is WalletUiState.Loaded -> WalletScreenLoaded( + uiState = uiState, onWalletConfigChange = onWalletConfigChange, + onTransactionClick = onTransactionClick, onBackClick = onBackClick, modifier = modifier, ) + + WalletUiState.Error -> WalletScreenError(onBackClick = onBackClick) + WalletUiState.Loading -> WalletScreenLoading(onBackClick = onBackClick) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun WalletScreenLoaded( + uiState: WalletUiState.Loaded, + onWalletConfigChange: (WalletConfig) -> Unit, + onTransactionClick: (String, Long) -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + var showRenameDialog by remember { mutableStateOf(false) } + + Scaffold(topBar = { + Toolbar(navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + imageVector = AppIcons.ArrowBack, + contentDescription = "Back", + ) + } + }, actions = { + WalletKebabMenu( + onRenameClick = { showRenameDialog = true }, + onDeleteClick = { }, + ) + }) + }) { padding -> + Column( + modifier = modifier + .fillMaxSize() + .padding(padding), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text(style = MaterialTheme.typography.headlineLarge, text = buildAnnotatedString { + append(MoneroCurrency.SYMBOL + " ") + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append( + 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", "Transactions"), + selectedTabIndex = selectedTabIndex, + onTabSelected = { index -> selectedTabIndex = index }, + ) + + when (selectedTabIndex) { + 0 -> { + WalletBalanceView(balance = uiState.balance, blockchainTime = uiState.blockchainTime) + } + + 1 -> { + LazyColumn { + transactionCardItems( + items = uiState.transactions, + onTransactionClick = onTransactionClick, + ) + } + } + } + } + + if (showRenameDialog) { + var name by remember { mutableStateOf(uiState.config.name) } + AlertDialog( + onDismissRequest = { showRenameDialog = false }, + title = { Text("Enter wallet name") }, + text = { + OutlinedTextField( + value = name, + onValueChange = { name = it }, + singleLine = true, + ) + }, + confirmButton = { + TextButton(onClick = { + onWalletConfigChange(uiState.config.copy(name = name)) + showRenameDialog = false + }) { + Text("Rename") + } + }, + ) + } } } @@ -71,85 +171,6 @@ private fun WalletScreenLoading( ) { } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun WalletScreenPopulated( - walletConfig: WalletConfig, - ledger: Ledger, - onWalletConfigChange: (WalletConfig) -> Unit, - onBackClick: () -> Unit, - modifier: Modifier = Modifier, -) { - var showRenameDialog by remember { mutableStateOf(false) } - - Scaffold( - topBar = { - Toolbar( - navigationIcon = { - IconButton(onClick = onBackClick) { - Icon( - imageVector = AppIcons.ArrowBack, - contentDescription = "Back", - ) - } - }, - actions = { - WalletKebabMenu( - onRenameClick = { showRenameDialog = true }, - onDeleteClick = { }, - ) - } - ) - } - ) { padding -> - Column( - modifier = modifier - .fillMaxSize() - .padding(padding), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - style = MaterialTheme.typography.headlineLarge, - text = buildAnnotatedString { - append(MoneroCurrency.SYMBOL + " ") - withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { - append(MoneroCurrency.Format(precision = 5).format(ledger.balance.confirmedAmount)) - } - } - ) - Text(text = walletConfig.name, style = MaterialTheme.typography.headlineSmall) - - WalletBalanceView( - balance = ledger.balance, - blockchainTime = ledger.checkedAt, - ) - } - - if (showRenameDialog) { - var name by remember { mutableStateOf(walletConfig.name) } - AlertDialog( - onDismissRequest = { showRenameDialog = false }, - title = { Text("Enter wallet name") }, - text = { - OutlinedTextField( - value = name, - onValueChange = { name = it }, - singleLine = true, - ) - }, - confirmButton = { - TextButton(onClick = { - onWalletConfigChange(walletConfig.copy(name = name)) - showRenameDialog = false - }) { - Text("Rename") - } - }, - ) - } - } -} - @Composable private fun WalletKebabMenu( onRenameClick: () -> Unit, @@ -184,26 +205,57 @@ private fun WalletKebabMenu( } } +@Composable +fun WalletHeaderTabs( + titles: List, + selectedTabIndex: Int, + onTabSelected: (Int) -> Unit, +) { + Column { + TabRow(selectedTabIndex = selectedTabIndex) { + titles.forEachIndexed { index, title -> + Tab( + selected = index == selectedTabIndex, + onClick = { onTabSelected(index) }, + text = { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + overflow = TextOverflow.Ellipsis, + ) + }, + ) + } + } + } +} + @Preview @Composable -private fun WalletScreenPreview() { +private fun WalletScreenPopulated( + @PreviewParameter(WalletScreenPreviewParameterProvider::class) ledger: Ledger, +) { AppTheme { WalletScreen( - uiState = WalletUiState.Success( - WalletConfig( + uiState = WalletUiState.Loaded( + config = WalletConfig( id = 0, - publicAddress = "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H", + publicAddress = ledger.primaryAddress, filename = "", name = "Personal", remoteNodes = emptySet(), ), - Ledger( - publicAddress = "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H", - transactions = emptyMap(), - enotes = emptySet(), - checkedAt = BlockchainTime.Genesis, - ), + balance = ledger.balance, + blockchainTime = ledger.checkedAt, + transactions = emptyList(), ), + onWalletConfigChange = {}, + onTransactionClick = { _: String, _: Long -> }, + onBackClick = {}, ) } } + +private class WalletScreenPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf(PreviewParameterData.ledger) +} diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletViewModel.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletViewModel.kt index 8d30307..becc6d4 100644 --- a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletViewModel.kt +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletViewModel.kt @@ -4,21 +4,23 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory -import im.molly.monero.Ledger +import im.molly.monero.Balance +import im.molly.monero.BlockchainTime 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.WalletConfig +import im.molly.monero.demo.data.model.WalletTransaction import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch class WalletViewModel( - private val walletId: Long, + walletId: Long, private val walletRepository: WalletRepository = AppModule.walletRepository, ) : ViewModel() { - val walletUiState: StateFlow = walletUiState( + val uiState: StateFlow = walletUiState( walletId = walletId, walletRepository = walletRepository, ).stateIn( @@ -33,6 +35,8 @@ class WalletViewModel( WalletViewModel(walletId) } } + + fun key(walletId: Long): String = "wallet_$walletId" } fun updateConfig(config: WalletConfig) { @@ -53,11 +57,19 @@ private fun walletUiState( ).asResult().map { result -> when (result) { is Result.Success -> { - WalletUiState.Success(result.data.first, result.data.second) + val config = result.data.first + val ledger = result.data.second + val balance = ledger.balance + val blockchainTime = ledger.checkedAt + val transactions = + ledger.transactions.map { WalletTransaction(config.id, it.value) } + WalletUiState.Loaded(config, blockchainTime, balance, transactions) } + is Result.Loading -> { WalletUiState.Loading } + is Result.Error -> { WalletUiState.Error } @@ -66,9 +78,11 @@ private fun walletUiState( } sealed interface WalletUiState { - data class Success( + data class Loaded( val config: WalletConfig, - val ledger: Ledger, + val blockchainTime: BlockchainTime, + val balance: Balance, + val transactions: List, ) : WalletUiState data object Error : WalletUiState diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/navigation/HistoryNavigation.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/navigation/HistoryNavigation.kt index 1138225..22e8792 100644 --- a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/navigation/HistoryNavigation.kt +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/navigation/HistoryNavigation.kt @@ -12,8 +12,12 @@ fun NavController.navigateToHistory(navOptions: NavOptions? = null) { navigate(historyNavRoute, navOptions) } -fun NavGraphBuilder.historyScreen() { +fun NavGraphBuilder.historyScreen( + navigateToTransaction: (String, Long) -> Unit, +) { composable(route = historyNavRoute) { - HistoryRoute() + HistoryRoute( + navigateToTransaction = navigateToTransaction, + ) } } diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/navigation/NavGraph.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/navigation/NavGraph.kt index 67ab25c..54267a5 100644 --- a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/navigation/NavGraph.kt +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/navigation/NavGraph.kt @@ -25,13 +25,23 @@ fun NavGraph( navController.navigateToAddWalletWizardGraph() }, ) - historyScreen() + historyScreen( + navigateToTransaction = { txId, walletId -> + navController.navigateToTransaction(txId, walletId) + }, + ) settingsScreen( navigateToEditRemoteNode = { remoteNodeId -> navController.navigateToEditRemoteNode(remoteNodeId) }, ) walletScreen( + navigateToTransaction = { txId, walletId -> + navController.navigateToTransaction(txId, walletId) + }, + onBackClick = onBackClick, + ) + transactionScreen( onBackClick = onBackClick, ) editRemoteNodeDialog( diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/navigation/TransactionNavigation.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/navigation/TransactionNavigation.kt new file mode 100644 index 0000000..12e7bfe --- /dev/null +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/navigation/TransactionNavigation.kt @@ -0,0 +1,36 @@ +package im.molly.monero.demo.ui.navigation + +import androidx.navigation.* +import androidx.navigation.compose.composable +import im.molly.monero.demo.ui.TransactionRoute + +const val transactionNavRoute = "tx" + +private const val txIdArg = "txId" +private const val walletIdArg = "walletId" + +fun NavController.navigateToTransaction(txId: String, walletId: Long) { + val route = "$transactionNavRoute/$txId/$walletId" + navigate(route) +} + +fun NavGraphBuilder.transactionScreen( + onBackClick: () -> Unit, +) { + composable( + route = "$transactionNavRoute/{$txIdArg}/{$walletIdArg}", + arguments = listOf( + navArgument(txIdArg) { type = NavType.StringType }, + navArgument(walletIdArg) { type = NavType.LongType }, + ) + ) { + val arguments = requireNotNull(it.arguments) + val txId = arguments.getString(txIdArg) + val walletId = arguments.getLong(walletIdArg) + TransactionRoute( + txId = txId!!, + walletId = walletId, + onBackClick = onBackClick, + ) + } +} diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/navigation/WalletNavigation.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/navigation/WalletNavigation.kt index a99f437..ace6585 100644 --- a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/navigation/WalletNavigation.kt +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/navigation/WalletNavigation.kt @@ -32,6 +32,7 @@ fun NavController.navigateToAddWalletSecondStep(restoreWallet: Boolean) { } fun NavGraphBuilder.walletScreen( + navigateToTransaction: (String, Long) -> Unit, onBackClick: () -> Unit, ) { composable( @@ -44,6 +45,7 @@ fun NavGraphBuilder.walletScreen( val walletId = arguments.getLong(walletIdArg) WalletRoute( walletId = walletId, + onTransactionClick = navigateToTransaction, onBackClick = onBackClick, ) } diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/preview/PreviewParameterData.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/preview/PreviewParameterData.kt new file mode 100644 index 0000000..49eb36d --- /dev/null +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/preview/PreviewParameterData.kt @@ -0,0 +1,37 @@ +package im.molly.monero.demo.ui.preview + +import im.molly.monero.BlockHeader +import im.molly.monero.BlockchainTime +import im.molly.monero.HashDigest +import im.molly.monero.Ledger +import im.molly.monero.MoneroAmount +import im.molly.monero.PaymentDetail +import im.molly.monero.PublicAddress +import im.molly.monero.Transaction +import im.molly.monero.TxState +import im.molly.monero.xmr + +object PreviewParameterData { + val recipients = + listOf(PublicAddress.parse("888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H")) + + val transactions = listOf( + Transaction( + hash = HashDigest("e7a60483591378d536792d070f2bf6ccb7d0666df03b57f485ddaf66899a294b"), + state = TxState.OnChain(BlockHeader(height = 2999840, epochSecond = 1697792826)), + timeLock = BlockchainTime.Block(2999850), + sent = emptySet(), + received = emptySet(), + payments = listOf(PaymentDetail((0.10).xmr, recipients.first())), + fee = 0.00093088.xmr, + change = MoneroAmount.ZERO, + ), + ) + + val ledger = Ledger( + primaryAddress = "4AYjQM9HoAFNUeC3cvSfgeAN89oMMpMqiByvunzSzhn97cj726rJj3x8hCbH58UnMqQJShczCxbpWRiCJQ3HCUDHLiKuo4T", + checkedAt = BlockchainTime.Block(2999840), + enotes = emptySet(), + transactions = transactions.associateBy { it.txId }, + ) +} 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 8dcade7..2026e78 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/AccountAddress.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/AccountAddress.kt @@ -6,14 +6,18 @@ data class AccountAddress( val subAddressIndex: Int = 0, ) : PublicAddress by publicAddress { + val isPrimaryAddress: Boolean + get() = accountIndex == 0 && subAddressIndex == 0 + init { when (publicAddress) { - is SubAddress -> require(accountIndex != -1 && subAddressIndex != -1) - else -> require(accountIndex == 0 && subAddressIndex == 0) + is StandardAddress -> require(isPrimaryAddress) { + "Standard addresses must have subaddress indices set to zero" + } + is SubAddress -> require(accountIndex != -1 && subAddressIndex != -1) { + "Invalid subaddress indices" + } + else -> throw IllegalArgumentException("Unsupported address type") } } - - fun belongsTo(targetAccountIndex: Int): Boolean { - return accountIndex == targetAccountIndex - } } diff --git a/lib/android/src/main/kotlin/im/molly/monero/Enote.kt b/lib/android/src/main/kotlin/im/molly/monero/Enote.kt index 0fb7fc6..de49778 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/Enote.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/Enote.kt @@ -9,7 +9,8 @@ class Enote( val sourceTxId: String?, ) { init { - require(age >= 0) { "Enote age $age must not be negative" } + require(amount > 0) { "Amount must be greater than 0" } + require(age >= 0) { "Age cannot be negative" } } var spent: Boolean = false diff --git a/lib/android/src/main/kotlin/im/molly/monero/Ledger.kt b/lib/android/src/main/kotlin/im/molly/monero/Ledger.kt index fc9f02b..11ce457 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/Ledger.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/Ledger.kt @@ -3,7 +3,7 @@ package im.molly.monero //import im.molly.monero.proto.LedgerProto data class Ledger( - val publicAddress: String, + val primaryAddress: String, val transactions: Map, val enotes: Set>, val checkedAt: BlockchainTime, diff --git a/lib/android/src/main/kotlin/im/molly/monero/MoneroAmount.kt b/lib/android/src/main/kotlin/im/molly/monero/MoneroAmount.kt index e96a8f6..4d6267b 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/MoneroAmount.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/MoneroAmount.kt @@ -15,7 +15,8 @@ value class MoneroAmount(val atomicUnits: Long) : Parcelable { val ZERO = MoneroAmount(0) } - fun toXmr(): BigDecimal = BigDecimal.valueOf(atomicUnits, ATOMIC_UNIT_SCALE) + val xmr: BigDecimal + get() = BigDecimal.valueOf(atomicUnits, ATOMIC_UNIT_SCALE) override fun toString() = atomicUnits.toString() diff --git a/lib/android/src/main/kotlin/im/molly/monero/MoneroCurrency.kt b/lib/android/src/main/kotlin/im/molly/monero/MoneroCurrency.kt index 635e06e..7615532 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/MoneroCurrency.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/MoneroCurrency.kt @@ -8,10 +8,8 @@ object MoneroCurrency { const val MAX_PRECISION = MoneroAmount.ATOMIC_UNIT_SCALE - val DefaultFormat = Format() - - data class Format( - val precision: Int = MAX_PRECISION, + open class Format( + val precision: Int, val locale: Locale = Locale.US, ) { init { @@ -24,10 +22,36 @@ object MoneroCurrency { minimumFractionDigits = precision } - fun format(amount: MoneroAmount): String = numberFormat.format(amount.toXmr()) + open fun format(amount: MoneroAmount): String { + return numberFormat.format(amount.xmr) + } - fun parse(source: String): MoneroAmount { + open fun parse(source: String): MoneroAmount { TODO() } } + + val ExactFormat = object : Format(MoneroAmount.ATOMIC_UNIT_SCALE) { + override fun format(amount: MoneroAmount) = buildString { + val num = amount.atomicUnits.toString() + + if (precision < num.length) { + val point = num.length - precision + append(num.substring(0, point)) + append('.') + append(num.substring(point)) + } else { + append("0.") + for (i in 1..(precision - num.length)) { + append('0') + } + append(num) + } + } + } + + + fun format(amount: MoneroAmount, outputFormat: Format = ExactFormat): String { + return outputFormat.format(amount) + } } 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 99d60d4..3f62eb3 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/PublicAddress.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/PublicAddress.kt @@ -2,7 +2,8 @@ package im.molly.monero import im.molly.monero.util.decodeBase58 -interface PublicAddress { +sealed interface PublicAddress { + val address: String val network: MoneroNetwork val subAddress: Boolean // viewPublicKey: ByteArray @@ -19,19 +20,18 @@ interface PublicAddress { throw InvalidAddress("Address too short") } - val prefix = decoded[0].toLong() - - StandardAddress.prefixes[prefix]?.let { network -> - return StandardAddress(network) + return when (val prefix = decoded[0].toLong()) { + in StandardAddress.prefixes -> { + StandardAddress(publicAddress, StandardAddress.prefixes[prefix]!!) + } + in SubAddress.prefixes -> { + SubAddress(publicAddress, SubAddress.prefixes[prefix]!!) + } + in IntegratedAddress.prefixes -> { + TODO() + } + else -> throw InvalidAddress("Unrecognized address prefix") } - SubAddress.prefixes[prefix]?.let { network -> - return SubAddress(network) - } - IntegratedAddress.prefixes[prefix]?.let { network -> - TODO() - } - - throw InvalidAddress("Unrecognized address prefix") } } } @@ -39,6 +39,7 @@ interface PublicAddress { class InvalidAddress(message: String, cause: Throwable? = null) : Exception(message, cause) data class StandardAddress( + override val address: String, override val network: MoneroNetwork, ) : PublicAddress { override val subAddress = false @@ -50,9 +51,12 @@ data class StandardAddress( 24L to MoneroNetwork.Stagenet, ) } + + override fun toString(): String = address } data class SubAddress( + override val address: String, override val network: MoneroNetwork, ) : PublicAddress { override val subAddress = true @@ -64,9 +68,12 @@ data class SubAddress( 36L to MoneroNetwork.Stagenet, ) } + + override fun toString(): String = address } data class IntegratedAddress( + override val address: String, override val network: MoneroNetwork, val paymentId: Long, ) : PublicAddress { @@ -79,4 +86,6 @@ data class IntegratedAddress( 25L to MoneroNetwork.Stagenet, ) } + + override fun toString(): String = address }