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:
parent
1937c19a14
commit
ed7577f4a8
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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!"
|
||||
|
|
|
@ -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)) {
|
||||
|
|
Loading…
Reference in New Issue