Starting the synchronizer in a more clear way with less room for error.

This was a key benefit of the refactor for #39. Also removed the bandaid of exposing the rustBackend in the Synchronizer, which is a huge win.
This commit is contained in:
Kevin Gorham 2020-01-07 01:45:24 -05:00
parent 1937c19a14
commit ed7577f4a8
No known key found for this signature in database
GPG Key ID: CCA55602DF49FC38
6 changed files with 99 additions and 90 deletions

View File

@ -21,14 +21,13 @@ import androidx.navigation.findNavController
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.di.component.MainActivitySubcomponent
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.feedback.*
import cash.z.ecc.android.di.component.SynchronizerSubcomponent
import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.feedback.FeedbackCoordinator
import cash.z.ecc.android.feedback.LaunchMetric
import cash.z.ecc.android.feedback.Report.NonUserAction.FEEDBACK_STOPPED
import cash.z.ecc.android.feedback.Report.NonUserAction.SYNC_START
import cash.z.ecc.android.ui.send.SendViewModel
import cash.z.wallet.sdk.Initializer
import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.ext.twig
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -36,8 +35,6 @@ import javax.inject.Inject
class MainActivity : AppCompatActivity() {
private var syncInit: (() -> Unit)? = null
@Inject
lateinit var feedback: Feedback
@ -47,8 +44,6 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var clipboard: ClipboardManager
val sendViewModel: SendViewModel by viewModel()
private val mediaPlayer: MediaPlayer = MediaPlayer()
@ -56,13 +51,12 @@ class MainActivity : AppCompatActivity() {
lateinit var navController: NavController
lateinit var synchronizer: Synchronizer
lateinit var component: MainActivitySubcomponent
lateinit var synchronizerComponent: SynchronizerSubcomponent
override fun onCreate(savedInstanceState: Bundle?) {
component = ZcashWalletApp.component.mainActivityComponent().create(this).also {
component = ZcashWalletApp.component.mainActivitySubcomponent().create(this).also {
it.inject(this)
}
super.onCreate(savedInstanceState)
@ -125,27 +119,10 @@ class MainActivity : AppCompatActivity() {
}
}
fun initSync() {
twig("Initializing synchronizer")
if (!::synchronizer.isInitialized) {
twig("Synchronizer didn't exist yet (this means we're opening an existing wallet). Creating it now.")
val initializer = Initializer(ZcashWalletApp.instance, "lightd-main.zecwallet.co", 443).also { it.open() }
synchronizer = Synchronizer(ZcashWalletApp.instance, initializer)
}
fun startSync(initializer: Initializer) {
synchronizerComponent = ZcashWalletApp.component.synchronizerSubcomponent().create(initializer)
feedback.report(SYNC_START)
synchronizer.start(lifecycleScope)
if (syncInit != null) {
syncInit!!()
syncInit = null
}
}
fun initializeAccount(seed: ByteArray, birthday: Initializer.WalletBirthday? = null) {
twig("Initializing accounts")
feedback.measure(Report.MetricType.ACCOUNT_CREATED) {
synchronizer =
Synchronizer(ZcashWalletApp.instance, "lightd-main.zecwallet.co", 443, seed, birthday)
}
synchronizerComponent.synchronizer().start(lifecycleScope)
}
fun playSound(fileName: String) {
@ -177,7 +154,7 @@ class MainActivity : AppCompatActivity() {
clipboard.setPrimaryClip(
ClipData.newPlainText(
"Z-Address",
synchronizer.getAddress()
synchronizerComponent.synchronizer().getAddress()
)
)
showMessage("Address copied!", "Sweet")
@ -213,13 +190,4 @@ class MainActivity : AppCompatActivity() {
if (!it.isShownOrQueued) it.show()
}
}
// TODO: refactor initialization and remove the need for this
fun onSyncInit(initBlock: () -> Unit) {
if (::synchronizer.isInitialized) {
initBlock()
} else {
syncInit = initBlock
}
}
}

View File

@ -19,7 +19,6 @@ import cash.z.ecc.android.ui.home.HomeFragment.BannerAction.*
import cash.z.ecc.android.ui.send.SendViewModel
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.NO_SEED
import cash.z.wallet.sdk.SdkSynchronizer
import cash.z.wallet.sdk.Synchronizer.Status.SYNCING
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import cash.z.wallet.sdk.ext.convertZecToZatoshi
@ -36,7 +35,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
private lateinit var numberPad: List<TextView>
private lateinit var uiModel: HomeViewModel.UiModel
private val walletSetup: WalletSetupViewModel by activityViewModel()
private val walletSetup: WalletSetupViewModel by activityViewModel(false)
private val sendViewModel: SendViewModel by activityViewModel()
private val viewModel: HomeViewModel by viewModel()
@ -53,23 +52,19 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
override fun onAttach(context: Context) {
twig("HomeFragment.onAttach")
mainActivity?.component?.inject(this)
super.onAttach(context)
// call initSync either now or later (after initializing DBs with newly created seed)
// this will call startSync either now or later (after initializing with newly created seed)
walletSetup.checkSeed().onEach {
twig("Checking seed")
when(it) {
NO_SEED -> {
twig("Seed not found, therefore, launching seed creation flow")
// interact with user to create, backup and verify seed
mainActivity?.navController?.navigate(R.id.action_nav_home_to_create_wallet)
// leads to a call to initSync(), later (after accounts are created from seed)
}
else -> {
twig("Found seed. Re-opening existing wallet")
mainActivity?.initSync()
}
if (it == NO_SEED) {
// interact with user to create, backup and verify seed
// leads to a call to startSync(), later (after accounts are created from seed)
twig("Seed not found, therefore, launching seed creation flow")
mainActivity?.navController?.navigate(R.id.action_nav_home_to_create_wallet)
} else {
twig("Found seed. Re-opening existing wallet")
mainActivity?.startSync(walletSetup.openWallet())
}
}.launchIn(lifecycleScope)
}
@ -104,19 +99,15 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
onSend()
}
}
if (::uiModel.isInitialized) {
twig("uiModel exists!")
onModelUpdated(HomeViewModel.UiModel(), uiModel)
} else {
twig("uiModel does not exist!")
mainActivity?.onSyncInit {
viewModel.initialize(mainActivity!!.synchronizer, typedChars)
}
}
// if (::uiModel.isInitialized) {
// twig("uiModel exists!")
// onModelUpdated(HomeViewModel.UiModel(), uiModel)
// }
}
override fun onResume() {
super.onResume()
viewModel.initialize(typedChars)
twig("HomeFragment.onResume resumeScope.isActive: ${resumedScope.isActive} $resumedScope")
viewModel.uiModels.scanReduce { old, new ->
onModelUpdated(old, new)
@ -130,7 +121,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
// but for some reason, this doesn't always happen, which kind of defeats the purpose
// of having a cold stream in the view model
resumedScope.launch {
(mainActivity!!.synchronizer as SdkSynchronizer).refreshBalance()
viewModel.refreshBalance()
}
}

View File

@ -2,26 +2,37 @@ package cash.z.ecc.android.ui.home
import android.os.Parcelable
import androidx.lifecycle.ViewModel
import cash.z.wallet.sdk.SdkSynchronizer
import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.Synchronizer.Status.DISCONNECTED
import cash.z.wallet.sdk.Synchronizer.Status.SYNCED
import cash.z.wallet.sdk.ext.ZcashSdk
import cash.z.wallet.sdk.ext.ZcashSdk.MINERS_FEE_ZATOSHI
import cash.z.wallet.sdk.ext.ZcashSdk.ZATOSHI_PER_ZEC
import cash.z.wallet.sdk.ext.twig
import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.scan
import javax.inject.Inject
class HomeViewModel @Inject constructor() : ViewModel() {
@Inject
lateinit var synchronizer: Synchronizer
lateinit var uiModels: Flow<UiModel>
var initialized = false
fun initialize(
synchronizer: Synchronizer,
typedChars: Flow<Char>
) {
twig("init called")
if (initialized) {
twig("Warning already initialized HomeViewModel. Ignoring call to initialize.")
return
}
val zec = typedChars.scan("0") { acc, c ->
when {
// no-op cases
@ -56,6 +67,10 @@ class HomeViewModel @Inject constructor() : ViewModel() {
twig("HomeViewModel cleared!")
}
suspend fun refreshBalance() {
(synchronizer as SdkSynchronizer).refreshBalance()
}
@Parcelize
data class UiModel( // <- THIS ERROR IS AN IDE BUG WITH PARCELIZE
val status: Synchronizer.Status = DISCONNECTED,

View File

@ -3,7 +3,7 @@ package cash.z.ecc.android.ui.send
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
import cash.z.wallet.sdk.SdkSynchronizer
import cash.z.wallet.sdk.Initializer
import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.entity.PendingTransaction
import cash.z.wallet.sdk.ext.twig
@ -16,8 +16,14 @@ class SendViewModel @Inject constructor() : ViewModel() {
@Inject
lateinit var lockBox: LockBox
fun send(synchronizer: Synchronizer): Flow<PendingTransaction> {
val keys = (synchronizer as SdkSynchronizer).rustBackend!!.deriveSpendingKeys(
@Inject
lateinit var synchronizer: Synchronizer
@Inject
lateinit var initializer: Initializer
fun send(): Flow<PendingTransaction> {
val keys = initializer.deriveSpendingKeys(
lockBox.getBytes(WalletSetupViewModel.LockBoxKey.SEED)!!
)
return synchronizer.sendToAddress(

View File

@ -5,10 +5,12 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentLandingBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITHOUT_BACKUP
@ -21,7 +23,7 @@ import kotlinx.coroutines.launch
class LandingFragment : BaseFragment<FragmentLandingBinding>() {
val walletSetup: WalletSetupViewModel by viewModel()
private val walletSetup: WalletSetupViewModel by activityViewModel(false)
private var skipCount: Int = 0
@ -95,19 +97,14 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
Toast.makeText(activity, "Coming soon!", Toast.LENGTH_SHORT).show()
}
// AKA import wallet
private fun onUseDevWallet() {
val seedPhrase =
"still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
val seedPhrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
val birthday = 663174//626599
mainActivity?.apply {
lifecycleScope.launch {
initializeAccount(
walletSetup.importWallet(feedback, seedPhrase.toCharArray()),
Initializer.loadBirthdayFromAssets(ZcashWalletApp.instance, birthday)
)
initSync()
mainActivity?.startSync(walletSetup.importWallet(seedPhrase, birthday))
}
binding.buttonPositive.isEnabled = true
binding.textMessage.text = "Wallet imported! Congratulations!"
binding.buttonNegative.text = "Skip"
@ -117,17 +114,13 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
}
}
// TODO: move this to the ViewModel but doing so requires fixing dagger so do that as a separate PR
private fun onNewWallet() {
lifecycleScope.launch {
val ogText = binding.buttonPositive.text
binding.buttonPositive.text = "creating"
binding.buttonPositive.isEnabled = false
mainActivity?.apply {
initializeAccount(walletSetup.createWallet(feedback))
initSync()
}
mainActivity?.startSync(walletSetup.newWallet())
binding.buttonPositive.isEnabled = true
binding.textMessage.text = "Wallet created! Congratulations!"

View File

@ -1,12 +1,16 @@
package cash.z.ecc.android.ui.setup
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.MetricType.*
import cash.z.ecc.android.feedback.measure
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.*
import cash.z.ecc.kotlin.mnemonic.Mnemonics
import cash.z.wallet.sdk.Initializer
import cash.z.wallet.sdk.ext.twig
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
@ -21,6 +25,9 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() {
@Inject
lateinit var lockBox: LockBox
@Inject
lateinit var feedback: Feedback
enum class WalletSetupState {
UNKNOWN, SEED_WITH_BACKUP, SEED_WITHOUT_BACKUP, NO_SEED
}
@ -33,12 +40,42 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() {
}
}
/**
* Re-open an existing wallet. This is the most common use case, where a user has previously
* created or imported their seed and is returning to the wallet. In other words, this is the
* non-FTUE case.
*/
fun openWallet(): Initializer {
twig("Opening existing wallet")
return ZcashWalletApp.component.initializerSubcomponent().create().run {
initializer().open(birthdayStore().getBirthday())
}
}
suspend fun newWallet(): Initializer {
twig("Initializing new wallet")
return ZcashWalletApp.component.initializerSubcomponent().create().run {
initializer().apply {
new(createWallet(), birthdayStore().newWalletBirthday)
}
}
}
suspend fun importWallet(seedPhrase: String, birthdayHeight: Int): Initializer {
twig("Importing wallet")
return ZcashWalletApp.component.initializerSubcomponent().create(Initializer.DefaultBirthdayStore(ZcashWalletApp.instance, birthdayHeight)).run {
initializer().apply {
import(importWallet(seedPhrase.toCharArray()), birthdayStore().getBirthday())
}
}
}
/**
* Take all the steps necessary to create a new wallet and measure how long it takes.
*
* @param feedback the object used for measurement.
*/
suspend fun createWallet(feedback: Feedback): ByteArray = withContext(Dispatchers.IO){
private suspend fun createWallet(): ByteArray = withContext(Dispatchers.IO){
check(!lockBox.getBoolean(LockBoxKey.HAS_SEED)) {
"Error! Cannot create a seed when one already exists! This would overwrite the" +
" existing seed and could lead to a loss of funds if the user has no backup!"
@ -69,8 +106,7 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() {
*
* @param feedback the object used for measurement.
*/
suspend fun importWallet(
feedback: Feedback,
private suspend fun importWallet(
seedPhrase: CharArray
): ByteArray = withContext(Dispatchers.IO) {
check(!lockBox.getBoolean(LockBoxKey.HAS_SEED)) {