diff --git a/app/src/main/java/com/babylon/wallet/android/WalletApp.kt b/app/src/main/java/com/babylon/wallet/android/WalletApp.kt index ddb5c8d3a8..f641dac6e8 100644 --- a/app/src/main/java/com/babylon/wallet/android/WalletApp.kt +++ b/app/src/main/java/com/babylon/wallet/android/WalletApp.kt @@ -21,6 +21,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.babylon.wallet.android.domain.model.MessageFromDataChannel import com.babylon.wallet.android.domain.userFriendlyMessage +import com.babylon.wallet.android.presentation.accessfactorsources.accessFactorSources import com.babylon.wallet.android.presentation.dapp.authorized.login.dAppLoginAuthorized import com.babylon.wallet.android.presentation.dapp.unauthorized.login.dAppLoginUnauthorized import com.babylon.wallet.android.presentation.main.MAIN_ROUTE @@ -43,7 +44,6 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController import kotlinx.coroutines.flow.Flow @Composable -@Suppress("ModifierMissing") fun WalletApp( modifier: Modifier = Modifier, mainViewModel: MainViewModel, @@ -103,6 +103,10 @@ fun WalletApp( ) } } + HandleAccessFactorSourcesEvents( + navController = navController, + accessFactorSourcesEvents = mainViewModel.accessFactorSourcesEvents + ) HandleStatusEvents( navController = navController, statusEvents = mainViewModel.statusEvents @@ -175,7 +179,26 @@ private fun SyncStatusBarWithScreenChanges(navController: NavHostController) { } @Composable -fun HandleStatusEvents(navController: NavController, statusEvents: Flow) { +private fun HandleAccessFactorSourcesEvents( + navController: NavController, + accessFactorSourcesEvents: Flow +) { + LaunchedEffect(Unit) { + accessFactorSourcesEvents.collect { event -> + when (event) { + is AppEvent.AccessFactorSources -> { + navController.accessFactorSources() + } + } + } + } +} + +@Composable +private fun HandleStatusEvents( + navController: NavController, + statusEvents: Flow +) { LaunchedEffect(Unit) { statusEvents.collect { event -> when (event) { @@ -192,7 +215,7 @@ fun HandleStatusEvents(navController: NavController, statusEvents: Flow Unit, onHighPriorityScreen: () -> Unit diff --git a/app/src/main/java/com/babylon/wallet/android/di/UiModule.kt b/app/src/main/java/com/babylon/wallet/android/di/UiModule.kt new file mode 100644 index 0000000000..f0a4fea7e1 --- /dev/null +++ b/app/src/main/java/com/babylon/wallet/android/di/UiModule.kt @@ -0,0 +1,24 @@ +package com.babylon.wallet.android.di + +import com.babylon.wallet.android.presentation.accessfactorsources.AccessFactorSourcesProxy +import com.babylon.wallet.android.presentation.accessfactorsources.AccessFactorSourcesProxyImpl +import com.babylon.wallet.android.presentation.accessfactorsources.AccessFactorSourcesUiProxy +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent + +@Module +@InstallIn(ActivityRetainedComponent::class) +interface UiModule { + + @Binds + fun bindAccessFactorSourcesUiProxy( + accessFactorSourcesProxyImpl: AccessFactorSourcesProxyImpl + ): AccessFactorSourcesUiProxy + + @Binds + fun bindAccessFactorSourcesProxy( + accessFactorSourcesProxyImpl: AccessFactorSourcesProxyImpl + ): AccessFactorSourcesProxy +} diff --git a/app/src/main/java/com/babylon/wallet/android/domain/usecases/CreateAccountUseCase.kt b/app/src/main/java/com/babylon/wallet/android/domain/usecases/CreateAccountUseCase.kt new file mode 100644 index 0000000000..7aeffb874e --- /dev/null +++ b/app/src/main/java/com/babylon/wallet/android/domain/usecases/CreateAccountUseCase.kt @@ -0,0 +1,57 @@ +package com.babylon.wallet.android.domain.usecases + +import com.babylon.wallet.android.data.repository.ResolveAccountsLedgerStateRepository +import com.babylon.wallet.android.presentation.accessfactorsources.AccessFactorSourcesOutput +import kotlinx.coroutines.flow.first +import rdx.works.profile.data.model.apppreferences.Radix +import rdx.works.profile.data.model.currentNetwork +import rdx.works.profile.data.model.extensions.createAccount +import rdx.works.profile.data.model.factorsources.FactorSource +import rdx.works.profile.data.model.pernetwork.Network +import rdx.works.profile.data.model.pernetwork.addAccounts +import rdx.works.profile.data.repository.ProfileRepository +import rdx.works.profile.data.repository.profile +import rdx.works.profile.derivation.model.NetworkId +import javax.inject.Inject + +class CreateAccountUseCase @Inject constructor( + private val profileRepository: ProfileRepository, + private val resolveAccountsLedgerStateRepository: ResolveAccountsLedgerStateRepository +) { + + suspend operator fun invoke( + displayName: String, + factorSource: FactorSource.CreatingEntity, + publicKeyAndDerivationPath: AccessFactorSourcesOutput.PublicKeyAndDerivationPath, + onNetworkId: NetworkId? + ): Network.Account { + val currentProfile = profileRepository.profile.first() + val networkId = onNetworkId ?: currentProfile.currentNetwork?.knownNetworkId ?: Radix.Gateway.default.network.networkId() + + val newAccount = currentProfile.createAccount( + displayName = displayName, + onNetworkId = networkId, + factorSource = factorSource, + derivationPath = publicKeyAndDerivationPath.derivationPath, + compressedPublicKey = publicKeyAndDerivationPath.compressedPublicKey, + onLedgerSettings = Network.Account.OnLedgerSettings.init() + ) + + val accountWithOnLedgerStatusResult = resolveAccountsLedgerStateRepository(listOf(newAccount)) + + val accountToAdd = if (accountWithOnLedgerStatusResult.isSuccess) { + accountWithOnLedgerStatusResult.getOrThrow().first().account + } else { + newAccount + } + + val updatedProfile = currentProfile.addAccounts( + accounts = listOf(accountToAdd), + onNetwork = networkId + ) + // Save updated profile + profileRepository.saveProfile(updatedProfile) + // Return new account + return accountToAdd + } +} diff --git a/app/src/main/java/com/babylon/wallet/android/domain/usecases/CreateAccountWithBabylonDeviceFactorSourceUseCase.kt b/app/src/main/java/com/babylon/wallet/android/domain/usecases/CreateAccountWithBabylonDeviceFactorSourceUseCase.kt deleted file mode 100644 index 242d6f0bfd..0000000000 --- a/app/src/main/java/com/babylon/wallet/android/domain/usecases/CreateAccountWithBabylonDeviceFactorSourceUseCase.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.babylon.wallet.android.domain.usecases - -import com.babylon.wallet.android.data.repository.ResolveAccountsLedgerStateRepository -import com.babylon.wallet.android.data.transaction.InteractionState -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.withContext -import rdx.works.profile.data.model.apppreferences.Radix -import rdx.works.profile.data.model.currentNetwork -import rdx.works.profile.data.model.extensions.mainBabylonFactorSource -import rdx.works.profile.data.model.factorsources.DerivationPathScheme -import rdx.works.profile.data.model.pernetwork.Network -import rdx.works.profile.data.model.pernetwork.addAccounts -import rdx.works.profile.data.model.pernetwork.nextAccountIndex -import rdx.works.profile.data.model.pernetwork.nextAppearanceId -import rdx.works.profile.data.repository.MnemonicRepository -import rdx.works.profile.data.repository.ProfileRepository -import rdx.works.profile.derivation.model.NetworkId -import rdx.works.profile.di.coroutines.DefaultDispatcher -import rdx.works.profile.domain.EnsureBabylonFactorSourceExistUseCase -import javax.inject.Inject - -class CreateAccountWithBabylonDeviceFactorSourceUseCase @Inject constructor( - private val mnemonicRepository: MnemonicRepository, - private val ensureBabylonFactorSourceExistUseCase: EnsureBabylonFactorSourceExistUseCase, - private val profileRepository: ProfileRepository, - private val resolveAccountsLedgerStateRepository: ResolveAccountsLedgerStateRepository, - @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher -) { - - private val _interactionState = MutableStateFlow(null) - val interactionState: Flow = _interactionState.asSharedFlow() - - suspend operator fun invoke( - displayName: String, - networkID: NetworkId? = null - ): Network.Account { - return withContext(defaultDispatcher) { - val profile = ensureBabylonFactorSourceExistUseCase() - val factorSource = profile.mainBabylonFactorSource() - ?: error("Babylon factor source is not present") - _interactionState.update { InteractionState.Device.DerivingAccounts(factorSource) } - - // Construct new account - val networkId = networkID ?: profile.currentNetwork?.knownNetworkId ?: Radix.Gateway.default.network.networkId() - val nextAccountIndex = profile.nextAccountIndex(DerivationPathScheme.CAP_26, networkId, factorSource.id) - val nextAppearanceId = profile.nextAppearanceId(networkId) - val mnemonicWithPassphrase = requireNotNull(mnemonicRepository.readMnemonic(factorSource.id).getOrNull()) - val newAccount = Network.Account.initAccountWithBabylonDeviceFactorSource( - entityIndex = nextAccountIndex, - displayName = displayName, - mnemonicWithPassphrase = mnemonicWithPassphrase, - deviceFactorSource = factorSource, - networkId = networkId, - appearanceID = nextAppearanceId - ) - val resolveResult = resolveAccountsLedgerStateRepository.invoke(listOf(newAccount)) - // Add account to the profile - val accountToAdd = if (resolveResult.isSuccess) { - resolveResult.getOrThrow().first().account - } else { - newAccount - } - val updatedProfile = profile.addAccounts( - accounts = listOf(accountToAdd), - onNetwork = networkId - ) - // Save updated profile - profileRepository.saveProfile(updatedProfile) - _interactionState.update { null } - // Return new account - accountToAdd - } - } -} diff --git a/app/src/main/java/com/babylon/wallet/android/domain/usecases/CreateAccountWithLedgerFactorSourceUseCase.kt b/app/src/main/java/com/babylon/wallet/android/domain/usecases/CreateAccountWithLedgerFactorSourceUseCase.kt deleted file mode 100644 index 25a19f1bcf..0000000000 --- a/app/src/main/java/com/babylon/wallet/android/domain/usecases/CreateAccountWithLedgerFactorSourceUseCase.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.babylon.wallet.android.domain.usecases - -import com.babylon.wallet.android.data.repository.ResolveAccountsLedgerStateRepository -import com.babylon.wallet.android.designsystem.theme.AccountGradientList -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.withContext -import rdx.works.profile.data.model.apppreferences.Radix -import rdx.works.profile.data.model.currentNetwork -import rdx.works.profile.data.model.factorsources.FactorSource -import rdx.works.profile.data.model.factorsources.LedgerHardwareWalletFactorSource -import rdx.works.profile.data.model.pernetwork.DerivationPath -import rdx.works.profile.data.model.pernetwork.Network -import rdx.works.profile.data.model.pernetwork.addAccounts -import rdx.works.profile.data.model.pernetwork.nextAccountIndex -import rdx.works.profile.data.repository.ProfileRepository -import rdx.works.profile.data.repository.profile -import rdx.works.profile.derivation.model.NetworkId -import rdx.works.profile.di.coroutines.DefaultDispatcher -import javax.inject.Inject - -class CreateAccountWithLedgerFactorSourceUseCase @Inject constructor( - private val profileRepository: ProfileRepository, - private val resolveAccountsLedgerStateRepository: ResolveAccountsLedgerStateRepository, - @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher -) { - suspend operator fun invoke( - displayName: String, - derivedPublicKeyHex: String, - ledgerFactorSourceID: FactorSource.FactorSourceID.FromHash, - derivationPath: DerivationPath, - networkID: NetworkId? = null - ): Network.Account { - return withContext(defaultDispatcher) { - val profile = profileRepository.profile.first() - - val ledgerHardwareWalletFactorSource = profile.factorSources - .first { - it.id == ledgerFactorSourceID - } as LedgerHardwareWalletFactorSource - // Construct new account - val networkId = networkID ?: profile.currentNetwork?.knownNetworkId ?: Radix.Gateway.default.network.networkId() - val totalAccountsOnNetwork = profile.networks.find { it.networkID == networkId.value }?.accounts?.size ?: 0 - val newAccount = Network.Account.initAccountWithLedgerFactorSource( - entityIndex = profile.nextAccountIndex(derivationPath.scheme, networkId, ledgerFactorSourceID), - displayName = displayName, - derivedPublicKeyHex = derivedPublicKeyHex, - ledgerFactorSource = ledgerHardwareWalletFactorSource, - networkId = networkId, - derivationPath = derivationPath, - appearanceID = totalAccountsOnNetwork % AccountGradientList.count() - ) - val resolveResult = resolveAccountsLedgerStateRepository.invoke(listOf(newAccount)) - // Add account to the profile - val accountToAdd = if (resolveResult.isSuccess) { - resolveResult.getOrThrow().first().account - } else { - newAccount - } - val updatedProfile = profile.addAccounts( - accounts = listOf(accountToAdd), - onNetwork = networkId - ) - // Save updated profile - profileRepository.saveProfile(updatedProfile) - // Return new account - newAccount - } - } -} diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/accessfactorsources/AccessFactorSourceNavGraph.kt b/app/src/main/java/com/babylon/wallet/android/presentation/accessfactorsources/AccessFactorSourceNavGraph.kt new file mode 100644 index 0000000000..6089153028 --- /dev/null +++ b/app/src/main/java/com/babylon/wallet/android/presentation/accessfactorsources/AccessFactorSourceNavGraph.kt @@ -0,0 +1,25 @@ +package com.babylon.wallet.android.presentation.accessfactorsources + +import androidx.compose.ui.window.DialogProperties +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.dialog + +fun NavController.accessFactorSources() { + navigate("access_factor_source_bottom_sheet") +} + +fun NavGraphBuilder.accessFactorSources( + onDismiss: () -> Unit +) { + dialog( + route = "access_factor_source_bottom_sheet", + dialogProperties = DialogProperties(usePlatformDefaultWidth = false) + ) { + AccessFactorSourcesDialog( + viewModel = hiltViewModel(), + onDismiss = onDismiss + ) + } +} diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/accessfactorsources/AccessFactorSourceProxyContract.kt b/app/src/main/java/com/babylon/wallet/android/presentation/accessfactorsources/AccessFactorSourceProxyContract.kt new file mode 100644 index 0000000000..e5a5ca8309 --- /dev/null +++ b/app/src/main/java/com/babylon/wallet/android/presentation/accessfactorsources/AccessFactorSourceProxyContract.kt @@ -0,0 +1,58 @@ +package com.babylon.wallet.android.presentation.accessfactorsources + +import rdx.works.profile.data.model.factorsources.FactorSource +import rdx.works.profile.data.model.pernetwork.DerivationPath +import rdx.works.profile.derivation.model.NetworkId + +// interface for clients that need access to factor sources +interface AccessFactorSourcesProxy { + + suspend fun getPublicKeyAndDerivationPathForFactorSource( + accessFactorSourcesInput: AccessFactorSourcesInput.ToDerivePublicKey + ): Result +} + +// interface for the AccessFactorSourceViewModel that works as a mediator between the clients +// and the AccessFactorSourcesProvider +interface AccessFactorSourcesUiProxy { + + fun getInput(): AccessFactorSourcesInput + + suspend fun setOutput(output: AccessFactorSourcesOutput) +} + +// ----- Models for input/output ----- // + +sealed interface AccessFactorSourcesInput { + + data class ToDerivePublicKey( + val forNetworkId: NetworkId, + val factorSource: FactorSource.CreatingEntity? = null + ) : AccessFactorSourcesInput + + // just for demonstration - will change in next PR + data class ToSign( + val someData: List + ) : AccessFactorSourcesInput + + data object Init : AccessFactorSourcesInput +} + +sealed interface AccessFactorSourcesOutput { + + data class PublicKeyAndDerivationPath( + val compressedPublicKey: ByteArray, + val derivationPath: DerivationPath + ) : AccessFactorSourcesOutput + + // just for demonstration - will change in next PR + data class Signers( + val someData: List + ) : AccessFactorSourcesOutput + + data class Failure( + val error: Throwable + ) : AccessFactorSourcesOutput + + data object Init : AccessFactorSourcesOutput +} diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/accessfactorsources/AccessFactorSourcesDialog.kt b/app/src/main/java/com/babylon/wallet/android/presentation/accessfactorsources/AccessFactorSourcesDialog.kt new file mode 100644 index 0000000000..619b962d67 --- /dev/null +++ b/app/src/main/java/com/babylon/wallet/android/presentation/accessfactorsources/AccessFactorSourcesDialog.kt @@ -0,0 +1,190 @@ +package com.babylon.wallet.android.presentation.accessfactorsources + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.babylon.wallet.android.R +import com.babylon.wallet.android.designsystem.theme.RadixTheme +import com.babylon.wallet.android.designsystem.theme.RadixWalletTheme +import com.babylon.wallet.android.presentation.accessfactorsources.AccessFactorSourcesViewModel.AccessFactorSourcesUiState +import com.babylon.wallet.android.presentation.ui.composables.BottomSheetDialogWrapper +import com.babylon.wallet.android.utils.biometricAuthenticate +import com.babylon.wallet.android.utils.formattedSpans +import rdx.works.profile.domain.TestData.ledgerFactorSource + +@Composable +fun AccessFactorSourcesDialog( + modifier: Modifier = Modifier, + viewModel: AccessFactorSourcesViewModel, + onDismiss: () -> Unit +) { + val state by viewModel.state.collectAsState() + val context = LocalContext.current + + LaunchedEffect(Unit) { + viewModel.oneOffEvent.collect { event -> + when (event) { + AccessFactorSourcesViewModel.Event.RequestBiometricPrompt -> { + context.biometricAuthenticate { isAuthenticated -> + viewModel.biometricAuthenticationCompleted(isAuthenticated) + if (isAuthenticated.not()) { + onDismiss() + } + } + } + } + } + } + + AccessFactorSourcesBottomSheetContent( + modifier = modifier, + isAccessingFactorSourceInProgress = state.isAccessingFactorSourceInProgress, + isAccessingFactorSourceCompleted = state.isAccessingFactorSourceCompleted, + showContentForFactorSource = state.showContentFor, + onDismiss = onDismiss + ) +} + +@Composable +private fun AccessFactorSourcesBottomSheetContent( + modifier: Modifier = Modifier, + isAccessingFactorSourceInProgress: Boolean, + isAccessingFactorSourceCompleted: Boolean, + showContentForFactorSource: AccessFactorSourcesUiState.ShowContentFor, + onDismiss: () -> Unit +) { + if (isAccessingFactorSourceCompleted) { + onDismiss() + } + + BottomSheetDialogWrapper( + modifier = modifier, + onDismiss = { + onDismiss() + } + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = RadixTheme.dimensions.paddingXLarge) + .background(RadixTheme.colors.defaultBackground), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(Modifier.height(40.dp)) + Icon( + modifier = Modifier.size(80.dp), + painter = painterResource( + id = com.babylon.wallet.android.designsystem.R.drawable.ic_security_key + ), + contentDescription = null, + tint = RadixTheme.colors.gray3 + ) + Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingDefault)) + Text( + style = RadixTheme.typography.title, + text = stringResource(id = R.string.derivePublicKeys_titleCreateAccount) + ) + Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingSemiLarge)) + when (showContentForFactorSource) { + AccessFactorSourcesUiState.ShowContentFor.Device -> { + Text( + style = RadixTheme.typography.body1Regular, + text = stringResource(id = R.string.derivePublicKeys_subtitleDevice) + ) + } + + is AccessFactorSourcesUiState.ShowContentFor.Ledger -> { + Text( + style = RadixTheme.typography.body1Regular, + text = stringResource(id = R.string.derivePublicKeys_subtitleLedger) + .formattedSpans(SpanStyle(fontWeight = FontWeight.Bold)) + ) + Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingXXLarge)) + RoundLedgerItem(ledgerName = ledgerFactorSource.hint.name) + } + } + Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingXLarge)) + if (isAccessingFactorSourceInProgress) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = RadixTheme.colors.gray1 + ) + } + Spacer(Modifier.height(120.dp)) + } + } +} + +@Composable +private fun RoundLedgerItem(ledgerName: String) { + Row( + modifier = Modifier + .background(RadixTheme.colors.gray5, RadixTheme.shapes.circle) + .padding(RadixTheme.dimensions.paddingDefault), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(RadixTheme.dimensions.paddingSmall) + ) { + Icon( + painter = painterResource( + id = com.babylon.wallet.android.designsystem.R.drawable.ic_security_key + ), + contentDescription = null, + tint = RadixTheme.colors.gray3 + ) + Text( + text = ledgerName, + style = RadixTheme.typography.secondaryHeader, + color = RadixTheme.colors.gray1 + ) + } +} + +@Preview(showBackground = false) +@Composable +fun AccessFactorSourcesDialogDevicePreview() { + RadixWalletTheme { + AccessFactorSourcesBottomSheetContent( + isAccessingFactorSourceInProgress = false, + isAccessingFactorSourceCompleted = false, + showContentForFactorSource = AccessFactorSourcesUiState.ShowContentFor.Device, + onDismiss = {} + ) + } +} + +@Preview(showBackground = false) +@Composable +fun AccessFactorSourcesDialogLedgerPreview() { + RadixWalletTheme { + AccessFactorSourcesBottomSheetContent( + isAccessingFactorSourceInProgress = false, + isAccessingFactorSourceCompleted = false, + showContentForFactorSource = AccessFactorSourcesUiState.ShowContentFor.Ledger( + selectedLedgerDevice = ledgerFactorSource + ), + onDismiss = {} + ) + } +} diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/accessfactorsources/AccessFactorSourcesProxyImpl.kt b/app/src/main/java/com/babylon/wallet/android/presentation/accessfactorsources/AccessFactorSourcesProxyImpl.kt new file mode 100644 index 0000000000..25211da6ea --- /dev/null +++ b/app/src/main/java/com/babylon/wallet/android/presentation/accessfactorsources/AccessFactorSourcesProxyImpl.kt @@ -0,0 +1,45 @@ +package com.babylon.wallet.android.presentation.accessfactorsources + +import com.babylon.wallet.android.utils.AppEvent +import com.babylon.wallet.android.utils.AppEventBus +import dagger.hilt.android.scopes.ActivityRetainedScoped +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first +import javax.inject.Inject + +@ActivityRetainedScoped +class AccessFactorSourcesProxyImpl @Inject constructor( + private val appEventBus: AppEventBus +) : AccessFactorSourcesProxy, AccessFactorSourcesUiProxy { + + private var input: AccessFactorSourcesInput = AccessFactorSourcesInput.Init + private val _output = MutableSharedFlow() + + override suspend fun getPublicKeyAndDerivationPathForFactorSource( + accessFactorSourcesInput: AccessFactorSourcesInput.ToDerivePublicKey + ): Result { + input = accessFactorSourcesInput + appEventBus.sendEvent(event = AppEvent.AccessFactorSources.DeriveAccountPublicKey) + val result = _output.first() + + return if (result is AccessFactorSourcesOutput.Failure) { + Result.failure(result.error) + } else { + Result.success(result as AccessFactorSourcesOutput.PublicKeyAndDerivationPath) + } + } + + override fun getInput(): AccessFactorSourcesInput { + return input + } + + override suspend fun setOutput(output: AccessFactorSourcesOutput) { + _output.emit(output) + reset() // access to factor sources is done + } + + private suspend fun reset() { + input = AccessFactorSourcesInput.Init + _output.emit(AccessFactorSourcesOutput.Init) + } +} diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/accessfactorsources/AccessFactorSourcesViewModel.kt b/app/src/main/java/com/babylon/wallet/android/presentation/accessfactorsources/AccessFactorSourcesViewModel.kt new file mode 100644 index 0000000000..040a1339d2 --- /dev/null +++ b/app/src/main/java/com/babylon/wallet/android/presentation/accessfactorsources/AccessFactorSourcesViewModel.kt @@ -0,0 +1,161 @@ +package com.babylon.wallet.android.presentation.accessfactorsources + +import androidx.lifecycle.viewModelScope +import com.babylon.wallet.android.data.dapp.LedgerMessenger +import com.babylon.wallet.android.data.dapp.model.Curve +import com.babylon.wallet.android.data.dapp.model.LedgerInteractionRequest +import com.babylon.wallet.android.presentation.accessfactorsources.AccessFactorSourcesOutput.PublicKeyAndDerivationPath +import com.babylon.wallet.android.presentation.accessfactorsources.AccessFactorSourcesViewModel.AccessFactorSourcesUiState.ShowContentFor +import com.babylon.wallet.android.presentation.common.OneOffEvent +import com.babylon.wallet.android.presentation.common.OneOffEventHandler +import com.babylon.wallet.android.presentation.common.OneOffEventHandlerImpl +import com.babylon.wallet.android.presentation.common.StateViewModel +import com.babylon.wallet.android.presentation.common.UiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import rdx.works.core.UUIDGenerator +import rdx.works.core.decodeHex +import rdx.works.profile.data.model.extensions.mainBabylonFactorSource +import rdx.works.profile.data.model.factorsources.DeviceFactorSource +import rdx.works.profile.data.model.factorsources.LedgerHardwareWalletFactorSource +import rdx.works.profile.data.repository.AccessFactorSourcesProvider +import rdx.works.profile.derivation.model.NetworkId +import rdx.works.profile.domain.EnsureBabylonFactorSourceExistUseCase +import java.util.concurrent.CancellationException +import javax.inject.Inject + +@HiltViewModel +class AccessFactorSourcesViewModel @Inject constructor( + private val accessFactorSourcesProvider: AccessFactorSourcesProvider, + private val accessFactorSourcesUiProxy: AccessFactorSourcesUiProxy, + private val ensureBabylonFactorSourceExistUseCase: EnsureBabylonFactorSourceExistUseCase, + private val ledgerMessenger: LedgerMessenger +) : StateViewModel(), + OneOffEventHandler by OneOffEventHandlerImpl() { + + override fun initialState(): AccessFactorSourcesUiState = AccessFactorSourcesUiState() + + init { + viewModelScope.launch { + sendEvent(Event.RequestBiometricPrompt) + } + } + + fun biometricAuthenticationCompleted(isAuthenticated: Boolean) { + viewModelScope.launch { + if (isAuthenticated) { + _state.update { uiState -> + uiState.copy(isAccessingFactorSourceInProgress = true) + } + when (val input = accessFactorSourcesUiProxy.getInput()) { + is AccessFactorSourcesInput.ToDerivePublicKey -> { + derivePublicKey(input) + } + + else -> { /* do nothing */ } + } + + _state.update { uiState -> + uiState.copy( + isAccessingFactorSourceInProgress = false, + isAccessingFactorSourceCompleted = true + ) + } + } else { + biometricAuthenticationDismissed() + } + } + } + + private suspend fun derivePublicKey(input: AccessFactorSourcesInput.ToDerivePublicKey) { + val profile = ensureBabylonFactorSourceExistUseCase() + + if (input.factorSource == null) { // device factor source + val deviceFactorSource = profile.mainBabylonFactorSource() ?: error("Babylon factor source is not present") + derivePublicKeyFromDeviceFactorSource( + forNetworkId = input.forNetworkId, + deviceFactorSource = deviceFactorSource + ) + } else { // ledger factor source + val ledgerFactorSource = input.factorSource as LedgerHardwareWalletFactorSource + _state.update { uiState -> + uiState.copy( + showContentFor = ShowContentFor.Ledger(selectedLedgerDevice = ledgerFactorSource) + ) + } + derivePublicKeyFromLedgerFactorSource( + forNetworkId = input.forNetworkId, + ledgerFactorSource = ledgerFactorSource + ) + } + } + + private suspend fun derivePublicKeyFromDeviceFactorSource( + forNetworkId: NetworkId, + deviceFactorSource: DeviceFactorSource + ) { + val derivationPath = accessFactorSourcesProvider.getNextDerivationPathForFactorSource( + forNetworkId = forNetworkId, + factorSource = deviceFactorSource + ) + val compressedPublicKey = accessFactorSourcesProvider.derivePublicKeyForDeviceFactorSource( + deviceFactorSource = deviceFactorSource, + derivationPath = derivationPath + ) + val output = PublicKeyAndDerivationPath( + compressedPublicKey = compressedPublicKey, + derivationPath = derivationPath + ) + + accessFactorSourcesUiProxy.setOutput(output) + } + + private suspend fun derivePublicKeyFromLedgerFactorSource( + forNetworkId: NetworkId, + ledgerFactorSource: LedgerHardwareWalletFactorSource + ) { + val derivationPath = accessFactorSourcesProvider.getNextDerivationPathForFactorSource( + forNetworkId = forNetworkId, + factorSource = ledgerFactorSource + ) + ledgerMessenger.sendDerivePublicKeyRequest( + interactionId = UUIDGenerator.uuid().toString(), + keyParameters = listOf(LedgerInteractionRequest.KeyParameters(Curve.Curve25519, derivationPath.path)), + ledgerDevice = LedgerInteractionRequest.LedgerDevice.from(ledgerFactorSource = ledgerFactorSource) + ).onSuccess { derivePublicKeyResponse -> + val publicKey = derivePublicKeyResponse.publicKeysHex.first().publicKeyHex.decodeHex() + val publicKeyAndDerivationPath = PublicKeyAndDerivationPath( + compressedPublicKey = publicKey, + derivationPath = derivationPath + ) + accessFactorSourcesUiProxy.setOutput(publicKeyAndDerivationPath) + }.onFailure { error -> + accessFactorSourcesUiProxy.setOutput(AccessFactorSourcesOutput.Failure(error)) + } + } + + private fun biometricAuthenticationDismissed() { + viewModelScope.launch { + accessFactorSourcesUiProxy.setOutput( + output = AccessFactorSourcesOutput.Failure(CancellationException("Authentication dismissed")) + ) + } + } + + data class AccessFactorSourcesUiState( + val isAccessingFactorSourceInProgress: Boolean = false, + val isAccessingFactorSourceCompleted: Boolean = false, + val showContentFor: ShowContentFor = ShowContentFor.Device + ) : UiState { + + sealed interface ShowContentFor { + data object Device : ShowContentFor + data class Ledger(val selectedLedgerDevice: LedgerHardwareWalletFactorSource) : ShowContentFor + } + } + + sealed interface Event : OneOffEvent { + data object RequestBiometricPrompt : Event + } +} diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/CreateAccountNav.kt b/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/CreateAccountNav.kt index d9aeda7035..eec1c63d4f 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/CreateAccountNav.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/CreateAccountNav.kt @@ -2,7 +2,6 @@ package com.babylon.wallet.android.presentation.account.createaccount import androidx.annotation.VisibleForTesting import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.ExperimentalAnimationApi import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController @@ -70,11 +69,10 @@ fun NavController.createAccountScreen( ) } -@OptIn(ExperimentalAnimationApi::class) fun NavGraphBuilder.createAccountScreen( onBackClick: () -> Unit, onContinueClick: (accountId: String, requestSource: CreateAccountRequestSource?) -> Unit, - onAddLedgerDevice: (Int) -> Unit + onAddLedgerDevice: () -> Unit ) { markAsHighPriority(route = ROUTE_CREATE_ACCOUNT) composable( diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/CreateAccountScreen.kt b/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/CreateAccountScreen.kt index 10038c8ed5..960566cc7a 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/CreateAccountScreen.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/CreateAccountScreen.kt @@ -15,13 +15,14 @@ import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -34,9 +35,11 @@ import com.babylon.wallet.android.designsystem.theme.RadixTheme import com.babylon.wallet.android.designsystem.theme.RadixWalletTheme import com.babylon.wallet.android.presentation.account.createaccount.confirmation.CreateAccountRequestSource import com.babylon.wallet.android.presentation.common.FullscreenCircularProgressContent +import com.babylon.wallet.android.presentation.common.UiMessage import com.babylon.wallet.android.presentation.ui.composables.BackIconType import com.babylon.wallet.android.presentation.ui.composables.RadixCenteredTopAppBar -import com.babylon.wallet.android.utils.biometricAuthenticate +import com.babylon.wallet.android.presentation.ui.composables.RadixSnackbarHost +import com.babylon.wallet.android.presentation.ui.composables.SnackbarUIMessage @Composable fun CreateAccountScreen( @@ -47,7 +50,7 @@ fun CreateAccountScreen( accountId: String, requestSource: CreateAccountRequestSource?, ) -> Unit = { _: String, _: CreateAccountRequestSource? -> }, - onAddLedgerDevice: (Int) -> Unit + onAddLedgerDevice: () -> Unit ) { val state by viewModel.state.collectAsStateWithLifecycle() BackHandler(onBack = viewModel::onBackClick) @@ -68,8 +71,10 @@ fun CreateAccountScreen( onBackClick = viewModel::onBackClick, modifier = modifier, firstTime = state.firstTime, - useLedgerSelected = state.useLedgerSelected, - onUseLedgerSelectionChanged = viewModel::onUseLedgerSelectionChanged + isWithLedger = state.isWithLedger, + onUseLedgerSelectionChanged = viewModel::onUseLedgerSelectionChanged, + uiMessage = state.uiMessage, + onUiMessageShown = viewModel::onUiMessageShown ) } LaunchedEffect(Unit) { @@ -80,7 +85,7 @@ fun CreateAccountScreen( event.requestSource ) - is CreateAccountEvent.AddLedgerDevice -> onAddLedgerDevice(event.networkId) + is CreateAccountEvent.AddLedgerDevice -> onAddLedgerDevice() is CreateAccountEvent.Dismiss -> onBackClick() } } @@ -90,7 +95,7 @@ fun CreateAccountScreen( @Composable fun CreateAccountContent( onAccountNameChange: (String) -> Unit, - onAccountCreateClick: () -> Unit, + onAccountCreateClick: (Boolean) -> Unit, accountName: String, isAccountNameLengthMoreThanTheMaximum: Boolean, buttonEnabled: Boolean, @@ -98,9 +103,18 @@ fun CreateAccountContent( cancelable: Boolean, modifier: Modifier, firstTime: Boolean, - useLedgerSelected: Boolean, - onUseLedgerSelectionChanged: (Boolean) -> Unit + isWithLedger: Boolean, + onUseLedgerSelectionChanged: (Boolean) -> Unit, + uiMessage: UiMessage? = null, + onUiMessageShown: () -> Unit = {} ) { + val snackBarHostState = remember { SnackbarHostState() } + SnackbarUIMessage( + message = uiMessage, + snackbarHostState = snackBarHostState, + onMessageShown = onUiMessageShown + ) + Scaffold( modifier = modifier.imePadding(), topBar = { @@ -112,18 +126,10 @@ fun CreateAccountContent( ) }, bottomBar = { - val context = LocalContext.current RadixPrimaryButton( text = stringResource(id = R.string.createAccount_nameNewAccount_continue), onClick = { - when { - useLedgerSelected -> onAccountCreateClick() - else -> context.biometricAuthenticate { authenticatedSuccessfully -> - if (authenticatedSuccessfully) { - onAccountCreateClick() - } - } - } + onAccountCreateClick(isWithLedger) }, modifier = Modifier .fillMaxWidth() @@ -136,7 +142,13 @@ fun CreateAccountContent( throttleClicks = true ) }, - containerColor = RadixTheme.colors.defaultBackground + containerColor = RadixTheme.colors.defaultBackground, + snackbarHost = { + RadixSnackbarHost( + hostState = snackBarHostState, + modifier = Modifier.padding(RadixTheme.dimensions.paddingDefault) + ) + } ) { padding -> Column( modifier = Modifier @@ -185,7 +197,7 @@ fun CreateAccountContent( ) Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingLarge)) CreateWithLedgerSwitch( - useLedgerSelected = useLedgerSelected, + isChecked = isWithLedger, onUseLedgerSelectionChanged = onUseLedgerSelectionChanged, modifier = Modifier.fillMaxWidth() ) @@ -195,7 +207,7 @@ fun CreateAccountContent( @Composable private fun CreateWithLedgerSwitch( - useLedgerSelected: Boolean, + isChecked: Boolean, onUseLedgerSelectionChanged: (Boolean) -> Unit, modifier: Modifier = Modifier ) { @@ -216,7 +228,7 @@ private fun CreateWithLedgerSwitch( color = RadixTheme.colors.gray2 ) } - RadixSwitch(checked = useLedgerSelected, onCheckedChange = onUseLedgerSelectionChanged) + RadixSwitch(checked = isChecked, onCheckedChange = onUseLedgerSelectionChanged) } } @@ -235,7 +247,7 @@ fun CreateAccountContentPreview() { cancelable = true, modifier = Modifier, firstTime = false, - useLedgerSelected = false, + isWithLedger = false, onUseLedgerSelectionChanged = {} ) } diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/CreateAccountViewModel.kt b/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/CreateAccountViewModel.kt index 836dd5868c..8f056a8d34 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/CreateAccountViewModel.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/CreateAccountViewModel.kt @@ -3,13 +3,15 @@ package com.babylon.wallet.android.presentation.account.createaccount import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.babylon.wallet.android.data.transaction.InteractionState -import com.babylon.wallet.android.domain.usecases.CreateAccountWithBabylonDeviceFactorSourceUseCase -import com.babylon.wallet.android.domain.usecases.CreateAccountWithLedgerFactorSourceUseCase +import com.babylon.wallet.android.domain.usecases.CreateAccountUseCase +import com.babylon.wallet.android.presentation.accessfactorsources.AccessFactorSourcesInput +import com.babylon.wallet.android.presentation.accessfactorsources.AccessFactorSourcesProxy import com.babylon.wallet.android.presentation.account.createaccount.confirmation.CreateAccountRequestSource import com.babylon.wallet.android.presentation.common.OneOffEvent import com.babylon.wallet.android.presentation.common.OneOffEventHandler import com.babylon.wallet.android.presentation.common.OneOffEventHandlerImpl import com.babylon.wallet.android.presentation.common.StateViewModel +import com.babylon.wallet.android.presentation.common.UiMessage import com.babylon.wallet.android.presentation.common.UiState import com.babylon.wallet.android.utils.AppEvent import com.babylon.wallet.android.utils.AppEventBus @@ -17,16 +19,20 @@ import com.babylon.wallet.android.utils.Constants.ACCOUNT_NAME_MAX_LENGTH import com.babylon.wallet.android.utils.decodeUtf8 import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import rdx.works.core.preferences.PreferencesManager +import rdx.works.profile.data.model.apppreferences.Radix +import rdx.works.profile.data.model.currentNetwork +import rdx.works.profile.data.model.extensions.mainBabylonFactorSource import rdx.works.profile.data.model.factorsources.FactorSource -import rdx.works.profile.data.model.pernetwork.DerivationPath import rdx.works.profile.data.model.pernetwork.Network import rdx.works.profile.derivation.model.NetworkId import rdx.works.profile.domain.DeleteProfileUseCase import rdx.works.profile.domain.GenerateProfileUseCase import rdx.works.profile.domain.GetProfileStateUseCase +import rdx.works.profile.domain.GetProfileUseCase import rdx.works.profile.domain.account.SwitchNetworkUseCase import rdx.works.profile.domain.backup.BackupType import rdx.works.profile.domain.backup.DiscardTemporaryRestoredFileForBackupUseCase @@ -37,16 +43,17 @@ import javax.inject.Inject @Suppress("LongParameterList") class CreateAccountViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, + private val createAccountUseCase: CreateAccountUseCase, + private val accessFactorSourcesProxy: AccessFactorSourcesProxy, + private val getProfileUseCase: GetProfileUseCase, private val getProfileStateUseCase: GetProfileStateUseCase, private val generateProfileUseCase: GenerateProfileUseCase, private val deleteProfileUseCase: DeleteProfileUseCase, - private val createAccountWithBabylonDeviceFactorSourceUseCase: CreateAccountWithBabylonDeviceFactorSourceUseCase, - private val createAccountWithLedgerFactorSourceUseCase: CreateAccountWithLedgerFactorSourceUseCase, private val discardTemporaryRestoredFileForBackupUseCase: DiscardTemporaryRestoredFileForBackupUseCase, - private val switchNetworkUseCase: SwitchNetworkUseCase, private val preferencesManager: PreferencesManager, + private val switchNetworkUseCase: SwitchNetworkUseCase, private val appEventBus: AppEventBus -) : StateViewModel(), +) : StateViewModel(), OneOffEventHandler by OneOffEventHandlerImpl() { private val args = CreateAccountNavArgs(savedStateHandle) @@ -54,84 +61,18 @@ class CreateAccountViewModel @Inject constructor( val buttonEnabled = savedStateHandle.getStateFlow(CREATE_ACCOUNT_BUTTON_ENABLED, false) val isAccountNameLengthMoreThanTheMax = savedStateHandle.getStateFlow(IS_ACCOUNT_NAME_LENGTH_MORE_THAN_THE_MAX, false) - init { - viewModelScope.launch { - appEventBus.events - .filterIsInstance() - .collect { - createLedgerAccount( - factorSourceID = it.factorSourceID, - derivationPath = it.derivationPath, - derivedPublicKeyHex = it.derivedPublicKeyHex - ) - } - } - viewModelScope.launch { - createAccountWithBabylonDeviceFactorSourceUseCase.interactionState.collect { interactionState -> - _state.update { it.copy(interactionState = interactionState) } - } - } - } - - private suspend fun createLedgerAccount( - factorSourceID: FactorSource.FactorSourceID.FromHash, - derivationPath: DerivationPath, - derivedPublicKeyHex: String - ) { - viewModelScope.launch { - handleAccountCreate { accountName, networkId -> - createAccountWithLedgerFactorSourceUseCase( - displayName = accountName, - networkID = networkId, - derivedPublicKeyHex = derivedPublicKeyHex, - ledgerFactorSourceID = factorSourceID, - derivationPath = derivationPath - ) - } - } - } - - private suspend fun handleAccountCreate( - accountProvider: suspend (String, NetworkId?) -> Network.Account - ) { - _state.update { it.copy(loading = true) } - val accountName = accountName.value.trim() - val networkId = switchNetworkIfNeeded() - val account = accountProvider(accountName, networkId) - val accountId = account.address - - _state.update { - it.copy( - loading = true, - accountId = accountId, - accountName = accountName - ) - } - - if (args.requestSource == CreateAccountRequestSource.FirstTime) { - preferencesManager.setRadixBannerVisibility(isVisible = true) - } - - sendEvent( - CreateAccountEvent.Complete( - accountId = accountId, - args.requestSource - ) - ) - } - - override fun initialState(): CreateAccountState = CreateAccountState( + override fun initialState(): CreateAccountUiState = CreateAccountUiState( firstTime = args.requestSource?.isFirstTime() == true, isCancelable = true, ) fun onAccountNameChange(accountName: String) { - savedStateHandle[ACCOUNT_NAME] = accountName // .take(ACCOUNT_NAME_MAX_LENGTH) + savedStateHandle[ACCOUNT_NAME] = accountName savedStateHandle[IS_ACCOUNT_NAME_LENGTH_MORE_THAN_THE_MAX] = accountName.count() > ACCOUNT_NAME_MAX_LENGTH savedStateHandle[CREATE_ACCOUNT_BUTTON_ENABLED] = accountName.trim().isNotEmpty() && accountName.count() <= ACCOUNT_NAME_MAX_LENGTH } - fun onAccountCreateClick() { + fun onAccountCreateClick(isWithLedger: Boolean) { viewModelScope.launch { if (!getProfileStateUseCase.isInitialized()) { generateProfileUseCase() @@ -141,13 +82,51 @@ class CreateAccountViewModel @Inject constructor( discardTemporaryRestoredFileForBackupUseCase(BackupType.Cloud) } - if (state.value.useLedgerSelected) { - sendEvent(CreateAccountEvent.AddLedgerDevice(args.networkId)) + val onNetworkId = if (args.networkId != -1) { + NetworkId.from(args.networkId) } else { - handleAccountCreate { name, networkId -> - createAccountWithBabylonDeviceFactorSourceUseCase( - displayName = name, - networkID = networkId + val profile = getProfileUseCase.invoke().first() + profile.currentNetwork?.knownNetworkId ?: Radix.Gateway.default.network.networkId() + } + + // at the moment you can create a account either with device factor source or ledger factor source + var selectedFactorSource: FactorSource.CreatingEntity? = null + + if (isWithLedger) { // get the selected ledger device + sendEvent(CreateAccountEvent.AddLedgerDevice) + selectedFactorSource = appEventBus.events + .filterIsInstance() + .first().ledgerFactorSource + } + + // if main babylon factor source is not present, it will be created during the public key derivation + accessFactorSourcesProxy.getPublicKeyAndDerivationPathForFactorSource( + accessFactorSourcesInput = AccessFactorSourcesInput.ToDerivePublicKey( + forNetworkId = onNetworkId, + factorSource = if (isWithLedger && selectedFactorSource != null) { + selectedFactorSource + } else { + null + } + ) + ).onSuccess { + handleAccountCreate { nameOfAccount, networkId -> + // when we reach this point main babylon factor source has already created + if (selectedFactorSource == null && isWithLedger.not()) { // so take it if it is a creation with device + val profile = getProfileUseCase.invoke().first() // get again the profile with its updated state + selectedFactorSource = profile.mainBabylonFactorSource() ?: error("Babylon factor source is not present") + } + createAccountUseCase( + displayName = nameOfAccount, + factorSource = selectedFactorSource ?: error("factor source must not be null"), + publicKeyAndDerivationPath = it, + onNetworkId = networkId + ) + } + }.onFailure { error -> + _state.update { state -> + state.copy( + uiMessage = UiMessage.ErrorMessage(error) ) } } @@ -167,6 +146,43 @@ class CreateAccountViewModel @Inject constructor( _state.update { it.copy(interactionState = null) } } + fun onUseLedgerSelectionChanged(selected: Boolean) { + _state.update { it.copy(isWithLedger = selected) } + } + + fun onUiMessageShown() { + _state.update { it.copy(uiMessage = null) } + } + + private suspend fun handleAccountCreate( + accountProvider: suspend (String, NetworkId?) -> Network.Account + ) { + _state.update { it.copy(loading = true) } + val accountName = accountName.value.trim() + val networkId = switchNetworkIfNeeded() + val account = accountProvider(accountName, networkId) + val accountId = account.address + + _state.update { + it.copy( + loading = true, + accountId = accountId, + accountName = accountName + ) + } + + if (args.requestSource == CreateAccountRequestSource.FirstTime) { + preferencesManager.setRadixBannerVisibility(isVisible = true) + } + + sendEvent( + CreateAccountEvent.Complete( + accountId = accountId, + requestSource = args.requestSource + ) + ) + } + @Suppress("UnsafeCallOnNullableType") private suspend fun switchNetworkIfNeeded(): NetworkId? { val switchNetwork = args.switchNetwork ?: false @@ -179,18 +195,15 @@ class CreateAccountViewModel @Inject constructor( return networkId } - fun onUseLedgerSelectionChanged(selected: Boolean) { - _state.update { it.copy(useLedgerSelected = selected) } - } - - data class CreateAccountState( + data class CreateAccountUiState( val loading: Boolean = false, val accountId: String = "", val accountName: String = "", val firstTime: Boolean = false, - val useLedgerSelected: Boolean = false, + val isWithLedger: Boolean = false, val isCancelable: Boolean = true, - val interactionState: InteractionState? = null + val interactionState: InteractionState? = null, + val uiMessage: UiMessage? = null ) : UiState companion object { @@ -206,6 +219,6 @@ internal sealed interface CreateAccountEvent : OneOffEvent { val requestSource: CreateAccountRequestSource?, ) : CreateAccountEvent - data class AddLedgerDevice(val networkId: Int) : CreateAccountEvent + data object AddLedgerDevice : CreateAccountEvent data object Dismiss : CreateAccountEvent } diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/withledger/ChooseLedgerNav.kt b/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/withledger/ChooseLedgerNav.kt index b8445b79b7..8b7f5b2567 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/withledger/ChooseLedgerNav.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/withledger/ChooseLedgerNav.kt @@ -10,34 +10,26 @@ import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.navArgument import com.babylon.wallet.android.presentation.navigation.markAsHighPriority -import com.babylon.wallet.android.utils.Constants import rdx.works.profile.data.model.factorsources.FactorSource -@VisibleForTesting -const val ARG_NETWORK_ID = "arg_network_id" - @VisibleForTesting const val ARG_SELECTION_PURPOSE = "arg_selection_purpose" -private const val ROUTE_CHOOSE_LEDGER = "route_choose_ledger?$ARG_NETWORK_ID" + - "={$ARG_NETWORK_ID}&$ARG_SELECTION_PURPOSE={$ARG_SELECTION_PURPOSE}" +private const val ROUTE_CHOOSE_LEDGER = "route_choose_ledger?$ARG_SELECTION_PURPOSE={$ARG_SELECTION_PURPOSE}" internal class ChooserLedgerArgs( - val networkId: Int, val ledgerSelectionPurpose: LedgerSelectionPurpose ) { constructor(savedStateHandle: SavedStateHandle) : this( - checkNotNull(savedStateHandle.get(ARG_NETWORK_ID)), checkNotNull(savedStateHandle.get(ARG_SELECTION_PURPOSE)) ) } fun NavController.chooseLedger( - networkId: Int = Constants.USE_CURRENT_NETWORK, - ledgerSelectionPurpose: LedgerSelectionPurpose = LedgerSelectionPurpose.CreateAccount + ledgerSelectionPurpose: LedgerSelectionPurpose = LedgerSelectionPurpose.DerivePublicKey ) { navigate( - route = "route_choose_ledger?$ARG_NETWORK_ID=$networkId&$ARG_SELECTION_PURPOSE=$ledgerSelectionPurpose" + route = "route_choose_ledger?$ARG_SELECTION_PURPOSE=$ledgerSelectionPurpose" ) } @@ -56,13 +48,9 @@ fun NavGraphBuilder.chooseLedger( slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Down) }, arguments = listOf( - navArgument(ARG_NETWORK_ID) { - type = NavType.IntType - defaultValue = Constants.USE_CURRENT_NETWORK - }, navArgument(ARG_SELECTION_PURPOSE) { type = NavType.EnumType(LedgerSelectionPurpose::class.java) - defaultValue = LedgerSelectionPurpose.CreateAccount + defaultValue = LedgerSelectionPurpose.DerivePublicKey } ) ) { @@ -78,5 +66,5 @@ fun NavGraphBuilder.chooseLedger( } enum class LedgerSelectionPurpose { - CreateAccount, RecoveryScanOlympia, RecoveryScanBabylon + DerivePublicKey, RecoveryScanOlympia, RecoveryScanBabylon } diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/withledger/ChooseLedgerScreen.kt b/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/withledger/ChooseLedgerScreen.kt index c5bd25df13..3d80121599 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/withledger/ChooseLedgerScreen.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/withledger/ChooseLedgerScreen.kt @@ -16,7 +16,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -40,7 +39,6 @@ import com.babylon.wallet.android.presentation.ui.composables.LinkConnectorScree import com.babylon.wallet.android.presentation.ui.composables.RadixCenteredTopAppBar import com.babylon.wallet.android.presentation.ui.composables.RadixSnackbarHost import com.babylon.wallet.android.presentation.ui.composables.SnackbarUIMessage -import com.babylon.wallet.android.utils.biometricAuthenticateSuspend import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.launch @@ -57,7 +55,6 @@ fun ChooseLedgerScreen( goBackToCreateAccount: () -> Unit, onStartRecovery: (FactorSource, Boolean) -> Unit ) { - val context = LocalContext.current val state by viewModel.state.collectAsStateWithLifecycle() val addLedgerDeviceState by addLedgerDeviceViewModel.state.collectAsStateWithLifecycle() val addLinkConnectorState by addLinkConnectorViewModel.state.collectAsStateWithLifecycle() @@ -66,7 +63,7 @@ fun ChooseLedgerScreen( LaunchedEffect(Unit) { viewModel.oneOffEvent.collect { event -> when (event) { - is ChooseLedgerEvent.DerivedPublicKeyForAccount -> goBackToCreateAccount() + is ChooseLedgerEvent.LedgerSelected -> goBackToCreateAccount() is ChooseLedgerEvent.RecoverAccounts -> onStartRecovery(event.factorSource, event.isOlympia) } } @@ -103,11 +100,7 @@ fun ChooseLedgerScreen( ledgerDevices = state.ledgerDevices, onLedgerDeviceSelected = viewModel::onLedgerDeviceSelected, onAddLedgerDeviceClick = viewModel::onAddLedgerDeviceClick, - onUseLedgerContinueClick = { - viewModel.onUseLedgerContinueClick(deviceBiometricAuthenticationProvider = { - context.biometricAuthenticateSuspend() - }) - }, + onUseLedgerContinueClick = viewModel::onUseLedgerContinueClick, isAddingNewLinkConnectorInProgress = addLinkConnectorState.isAddingNewLinkConnectorInProgress, uiMessage = state.uiMessage, onMessageShown = viewModel::onMessageShown diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/withledger/ChooseLedgerViewModel.kt b/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/withledger/ChooseLedgerViewModel.kt index f91fadffe5..d0b2be1725 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/withledger/ChooseLedgerViewModel.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/account/createaccount/withledger/ChooseLedgerViewModel.kt @@ -2,9 +2,6 @@ package com.babylon.wallet.android.presentation.account.createaccount.withledger import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.babylon.wallet.android.data.dapp.LedgerMessenger -import com.babylon.wallet.android.data.dapp.model.Curve -import com.babylon.wallet.android.data.dapp.model.LedgerInteractionRequest import com.babylon.wallet.android.domain.model.Selectable import com.babylon.wallet.android.presentation.common.OneOffEvent import com.babylon.wallet.android.presentation.common.OneOffEventHandler @@ -15,7 +12,6 @@ import com.babylon.wallet.android.presentation.common.UiState import com.babylon.wallet.android.presentation.settings.accountsecurity.ledgerhardwarewallets.ShowLinkConnectorPromptState import com.babylon.wallet.android.utils.AppEvent import com.babylon.wallet.android.utils.AppEventBus -import com.babylon.wallet.android.utils.Constants import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -24,24 +20,16 @@ import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import rdx.works.core.UUIDGenerator -import rdx.works.profile.data.model.factorsources.DerivationPathScheme import rdx.works.profile.data.model.factorsources.FactorSource import rdx.works.profile.data.model.factorsources.LedgerHardwareWalletFactorSource -import rdx.works.profile.domain.EnsureBabylonFactorSourceExistUseCase import rdx.works.profile.domain.GetProfileUseCase -import rdx.works.profile.domain.gateway.GetCurrentGatewayUseCase import rdx.works.profile.domain.ledgerFactorSources -import rdx.works.profile.domain.nextDerivationPathForAccountOnNetwork import rdx.works.profile.domain.p2pLinks import javax.inject.Inject @HiltViewModel class ChooseLedgerViewModel @Inject constructor( private val getProfileUseCase: GetProfileUseCase, - private val getCurrentGatewayUseCase: GetCurrentGatewayUseCase, - private val ledgerMessenger: LedgerMessenger, - private val ensureBabylonFactorSourceExistUseCase: EnsureBabylonFactorSourceExistUseCase, private val appEventBus: AppEventBus, savedStateHandle: SavedStateHandle ) : StateViewModel(), @@ -104,7 +92,7 @@ class ChooseLedgerViewModel @Inject constructor( } @Suppress("LongMethod") - fun onUseLedgerContinueClick(deviceBiometricAuthenticationProvider: suspend () -> Boolean) { + fun onUseLedgerContinueClick() { state.value.ledgerDevices.firstOrNull { selectableLedgerDevice -> selectableLedgerDevice.selected }?.let { ledgerFactorSource -> @@ -122,42 +110,13 @@ class ChooseLedgerViewModel @Inject constructor( } when (args.ledgerSelectionPurpose) { - LedgerSelectionPurpose.CreateAccount -> { - // check again if link connector exists - if (ensureBabylonFactorSourceExistUseCase.babylonFactorSourceExist().not()) { - val authenticationResult = deviceBiometricAuthenticationProvider() - if (authenticationResult) { - ensureBabylonFactorSourceExistUseCase() - } else { - // don't move forward without babylon factor source - return@launch - } - } - val derivationPath = getProfileUseCase.nextDerivationPathForAccountOnNetwork( - DerivationPathScheme.CAP_26, - networkIdToCreateAccountOn(), - ledgerFactorSource.data.id - ) - ledgerMessenger.sendDerivePublicKeyRequest( - interactionId = UUIDGenerator.uuid().toString(), - keyParameters = listOf(LedgerInteractionRequest.KeyParameters(Curve.Curve25519, derivationPath.path)), - ledgerDevice = LedgerInteractionRequest.LedgerDevice.from(ledgerFactorSource.data) - ).onSuccess { response -> - appEventBus.sendEvent( - AppEvent.DerivedAccountPublicKeyWithLedger( - factorSourceID = ledgerFactorSource.data.id, - derivationPath = derivationPath, - derivedPublicKeyHex = response.publicKeysHex.first().publicKeyHex - ) + LedgerSelectionPurpose.DerivePublicKey -> { + appEventBus.sendEvent( + event = AppEvent.AccessFactorSources.SelectedLedgerDevice( + ledgerFactorSource = ledgerFactorSource.data ) - sendEvent(ChooseLedgerEvent.DerivedPublicKeyForAccount) - }.onFailure { error -> - _state.update { state -> - state.copy( - uiMessage = UiMessage.ErrorMessage(error) - ) - } - } + ) + sendEvent(ChooseLedgerEvent.LedgerSelected) } LedgerSelectionPurpose.RecoveryScanBabylon, @@ -217,14 +176,6 @@ class ChooseLedgerViewModel @Inject constructor( fun onMessageShown() { _state.update { it.copy(uiMessage = null) } } - - private suspend fun networkIdToCreateAccountOn(): Int { - return if (args.networkId == Constants.USE_CURRENT_NETWORK) { - getCurrentGatewayUseCase.invoke().network.id - } else { - args.networkId - } - } } data class ChooseLedgerUiState( @@ -246,6 +197,6 @@ data class ChooseLedgerUiState( } internal sealed interface ChooseLedgerEvent : OneOffEvent { - data object DerivedPublicKeyForAccount : ChooseLedgerEvent + data object LedgerSelected : ChooseLedgerEvent data class RecoverAccounts(val factorSource: FactorSource, val isOlympia: Boolean) : ChooseLedgerEvent } diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/account/settings/devsettings/DevSettingsNav.kt b/app/src/main/java/com/babylon/wallet/android/presentation/account/settings/devsettings/DevSettingsNav.kt index fb1783d7c9..99c7074e50 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/account/settings/devsettings/DevSettingsNav.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/account/settings/devsettings/DevSettingsNav.kt @@ -3,7 +3,6 @@ package com.babylon.wallet.android.presentation.account.settings.devsettings import androidx.annotation.VisibleForTesting import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExperimentalAnimationApi import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController @@ -27,7 +26,6 @@ fun NavController.devSettings(address: String) { navigate("dev_account_settings_route/$address") } -@OptIn(ExperimentalAnimationApi::class) fun NavGraphBuilder.devSettings( onBackClick: () -> Unit ) { diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/account/settings/specificassets/SpecificAssetsDepositsNav.kt b/app/src/main/java/com/babylon/wallet/android/presentation/account/settings/specificassets/SpecificAssetsDepositsNav.kt index 76b8825fb9..8858a9dc49 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/account/settings/specificassets/SpecificAssetsDepositsNav.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/account/settings/specificassets/SpecificAssetsDepositsNav.kt @@ -2,7 +2,6 @@ package com.babylon.wallet.android.presentation.account.settings.specificassets import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.runtime.remember import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController @@ -15,7 +14,6 @@ fun NavController.specificAssets() { navigate("account_specific_assets_route") } -@OptIn(ExperimentalAnimationApi::class) fun NavGraphBuilder.specificAssets(navController: NavController, onBackClick: () -> Unit) { composable( route = "account_specific_assets_route", diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/dapp/authorized/personaongoing/PersonaDataOngoingNav.kt b/app/src/main/java/com/babylon/wallet/android/presentation/dapp/authorized/personaongoing/PersonaDataOngoingNav.kt index fdd39b6b14..13e626e8ae 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/dapp/authorized/personaongoing/PersonaDataOngoingNav.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/dapp/authorized/personaongoing/PersonaDataOngoingNav.kt @@ -2,7 +2,6 @@ package com.babylon.wallet.android.presentation.dapp.authorized.personaongoing import android.net.Uri import androidx.annotation.VisibleForTesting -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.runtime.remember import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.SavedStateHandle @@ -41,7 +40,6 @@ fun NavController.personaDataOngoing(personaAddress: String, request: RequiredPe } @Suppress("LongParameterList") -@OptIn(ExperimentalAnimationApi::class) fun NavGraphBuilder.personaDataOngoing( onEdit: (PersonaDataOngoingEvent.OnEditPersona) -> Unit, onBackClick: () -> Unit, diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/main/MainViewModel.kt b/app/src/main/java/com/babylon/wallet/android/presentation/main/MainViewModel.kt index 29b1ba2122..8a232f2c2d 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/main/MainViewModel.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/main/MainViewModel.kt @@ -87,6 +87,10 @@ class MainViewModel @Inject constructor( .events .filterIsInstance() + val accessFactorSourcesEvents = appEventBus + .events + .filterIsInstance() + val isDevBannerVisible = getProfileStateUseCase().map { profileState -> when (profileState) { is ProfileState.Restored -> { diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/navigation/NavigationHost.kt b/app/src/main/java/com/babylon/wallet/android/presentation/navigation/NavigationHost.kt index 14534eea84..e2a91c3f0a 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/navigation/NavigationHost.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/navigation/NavigationHost.kt @@ -2,7 +2,6 @@ package com.babylon.wallet.android.presentation.navigation import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel @@ -13,6 +12,7 @@ import androidx.navigation.compose.composable import androidx.navigation.navArgument import com.babylon.wallet.android.domain.model.TransferableAsset import com.babylon.wallet.android.domain.model.resources.XrdResource +import com.babylon.wallet.android.presentation.accessfactorsources.accessFactorSources import com.babylon.wallet.android.presentation.account.AccountScreen import com.babylon.wallet.android.presentation.account.createaccount.ROUTE_CREATE_ACCOUNT import com.babylon.wallet.android.presentation.account.createaccount.confirmation.CreateAccountRequestSource @@ -76,7 +76,6 @@ import rdx.works.profile.derivation.model.NetworkId import rdx.works.profile.domain.backup.BackupType @Suppress("CyclomaticComplexMethod") -@OptIn(ExperimentalAnimationApi::class) @Composable fun NavigationHost( modifier: Modifier = Modifier, @@ -242,6 +241,11 @@ fun NavigationHost( } ) } + accessFactorSources( + onDismiss = { + navController.popBackStack() + } + ) createAccountScreen( onBackClick = { navController.navigateUp() @@ -253,7 +257,7 @@ fun NavigationHost( ) }, onAddLedgerDevice = { - navController.chooseLedger(networkId = it) + navController.chooseLedger() } ) chooseLedger( diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/onboarding/OnboardingScreen.kt b/app/src/main/java/com/babylon/wallet/android/presentation/onboarding/OnboardingScreen.kt index fbdc4a3ead..f6791cb38b 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/onboarding/OnboardingScreen.kt @@ -1,7 +1,6 @@ package com.babylon.wallet.android.presentation.onboarding import androidx.activity.compose.BackHandler -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -28,7 +27,6 @@ import com.babylon.wallet.android.designsystem.composable.RadixTextButton import com.babylon.wallet.android.designsystem.theme.RadixTheme import com.babylon.wallet.android.designsystem.theme.RadixWalletTheme -@ExperimentalAnimationApi @Composable fun OnboardingScreen( viewModel: OnboardingViewModel, diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/settings/accountsecurity/seedphrases/confirm/ConfirmMnemonicNav.kt b/app/src/main/java/com/babylon/wallet/android/presentation/settings/accountsecurity/seedphrases/confirm/ConfirmMnemonicNav.kt index 80bd2c3e00..d51b3b1340 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/settings/accountsecurity/seedphrases/confirm/ConfirmMnemonicNav.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/settings/accountsecurity/seedphrases/confirm/ConfirmMnemonicNav.kt @@ -1,7 +1,6 @@ package com.babylon.wallet.android.presentation.settings.accountsecurity.seedphrases.confirm import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.ExperimentalAnimationApi import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController @@ -29,7 +28,6 @@ internal class ConfirmSeedPhraseArgs(val factorSourceId: String, val mnemonicSiz ) } -@OptIn(ExperimentalAnimationApi::class) fun NavGraphBuilder.confirmSeedPhrase( onMnemonicBackedUp: () -> Unit, onDismiss: () -> Unit diff --git a/app/src/main/java/com/babylon/wallet/android/utils/AppEventBus.kt b/app/src/main/java/com/babylon/wallet/android/utils/AppEventBus.kt index 138ce0f06e..9abbbbcf37 100644 --- a/app/src/main/java/com/babylon/wallet/android/utils/AppEventBus.kt +++ b/app/src/main/java/com/babylon/wallet/android/utils/AppEventBus.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import rdx.works.profile.data.model.factorsources.FactorSource -import rdx.works.profile.data.model.pernetwork.DerivationPath +import rdx.works.profile.data.model.factorsources.LedgerHardwareWalletFactorSource import javax.inject.Inject import javax.inject.Singleton @@ -28,11 +28,13 @@ sealed interface AppEvent { data object RestoredMnemonic : AppEvent data object BabylonFactorSourceDoesNotExist : AppEvent data class BabylonFactorSourceNeedsRecovery(val factorSourceID: FactorSource.FactorSourceID.FromHash) : AppEvent - data class DerivedAccountPublicKeyWithLedger( - val factorSourceID: FactorSource.FactorSourceID.FromHash, - val derivationPath: DerivationPath, - val derivedPublicKeyHex: String - ) : AppEvent + + sealed interface AccessFactorSources : AppEvent { + + data class SelectedLedgerDevice(val ledgerFactorSource: LedgerHardwareWalletFactorSource) : AccessFactorSources + + data object DeriveAccountPublicKey : AccessFactorSources + } sealed class Status : AppEvent { abstract val requestId: String diff --git a/app/src/test/java/com/babylon/wallet/android/domain/usecases/CreateAccountUseCaseTest.kt b/app/src/test/java/com/babylon/wallet/android/domain/usecases/CreateAccountUseCaseTest.kt new file mode 100644 index 0000000000..4983006364 --- /dev/null +++ b/app/src/test/java/com/babylon/wallet/android/domain/usecases/CreateAccountUseCaseTest.kt @@ -0,0 +1,87 @@ +package com.babylon.wallet.android.domain.usecases + +import com.babylon.wallet.android.data.repository.ResolveAccountsLedgerStateRepository +import com.babylon.wallet.android.presentation.accessfactorsources.AccessFactorSourcesOutput +import com.radixdlt.extensions.removeLeadingZero +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import rdx.works.profile.data.model.MnemonicWithPassphrase +import rdx.works.profile.data.model.ProfileState +import rdx.works.profile.data.model.apppreferences.Radix +import rdx.works.profile.data.model.compressedPublicKey +import rdx.works.profile.data.model.extensions.nextAccountIndex +import rdx.works.profile.data.model.factorsources.DerivationPathScheme +import rdx.works.profile.data.model.pernetwork.DerivationPath +import rdx.works.profile.data.model.pernetwork.addAccounts +import rdx.works.profile.data.repository.ProfileRepository +import rdx.works.profile.derivation.model.KeyType +import rdx.works.profile.domain.TestData + +class CreateAccountUseCaseTest { + + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private val mnemonicWithPassphrase = MnemonicWithPassphrase( + mnemonic = "prison post shoot verb lunch blue limb stick later winner tide roof situate excuse joy muffin cruel fix bag evil call glide resist aware", + bip39Passphrase = "" + ) + private val profile = TestData.testProfile2Networks2AccountsEach(mnemonicWithPassphrase) + val gateway = Radix.Gateway.hammunet + private val profileRepository = Mockito.mock(ProfileRepository::class.java) + private val resolveAccountsLedgerStateRepository = mockk() + private val derivationPath = DerivationPath.forAccount( + networkId = gateway.network.networkId(), + accountIndex = profile.nextAccountIndex( + factorSource = TestData.ledgerFactorSource, + derivationPathScheme = DerivationPathScheme.CAP_26, + forNetworkId = gateway.network.networkId() + ), + keyType = KeyType.TRANSACTION_SIGNING + ) + + @Before + fun setUp() { + coEvery { resolveAccountsLedgerStateRepository(any()) } returns Result.failure(Exception("")) + } + + @Test + fun `given a account name, a factor source, and a public key with derivation path, when CreateAccountUseCase, then create new account and save it to the profile`() { + testScope.runTest { + // given + val displayName = "A" + val factorSource = TestData.ledgerFactorSource + val publicKeyAndDerivationPath = AccessFactorSourcesOutput.PublicKeyAndDerivationPath( + compressedPublicKey = mnemonicWithPassphrase.compressedPublicKey(derivationPath = derivationPath) + .removeLeadingZero(), + derivationPath = derivationPath, + ) + + // when + whenever(profileRepository.profileState).thenReturn(flowOf(ProfileState.Restored(profile))) + val createAccountUseCase = CreateAccountUseCase(profileRepository, resolveAccountsLedgerStateRepository) + val account = createAccountUseCase.invoke( + displayName = displayName, + factorSource = factorSource, + publicKeyAndDerivationPath = publicKeyAndDerivationPath, + onNetworkId = gateway.network.networkId() + ) + + // then + val updatedProfile = profile.addAccounts( + accounts = listOf(account), + onNetwork = gateway.network.networkId() + ) + verify(profileRepository).saveProfile(updatedProfile) + } + } +} \ No newline at end of file diff --git a/app/src/test/java/com/babylon/wallet/android/domain/usecases/CreateAccountWithBabylonDeviceFactorSourceUseCaseTest.kt b/app/src/test/java/com/babylon/wallet/android/domain/usecases/CreateAccountWithBabylonDeviceFactorSourceUseCaseTest.kt deleted file mode 100644 index e42716fb47..0000000000 --- a/app/src/test/java/com/babylon/wallet/android/domain/usecases/CreateAccountWithBabylonDeviceFactorSourceUseCaseTest.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.babylon.wallet.android.domain.usecases - -import com.babylon.wallet.android.data.repository.ResolveAccountsLedgerStateRepository -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockk -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.mockito.Mockito -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import rdx.works.profile.data.model.MnemonicWithPassphrase -import rdx.works.profile.data.model.ProfileState -import rdx.works.profile.data.model.apppreferences.Radix -import rdx.works.profile.data.model.extensions.mainBabylonFactorSource -import rdx.works.profile.data.model.pernetwork.addAccounts -import rdx.works.profile.data.repository.MnemonicRepository -import rdx.works.profile.data.repository.ProfileRepository -import rdx.works.profile.domain.EnsureBabylonFactorSourceExistUseCase -import rdx.works.profile.domain.TestData - -class CreateAccountWithBabylonDeviceFactorSourceUseCaseTest { - private val testDispatcher = StandardTestDispatcher() - private val testScope = TestScope(testDispatcher) - private val ensureBabylonFactorSourceExistUseCase = mockk() - private val resolveAccountsLedgerStateRepository = mockk() - private val mnemonicWithPassphrase = MnemonicWithPassphrase( - mnemonic = "prison post shoot verb lunch blue limb stick later winner tide roof situate excuse joy muffin cruel fix bag evil call glide resist aware", - bip39Passphrase = "" - ) - - @Before - fun setUp() { - coEvery { ensureBabylonFactorSourceExistUseCase() } returns TestData.testProfile2Networks2AccountsEach(mnemonicWithPassphrase) - coEvery { resolveAccountsLedgerStateRepository(any()) } returns Result.failure(Exception("")) - } - - @Test - fun `given profile already exists, when creating new account, verify its returned and persisted to the profile`() { - testScope.runTest { - // given - val accountName = "First account" - val network = Radix.Gateway.hammunet - val profile = TestData.testProfile2Networks2AccountsEach(mnemonicWithPassphrase) - val mnemonicRepository = mock { - onBlocking { - readMnemonic(checkNotNull(profile.mainBabylonFactorSource()?.id)) - } doReturn Result.success(mnemonicWithPassphrase) - } - - val profileRepository = Mockito.mock(ProfileRepository::class.java) - whenever(profileRepository.profileState).thenReturn(flowOf(ProfileState.Restored(profile))) - coEvery { ensureBabylonFactorSourceExistUseCase() } returns profile - - val createAccountWithBabylonDeviceFactorSourceUseCase = CreateAccountWithBabylonDeviceFactorSourceUseCase( - mnemonicRepository = mnemonicRepository, - profileRepository = profileRepository, - ensureBabylonFactorSourceExistUseCase = ensureBabylonFactorSourceExistUseCase, - defaultDispatcher = testDispatcher, - resolveAccountsLedgerStateRepository = resolveAccountsLedgerStateRepository - ) - - val account = createAccountWithBabylonDeviceFactorSourceUseCase( - displayName = accountName - ) - - val updatedProfile = profile.addAccounts( - accounts = listOf(account), - onNetwork = network.network.networkId() - ) - - verify(profileRepository).saveProfile(updatedProfile) - coVerify(exactly = 1) { ensureBabylonFactorSourceExistUseCase() } - coVerify(exactly = 1) { resolveAccountsLedgerStateRepository(any()) } - } - } -} \ No newline at end of file diff --git a/app/src/test/java/com/babylon/wallet/android/domain/usecases/CreateAccountWithLedgerFactorSourceUseCaseTest.kt b/app/src/test/java/com/babylon/wallet/android/domain/usecases/CreateAccountWithLedgerFactorSourceUseCaseTest.kt deleted file mode 100644 index 7c6b565886..0000000000 --- a/app/src/test/java/com/babylon/wallet/android/domain/usecases/CreateAccountWithLedgerFactorSourceUseCaseTest.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.babylon.wallet.android.domain.usecases - -import com.babylon.wallet.android.data.repository.ResolveAccountsLedgerStateRepository -import io.mockk.coEvery -import io.mockk.mockk -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.mockito.Mockito -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import rdx.works.profile.data.model.MnemonicWithPassphrase -import rdx.works.profile.data.model.ProfileState -import rdx.works.profile.data.model.apppreferences.Radix -import rdx.works.profile.data.model.factorsources.DerivationPathScheme -import rdx.works.profile.data.model.pernetwork.DerivationPath -import rdx.works.profile.data.model.pernetwork.addAccounts -import rdx.works.profile.data.model.pernetwork.nextAccountIndex -import rdx.works.profile.data.repository.ProfileRepository -import rdx.works.profile.derivation.model.KeyType -import rdx.works.profile.domain.TestData - -internal class CreateAccountWithLedgerFactorSourceUseCaseTest { - private val testDispatcher = StandardTestDispatcher() - private val testScope = TestScope(testDispatcher) - private val resolveAccountsLedgerStateRepository = mockk() - - @Before - fun setUp() { - coEvery { resolveAccountsLedgerStateRepository.invoke(any()) } returns Result.failure(Exception("")) - } - - @Test - fun `given profile already exists, creating new ledger account adds it to profile`() { - testScope.runTest { - // given - val mnemonicWithPassphrase = MnemonicWithPassphrase( - mnemonic = "noodle question hungry sail type offer grocery clay nation hello mixture forum", - bip39Passphrase = "" - ) - val accountName = "First account" - val network = Radix.Gateway.hammunet.network - val profile = TestData.testProfile2Networks2AccountsEach(mnemonicWithPassphrase) - - val profileRepository = Mockito.mock(ProfileRepository::class.java) - whenever(profileRepository.profileState).thenReturn(flowOf(ProfileState.Restored(profile))) - - val createAccountWithLedgerFactorSourceUseCase = CreateAccountWithLedgerFactorSourceUseCase( - profileRepository = profileRepository, - resolveAccountsLedgerStateRepository = resolveAccountsLedgerStateRepository, - testDispatcher - ) - val derivationPath = DerivationPath.forAccount( - networkId = network.networkId(), - accountIndex = profile.nextAccountIndex( - derivationPathScheme = DerivationPathScheme.CAP_26, - forNetworkId = network.networkId(), - factorSourceID = TestData.ledgerFactorSource.id - ), - keyType = KeyType.TRANSACTION_SIGNING - ) - val account = createAccountWithLedgerFactorSourceUseCase( - displayName = accountName, - derivedPublicKeyHex = "7229e3b98ffa35a4ce28b891ff0a9f95c9d959eff58d0e61015fab3a3b2d18f9", - derivationPath = derivationPath, - ledgerFactorSourceID = TestData.ledgerFactorSource.id - ) - - val updatedProfile = profile.addAccounts( - accounts = listOf(account), - onNetwork = network.networkId() - ) - - verify(profileRepository).saveProfile(updatedProfile) - } - } -} \ No newline at end of file diff --git a/app/src/test/java/com/babylon/wallet/android/presentation/createaccount/withledger/ChooseLedgerViewModelTest.kt b/app/src/test/java/com/babylon/wallet/android/presentation/createaccount/withledger/ChooseLedgerViewModelTest.kt index f391bb1286..83c32f6d99 100644 --- a/app/src/test/java/com/babylon/wallet/android/presentation/createaccount/withledger/ChooseLedgerViewModelTest.kt +++ b/app/src/test/java/com/babylon/wallet/android/presentation/createaccount/withledger/ChooseLedgerViewModelTest.kt @@ -3,28 +3,22 @@ package com.babylon.wallet.android.presentation.createaccount.withledger import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.babylon.wallet.android.data.dapp.LedgerMessenger -import com.babylon.wallet.android.domain.model.MessageFromDataChannel import com.babylon.wallet.android.mockdata.profile import com.babylon.wallet.android.presentation.StateViewModelTest -import com.babylon.wallet.android.presentation.account.createaccount.withledger.ARG_NETWORK_ID import com.babylon.wallet.android.presentation.account.createaccount.withledger.ARG_SELECTION_PURPOSE import com.babylon.wallet.android.presentation.account.createaccount.withledger.ChooseLedgerViewModel import com.babylon.wallet.android.presentation.account.createaccount.withledger.LedgerSelectionPurpose -import com.babylon.wallet.android.utils.AppEvent import com.babylon.wallet.android.utils.AppEventBus import io.mockk.Runs import io.mockk.coEvery -import io.mockk.coVerify import io.mockk.every import io.mockk.just import io.mockk.mockk -import io.mockk.slot import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Before -import org.junit.Ignore import org.junit.Test import rdx.works.core.HexCoded32Bytes import rdx.works.profile.data.model.apppreferences.P2PLink @@ -32,10 +26,8 @@ import rdx.works.profile.data.model.apppreferences.Radix import rdx.works.profile.data.model.factorsources.LedgerHardwareWalletFactorSource import rdx.works.profile.domain.AddLedgerFactorSourceResult import rdx.works.profile.domain.AddLedgerFactorSourceUseCase -import rdx.works.profile.domain.EnsureBabylonFactorSourceExistUseCase import rdx.works.profile.domain.GetProfileUseCase import rdx.works.profile.domain.gateway.GetCurrentGatewayUseCase -import rdx.works.profile.domain.p2pLinks @OptIn(ExperimentalCoroutinesApi::class) internal class ChooseLedgerViewModelTest : StateViewModelTest() { @@ -44,7 +36,6 @@ internal class ChooseLedgerViewModelTest : StateViewModelTest() private val getCurrentGatewayUseCase = mockk() private val addLedgerFactorSourceUseCase = mockk() - private val ensureBabylonFactorSourceExistUseCase = mockk() private val eventBus = mockk() private val savedStateHandle = mockk() @@ -55,9 +46,6 @@ internal class ChooseLedgerViewModelTest : StateViewModelTest(ARG_NETWORK_ID) } returns Radix.Gateway.mainnet.network.id - every { savedStateHandle.get(ARG_SELECTION_PURPOSE) } returns LedgerSelectionPurpose.CreateAccount + every { savedStateHandle.get(ARG_SELECTION_PURPOSE) } returns LedgerSelectionPurpose.DerivePublicKey coEvery { getCurrentGatewayUseCase() } returns Radix.Gateway.mainnet coEvery { addLedgerFactorSourceUseCase( @@ -85,18 +72,6 @@ internal class ChooseLedgerViewModelTest : StateViewModelTest() - coVerify(exactly = 1) { - ledgerMessenger.sendDerivePublicKeyRequest(any(), any(), any()) - } - coVerify(exactly = 1) { - eventBus.sendEvent(capture(event)) - } - assert(event.captured.derivedPublicKeyHex == "publicKeyHex") - assert(event.captured.factorSourceID.body.value == secondDeviceId) - } } diff --git a/designsystem/src/main/res/drawable/ic_create_account.xml b/designsystem/src/main/res/drawable/ic_create_account.xml new file mode 100644 index 0000000000..75729bbdf1 --- /dev/null +++ b/designsystem/src/main/res/drawable/ic_create_account.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/profile/src/main/java/rdx/works/profile/data/model/extensions/CreateAccountExtensions.kt b/profile/src/main/java/rdx/works/profile/data/model/extensions/CreateAccountExtensions.kt new file mode 100644 index 0000000000..dca106d460 --- /dev/null +++ b/profile/src/main/java/rdx/works/profile/data/model/extensions/CreateAccountExtensions.kt @@ -0,0 +1,67 @@ +package rdx.works.profile.data.model.extensions + +import com.babylon.wallet.android.designsystem.theme.AccountGradientList +import com.radixdlt.ret.PublicKey +import com.radixdlt.ret.deriveVirtualAccountAddressFromPublicKey +import rdx.works.core.toHexString +import rdx.works.profile.data.model.Profile +import rdx.works.profile.data.model.factorsources.DerivationPathScheme +import rdx.works.profile.data.model.factorsources.FactorSource +import rdx.works.profile.data.model.factorsources.Slip10Curve +import rdx.works.profile.data.model.pernetwork.DerivationPath +import rdx.works.profile.data.model.pernetwork.FactorInstance +import rdx.works.profile.data.model.pernetwork.Network +import rdx.works.profile.data.model.pernetwork.SecurityState +import rdx.works.profile.derivation.model.NetworkId + +@Suppress("LongParameterList") +fun Profile.createAccount( + displayName: String, + onNetworkId: NetworkId, + compressedPublicKey: ByteArray, + derivationPath: DerivationPath, + factorSource: FactorSource.CreatingEntity, + onLedgerSettings: Network.Account.OnLedgerSettings +): Network.Account { + val address = deriveVirtualAccountAddressFromPublicKey( + PublicKey.Ed25519(compressedPublicKey), + onNetworkId.value.toUByte() + ).addressString() + + val unsecuredSecurityState = SecurityState.unsecured( + publicKey = FactorInstance.PublicKey(compressedPublicKey.toHexString(), Slip10Curve.CURVE_25519), + derivationPath = derivationPath, + factorSourceId = (factorSource as FactorSource).id as FactorSource.FactorSourceID.FromHash + ) + + return Network.Account( + address = address, + appearanceID = nextAppearanceId(forNetworkId = onNetworkId), + displayName = displayName, + networkID = onNetworkId.value, + securityState = unsecuredSecurityState, + onLedgerSettings = onLedgerSettings + ) +} + +fun Profile.nextAccountIndex( + factorSource: FactorSource, + derivationPathScheme: DerivationPathScheme, + forNetworkId: NetworkId +): Int { + val forNetwork = networks.firstOrNull { it.networkID == forNetworkId.value } ?: return 0 + val accountsControlledByFactorSource = forNetwork.accounts.filter { + it.factorSourceId == factorSource.id && it.derivationPathScheme == derivationPathScheme + } + + return if (accountsControlledByFactorSource.isEmpty()) { + 0 + } else { + accountsControlledByFactorSource.maxOf { it.derivationPathEntityIndex } + 1 + } +} + +fun Profile.nextAppearanceId(forNetworkId: NetworkId): Int { + val forNetwork = networks.firstOrNull { it.networkID == forNetworkId.value } ?: return 0 + return forNetwork.accounts.count() % AccountGradientList.count() +} diff --git a/profile/src/main/java/rdx/works/profile/data/model/factorsources/DeviceFactorSource.kt b/profile/src/main/java/rdx/works/profile/data/model/factorsources/DeviceFactorSource.kt index e1843c1162..ee029b47af 100644 --- a/profile/src/main/java/rdx/works/profile/data/model/factorsources/DeviceFactorSource.kt +++ b/profile/src/main/java/rdx/works/profile/data/model/factorsources/DeviceFactorSource.kt @@ -15,7 +15,7 @@ data class DeviceFactorSource( override val common: Common, @SerialName("hint") val hint: Hint -) : FactorSource() { +) : FactorSource(), FactorSource.CreatingEntity { @Serializable data class Hint( diff --git a/profile/src/main/java/rdx/works/profile/data/model/factorsources/FactorSource.kt b/profile/src/main/java/rdx/works/profile/data/model/factorsources/FactorSource.kt index 4bc233b556..30fd01b5f0 100644 --- a/profile/src/main/java/rdx/works/profile/data/model/factorsources/FactorSource.kt +++ b/profile/src/main/java/rdx/works/profile/data/model/factorsources/FactorSource.kt @@ -18,6 +18,8 @@ import java.time.Instant @Serializable(with = FactorSourceSerializer::class) sealed class FactorSource : Identified { + sealed interface CreatingEntity + override val identifier: String get() = when (this) { is DeviceFactorSource -> id.body.value diff --git a/profile/src/main/java/rdx/works/profile/data/model/factorsources/LedgerHardwareWalletFactorSource.kt b/profile/src/main/java/rdx/works/profile/data/model/factorsources/LedgerHardwareWalletFactorSource.kt index fed69b310c..583f18e1a1 100644 --- a/profile/src/main/java/rdx/works/profile/data/model/factorsources/LedgerHardwareWalletFactorSource.kt +++ b/profile/src/main/java/rdx/works/profile/data/model/factorsources/LedgerHardwareWalletFactorSource.kt @@ -13,7 +13,7 @@ data class LedgerHardwareWalletFactorSource( override val common: Common, @SerialName("hint") val hint: Hint -) : FactorSource() { +) : FactorSource(), FactorSource.CreatingEntity { @Serializable data class Hint( diff --git a/profile/src/main/java/rdx/works/profile/data/model/pernetwork/Network.kt b/profile/src/main/java/rdx/works/profile/data/model/pernetwork/Network.kt index 1132443dce..ea243262e9 100644 --- a/profile/src/main/java/rdx/works/profile/data/model/pernetwork/Network.kt +++ b/profile/src/main/java/rdx/works/profile/data/model/pernetwork/Network.kt @@ -3,7 +3,6 @@ package rdx.works.profile.data.model.pernetwork -import com.babylon.wallet.android.designsystem.theme.AccountGradientList import com.radixdlt.extensions.removeLeadingZero import com.radixdlt.ret.Address import com.radixdlt.ret.OlympiaNetwork @@ -66,7 +65,7 @@ data class Network( ) { val knownNetworkId: NetworkId? - get() = NetworkId.values().find { it.value == networkID } + get() = NetworkId.entries.find { it.value == networkID } @Serializable data class Account( @@ -663,23 +662,6 @@ fun Profile.usedAccountDerivationIndices( }.map { it.derivationPathEntityIndex }.toSet() } -fun Profile.nextAccountIndex( - derivationPathScheme: DerivationPathScheme, - forNetworkId: NetworkId? = null, - factorSourceID: FactorSource.FactorSourceID? = null -): Int { - val network = networks.firstOrNull { it.networkID == forNetworkId?.value } ?: return 0 - val factorSource = factorSources.find { it.id == factorSourceID } ?: mainBabylonFactorSource() ?: return 0 - val accountsControlledByFactorSource = network.accounts.filter { - it.factorSourceId == factorSource.id && it.derivationPathScheme == derivationPathScheme - } - return if (accountsControlledByFactorSource.isEmpty()) { - 0 - } else { - accountsControlledByFactorSource.maxOf { it.derivationPathEntityIndex } + 1 - } -} - fun Profile.nextPersonaIndex( derivationPathScheme: DerivationPathScheme, forNetworkId: NetworkId, @@ -697,20 +679,6 @@ fun Profile.nextPersonaIndex( } } -fun Profile.nextAppearanceId( - forNetworkId: NetworkId? = null, - factorSourceID: FactorSource.FactorSourceID? = null -): Int { - val network = networks.firstOrNull { it.networkID == forNetworkId?.value } ?: return 0 - val factorSource = factorSources.find { it.id == factorSourceID } ?: mainBabylonFactorSource() ?: return 0 - val accountsControlledByFactorSource = network.accounts.filter { it.factorSourceId == factorSource.id } - return if (accountsControlledByFactorSource.isEmpty()) { - 0 - } else { - (accountsControlledByFactorSource.maxOf { it.appearanceID } + 1) % AccountGradientList.size - } -} - fun Profile.updatePersona( persona: Network.Persona ): Profile { diff --git a/profile/src/main/java/rdx/works/profile/data/repository/AccessFactorSourcesProvider.kt b/profile/src/main/java/rdx/works/profile/data/repository/AccessFactorSourcesProvider.kt new file mode 100644 index 0000000000..8506638ba9 --- /dev/null +++ b/profile/src/main/java/rdx/works/profile/data/repository/AccessFactorSourcesProvider.kt @@ -0,0 +1,43 @@ +package rdx.works.profile.data.repository + +import com.radixdlt.extensions.removeLeadingZero +import kotlinx.coroutines.flow.first +import rdx.works.profile.data.model.compressedPublicKey +import rdx.works.profile.data.model.extensions.nextAccountIndex +import rdx.works.profile.data.model.factorsources.DerivationPathScheme +import rdx.works.profile.data.model.factorsources.DeviceFactorSource +import rdx.works.profile.data.model.factorsources.FactorSource +import rdx.works.profile.data.model.pernetwork.DerivationPath +import rdx.works.profile.derivation.model.KeyType +import rdx.works.profile.derivation.model.NetworkId +import javax.inject.Inject + +class AccessFactorSourcesProvider @Inject constructor( + private val mnemonicRepository: MnemonicRepository, + private val profileRepository: ProfileRepository +) { + + /** + * CAP26 derivation path scheme + */ + suspend fun getNextDerivationPathForFactorSource( + forNetworkId: NetworkId, + factorSource: FactorSource + ): DerivationPath { + val profile = profileRepository.profile.first() + val accountIndex = profile.nextAccountIndex(factorSource, DerivationPathScheme.CAP_26, forNetworkId) + return DerivationPath.forAccount( + networkId = forNetworkId, + accountIndex = accountIndex, + keyType = KeyType.TRANSACTION_SIGNING + ) + } + + suspend fun derivePublicKeyForDeviceFactorSource( + deviceFactorSource: DeviceFactorSource, + derivationPath: DerivationPath + ): ByteArray { + val mnemonicWithPassphrase = requireNotNull(mnemonicRepository.readMnemonic(deviceFactorSource.id).getOrNull()) + return mnemonicWithPassphrase.compressedPublicKey(derivationPath = derivationPath).removeLeadingZero() + } +} diff --git a/profile/src/main/java/rdx/works/profile/domain/GetProfileUseCase.kt b/profile/src/main/java/rdx/works/profile/domain/GetProfileUseCase.kt index 76b339c092..437c9662ea 100644 --- a/profile/src/main/java/rdx/works/profile/domain/GetProfileUseCase.kt +++ b/profile/src/main/java/rdx/works/profile/domain/GetProfileUseCase.kt @@ -14,20 +14,15 @@ import rdx.works.profile.data.model.currentNetwork import rdx.works.profile.data.model.extensions.factorSourceId import rdx.works.profile.data.model.extensions.usesCurve25519 import rdx.works.profile.data.model.extensions.usesSecp256k1 -import rdx.works.profile.data.model.factorsources.DerivationPathScheme import rdx.works.profile.data.model.factorsources.DeviceFactorSource import rdx.works.profile.data.model.factorsources.EntityFlag import rdx.works.profile.data.model.factorsources.FactorSource import rdx.works.profile.data.model.factorsources.FactorSourceFlag import rdx.works.profile.data.model.factorsources.LedgerHardwareWalletFactorSource -import rdx.works.profile.data.model.pernetwork.DerivationPath import rdx.works.profile.data.model.pernetwork.Entity import rdx.works.profile.data.model.pernetwork.Network -import rdx.works.profile.data.model.pernetwork.nextAccountIndex import rdx.works.profile.data.repository.ProfileRepository import rdx.works.profile.data.repository.profile -import rdx.works.profile.derivation.model.KeyType -import rdx.works.profile.derivation.model.NetworkId import javax.inject.Inject class GetProfileUseCase @Inject constructor(private val profileRepository: ProfileRepository) { @@ -49,7 +44,8 @@ class GetProfileUseCase @Inject constructor(private val profileRepository: Profi */ val GetProfileUseCase.entitiesOnCurrentNetwork: Flow> get() = invoke().map { - it.currentNetwork?.accounts?.notHiddenAccounts().orEmpty() + it.currentNetwork?.personas?.notHiddenPersonas().orEmpty() + it.currentNetwork?.accounts?.notHiddenAccounts().orEmpty() + + it.currentNetwork?.personas?.notHiddenPersonas().orEmpty() } suspend fun GetProfileUseCase.currentNetwork(): Network? { @@ -137,20 +133,6 @@ suspend fun GetProfileUseCase.accountOnCurrentNetwork( account.address == withAddress } -suspend fun GetProfileUseCase.nextDerivationPathForAccountOnNetwork( - derivationPathScheme: DerivationPathScheme, - networkId: Int, - factorSourceId: FactorSource.FactorSourceID -): DerivationPath { - val profile = invoke().first() - val network = requireNotNull(NetworkId.from(networkId)) - return DerivationPath.forAccount( - networkId = network, - accountIndex = profile.nextAccountIndex(derivationPathScheme, network, factorSourceId), - keyType = KeyType.TRANSACTION_SIGNING - ) -} - suspend fun GetProfileUseCase.currentNetworkAccountHashes(): Set { return accountsOnCurrentNetwork().map { val addressData = Address(it.address).bytes() diff --git a/profile/src/main/java/rdx/works/profile/domain/account/MigrateOlympiaAccountsUseCase.kt b/profile/src/main/java/rdx/works/profile/domain/account/MigrateOlympiaAccountsUseCase.kt index 24b57b3d38..a507672744 100644 --- a/profile/src/main/java/rdx/works/profile/domain/account/MigrateOlympiaAccountsUseCase.kt +++ b/profile/src/main/java/rdx/works/profile/domain/account/MigrateOlympiaAccountsUseCase.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext import rdx.works.profile.data.model.apppreferences.Radix import rdx.works.profile.data.model.currentNetwork +import rdx.works.profile.data.model.extensions.nextAppearanceId import rdx.works.profile.data.model.factorsources.FactorSource import rdx.works.profile.data.model.factorsources.Slip10Curve import rdx.works.profile.data.model.pernetwork.DerivationPath @@ -15,7 +16,6 @@ import rdx.works.profile.data.model.pernetwork.FactorInstance import rdx.works.profile.data.model.pernetwork.Network import rdx.works.profile.data.model.pernetwork.SecurityState import rdx.works.profile.data.model.pernetwork.addAccounts -import rdx.works.profile.data.model.pernetwork.nextAppearanceId import rdx.works.profile.data.repository.ProfileRepository import rdx.works.profile.data.repository.profile import rdx.works.profile.di.coroutines.DefaultDispatcher @@ -33,7 +33,7 @@ class MigrateOlympiaAccountsUseCase @Inject constructor( return withContext(defaultDispatcher) { val profile = profileRepository.profile.first() val networkId = profile.currentNetwork?.knownNetworkId ?: Radix.Gateway.default.network.networkId() - val appearanceIdOffset = profile.nextAppearanceId(networkId) + val appearanceIdOffset = profile.nextAppearanceId(forNetworkId = networkId) val migratedAccounts = olympiaAccounts.mapIndexed { index, olympiaAccount -> val babylonAddress = Address.virtualAccountAddressFromOlympiaAddress( olympiaAccountAddress = OlympiaAddress(olympiaAccount.address), diff --git a/profile/src/test/java/rdx/works/profile/ProfileGenerationTest.kt b/profile/src/test/java/rdx/works/profile/ProfileGenerationTest.kt index 64f97d7978..f2fa690ce5 100644 --- a/profile/src/test/java/rdx/works/profile/ProfileGenerationTest.kt +++ b/profile/src/test/java/rdx/works/profile/ProfileGenerationTest.kt @@ -13,6 +13,7 @@ import rdx.works.profile.data.model.Profile import rdx.works.profile.data.model.apppreferences.P2PLink import rdx.works.profile.data.model.apppreferences.Radix import rdx.works.profile.data.model.extensions.addP2PLink +import rdx.works.profile.data.model.extensions.nextAccountIndex import rdx.works.profile.data.model.extensions.renameAccountDisplayName import rdx.works.profile.data.model.factorsources.DerivationPathScheme import rdx.works.profile.data.model.factorsources.DeviceFactorSource @@ -20,7 +21,6 @@ import rdx.works.profile.data.model.pernetwork.Network.Account.Companion.initAcc import rdx.works.profile.data.model.pernetwork.Network.Persona.Companion.init import rdx.works.profile.data.model.pernetwork.addAccounts import rdx.works.profile.data.model.pernetwork.addPersona -import rdx.works.profile.data.model.pernetwork.nextAccountIndex import rdx.works.profile.data.model.pernetwork.nextPersonaIndex import rdx.works.profile.data.repository.MnemonicRepository import rdx.works.profile.domain.TestData @@ -53,9 +53,9 @@ class ProfileGenerationTest { "Next derivation index for first account", 0, profile.nextAccountIndex( + factorSource = babylonFactorSource, derivationPathScheme = DerivationPathScheme.CAP_26, forNetworkId = defaultNetwork.networkId(), - factorSourceID = babylonFactorSource.id ) ) @@ -86,9 +86,9 @@ class ProfileGenerationTest { "Next derivation index for second account", 1, profile.nextAccountIndex( + factorSource = babylonFactorSource, derivationPathScheme = DerivationPathScheme.CAP_26, - forNetworkId = defaultNetwork.networkId(), - factorSourceID = babylonFactorSource.id + forNetworkId = defaultNetwork.networkId() ) )