From 70231790ee4fe42b8c7ddeabdd824225c4d66bb8 Mon Sep 17 00:00:00 2001 From: Oscar Mira Date: Fri, 26 May 2023 03:30:19 +0200 Subject: [PATCH] demo: add restore wallet screen --- .../1.json | 16 +-- .../im/molly/monero/demo/common/Result.kt | 10 ++ .../molly/monero/demo/data/MoneroSdkClient.kt | 17 +++ .../monero/demo/data/WalletRepository.kt | 23 ++++ .../monero/demo/data/entity/WalletEntity.kt | 3 - .../molly/monero/demo/service/SyncService.kt | 2 +- .../monero/demo/ui/AddWalletViewModel.kt | 65 ++++++++++ .../molly/monero/demo/ui/AddWalletWizard.kt | 108 +++++++++++++--- .../im/molly/monero/demo/ui/RemoteNodeList.kt | 33 +++-- .../im/molly/monero/demo/ui/WalletScreen.kt | 119 +++++++++--------- .../aidl/im/molly/monero/IWalletService.aidl | 2 +- .../main/cpp/monero/wallet_api/CMakeLists.txt | 1 - lib/android/src/main/cpp/wallet.cc | 16 ++- lib/android/src/main/cpp/wallet.h | 2 +- .../kotlin/im/molly/monero/RestorePoint.kt | 26 ++++ .../main/kotlin/im/molly/monero/SecretKey.kt | 29 +++-- .../kotlin/im/molly/monero/WalletNative.kt | 12 +- .../kotlin/im/molly/monero/WalletProvider.kt | 5 +- .../kotlin/im/molly/monero/WalletService.kt | 8 +- 19 files changed, 365 insertions(+), 132 deletions(-) create mode 100644 lib/android/src/main/kotlin/im/molly/monero/RestorePoint.kt diff --git a/demo/android/schemas/im.molly.monero.demo.data.AppDatabase/1.json b/demo/android/schemas/im.molly.monero.demo.data.AppDatabase/1.json index c91a2b6..2cfaadf 100644 --- a/demo/android/schemas/im.molly.monero.demo.data.AppDatabase/1.json +++ b/demo/android/schemas/im.molly.monero.demo.data.AppDatabase/1.json @@ -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')" ] } } \ No newline at end of file diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/common/Result.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/common/Result.kt index 9aaf8fa..63c579b 100644 --- a/demo/android/src/main/kotlin/im/molly/monero/demo/common/Result.kt +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/common/Result.kt @@ -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 + */ sealed interface Result { data class Success(val data: T) : Result data class Error(val exception: Throwable? = null) : Result object Loading : Result } +/** + * `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 Flow.asResult(): Flow> { return this .map> { diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/data/MoneroSdkClient.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/data/MoneroSdkClient.kt index 8f6ff10..ac85662 100644 --- a/demo/android/src/main/kotlin/im/molly/monero/demo/data/MoneroSdkClient.kt +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/data/MoneroSdkClient.kt @@ -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, 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 4bbdc9b..0ee65a6 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 @@ -74,6 +74,29 @@ class WalletRepository( return walletId to wallet } + suspend fun restoreWallet( + moneroNetwork: MoneroNetwork, + name: String, + remoteNodeIds: List, + secretSpendKey: SecretKey, + restorePoint: RestorePoint, + ): Pair { + 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) } diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/data/entity/WalletEntity.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/data/entity/WalletEntity.kt index 22be7a1..628b304 100644 --- a/demo/android/src/main/kotlin/im/molly/monero/demo/data/entity/WalletEntity.kt +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/data/entity/WalletEntity.kt @@ -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) diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/service/SyncService.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/service/SyncService.kt index 3c91ea5..2e1f784 100644 --- a/demo/android/src/main/kotlin/im/molly/monero/demo/service/SyncService.kt +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/service/SyncService.kt @@ -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) diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/AddWalletViewModel.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/AddWalletViewModel.kt index dfd477b..2807245 100644 --- a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/AddWalletViewModel.kt +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/AddWalletViewModel.kt @@ -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> = snapshotFlow { network } .flatMapLatest { @@ -44,6 +57,18 @@ class AddWalletViewModel( private fun getSelectedRemoteNodeIds() = selectedRemoteNodes.filterValues { checked -> checked }.keys.filterNotNull() + init { + val previousNodes = mutableSetOf() + + 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 + ) + } + } } diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/AddWalletWizard.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/AddWalletWizard.kt index 08cec4f..55643c5 100644 --- a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/AddWalletWizard.kt +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/AddWalletWizard.kt @@ -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, selectedRemoteNodeIds: MutableMap = 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(), ) diff --git a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/RemoteNodeList.kt b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/RemoteNodeList.kt index 25da62e..5c3aa75 100644 --- a/demo/android/src/main/kotlin/im/molly/monero/demo/ui/RemoteNodeList.kt +++ b/demo/android/src/main/kotlin/im/molly/monero/demo/ui/RemoteNodeList.kt @@ -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(), + ) +} 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 ea2145e..2f714c6 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 @@ -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 diff --git a/lib/android/src/main/aidl/im/molly/monero/IWalletService.aidl b/lib/android/src/main/aidl/im/molly/monero/IWalletService.aidl index 0ffbd2c..1d9a090 100644 --- a/lib/android/src/main/aidl/im/molly/monero/IWalletService.aidl +++ b/lib/android/src/main/aidl/im/molly/monero/IWalletService.aidl @@ -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); } diff --git a/lib/android/src/main/cpp/monero/wallet_api/CMakeLists.txt b/lib/android/src/main/cpp/monero/wallet_api/CMakeLists.txt index 94d4aae..a6b1c13 100644 --- a/lib/android/src/main/cpp/monero/wallet_api/CMakeLists.txt +++ b/lib/android/src/main/cpp/monero/wallet_api/CMakeLists.txt @@ -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 diff --git a/lib/android/src/main/cpp/wallet.cc b/lib/android/src/main/cpp/wallet.cc index 772d9b8..d4ba51d 100644 --- a/lib/android/src/main/cpp/wallet.cc +++ b/lib/android/src/main/cpp/wallet.cc @@ -54,16 +54,20 @@ void generateAccountKeys(cryptonote::account_base& account, LOG_FATAL_IF(gen != secret_key); } -void Wallet::restoreAccount(const std::vector& secret_scalar, uint64_t account_timestamp) { +void Wallet::restoreAccount(const std::vector& secret_scalar, uint64_t restore_point) { LOG_FATAL_IF(m_account_ready, "Account should not be reinitialized"); std::lock_guard 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(handle); std::vector secret_scalar = jvmToNativeByteArray( env, JvmParamRef(p_secret_scalar)); Eraser secret_eraser(secret_scalar); - wallet->restoreAccount(secret_scalar, account_timestamp); + wallet->restoreAccount(secret_scalar, restore_point); } extern "C" diff --git a/lib/android/src/main/cpp/wallet.h b/lib/android/src/main/cpp/wallet.h index b3eee7e..4ee6f08 100644 --- a/lib/android/src/main/cpp/wallet.h +++ b/lib/android/src/main/cpp/wallet.h @@ -26,7 +26,7 @@ class Wallet : tools::i_wallet2_callback { int network_id, const JvmRef& wallet_native); - void restoreAccount(const std::vector& secret_scalar, uint64_t account_timestamp); + void restoreAccount(const std::vector& secret_scalar, uint64_t restore_point); uint64_t estimateRestoreHeight(uint64_t timestamp); bool parseFrom(std::istream& input); diff --git a/lib/android/src/main/kotlin/im/molly/monero/RestorePoint.kt b/lib/android/src/main/kotlin/im/molly/monero/RestorePoint.kt new file mode 100644 index 0000000..7e272ec --- /dev/null +++ b/lib/android/src/main/kotlin/im/molly/monero/RestorePoint.kt @@ -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) + } +} diff --git a/lib/android/src/main/kotlin/im/molly/monero/SecretKey.kt b/lib/android/src/main/kotlin/im/molly/monero/SecretKey.kt index 4b3ad36..553a888 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/SecretKey.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/SecretKey.kt @@ -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 { - 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 { + override fun createFromParcel(parcel: Parcel): SecretKey { + return SecretKey(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is SecretKey) return false diff --git a/lib/android/src/main/kotlin/im/molly/monero/WalletNative.kt b/lib/android/src/main/kotlin/im/molly/monero/WalletNative.kt index cf89930..3bea883 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/WalletNative.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/WalletNative.kt @@ -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 diff --git a/lib/android/src/main/kotlin/im/molly/monero/WalletProvider.kt b/lib/android/src/main/kotlin/im/molly/monero/WalletProvider.kt index be8415c..236ac0f 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/WalletProvider.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/WalletProvider.kt @@ -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() } } diff --git a/lib/android/src/main/kotlin/im/molly/monero/WalletService.kt b/lib/android/src/main/kotlin/im/molly/monero/WalletService.kt index 41facb9..613eed1 100644 --- a/lib/android/src/main/kotlin/im/molly/monero/WalletService.kt +++ b/lib/android/src/main/kotlin/im/molly/monero/WalletService.kt @@ -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, ) }