demo: add restore wallet screen

This commit is contained in:
Oscar Mira 2023-05-26 03:30:19 +02:00
parent e61021d8bd
commit 70231790ee
19 changed files with 365 additions and 132 deletions

View File

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "419c430462df613cab5f23d35923c2b7", "identityHash": "a717d86bf72794f75768dfa37ee61831",
"entities": [ "entities": [
{ {
"tableName": "wallets", "tableName": "wallets",
@ -39,17 +39,7 @@
"id" "id"
] ]
}, },
"indices": [ "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`)"
}
],
"foreignKeys": [] "foreignKeys": []
}, },
{ {
@ -179,7 +169,7 @@
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "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')"
] ]
} }
} }

View File

@ -5,12 +5,22 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
/**
* A generic class that holds a value with its loading status.
* @param <T>
*/
sealed interface Result<out T> { sealed interface Result<out T> {
data class Success<T>(val data: T) : Result<T> data class Success<T>(val data: T) : Result<T>
data class Error(val exception: Throwable? = null) : Result<Nothing> data class Error(val exception: Throwable? = null) : Result<Nothing>
object Loading : 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>> { fun <T> Flow<T>.asResult(): Flow<Result<T>> {
return this return this
.map<T, Result<T>> { .map<T, Result<T>> {

View File

@ -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( suspend fun openWallet(
network: MoneroNetwork, network: MoneroNetwork,
filename: String, filename: String,

View File

@ -74,6 +74,29 @@ class WalletRepository(
return walletId to wallet 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) = suspend fun updateWalletConfig(walletConfig: WalletConfig) =
walletDataSource.updateWalletConfig(walletConfig) walletDataSource.updateWalletConfig(walletConfig)
} }

View File

@ -8,9 +8,6 @@ import im.molly.monero.demo.data.model.WalletConfig
@Entity( @Entity(
tableName = "wallets", tableName = "wallets",
indices = [
Index(value = ["public_address"], unique = true)
],
) )
data class WalletEntity( data class WalletEntity(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)

View File

@ -47,7 +47,7 @@ class SyncService(
while (isActive) { while (isActive) {
val result = wallet.awaitRefresh() val result = wallet.awaitRefresh()
if (result.isError()) { if (result.isError()) {
break // TODO: Handle non-recoverable errors
} }
wallet.commit() wallet.commit()
delay(10.seconds) delay(10.seconds)

View File

@ -4,15 +4,19 @@ import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import im.molly.monero.MoneroNetwork 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.AppModule
import im.molly.monero.demo.data.RemoteNodeRepository import im.molly.monero.demo.data.RemoteNodeRepository
import im.molly.monero.demo.data.WalletRepository import im.molly.monero.demo.data.WalletRepository
import im.molly.monero.demo.data.model.DefaultMoneroNetwork import im.molly.monero.demo.data.model.DefaultMoneroNetwork
import im.molly.monero.demo.data.model.RemoteNode import im.molly.monero.demo.data.model.RemoteNode
import im.molly.monero.util.parseHex
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.LocalDate
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class AddWalletViewModel( class AddWalletViewModel(
@ -26,6 +30,15 @@ class AddWalletViewModel(
var walletName by mutableStateOf("") var walletName by mutableStateOf("")
private set 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>> = val currentRemoteNodes: StateFlow<List<RemoteNode>> =
snapshotFlow { network } snapshotFlow { network }
.flatMapLatest { .flatMapLatest {
@ -44,6 +57,18 @@ class AddWalletViewModel(
private fun getSelectedRemoteNodeIds() = private fun getSelectedRemoteNodeIds() =
selectedRemoteNodes.filterValues { checked -> checked }.keys.filterNotNull() 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) { fun toggleSelectedNetwork(network: MoneroNetwork) {
this.network = network this.network = network
} }
@ -52,7 +77,47 @@ class AddWalletViewModel(
this.walletName = name 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 { fun createWallet() = viewModelScope.launch {
walletRepository.addWallet(network, walletName, getSelectedRemoteNodeIds()) 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
)
}
}
} }

View File

@ -2,12 +2,13 @@ package im.molly.monero.demo.ui
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -71,7 +72,7 @@ private fun FirstStepScreen(
Text("Create a new wallet") Text("Create a new wallet")
} }
OutlinedButton( OutlinedButton(
onClick = {}, // TODO: onRestoreClick, onClick = onRestoreClick,
modifier = Modifier.padding(top = 8.dp), modifier = Modifier.padding(top = 8.dp),
) { ) {
Text("I already have a wallet") Text("I already have a wallet")
@ -95,13 +96,26 @@ fun AddWalletSecondStepRoute(
modifier = modifier, modifier = modifier,
onBackClick = onBackClick, onBackClick = onBackClick,
onCreateClick = { onCreateClick = {
if (showRestoreOptions) {
viewModel.restoreWallet()
} else {
viewModel.createWallet() viewModel.createWallet()
}
onNavigateToHome() onNavigateToHome()
}, },
walletName = viewModel.walletName, walletName = viewModel.walletName,
network = viewModel.network, 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) }, onWalletNameChanged = { name -> viewModel.updateWalletName(name) },
onNetworkChanged = { network -> viewModel.toggleSelectedNetwork(network) }, onNetworkChanged = { network -> viewModel.toggleSelectedNetwork(network) },
onSecretSpendKeyHexChanged = { value -> viewModel.updateSecretSpendKeyHex(value) },
onCreationDateChanged = { value -> viewModel.updateCreationDate(value) },
onRestoreHeightChanged = { value -> viewModel.updateRestoreHeight(value) },
remoteNodes = remoteNodes, remoteNodes = remoteNodes,
selectedRemoteNodeIds = viewModel.selectedRemoteNodes, selectedRemoteNodeIds = viewModel.selectedRemoteNodes,
) )
@ -112,12 +126,21 @@ fun AddWalletSecondStepRoute(
private fun SecondStepScreen( private fun SecondStepScreen(
showRestoreOptions: Boolean, showRestoreOptions: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onBackClick: () -> Unit, onBackClick: () -> Unit = {},
onCreateClick: () -> Unit, onCreateClick: () -> Unit = {},
walletName: String, walletName: String,
secretSpendKeyHex: String,
secretSpendKeyHexError: Boolean,
creationDate: String,
creationDateError: Boolean,
restoreHeight: String,
restoreHeightError: Boolean,
network: MoneroNetwork, network: MoneroNetwork,
onWalletNameChanged: (String) -> Unit, onWalletNameChanged: (String) -> Unit = {},
onNetworkChanged: (MoneroNetwork) -> Unit, onNetworkChanged: (MoneroNetwork) -> Unit = {},
onSecretSpendKeyHexChanged: (String) -> Unit = {},
onCreationDateChanged: (String) -> Unit = {},
onRestoreHeightChanged: (String) -> Unit = {},
remoteNodes: List<RemoteNode>, remoteNodes: List<RemoteNode>,
selectedRemoteNodeIds: MutableMap<Long?, Boolean> = mutableMapOf(), selectedRemoteNodeIds: MutableMap<Long?, Boolean> = mutableMapOf(),
) { ) {
@ -139,6 +162,7 @@ private fun SecondStepScreen(
Column( Column(
modifier = modifier modifier = modifier
.padding(padding) .padding(padding)
.imePadding()
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
) { ) {
OutlinedTextField( OutlinedTextField(
@ -173,13 +197,63 @@ private fun SecondStepScreen(
modifier = Modifier modifier = Modifier
.padding(start = 16.dp), .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( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
val validInput = !showRestoreOptions || !(secretSpendKeyHexError || creationDateError || restoreHeightError)
Button( Button(
onClick = onCreateClick, onClick = onCreateClick,
enabled = validInput,
modifier = Modifier modifier = Modifier
.padding(16.dp), .padding(16.dp),
) { ) {
@ -196,12 +270,14 @@ private fun CreateWalletScreenPreview() {
AppTheme { AppTheme {
SecondStepScreen( SecondStepScreen(
showRestoreOptions = false, showRestoreOptions = false,
onBackClick = {},
onCreateClick = {},
walletName = "Personal", walletName = "Personal",
network = DefaultMoneroNetwork, network = DefaultMoneroNetwork,
onWalletNameChanged = {}, secretSpendKeyHex = "d2ca26e22489bd9871c910c58dee3ab08e66b9d566825a064c8c0af061cd8706",
onNetworkChanged = {}, secretSpendKeyHexError = false,
creationDate = "",
creationDateError = false,
restoreHeight = "",
restoreHeightError = false,
remoteNodes = listOf(RemoteNode.EMPTY), remoteNodes = listOf(RemoteNode.EMPTY),
selectedRemoteNodeIds = mutableMapOf(), selectedRemoteNodeIds = mutableMapOf(),
) )
@ -214,12 +290,14 @@ private fun RestoreWalletScreenPreview() {
AppTheme { AppTheme {
SecondStepScreen( SecondStepScreen(
showRestoreOptions = true, showRestoreOptions = true,
onBackClick = {},
onCreateClick = {},
walletName = "Personal", walletName = "Personal",
network = DefaultMoneroNetwork, network = DefaultMoneroNetwork,
onWalletNameChanged = {}, secretSpendKeyHex = "d2ca26e22489bd9871c910c58dee3ab08e66b9d566825a064c8c0af061cd8706",
onNetworkChanged = {}, secretSpendKeyHexError = false,
creationDate = "",
creationDateError = false,
restoreHeight = "",
restoreHeightError = false,
remoteNodes = listOf(RemoteNode.EMPTY), remoteNodes = listOf(RemoteNode.EMPTY),
selectedRemoteNodeIds = mutableMapOf(), selectedRemoteNodeIds = mutableMapOf(),
) )

View File

@ -18,6 +18,7 @@ fun MultiSelectRemoteNodeList(
Column( Column(
modifier = modifier, modifier = modifier,
) { ) {
if (remoteNodes.isNotEmpty()) {
remoteNodes.forEach { remoteNode -> remoteNodes.forEach { remoteNode ->
RemoteNodeItem( RemoteNodeItem(
remoteNode, remoteNode,
@ -28,6 +29,12 @@ fun MultiSelectRemoteNodeList(
}, },
) )
} }
} else {
Text(
text = "No matching remote nodes",
style = MaterialTheme.typography.labelMedium,
)
}
} }
} }
@ -124,3 +131,13 @@ private fun MultiSelectRemoteNodeListPreview() {
selectedIds = mutableMapOf(), selectedIds = mutableMapOf(),
) )
} }
@Preview
@Composable
private fun EmptyMultiSelectRemoteNodeListPreview() {
val aNode = RemoteNode.EMPTY.copy(uri = Uri.parse("http://node.monero"))
MultiSelectRemoteNodeList(
remoteNodes = listOf(),
selectedIds = mutableMapOf(),
)
}

View File

@ -9,6 +9,7 @@ import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import im.molly.monero.Balance import im.molly.monero.Balance
@ -80,8 +81,6 @@ private fun WalletScreenPopulated(
) { ) {
var showRenameDialog by remember { mutableStateOf(false) } var showRenameDialog by remember { mutableStateOf(false) }
val amountValueString =
Scaffold( Scaffold(
topBar = { topBar = {
Toolbar( Toolbar(

View File

@ -7,7 +7,7 @@ import im.molly.monero.WalletConfig;
interface IWalletService { interface IWalletService {
oneway void createWallet(in WalletConfig config, in IWalletServiceCallbacks callback); 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); oneway void openWallet(in WalletConfig config, in IWalletServiceCallbacks callback);
void setListener(in IWalletServiceListener listener); void setListener(in IWalletServiceListener listener);
} }

View File

@ -73,7 +73,6 @@ set(WALLET_API_SOURCES
src/net/http.cpp src/net/http.cpp
src/net/i2p_address.cpp src/net/i2p_address.cpp
src/net/parse.cpp src/net/parse.cpp
src/net/parse.cpp
src/net/socks.cpp src/net/socks.cpp
src/net/socks_connect.cpp src/net/socks_connect.cpp
src/net/tor_address.cpp src/net/tor_address.cpp

View File

@ -54,16 +54,20 @@ void generateAccountKeys(cryptonote::account_base& account,
LOG_FATAL_IF(gen != secret_key); 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"); LOG_FATAL_IF(m_account_ready, "Account should not be reinitialized");
std::lock_guard<std::mutex> lock(m_wallet_mutex); std::lock_guard<std::mutex> lock(m_wallet_mutex);
auto& account = m_wallet.get_account(); auto& account = m_wallet.get_account();
generateAccountKeys(account, secret_scalar); generateAccountKeys(account, secret_scalar);
if (account_timestamp > account.get_createtime()) { if (restore_point < CRYPTONOTE_MAX_BLOCK_NUMBER) {
account.set_createtime(account_timestamp); 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_wallet.rescan_blockchain(true, false, false);
m_restore_height = estimateRestoreHeight(account.get_createtime());
m_account_ready = true; m_account_ready = true;
} }
@ -256,12 +260,12 @@ Java_im_molly_monero_WalletNative_nativeRestoreAccount(
jobject thiz, jobject thiz,
jlong handle, jlong handle,
jbyteArray p_secret_scalar, jbyteArray p_secret_scalar,
jlong account_timestamp) { jlong restore_point) {
auto* wallet = reinterpret_cast<Wallet*>(handle); auto* wallet = reinterpret_cast<Wallet*>(handle);
std::vector<char> secret_scalar = jvmToNativeByteArray( std::vector<char> secret_scalar = jvmToNativeByteArray(
env, JvmParamRef<jbyteArray>(p_secret_scalar)); env, JvmParamRef<jbyteArray>(p_secret_scalar));
Eraser secret_eraser(secret_scalar); Eraser secret_eraser(secret_scalar);
wallet->restoreAccount(secret_scalar, account_timestamp); wallet->restoreAccount(secret_scalar, restore_point);
} }
extern "C" extern "C"

View File

@ -26,7 +26,7 @@ class Wallet : tools::i_wallet2_callback {
int network_id, int network_id,
const JvmRef<jobject>& wallet_native); 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); uint64_t estimateRestoreHeight(uint64_t timestamp);
bool parseFrom(std::istream& input); bool parseFrom(std::istream& input);

View 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)
}
}

View File

@ -2,8 +2,6 @@ package im.molly.monero
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import java.io.Closeable import java.io.Closeable
import java.security.MessageDigest import java.security.MessageDigest
import java.security.SecureRandom 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 * SecretKey wraps a secret scalar value, helping to prevent accidental exposure and securely
* erasing the value from memory. * erasing the value from memory.
*/ */
@Parcelize
class SecretKey : Destroyable, Closeable, Parcelable { class SecretKey : Destroyable, Closeable, Parcelable {
private val secret = ByteArray(32) private val secret = ByteArray(32)
@ -33,14 +30,6 @@ class SecretKey : Destroyable, Closeable, Parcelable {
parcel.readByteArray(secret) 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 val bytes: ByteArray
get() { get() {
check(!destroyed) { "Secret key has been already destroyed" } 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 { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is SecretKey) return false if (other !is SecretKey) return false

View File

@ -24,7 +24,7 @@ class WalletNative private constructor(
storageAdapter: IStorageAdapter? = null, storageAdapter: IStorageAdapter? = null,
remoteNodeClient: IRemoteNodeClient? = null, remoteNodeClient: IRemoteNodeClient? = null,
secretSpendKey: SecretKey? = null, secretSpendKey: SecretKey? = null,
accountTimestamp: Long? = null, restorePoint: Long? = null,
coroutineContext: CoroutineContext = Dispatchers.Default + SupervisorJob(), coroutineContext: CoroutineContext = Dispatchers.Default + SupervisorJob(),
ioDispatcher: CoroutineDispatcher = Dispatchers.IO, ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) = WalletNative( ) = WalletNative(
@ -36,13 +36,13 @@ class WalletNative private constructor(
).apply { ).apply {
when { when {
secretSpendKey != null -> { secretSpendKey != null -> {
require(accountTimestamp == null || accountTimestamp >= 0) require(restorePoint == null || restorePoint >= 0)
val timestampOrNow = accountTimestamp ?: (System.currentTimeMillis() / 1000) val restorePointOrNow = restorePoint ?: (System.currentTimeMillis() / 1000)
nativeRestoreAccount(handle, secretSpendKey.bytes, timestampOrNow) nativeRestoreAccount(handle, secretSpendKey.bytes, restorePointOrNow)
tryWriteState() tryWriteState()
} }
else -> { else -> {
require(accountTimestamp == null) require(restorePoint == null)
readState() readState()
} }
} }
@ -251,7 +251,7 @@ class WalletNative private constructor(
private external fun nativeLoad(handle: Long, fd: Int): Boolean private external fun nativeLoad(handle: Long, fd: Int): Boolean
private external fun nativeNonReentrantRefresh(handle: Long, skipCoinbase: Boolean): Int private external fun nativeNonReentrantRefresh(handle: Long, skipCoinbase: Boolean): Int
private external fun nativeRestoreAccount( 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 private external fun nativeSave(handle: Long, fd: Int): Boolean

View File

@ -69,7 +69,7 @@ class WalletProvider private constructor(
dataStore: WalletDataStore? = null, dataStore: WalletDataStore? = null,
client: RemoteNodeClient? = null, client: RemoteNodeClient? = null,
secretSpendKey: SecretKey, secretSpendKey: SecretKey,
accountCreationTime: Instant, restorePoint: RestorePoint,
): MoneroWallet { ): MoneroWallet {
val storageAdapter = StorageAdapter(dataStore) val storageAdapter = StorageAdapter(dataStore)
val wallet = suspendCancellableCoroutine { continuation -> val wallet = suspendCancellableCoroutine { continuation ->
@ -77,7 +77,7 @@ class WalletProvider private constructor(
buildConfig(network, StorageAdapter(dataStore), client), buildConfig(network, StorageAdapter(dataStore), client),
WalletResultCallback(continuation), WalletResultCallback(continuation),
secretSpendKey, secretSpendKey,
accountCreationTime.epochSecond, restorePoint.heightOrTimestamp,
) )
} }
return MoneroWallet(wallet, storageAdapter, client) return MoneroWallet(wallet, storageAdapter, client)
@ -122,6 +122,7 @@ class WalletProvider private constructor(
wallet.close() wallet.close()
} }
} }
else -> TODO() else -> TODO()
} }
} }

View File

@ -57,11 +57,11 @@ internal class WalletServiceImpl(
config: WalletConfig?, config: WalletConfig?,
callback: IWalletServiceCallbacks?, callback: IWalletServiceCallbacks?,
secretSpendKey: SecretKey?, secretSpendKey: SecretKey?,
accountCreationTimestamp: Long, restorePoint: Long,
) { ) {
serviceScope.launch { serviceScope.launch {
val wallet = secretSpendKey.use { secret -> val wallet = secretSpendKey.use { secret ->
createOrRestoreWallet(config, secret, accountCreationTimestamp) createOrRestoreWallet(config, secret, restorePoint)
} }
callback?.onWalletResult(wallet) callback?.onWalletResult(wallet)
} }
@ -86,7 +86,7 @@ internal class WalletServiceImpl(
private fun createOrRestoreWallet( private fun createOrRestoreWallet(
config: WalletConfig?, config: WalletConfig?,
secretSpendKey: SecretKey?, secretSpendKey: SecretKey?,
accountCreationTimestamp: Long? = null, restorePoint: Long? = null,
): IWallet { ): IWallet {
requireNotNull(config) requireNotNull(config)
requireNotNull(secretSpendKey) requireNotNull(secretSpendKey)
@ -95,7 +95,7 @@ internal class WalletServiceImpl(
storageAdapter = config.storageAdapter, storageAdapter = config.storageAdapter,
remoteNodeClient = config.remoteNodeClient, remoteNodeClient = config.remoteNodeClient,
secretSpendKey = secretSpendKey, secretSpendKey = secretSpendKey,
accountTimestamp = accountCreationTimestamp, restorePoint = restorePoint,
coroutineContext = serviceScope.coroutineContext, coroutineContext = serviceScope.coroutineContext,
) )
} }