Skip to content

Commit

Permalink
[#1393] Support server switching
Browse files Browse the repository at this point in the history
- This ensures that the SDK supports apps switching between different lightwalletd servers
- Synchronizer.validateServerEndpoint API added
- Demo app updated to leverage this new feature
- Changelog update
- Closes #1393
  • Loading branch information
HonzaR authored Mar 2, 2024
1 parent 36c803a commit 381bd42
Show file tree
Hide file tree
Showing 17 changed files with 622 additions and 31 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- `WalletBalanceFixture` class with mock values that are supposed to be used only for testing purposes
- `Memo.countLength(memoString: String)` to count memo length in bytes
- `PersistableWallet.toSafeString` is a safe alternative for the regular [toString] function that prints only
non-sensitive parts
- `Synchronizer.validateServerEndpoint` this function checks whether the provided server endpoint is valid.
The validation is based on comparing:
* network type
* sapling activation height
* consensus branch id

## [2.0.6] - 2024-01-30

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder
Expand All @@ -21,22 +26,27 @@ import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.demoapp.NavigationTargets.BALANCE
import cash.z.ecc.android.sdk.demoapp.NavigationTargets.HOME
import cash.z.ecc.android.sdk.demoapp.NavigationTargets.SEND
import cash.z.ecc.android.sdk.demoapp.NavigationTargets.SERVER
import cash.z.ecc.android.sdk.demoapp.NavigationTargets.TRANSACTIONS
import cash.z.ecc.android.sdk.demoapp.NavigationTargets.WALLET_ADDRESS_DETAILS
import cash.z.ecc.android.sdk.demoapp.ui.screen.addresses.view.Addresses
import cash.z.ecc.android.sdk.demoapp.ui.screen.balance.view.Balance
import cash.z.ecc.android.sdk.demoapp.ui.screen.home.view.Home
import cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel.SecretState
import cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel.WalletViewModel
import cash.z.ecc.android.sdk.demoapp.ui.screen.send.view.Send
import cash.z.ecc.android.sdk.demoapp.ui.screen.server.view.Server
import cash.z.ecc.android.sdk.demoapp.ui.screen.transactions.view.Transactions
import cash.z.ecc.android.sdk.demoapp.util.AndroidApiVersion
import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.type.ServerValidation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

@Composable
@Suppress("LongMethod", "ktlint:standard:function-naming")
@Suppress("LongMethod", "CyclomaticComplexMethod", "ktlint:standard:function-naming")
internal fun ComposeActivity.Navigation() {
val navController = rememberNavController()

Expand All @@ -63,6 +73,7 @@ internal fun ComposeActivity.Navigation() {
}
},
goTransactions = { navController.navigateJustOnce(TRANSACTIONS) },
goServer = { navController.navigateJustOnce(SERVER) },
resetSdk = { walletViewModel.resetSdk() },
rewind = { walletViewModel.rewind() }
)
Expand Down Expand Up @@ -136,6 +147,76 @@ internal fun ComposeActivity.Navigation() {
)
}
}
composable(SERVER) {
val secretState = walletViewModel.secretState.collectAsStateWithLifecycle().value

val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value

Twig.info { "Current secrets state: $secretState" }

if (synchronizer == null || secretState !is SecretState.Ready) {
// Display loading indicator
} else {
val wallet = secretState.persistableWallet

Twig.info { "Current persisted wallet: ${wallet.toSafeString()}" }

val scope = rememberCoroutineScope()

var validationResult: ServerValidation by remember { mutableStateOf(ServerValidation.Valid) }

val onBack = {
if (validationResult !is ServerValidation.Running) {
navController.popBackStackJustOnce(SERVER)
}
}

BackHandler { onBack() }

Server(
buildInNetwork = ZcashNetwork.fromResources(application),
onBack = onBack,
onServerChange = { newEndpoint ->
scope.launch {
validationResult = ServerValidation.Running
validationResult = synchronizer.validateServerEndpoint(application, newEndpoint)

Twig.info { "Validation result: $validationResult" }

when (validationResult) {
ServerValidation.Valid -> {
walletViewModel.closeSynchronizer()

val newWallet =
wallet.copy(
endpoint = newEndpoint
)

Twig.info { "New wallet: ${newWallet.toSafeString()}" }

walletViewModel.persistExistingWallet(newWallet)

Toast.makeText(
applicationContext,
"Server saved",
Toast.LENGTH_SHORT
).show()
}
is ServerValidation.InValid -> {
Twig.error { "Failed to validate the new endpoint: $newEndpoint" }
}
else -> {
// Should not happen
Twig.info { "Server validation state: $validationResult" }
}
}
}
},
validationResult = validationResult,
wallet = wallet,
)
}
}
composable(TRANSACTIONS) {
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
if (null == synchronizer) {
Expand Down Expand Up @@ -225,5 +306,7 @@ object NavigationTargets {

const val SEND = "send" // NON-NLS

const val SERVER = "server" // NON-NLS

const val TRANSACTIONS = "transactions" // NON-NLS
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,8 @@ internal val LightWalletEndpoint.Companion.Testnet
DEFAULT_PORT,
isSecure = true
)

const val MIN_PORT_NUMBER = 1
const val MAX_PORT_NUMBER = 65535

internal fun LightWalletEndpoint.isValid() = host.isNotBlank() && port in MIN_PORT_NUMBER..MAX_PORT_NUMBER
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ private fun ComposablePreviewHome() {
goAddressDetails = {},
goTransactions = {},
goTestnetFaucet = {},
goServer = {},
resetSdk = {},
rewind = {},
)
Expand All @@ -59,6 +60,7 @@ fun Home(
goSend: () -> Unit,
goAddressDetails: () -> Unit,
goTransactions: () -> Unit,
goServer: () -> Unit,
goTestnetFaucet: () -> Unit,
resetSdk: () -> Unit,
rewind: () -> Unit,
Expand All @@ -76,6 +78,7 @@ fun Home(
walletSnapshot,
goBalance = goBalance,
goSend = goSend,
goServer = goServer,
goAddressDetails = goAddressDetails,
goTransactions = goTransactions
)
Expand Down Expand Up @@ -155,8 +158,9 @@ private fun HomeMainContent(
walletSnapshot: WalletSnapshot,
goBalance: () -> Unit,
goSend: () -> Unit,
goServer: () -> Unit,
goAddressDetails: () -> Unit,
goTransactions: () -> Unit
goTransactions: () -> Unit,
) {
Column(
Modifier
Expand All @@ -179,6 +183,10 @@ private fun HomeMainContent(
Text(text = stringResource(id = R.string.menu_transactions))
}

Button(goServer) {
Text(text = stringResource(id = R.string.menu_server))
}

Text(text = stringResource(id = R.string.home_status, walletSnapshot.status.toString()))
if (walletSnapshot.status != Synchronizer.Status.SYNCED) {
@Suppress("MagicNumber")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.WalletCoordinator
import cash.z.ecc.android.sdk.WalletInitMode
Expand Down Expand Up @@ -267,6 +268,18 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
}
}
}

/**
* This safely and asynchronously stops the [Synchronizer].
*/
fun closeSynchronizer() {
val synchronizer = synchronizer.value
if (null != synchronizer) {
viewModelScope.launch {
(synchronizer as SdkSynchronizer).close()
}
}
}
}

/**
Expand Down
Loading

0 comments on commit 381bd42

Please sign in to comment.