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,
"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')"
]
}
}

View File

@ -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>> {

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(
network: MoneroNetwork,
filename: String,

View File

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

View File

@ -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)

View File

@ -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)

View File

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

View File

@ -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(),
)

View File

@ -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(),
)
}

View File

@ -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

View File

@ -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);
}

View File

@ -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

View File

@ -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"

View File

@ -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);

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.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

View File

@ -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

View File

@ -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()
}
}

View File

@ -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,
)
}