package cash.z.ecc.android.sdk.demoapp import android.app.Application 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.Synchronizer import cash.z.ecc.android.sdk.demoapp.type.fromResources import cash.z.ecc.android.sdk.ext.BenchmarkingExt import cash.z.ecc.android.sdk.ext.onFirst import cash.z.ecc.android.sdk.fixture.BlockRangeFixture import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.LightWalletEndpoint import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.model.defaultForNetwork import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.util.UUID import kotlin.time.Duration.Companion.seconds /** * Shared mutable state for the demo */ class SharedViewModel(application: Application) : AndroidViewModel(application) { private val _seedPhrase = MutableStateFlow(DemoConstants.INITIAL_SEED_WORDS) private val _blockHeight = MutableStateFlow( runBlocking { BlockHeight.ofLatestCheckpoint( getApplication(), ZcashNetwork.fromResources(application) ) } ) // publicly, this is read-only val seedPhrase: StateFlow get() = _seedPhrase // publicly, this is read-only val birthdayHeight: StateFlow get() = _blockHeight private val lockoutMutex = Mutex() private val lockoutIdFlow = MutableStateFlow(null) @OptIn(ExperimentalCoroutinesApi::class) private val synchronizerOrLockout: Flow = lockoutIdFlow.flatMapLatest { lockoutId -> if (null != lockoutId) { flowOf(InternalSynchronizerStatus.Lockout(lockoutId)) } else { callbackFlow { // Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already // have the seed stored val seedBytes = Mnemonics.MnemonicCode(seedPhrase.value).toSeed() val network = ZcashNetwork.fromResources(application) val synchronizer = Synchronizer.new( application, network, lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network), seed = seedBytes, birthday = if (BenchmarkingExt.isBenchmarking()) { BlockRangeFixture.new().start } else { birthdayHeight.value } ) send(InternalSynchronizerStatus.Available(synchronizer)) awaitClose { synchronizer.close() } } } } // Note that seed and birthday shouldn't be changed once a synchronizer is first collected val synchronizerFlow: StateFlow = synchronizerOrLockout.map { when (it) { is InternalSynchronizerStatus.Available -> it.synchronizer is InternalSynchronizerStatus.Lockout -> null } }.stateIn( viewModelScope, SharingStarted.WhileSubscribed(DEFAULT_ANDROID_STATE_TIMEOUT.inWholeMilliseconds, 0), initialValue = null ) fun updateSeedPhrase(newPhrase: String?): Boolean { return if (isValidSeedPhrase(newPhrase)) { _seedPhrase.value = newPhrase!! true } else { false } } fun resetSDK() { viewModelScope.launch { lockoutMutex.withLock { val lockoutId = UUID.randomUUID() lockoutIdFlow.value = lockoutId synchronizerOrLockout .filterIsInstance() .filter { it.id == lockoutId } .onFirst { val didDelete = Synchronizer.erase( appContext = getApplication(), network = ZcashNetwork.fromResources(getApplication()) ) twig("SDK erase result: $didDelete") } lockoutIdFlow.value = null } } } private fun isValidSeedPhrase(phrase: String?): Boolean { if (phrase.isNullOrEmpty()) { return false } return try { Mnemonics.MnemonicCode(phrase).validate() true } catch (e: Mnemonics.WordCountException) { twig("Seed phrase validation failed with WordCountException: ${e.message}, cause: ${e.cause}") false } catch (e: Mnemonics.InvalidWordException) { twig("Seed phrase validation failed with InvalidWordException: ${e.message}, cause: ${e.cause}") false } catch (e: Mnemonics.ChecksumException) { twig("Seed phrase validation failed with ChecksumException: ${e.message}, cause: ${e.cause}") false } } private sealed class InternalSynchronizerStatus { class Available(val synchronizer: Synchronizer) : InternalSynchronizerStatus() class Lockout(val id: UUID) : InternalSynchronizerStatus() } companion object { private val DEFAULT_ANDROID_STATE_TIMEOUT = 5.seconds } }