mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2024-10-01 03:45:36 -04:00
demo: add restore wallet screen
This commit is contained in:
parent
e61021d8bd
commit
70231790ee
@ -2,7 +2,7 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "419c430462df613cab5f23d35923c2b7",
|
||||
"identityHash": "a717d86bf72794f75768dfa37ee61831",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "wallets",
|
||||
@ -39,17 +39,7 @@
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_wallets_public_address",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"public_address"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_wallets_public_address` ON `${TABLE_NAME}` (`public_address`)"
|
||||
}
|
||||
],
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
@ -179,7 +169,7 @@
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '419c430462df613cab5f23d35923c2b7')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a717d86bf72794f75768dfa37ee61831')"
|
||||
]
|
||||
}
|
||||
}
|
@ -5,12 +5,22 @@ import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
|
||||
/**
|
||||
* A generic class that holds a value with its loading status.
|
||||
* @param <T>
|
||||
*/
|
||||
sealed interface Result<out T> {
|
||||
data class Success<T>(val data: T) : Result<T>
|
||||
data class Error(val exception: Throwable? = null) : Result<Nothing>
|
||||
object Loading : Result<Nothing>
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` if [Result] is of type [Result.Success] & holds non-null [Result.Success.data].
|
||||
*/
|
||||
val Result<*>.succeeded
|
||||
get() = this is Result.Success && data != null
|
||||
|
||||
fun <T> Flow<T>.asResult(): Flow<Result<T>> {
|
||||
return this
|
||||
.map<T, Result<T>> {
|
||||
|
@ -28,6 +28,23 @@ class MoneroSdkClient(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun restoreWallet(
|
||||
network: MoneroNetwork,
|
||||
filename: String,
|
||||
secretSpendKey: SecretKey,
|
||||
restorePoint: RestorePoint,
|
||||
): MoneroWallet {
|
||||
val provider = providerDeferred.await()
|
||||
return provider.restoreWallet(
|
||||
network = network,
|
||||
dataStore = WalletDataStoreFile(filename, newFile = true),
|
||||
secretSpendKey = secretSpendKey,
|
||||
restorePoint = restorePoint,
|
||||
).also { wallet ->
|
||||
wallet.commit()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun openWallet(
|
||||
network: MoneroNetwork,
|
||||
filename: String,
|
||||
|
@ -74,6 +74,29 @@ class WalletRepository(
|
||||
return walletId to wallet
|
||||
}
|
||||
|
||||
suspend fun restoreWallet(
|
||||
moneroNetwork: MoneroNetwork,
|
||||
name: String,
|
||||
remoteNodeIds: List<Long>,
|
||||
secretSpendKey: SecretKey,
|
||||
restorePoint: RestorePoint,
|
||||
): Pair<Long, MoneroWallet> {
|
||||
val uniqueFilename = UUID.randomUUID().toString()
|
||||
val wallet = moneroSdkClient.restoreWallet(
|
||||
moneroNetwork,
|
||||
uniqueFilename,
|
||||
secretSpendKey,
|
||||
restorePoint,
|
||||
)
|
||||
val walletId = walletDataSource.createWalletConfig(
|
||||
publicAddress = wallet.primaryAddress,
|
||||
filename = uniqueFilename,
|
||||
name = name,
|
||||
remoteNodeIds = remoteNodeIds,
|
||||
)
|
||||
return walletId to wallet
|
||||
}
|
||||
|
||||
suspend fun updateWalletConfig(walletConfig: WalletConfig) =
|
||||
walletDataSource.updateWalletConfig(walletConfig)
|
||||
}
|
||||
|
@ -8,9 +8,6 @@ import im.molly.monero.demo.data.model.WalletConfig
|
||||
|
||||
@Entity(
|
||||
tableName = "wallets",
|
||||
indices = [
|
||||
Index(value = ["public_address"], unique = true)
|
||||
],
|
||||
)
|
||||
data class WalletEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
|
@ -47,7 +47,7 @@ class SyncService(
|
||||
while (isActive) {
|
||||
val result = wallet.awaitRefresh()
|
||||
if (result.isError()) {
|
||||
break
|
||||
// TODO: Handle non-recoverable errors
|
||||
}
|
||||
wallet.commit()
|
||||
delay(10.seconds)
|
||||
|
@ -4,15 +4,19 @@ import androidx.compose.runtime.*
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import im.molly.monero.MoneroNetwork
|
||||
import im.molly.monero.RestorePoint
|
||||
import im.molly.monero.SecretKey
|
||||
import im.molly.monero.demo.AppModule
|
||||
import im.molly.monero.demo.data.RemoteNodeRepository
|
||||
import im.molly.monero.demo.data.WalletRepository
|
||||
import im.molly.monero.demo.data.model.DefaultMoneroNetwork
|
||||
import im.molly.monero.demo.data.model.RemoteNode
|
||||
import im.molly.monero.util.parseHex
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.LocalDate
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class AddWalletViewModel(
|
||||
@ -26,6 +30,15 @@ class AddWalletViewModel(
|
||||
var walletName by mutableStateOf("")
|
||||
private set
|
||||
|
||||
var secretSpendKeyHex by mutableStateOf("")
|
||||
private set
|
||||
|
||||
var creationDate by mutableStateOf("")
|
||||
private set
|
||||
|
||||
var restoreHeight by mutableStateOf("")
|
||||
private set
|
||||
|
||||
val currentRemoteNodes: StateFlow<List<RemoteNode>> =
|
||||
snapshotFlow { network }
|
||||
.flatMapLatest {
|
||||
@ -44,6 +57,18 @@ class AddWalletViewModel(
|
||||
private fun getSelectedRemoteNodeIds() =
|
||||
selectedRemoteNodes.filterValues { checked -> checked }.keys.filterNotNull()
|
||||
|
||||
init {
|
||||
val previousNodes = mutableSetOf<RemoteNode>()
|
||||
|
||||
currentRemoteNodes.onEach { remoteNodes ->
|
||||
val unseenNodes = remoteNodes.filter { it !in previousNodes }
|
||||
unseenNodes.forEach { node ->
|
||||
selectedRemoteNodes[node.id] = true
|
||||
previousNodes.add(node)
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun toggleSelectedNetwork(network: MoneroNetwork) {
|
||||
this.network = network
|
||||
}
|
||||
@ -52,7 +77,47 @@ class AddWalletViewModel(
|
||||
this.walletName = name
|
||||
}
|
||||
|
||||
fun updateSecretSpendKeyHex(value: String) {
|
||||
this.secretSpendKeyHex = value
|
||||
}
|
||||
|
||||
fun updateCreationDate(value: String) {
|
||||
this.creationDate = value
|
||||
}
|
||||
|
||||
fun updateRestoreHeight(value: String) {
|
||||
this.restoreHeight = value
|
||||
}
|
||||
|
||||
fun validateSecretSpendKeyHex(): Boolean =
|
||||
secretSpendKeyHex.length == 64 && runCatching { secretSpendKeyHex.parseHex() }.isSuccess
|
||||
|
||||
fun validateCreationDate(): Boolean =
|
||||
creationDate.isEmpty() || runCatching { LocalDate.parse(creationDate) }.isSuccess
|
||||
|
||||
fun validateRestoreHeight(): Boolean =
|
||||
restoreHeight.isEmpty() || runCatching { RestorePoint(restoreHeight.toLong()) }.isSuccess
|
||||
|
||||
fun createWallet() = viewModelScope.launch {
|
||||
walletRepository.addWallet(network, walletName, getSelectedRemoteNodeIds())
|
||||
}
|
||||
|
||||
fun restoreWallet() = viewModelScope.launch {
|
||||
val restorePoint = if (creationDate.isNotEmpty()) {
|
||||
RestorePoint(creationDate = LocalDate.parse(creationDate))
|
||||
} else if (restoreHeight.isNotEmpty()) {
|
||||
RestorePoint(blockHeight = restoreHeight.toLong())
|
||||
} else {
|
||||
RestorePoint(blockHeight = 0)
|
||||
}
|
||||
SecretKey(secretSpendKeyHex.parseHex()).use { secretSpendKey ->
|
||||
walletRepository.restoreWallet(
|
||||
network,
|
||||
walletName,
|
||||
getSelectedRemoteNodeIds(),
|
||||
secretSpendKey,
|
||||
restorePoint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,13 @@ package im.molly.monero.demo.ui
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@ -71,7 +72,7 @@ private fun FirstStepScreen(
|
||||
Text("Create a new wallet")
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = {}, // TODO: onRestoreClick,
|
||||
onClick = onRestoreClick,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
) {
|
||||
Text("I already have a wallet")
|
||||
@ -95,13 +96,26 @@ fun AddWalletSecondStepRoute(
|
||||
modifier = modifier,
|
||||
onBackClick = onBackClick,
|
||||
onCreateClick = {
|
||||
viewModel.createWallet()
|
||||
if (showRestoreOptions) {
|
||||
viewModel.restoreWallet()
|
||||
} else {
|
||||
viewModel.createWallet()
|
||||
}
|
||||
onNavigateToHome()
|
||||
},
|
||||
walletName = viewModel.walletName,
|
||||
network = viewModel.network,
|
||||
secretSpendKeyHex = viewModel.secretSpendKeyHex,
|
||||
secretSpendKeyHexError = !viewModel.validateSecretSpendKeyHex(),
|
||||
creationDate = viewModel.creationDate,
|
||||
creationDateError = !viewModel.validateCreationDate(),
|
||||
restoreHeight = viewModel.restoreHeight,
|
||||
restoreHeightError = !viewModel.validateRestoreHeight(),
|
||||
onWalletNameChanged = { name -> viewModel.updateWalletName(name) },
|
||||
onNetworkChanged = { network -> viewModel.toggleSelectedNetwork(network) },
|
||||
onSecretSpendKeyHexChanged = { value -> viewModel.updateSecretSpendKeyHex(value) },
|
||||
onCreationDateChanged = { value -> viewModel.updateCreationDate(value) },
|
||||
onRestoreHeightChanged = { value -> viewModel.updateRestoreHeight(value) },
|
||||
remoteNodes = remoteNodes,
|
||||
selectedRemoteNodeIds = viewModel.selectedRemoteNodes,
|
||||
)
|
||||
@ -112,12 +126,21 @@ fun AddWalletSecondStepRoute(
|
||||
private fun SecondStepScreen(
|
||||
showRestoreOptions: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onBackClick: () -> Unit,
|
||||
onCreateClick: () -> Unit,
|
||||
onBackClick: () -> Unit = {},
|
||||
onCreateClick: () -> Unit = {},
|
||||
walletName: String,
|
||||
secretSpendKeyHex: String,
|
||||
secretSpendKeyHexError: Boolean,
|
||||
creationDate: String,
|
||||
creationDateError: Boolean,
|
||||
restoreHeight: String,
|
||||
restoreHeightError: Boolean,
|
||||
network: MoneroNetwork,
|
||||
onWalletNameChanged: (String) -> Unit,
|
||||
onNetworkChanged: (MoneroNetwork) -> Unit,
|
||||
onWalletNameChanged: (String) -> Unit = {},
|
||||
onNetworkChanged: (MoneroNetwork) -> Unit = {},
|
||||
onSecretSpendKeyHexChanged: (String) -> Unit = {},
|
||||
onCreationDateChanged: (String) -> Unit = {},
|
||||
onRestoreHeightChanged: (String) -> Unit = {},
|
||||
remoteNodes: List<RemoteNode>,
|
||||
selectedRemoteNodeIds: MutableMap<Long?, Boolean> = mutableMapOf(),
|
||||
) {
|
||||
@ -139,6 +162,7 @@ private fun SecondStepScreen(
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(padding)
|
||||
.imePadding()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
OutlinedTextField(
|
||||
@ -173,13 +197,63 @@ private fun SecondStepScreen(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp),
|
||||
)
|
||||
if (showRestoreOptions) {
|
||||
Text(
|
||||
text = "Deterministic wallet recovery",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier
|
||||
.padding(16.dp),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = secretSpendKeyHex,
|
||||
label = { Text("Secret spend key") },
|
||||
onValueChange = onSecretSpendKeyHexChanged,
|
||||
singleLine = true,
|
||||
isError = secretSpendKeyHexError,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp),
|
||||
)
|
||||
Text(
|
||||
text = "Synchronization",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier
|
||||
.padding(16.dp),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = creationDate,
|
||||
label = { Text("Wallet creation date") },
|
||||
onValueChange = onCreationDateChanged,
|
||||
singleLine = true,
|
||||
isError = creationDateError,
|
||||
enabled = restoreHeight.isEmpty(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = restoreHeight,
|
||||
label = { Text("Restore height") },
|
||||
onValueChange = onRestoreHeightChanged,
|
||||
singleLine = true,
|
||||
isError = restoreHeightError,
|
||||
enabled = creationDate.isEmpty(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
val validInput = !showRestoreOptions || !(secretSpendKeyHexError || creationDateError || restoreHeightError)
|
||||
Button(
|
||||
onClick = onCreateClick,
|
||||
enabled = validInput,
|
||||
modifier = Modifier
|
||||
.padding(16.dp),
|
||||
) {
|
||||
@ -196,12 +270,14 @@ private fun CreateWalletScreenPreview() {
|
||||
AppTheme {
|
||||
SecondStepScreen(
|
||||
showRestoreOptions = false,
|
||||
onBackClick = {},
|
||||
onCreateClick = {},
|
||||
walletName = "Personal",
|
||||
network = DefaultMoneroNetwork,
|
||||
onWalletNameChanged = {},
|
||||
onNetworkChanged = {},
|
||||
secretSpendKeyHex = "d2ca26e22489bd9871c910c58dee3ab08e66b9d566825a064c8c0af061cd8706",
|
||||
secretSpendKeyHexError = false,
|
||||
creationDate = "",
|
||||
creationDateError = false,
|
||||
restoreHeight = "",
|
||||
restoreHeightError = false,
|
||||
remoteNodes = listOf(RemoteNode.EMPTY),
|
||||
selectedRemoteNodeIds = mutableMapOf(),
|
||||
)
|
||||
@ -214,12 +290,14 @@ private fun RestoreWalletScreenPreview() {
|
||||
AppTheme {
|
||||
SecondStepScreen(
|
||||
showRestoreOptions = true,
|
||||
onBackClick = {},
|
||||
onCreateClick = {},
|
||||
walletName = "Personal",
|
||||
network = DefaultMoneroNetwork,
|
||||
onWalletNameChanged = {},
|
||||
onNetworkChanged = {},
|
||||
secretSpendKeyHex = "d2ca26e22489bd9871c910c58dee3ab08e66b9d566825a064c8c0af061cd8706",
|
||||
secretSpendKeyHexError = false,
|
||||
creationDate = "",
|
||||
creationDateError = false,
|
||||
restoreHeight = "",
|
||||
restoreHeightError = false,
|
||||
remoteNodes = listOf(RemoteNode.EMPTY),
|
||||
selectedRemoteNodeIds = mutableMapOf(),
|
||||
)
|
||||
|
@ -18,14 +18,21 @@ fun MultiSelectRemoteNodeList(
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
remoteNodes.forEach { remoteNode ->
|
||||
RemoteNodeItem(
|
||||
remoteNode,
|
||||
checked = selectedIds[remoteNode.id] ?: false,
|
||||
showCheckbox = true,
|
||||
onCheckedChange = { checked ->
|
||||
selectedIds[remoteNode.id] = checked
|
||||
},
|
||||
if (remoteNodes.isNotEmpty()) {
|
||||
remoteNodes.forEach { remoteNode ->
|
||||
RemoteNodeItem(
|
||||
remoteNode,
|
||||
checked = selectedIds[remoteNode.id] ?: false,
|
||||
showCheckbox = true,
|
||||
onCheckedChange = { checked ->
|
||||
selectedIds[remoteNode.id] = checked
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = "No matching remote nodes",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -124,3 +131,13 @@ private fun MultiSelectRemoteNodeListPreview() {
|
||||
selectedIds = mutableMapOf(),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun EmptyMultiSelectRemoteNodeListPreview() {
|
||||
val aNode = RemoteNode.EMPTY.copy(uri = Uri.parse("http://node.monero"))
|
||||
MultiSelectRemoteNodeList(
|
||||
remoteNodes = listOf(),
|
||||
selectedIds = mutableMapOf(),
|
||||
)
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import im.molly.monero.Balance
|
||||
@ -80,69 +81,67 @@ private fun WalletScreenPopulated(
|
||||
) {
|
||||
var showRenameDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val amountValueString =
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
Toolbar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = AppIcons.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
WalletKebabMenu(
|
||||
onRenameClick = { showRenameDialog = true },
|
||||
onDeleteClick = { },
|
||||
Scaffold(
|
||||
topBar = {
|
||||
Toolbar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = AppIcons.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
) { 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(ledger.balance.totalAmount))
|
||||
}
|
||||
}
|
||||
)
|
||||
Text(text = walletConfig.name, style = MaterialTheme.typography.headlineSmall)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
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(ledger.balance.totalAmount))
|
||||
}
|
||||
}
|
||||
)
|
||||
Text(text = walletConfig.name, style = MaterialTheme.typography.headlineSmall)
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -7,7 +7,7 @@ import im.molly.monero.WalletConfig;
|
||||
|
||||
interface IWalletService {
|
||||
oneway void createWallet(in WalletConfig config, in IWalletServiceCallbacks callback);
|
||||
oneway void restoreWallet(in WalletConfig config, in IWalletServiceCallbacks callback, in SecretKey spendSecretKey, long accountCreationTimestamp);
|
||||
oneway void restoreWallet(in WalletConfig config, in IWalletServiceCallbacks callback, in SecretKey spendSecretKey, long restorePoint);
|
||||
oneway void openWallet(in WalletConfig config, in IWalletServiceCallbacks callback);
|
||||
void setListener(in IWalletServiceListener listener);
|
||||
}
|
||||
|
@ -73,7 +73,6 @@ set(WALLET_API_SOURCES
|
||||
src/net/http.cpp
|
||||
src/net/i2p_address.cpp
|
||||
src/net/parse.cpp
|
||||
src/net/parse.cpp
|
||||
src/net/socks.cpp
|
||||
src/net/socks_connect.cpp
|
||||
src/net/tor_address.cpp
|
||||
|
@ -54,16 +54,20 @@ void generateAccountKeys(cryptonote::account_base& account,
|
||||
LOG_FATAL_IF(gen != secret_key);
|
||||
}
|
||||
|
||||
void Wallet::restoreAccount(const std::vector<char>& secret_scalar, uint64_t account_timestamp) {
|
||||
void Wallet::restoreAccount(const std::vector<char>& secret_scalar, uint64_t restore_point) {
|
||||
LOG_FATAL_IF(m_account_ready, "Account should not be reinitialized");
|
||||
std::lock_guard<std::mutex> lock(m_wallet_mutex);
|
||||
auto& account = m_wallet.get_account();
|
||||
generateAccountKeys(account, secret_scalar);
|
||||
if (account_timestamp > account.get_createtime()) {
|
||||
account.set_createtime(account_timestamp);
|
||||
if (restore_point < CRYPTONOTE_MAX_BLOCK_NUMBER) {
|
||||
m_restore_height = restore_point;
|
||||
} else {
|
||||
if (restore_point > account.get_createtime()) {
|
||||
account.set_createtime(restore_point);
|
||||
}
|
||||
m_restore_height = estimateRestoreHeight(account.get_createtime());
|
||||
}
|
||||
m_wallet.rescan_blockchain(true, false, false);
|
||||
m_restore_height = estimateRestoreHeight(account.get_createtime());
|
||||
m_account_ready = true;
|
||||
}
|
||||
|
||||
@ -256,12 +260,12 @@ Java_im_molly_monero_WalletNative_nativeRestoreAccount(
|
||||
jobject thiz,
|
||||
jlong handle,
|
||||
jbyteArray p_secret_scalar,
|
||||
jlong account_timestamp) {
|
||||
jlong restore_point) {
|
||||
auto* wallet = reinterpret_cast<Wallet*>(handle);
|
||||
std::vector<char> secret_scalar = jvmToNativeByteArray(
|
||||
env, JvmParamRef<jbyteArray>(p_secret_scalar));
|
||||
Eraser secret_eraser(secret_scalar);
|
||||
wallet->restoreAccount(secret_scalar, account_timestamp);
|
||||
wallet->restoreAccount(secret_scalar, restore_point);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
|
@ -26,7 +26,7 @@ class Wallet : tools::i_wallet2_callback {
|
||||
int network_id,
|
||||
const JvmRef<jobject>& wallet_native);
|
||||
|
||||
void restoreAccount(const std::vector<char>& secret_scalar, uint64_t account_timestamp);
|
||||
void restoreAccount(const std::vector<char>& secret_scalar, uint64_t restore_point);
|
||||
uint64_t estimateRestoreHeight(uint64_t timestamp);
|
||||
|
||||
bool parseFrom(std::istream& input);
|
||||
|
26
lib/android/src/main/kotlin/im/molly/monero/RestorePoint.kt
Normal file
26
lib/android/src/main/kotlin/im/molly/monero/RestorePoint.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package im.molly.monero
|
||||
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
|
||||
class RestorePoint {
|
||||
val heightOrTimestamp: Long
|
||||
|
||||
constructor() {
|
||||
heightOrTimestamp = 0
|
||||
}
|
||||
|
||||
constructor(blockHeight: Long) {
|
||||
require(blockHeight >= 0) { "Block height cannot be negative" }
|
||||
require(blockHeight < 500_000_000) { "Block height too large" }
|
||||
heightOrTimestamp = blockHeight
|
||||
}
|
||||
|
||||
constructor(creationDate: LocalDate) {
|
||||
heightOrTimestamp = creationDate.toEpochDay().coerceAtLeast(500_000_000)
|
||||
}
|
||||
|
||||
constructor(creationDate: Instant) {
|
||||
heightOrTimestamp = creationDate.epochSecond.coerceAtLeast(500_000_000)
|
||||
}
|
||||
}
|
@ -2,8 +2,6 @@ package im.molly.monero
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parceler
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.io.Closeable
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
@ -15,7 +13,6 @@ import javax.security.auth.Destroyable
|
||||
* SecretKey wraps a secret scalar value, helping to prevent accidental exposure and securely
|
||||
* erasing the value from memory.
|
||||
*/
|
||||
@Parcelize
|
||||
class SecretKey : Destroyable, Closeable, Parcelable {
|
||||
|
||||
private val secret = ByteArray(32)
|
||||
@ -33,14 +30,6 @@ class SecretKey : Destroyable, Closeable, Parcelable {
|
||||
parcel.readByteArray(secret)
|
||||
}
|
||||
|
||||
companion object : Parceler<SecretKey> {
|
||||
override fun create(parcel: Parcel) = SecretKey(parcel)
|
||||
|
||||
override fun SecretKey.write(parcel: Parcel, flags: Int) {
|
||||
parcel.writeByteArray(secret)
|
||||
}
|
||||
}
|
||||
|
||||
val bytes: ByteArray
|
||||
get() {
|
||||
check(!destroyed) { "Secret key has been already destroyed" }
|
||||
@ -61,6 +50,24 @@ class SecretKey : Destroyable, Closeable, Parcelable {
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeByteArray(secret)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<SecretKey> {
|
||||
override fun createFromParcel(parcel: Parcel): SecretKey {
|
||||
return SecretKey(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<SecretKey?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is SecretKey) return false
|
||||
|
@ -24,7 +24,7 @@ class WalletNative private constructor(
|
||||
storageAdapter: IStorageAdapter? = null,
|
||||
remoteNodeClient: IRemoteNodeClient? = null,
|
||||
secretSpendKey: SecretKey? = null,
|
||||
accountTimestamp: Long? = null,
|
||||
restorePoint: Long? = null,
|
||||
coroutineContext: CoroutineContext = Dispatchers.Default + SupervisorJob(),
|
||||
ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
) = WalletNative(
|
||||
@ -36,13 +36,13 @@ class WalletNative private constructor(
|
||||
).apply {
|
||||
when {
|
||||
secretSpendKey != null -> {
|
||||
require(accountTimestamp == null || accountTimestamp >= 0)
|
||||
val timestampOrNow = accountTimestamp ?: (System.currentTimeMillis() / 1000)
|
||||
nativeRestoreAccount(handle, secretSpendKey.bytes, timestampOrNow)
|
||||
require(restorePoint == null || restorePoint >= 0)
|
||||
val restorePointOrNow = restorePoint ?: (System.currentTimeMillis() / 1000)
|
||||
nativeRestoreAccount(handle, secretSpendKey.bytes, restorePointOrNow)
|
||||
tryWriteState()
|
||||
}
|
||||
else -> {
|
||||
require(accountTimestamp == null)
|
||||
require(restorePoint == null)
|
||||
readState()
|
||||
}
|
||||
}
|
||||
@ -251,7 +251,7 @@ class WalletNative private constructor(
|
||||
private external fun nativeLoad(handle: Long, fd: Int): Boolean
|
||||
private external fun nativeNonReentrantRefresh(handle: Long, skipCoinbase: Boolean): Int
|
||||
private external fun nativeRestoreAccount(
|
||||
handle: Long, secretScalar: ByteArray, accountTimestamp: Long
|
||||
handle: Long, secretScalar: ByteArray, restorePoint: Long
|
||||
)
|
||||
|
||||
private external fun nativeSave(handle: Long, fd: Int): Boolean
|
||||
|
@ -69,7 +69,7 @@ class WalletProvider private constructor(
|
||||
dataStore: WalletDataStore? = null,
|
||||
client: RemoteNodeClient? = null,
|
||||
secretSpendKey: SecretKey,
|
||||
accountCreationTime: Instant,
|
||||
restorePoint: RestorePoint,
|
||||
): MoneroWallet {
|
||||
val storageAdapter = StorageAdapter(dataStore)
|
||||
val wallet = suspendCancellableCoroutine { continuation ->
|
||||
@ -77,7 +77,7 @@ class WalletProvider private constructor(
|
||||
buildConfig(network, StorageAdapter(dataStore), client),
|
||||
WalletResultCallback(continuation),
|
||||
secretSpendKey,
|
||||
accountCreationTime.epochSecond,
|
||||
restorePoint.heightOrTimestamp,
|
||||
)
|
||||
}
|
||||
return MoneroWallet(wallet, storageAdapter, client)
|
||||
@ -122,6 +122,7 @@ class WalletProvider private constructor(
|
||||
wallet.close()
|
||||
}
|
||||
}
|
||||
|
||||
else -> TODO()
|
||||
}
|
||||
}
|
||||
|
@ -57,11 +57,11 @@ internal class WalletServiceImpl(
|
||||
config: WalletConfig?,
|
||||
callback: IWalletServiceCallbacks?,
|
||||
secretSpendKey: SecretKey?,
|
||||
accountCreationTimestamp: Long,
|
||||
restorePoint: Long,
|
||||
) {
|
||||
serviceScope.launch {
|
||||
val wallet = secretSpendKey.use { secret ->
|
||||
createOrRestoreWallet(config, secret, accountCreationTimestamp)
|
||||
createOrRestoreWallet(config, secret, restorePoint)
|
||||
}
|
||||
callback?.onWalletResult(wallet)
|
||||
}
|
||||
@ -86,7 +86,7 @@ internal class WalletServiceImpl(
|
||||
private fun createOrRestoreWallet(
|
||||
config: WalletConfig?,
|
||||
secretSpendKey: SecretKey?,
|
||||
accountCreationTimestamp: Long? = null,
|
||||
restorePoint: Long? = null,
|
||||
): IWallet {
|
||||
requireNotNull(config)
|
||||
requireNotNull(secretSpendKey)
|
||||
@ -95,7 +95,7 @@ internal class WalletServiceImpl(
|
||||
storageAdapter = config.storageAdapter,
|
||||
remoteNodeClient = config.remoteNodeClient,
|
||||
secretSpendKey = secretSpendKey,
|
||||
accountTimestamp = accountCreationTimestamp,
|
||||
restorePoint = restorePoint,
|
||||
coroutineContext = serviceScope.coroutineContext,
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user