Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import co.electriccoin.zcash.ui.common.provider.RestoreTimestampStorageProvider
import co.electriccoin.zcash.ui.common.provider.RestoreTimestampStorageProviderImpl
import co.electriccoin.zcash.ui.common.provider.SelectedAccountUUIDProvider
import co.electriccoin.zcash.ui.common.provider.SelectedAccountUUIDProviderImpl
import co.electriccoin.zcash.ui.common.provider.ServerSelectionProvider
import co.electriccoin.zcash.ui.common.provider.ServerSelectionProviderImpl
import co.electriccoin.zcash.ui.common.provider.ShieldFundsInfoProvider
import co.electriccoin.zcash.ui.common.provider.ShieldFundsInfoProviderImpl
import co.electriccoin.zcash.ui.common.provider.SimpleSwapAssetProvider
Expand Down Expand Up @@ -65,6 +67,7 @@ val providerModule =
singleOf(::GetZcashCurrencyProvider)
singleOf(::SelectedAccountUUIDProviderImpl) bind SelectedAccountUUIDProvider::class
singleOf(::PersistableWalletProviderImpl) bind PersistableWalletProvider::class
singleOf(::ServerSelectionProviderImpl) bind ServerSelectionProvider::class
singleOf(::SynchronizerProviderImpl) bind SynchronizerProvider::class
singleOf(::ApplicationStateProviderImpl) bind ApplicationStateProvider::class
singleOf(::RestoreTimestampStorageProviderImpl) bind RestoreTimestampStorageProvider::class
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import co.electriccoin.zcash.ui.common.usecase.GetResyncDataFromHeightUseCase
import co.electriccoin.zcash.ui.common.usecase.GetSelectedEndpointUseCase
import co.electriccoin.zcash.ui.common.usecase.GetSelectedSwapAssetUseCase
import co.electriccoin.zcash.ui.common.usecase.GetSelectedWalletAccountUseCase
import co.electriccoin.zcash.ui.common.usecase.GetServerSelectionUseCase
import co.electriccoin.zcash.ui.common.usecase.GetSlippageUseCase
import co.electriccoin.zcash.ui.common.usecase.GetSupportUseCase
import co.electriccoin.zcash.ui.common.usecase.GetSwapAssetsUseCase
Expand Down Expand Up @@ -92,7 +93,7 @@ import co.electriccoin.zcash.ui.common.usecase.OptInExchangeRateUseCase
import co.electriccoin.zcash.ui.common.usecase.ParseKeystonePCZTUseCase
import co.electriccoin.zcash.ui.common.usecase.ParseKeystoneSignInRequestUseCase
import co.electriccoin.zcash.ui.common.usecase.ParseKeystoneUrToZashiAccountsUseCase
import co.electriccoin.zcash.ui.common.usecase.PersistEndpointUseCase
import co.electriccoin.zcash.ui.common.usecase.PersistServerSelectionUseCase
import co.electriccoin.zcash.ui.common.usecase.PrefillSendUseCase
import co.electriccoin.zcash.ui.common.usecase.PreselectSwapAssetUseCase
import co.electriccoin.zcash.ui.common.usecase.ProcessSwapTransactionUseCase
Expand Down Expand Up @@ -147,8 +148,9 @@ val useCaseModule =
module {
factoryOf(::ObserveFastestServersUseCase)
factoryOf(::GetSelectedEndpointUseCase)
factoryOf(::GetServerSelectionUseCase)
factoryOf(::RefreshFastestServersUseCase)
factoryOf(::PersistEndpointUseCase)
factoryOf(::PersistServerSelectionUseCase)
factoryOf(::ValidateEndpointUseCase)
factoryOf(::GetConfigurationUseCase)
factoryOf(::RescanBlockchainUseCase)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package co.electriccoin.zcash.ui.common.model

import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
import org.json.JSONObject

data class ServerSelection(
val mode: ConnectionMode,
val endpoint: LightWalletEndpoint? = null
) {
init {
require(mode == ConnectionMode.AUTOMATIC || endpoint != null) {
"Manual server selection requires an endpoint"
}
}

fun toJson() =
JSONObject().apply {
put(KEY_MODE, mode.persistedValue)
endpoint?.let {
put(KEY_ENDPOINT_HOST, it.host)
put(KEY_ENDPOINT_PORT, it.port)
put(KEY_ENDPOINT_IS_SECURE, it.isSecure)
}
}

companion object {
private const val KEY_MODE = "mode"
private const val KEY_ENDPOINT_HOST = "endpoint_host"
private const val KEY_ENDPOINT_PORT = "endpoint_port"
private const val KEY_ENDPOINT_IS_SECURE = "endpoint_is_secure"

fun automatic() = ServerSelection(ConnectionMode.AUTOMATIC)

fun manual(endpoint: LightWalletEndpoint) =
ServerSelection(
mode = ConnectionMode.MANUAL,
endpoint = endpoint
)

fun from(jsonObject: JSONObject): ServerSelection {
val mode =
ConnectionMode.fromPersistedValue(
jsonObject.getString(KEY_MODE)
)

return when (mode) {
ConnectionMode.AUTOMATIC -> automatic()
ConnectionMode.MANUAL -> manual(jsonObject.getEndpoint())
}
}

private fun JSONObject.getEndpoint() =
LightWalletEndpoint(
host = getString(KEY_ENDPOINT_HOST),
port = getInt(KEY_ENDPOINT_PORT),
isSecure = getBoolean(KEY_ENDPOINT_IS_SECURE)
)
}
}

enum class ConnectionMode(
val persistedValue: String
) {
AUTOMATIC("automatic"),
MANUAL("manual");

companion object {
fun fromPersistedValue(value: String) =
entries.firstOrNull { it.persistedValue == value } ?: AUTOMATIC
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package co.electriccoin.zcash.ui.common.provider

import co.electriccoin.zcash.preference.EncryptedPreferenceProvider
import co.electriccoin.zcash.preference.api.PreferenceProvider
import co.electriccoin.zcash.preference.model.entry.PreferenceDefault
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
import co.electriccoin.zcash.ui.common.model.ServerSelection
import kotlinx.coroutines.flow.Flow
import org.json.JSONObject

interface ServerSelectionProvider {
val serverSelection: Flow<ServerSelection?>

suspend fun store(serverSelection: ServerSelection)

suspend fun getServerSelection(): ServerSelection?
}

class ServerSelectionProviderImpl(
preferenceHolder: EncryptedPreferenceProvider
) : ServerSelectionProvider {
private val storageProvider = ServerSelectionStorageProviderImpl(preferenceHolder)

override val serverSelection = storageProvider.observe()

override suspend fun store(serverSelection: ServerSelection) {
storageProvider.store(serverSelection)
}

override suspend fun getServerSelection() = storageProvider.get()
}

private interface ServerSelectionStorageProvider : NullableStorageProvider<ServerSelection>

private class ServerSelectionStorageProviderImpl(
override val preferenceHolder: EncryptedPreferenceProvider,
) : BaseNullableStorageProvider<ServerSelection>(),
ServerSelectionStorageProvider {
override val default = ServerSelectionPreferenceDefault(PreferenceKey("server_selection"))
}

private class ServerSelectionPreferenceDefault(
override val key: PreferenceKey
) : PreferenceDefault<ServerSelection?> {
override suspend fun getValue(preferenceProvider: PreferenceProvider) =
preferenceProvider.getString(key)?.let { ServerSelection.from(JSONObject(it)) }

override suspend fun putValue(
preferenceProvider: PreferenceProvider,
newValue: ServerSelection?
) = preferenceProvider.putString(key, newValue?.toJson()?.toString())
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,22 @@ import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import cash.z.ecc.sdk.type.fromResources
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
import co.electriccoin.zcash.preference.StandardPreferenceProvider
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.common.datasource.RestoreTimestampDataSource
import co.electriccoin.zcash.ui.common.model.ConnectionMode
import co.electriccoin.zcash.ui.common.model.FastestServersState
import co.electriccoin.zcash.ui.common.model.OnboardingState
import co.electriccoin.zcash.ui.common.model.ServerSelection
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.provider.LightWalletEndpointProvider
import co.electriccoin.zcash.ui.common.provider.PersistableWalletProvider
import co.electriccoin.zcash.ui.common.provider.ServerSelectionProvider
import co.electriccoin.zcash.ui.common.provider.SynchronizerProvider
import co.electriccoin.zcash.ui.common.provider.WalletBackupFlagStorageProvider
import co.electriccoin.zcash.ui.common.provider.WalletRestoringStateProvider
import co.electriccoin.zcash.ui.common.viewmodel.SecretState
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
Expand All @@ -32,13 +37,17 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
Expand Down Expand Up @@ -72,6 +81,7 @@ class WalletRepositoryImpl(
private val application: Application,
private val lightWalletEndpointProvider: LightWalletEndpointProvider,
private val persistableWalletProvider: PersistableWalletProvider,
private val serverSelectionProvider: ServerSelectionProvider,
private val synchronizerProvider: SynchronizerProvider,
private val standardPreferenceProvider: StandardPreferenceProvider,
private val restoreTimestampDataSource: RestoreTimestampDataSource,
Expand Down Expand Up @@ -149,6 +159,13 @@ class WalletRepositoryImpl(
initialValue = FastestServersState(servers = emptyList(), isLoading = true)
)

init {
scope.launch {
migrateServerSelectionIfNeeded()
keepSelectedEndpointUpdated()
}
}

override val walletRestoringState: StateFlow<WalletRestoringState> =
walletRestoringStateProvider
.observe()
Expand All @@ -160,13 +177,17 @@ class WalletRepositoryImpl(

override fun updateWalletEndpoint(endpoint: LightWalletEndpoint) {
scope.launch {
val selectedWallet = persistableWalletProvider.getPersistableWallet() ?: return@launch
val selectedEndpoint = selectedWallet.endpoint
if (selectedEndpoint == endpoint) return@launch
persistWalletInternal(selectedWallet.copy(endpoint = endpoint))
updateWalletEndpointInternal(endpoint)
}
}

private suspend fun updateWalletEndpointInternal(endpoint: LightWalletEndpoint) {
val selectedWallet = persistableWalletProvider.getPersistableWallet() ?: return
val selectedEndpoint = selectedWallet.endpoint
if (selectedEndpoint == endpoint) return
persistWalletInternal(selectedWallet.copy(endpoint = endpoint))
}

private suspend fun persistWalletInternal(persistableWallet: PersistableWallet) {
synchronizerProvider.synchronizer.firstOrNull()?.let { (it as? SdkSynchronizer)?.close() }
persistableWalletProvider.store(persistableWallet)
Expand All @@ -175,6 +196,7 @@ class WalletRepositoryImpl(
override fun createNewWallet() {
scope.launch {
persistOnboardingStateInternal(OnboardingState.READY)
serverSelectionProvider.store(ServerSelection.automatic())
val zcashNetwork = ZcashNetwork.fromResources(application)
val newWallet =
PersistableWallet.new(
Expand Down Expand Up @@ -209,6 +231,7 @@ class WalletRepositoryImpl(
birthday: BlockHeight
) {
scope.launch {
serverSelectionProvider.store(ServerSelection.automatic())
val restoredWallet =
PersistableWallet(
network = network,
Expand All @@ -224,4 +247,60 @@ class WalletRepositoryImpl(
persistOnboardingStateInternal(OnboardingState.READY)
}
}

private suspend fun migrateServerSelectionIfNeeded() {
if (serverSelectionProvider.getServerSelection() != null) return

val existingWallet = persistableWalletProvider.getPersistableWallet()
val selection =
existingWallet
?.endpoint
?.let { endpoint ->
if (lightWalletEndpointProvider.getEndpoints().contains(endpoint)) {
ServerSelection.automatic()
} else {
ServerSelection.manual(endpoint)
}
} ?: ServerSelection.automatic()

if (serverSelectionProvider.getServerSelection() == null) {
serverSelectionProvider.store(selection)
}
}

@OptIn(ExperimentalCoroutinesApi::class)
private suspend fun keepSelectedEndpointUpdated() {
serverSelectionProvider
.serverSelection
.filterNotNull()
.distinctUntilChanged()
.flatMapLatest { selection ->
when (selection.mode) {
ConnectionMode.AUTOMATIC -> {
fastestEndpoints.map { fastestServers ->
if (fastestServers.isLoading) {
null
} else {
fastestServers.servers?.firstOrNull()
}
}
}

ConnectionMode.MANUAL -> {
flowOf(selection.endpoint)
}
}
}.distinctUntilChanged()
.collect { endpoint ->
endpoint ?: return@collect

try {
updateWalletEndpointInternal(endpoint)
} catch (e: CancellationException) {
throw e
} catch (t: Throwable) {
Twig.error(t) { "Unable to update selected server endpoint" }
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class ConfirmResyncUseCase(
) {
suspend operator fun invoke(blockHeight: BlockHeight) {
val synchronizer = synchronizerProvider.getSynchronizer()
synchronizer.rewindToHeight(blockHeight)
synchronizer.rewindToNearestHeight(blockHeight)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive by

walletRestoringStateProvider.store(WalletRestoringState.RESYNCING)
synchronizerProvider.resetSynchronizer()
isKeepScreenOnDuringRestoreProvider.clear()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ class GetSelectedEndpointUseCase(
persistableWalletProvider.persistableWallet
.map { it?.endpoint }
.distinctUntilChanged()

suspend operator fun invoke() = persistableWalletProvider.getPersistableWallet()?.endpoint
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package co.electriccoin.zcash.ui.common.usecase

import co.electriccoin.zcash.ui.common.model.ServerSelection
import co.electriccoin.zcash.ui.common.provider.ServerSelectionProvider
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map

class GetServerSelectionUseCase(
private val serverSelectionProvider: ServerSelectionProvider
) {
fun observe() =
serverSelectionProvider.serverSelection
.map { it ?: ServerSelection.automatic() }
.distinctUntilChanged()

suspend operator fun invoke() = serverSelectionProvider.getServerSelection() ?: ServerSelection.automatic()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package co.electriccoin.zcash.ui.common.usecase

class PersistEndpointException(
message: String?
) : Exception(message)
Loading
Loading