Skip to content

Commit

Permalink
Merge pull request #1344 from radixdlt/sp/add-factor
Browse files Browse the repository at this point in the history
Add device factor source
  • Loading branch information
sergiupuhalschi-rdx authored Feb 25, 2025
2 parents 8790c69 + 0fc7209 commit b45823c
Show file tree
Hide file tree
Showing 45 changed files with 2,251 additions and 229 deletions.
17 changes: 17 additions & 0 deletions app/src/main/java/com/babylon/wallet/android/WalletApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.babylon.wallet.android.presentation.accessfactorsources.derivepublick
import com.babylon.wallet.android.presentation.accessfactorsources.signatures.getSignatures
import com.babylon.wallet.android.presentation.accessfactorsources.spotcheck.spotCheck
import com.babylon.wallet.android.presentation.account.settings.delete.success.deletedAccountSuccess
import com.babylon.wallet.android.presentation.addfactorsource.addFactorSource
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.dialogs.address.addressDetails
Expand Down Expand Up @@ -138,6 +139,10 @@ fun WalletApp(
HandleDeletedAccountsDetectedEvent(
viewModel = mainViewModel
)
HandleAddFactorSourceEvents(
navController = navController,
addFactorSourceEvents = mainViewModel.addFactorSourceEvents
)
ObserveHighPriorityScreens(
navController = navController,
onLowPriorityScreen = mainViewModel::onLowPriorityScreen,
Expand Down Expand Up @@ -227,6 +232,18 @@ private fun HandleAccessFactorSourcesEvents(
}
}

@Composable
private fun HandleAddFactorSourceEvents(
navController: NavController,
addFactorSourceEvents: Flow<AppEvent.AddFactorSource>
) {
LaunchedEffect(Unit) {
addFactorSourceEvents.collect { _ ->
navController.addFactorSource()
}
}
}

@Composable
private fun HandleStatusEvents(
navController: NavController,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.babylon.wallet.android.data.repository.factors

import com.babylon.wallet.android.di.coroutines.DefaultDispatcher
import com.babylon.wallet.android.presentation.common.seedphrase.SeedPhraseWord
import com.babylon.wallet.android.utils.callSafely
import com.radixdlt.sargon.CommonException
import com.radixdlt.sargon.DeviceMnemonicBuildOutcome
import com.radixdlt.sargon.DeviceMnemonicBuilder
import com.radixdlt.sargon.os.SargonOsManager
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import rdx.works.core.mapWhen
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class DeviceMnemonicBuilderClient @Inject constructor(
private val sargonOsManager: SargonOsManager,
@DefaultDispatcher private val dispatcher: CoroutineDispatcher
) {

private var deviceMnemonicBuilder = DeviceMnemonicBuilder()

suspend fun generateMnemonicWords(): List<SeedPhraseWord> = withContext(dispatcher) {
executeMutating { generateNewMnemonic() }
getWords(SeedPhraseWord.State.ValidDisabled)
}

suspend fun createMnemonicFromWords(words: List<SeedPhraseWord>): List<SeedPhraseWord> = withContext(dispatcher) {
runCatching {
executeMutating { createMnemonicFromWords(words.map { it.value }) }
}.fold(
onSuccess = { getWords(SeedPhraseWord.State.Valid) },
onFailure = { throwable ->
val invalidIndices = (throwable as? CommonException.InvalidMnemonicWords)?.indicesInMnemonic?.map {
it.toInt()
} ?: List(words.size) { index -> index }
words.mapWhen(
predicate = { it.index in invalidIndices },
mutation = { it.copy(state = SeedPhraseWord.State.Invalid) }
)
}
)
}

suspend fun isFactorAlreadyInUse(): Result<Boolean> = sargonOsManager.callSafely(dispatcher) {
isFactorSourceAlreadyInUse(deviceMnemonicBuilder.getFactorSourceId())
}

suspend fun generateConfirmationWords(): List<SeedPhraseWord> = withContext(dispatcher) {
val indices = deviceMnemonicBuilder.getIndicesInMnemonicOfWordsToConfirm()
val lastWordIndex = indices.lastIndex
indices.mapIndexed { i, index ->
SeedPhraseWord(
index = index.toInt(),
lastWord = i == lastWordIndex
)
}
}

suspend fun confirmWords(words: List<SeedPhraseWord>): DeviceMnemonicBuildOutcome = withContext(dispatcher) {
deviceMnemonicBuilder.build(words.associate { it.index.toUByte() to it.value })
}

suspend fun getWords(state: SeedPhraseWord.State): List<SeedPhraseWord> = withContext(dispatcher) {
val bip39Words = deviceMnemonicBuilder.getWords()
val lastWordIndex = bip39Words.lastIndex
bip39Words.mapIndexed { index, bip39Word ->
SeedPhraseWord(
index = index,
value = bip39Word.word,
state = state,
lastWord = index == lastWordIndex
)
}
}

private suspend fun executeMutating(function: suspend DeviceMnemonicBuilder.() -> DeviceMnemonicBuilder) = withContext(dispatcher) {
deviceMnemonicBuilder = deviceMnemonicBuilder.function()
}
}
13 changes: 13 additions & 0 deletions app/src/main/java/com/babylon/wallet/android/di/UiModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package com.babylon.wallet.android.di
import com.babylon.wallet.android.presentation.accessfactorsources.AccessFactorSourcesIOHandler
import com.babylon.wallet.android.presentation.accessfactorsources.AccessFactorSourcesProxy
import com.babylon.wallet.android.presentation.accessfactorsources.AccessFactorSourcesProxyImpl
import com.babylon.wallet.android.presentation.addfactorsource.AddFactorSourceIOHandler
import com.babylon.wallet.android.presentation.addfactorsource.AddFactorSourceProxy
import com.babylon.wallet.android.presentation.addfactorsource.AddFactorSourceProxyImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
Expand All @@ -21,4 +24,14 @@ interface UiModule {
fun bindAccessFactorSourcesProxy(
accessFactorSourcesProxyImpl: AccessFactorSourcesProxyImpl
): AccessFactorSourcesProxy

@Binds
fun bindAddFactorSourceIOHandler(
addFactorSourceProxyImpl: AddFactorSourceProxyImpl
): AddFactorSourceIOHandler

@Binds
fun bindAddFactorSourceProxy(
addFactorSourceProxyImpl: AddFactorSourceProxyImpl
): AddFactorSourceProxy
}
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,13 @@ sealed class RadixWalletException(cause: Throwable? = null) : Throwable(cause =
)
)
}

sealed class AddFactorSource : RadixWalletException() {

data object FactorSourceAlreadyInUse : AddFactorSource()

data object FactorSourceNotCreated : AddFactorSource()
}
}

interface DappWalletInteractionThrowable {
Expand Down Expand Up @@ -421,6 +428,13 @@ fun RadixWalletException.CloudBackupException.toUserFriendlyMessage(): String =
is BackupServiceException.Unknown -> "Unknown error occurred cause: ${cause?.message}"
}

fun RadixWalletException.AddFactorSource.toUserFriendlyMessage(context: Context): String = context.getString(
when (this) {
RadixWalletException.AddFactorSource.FactorSourceAlreadyInUse -> R.string.newFactor_error_alreadyInUse
RadixWalletException.AddFactorSource.FactorSourceNotCreated -> R.string.newFactor_error_notCreated
}
)

fun RadixWalletException.toUserFriendlyMessage(context: Context): String {
return when (this) {
is RadixWalletException.ResourceCouldNotBeResolvedInTransaction -> context.getString(
Expand All @@ -435,6 +449,7 @@ fun RadixWalletException.toUserFriendlyMessage(context: Context): String {
is RadixWalletException.GatewayException -> toUserFriendlyMessage(context)
is RadixWalletException.LinkConnectionException -> toUserFriendlyMessage(context)
is RadixWalletException.CloudBackupException -> toUserFriendlyMessage()
is RadixWalletException.AddFactorSource -> toUserFriendlyMessage(context)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import rdx.works.profile.domain.GetProfileUseCase
import javax.inject.Inject

Expand Down Expand Up @@ -85,30 +86,32 @@ class DerivePublicKeysViewModel @Inject constructor(

fun onInputConfirmed() = accessDelegate.onInputConfirmed()

private suspend fun onAccess(factorSource: FactorSource): Result<Unit> = when (factorSource) {
is FactorSource.Device -> accessDeviceFactorSource.derivePublicKeys(
factorSource = factorSource,
input = proxyInput.request
)
is FactorSource.Ledger -> accessLedgerHardwareWalletFactorSource.derivePublicKeys(
factorSource = factorSource,
input = proxyInput.request
)
is FactorSource.ArculusCard -> accessArculusFactorSourceUseCase.derivePublicKeys(
factorSource = factorSource,
input = proxyInput.request
)
is FactorSource.OffDeviceMnemonic -> accessOffDeviceMnemonicFactorSource.derivePublicKeys(
factorSource = factorSource,
input = proxyInput.request
)
is FactorSource.Password -> accessPasswordFactorSourceUseCase.derivePublicKeys(
factorSource = factorSource,
input = proxyInput.request
)
}.mapCatching { factorInstances ->
finishWithSuccess(factorInstances)
}.toUnit()
private suspend fun onAccess(factorSource: FactorSource): Result<Unit> = withContext(defaultDispatcher) {
when (factorSource) {
is FactorSource.Device -> accessDeviceFactorSource.derivePublicKeys(
factorSource = factorSource,
input = proxyInput.request
)
is FactorSource.Ledger -> accessLedgerHardwareWalletFactorSource.derivePublicKeys(
factorSource = factorSource,
input = proxyInput.request
)
is FactorSource.ArculusCard -> accessArculusFactorSourceUseCase.derivePublicKeys(
factorSource = factorSource,
input = proxyInput.request
)
is FactorSource.OffDeviceMnemonic -> accessOffDeviceMnemonicFactorSource.derivePublicKeys(
factorSource = factorSource,
input = proxyInput.request
)
is FactorSource.Password -> accessPasswordFactorSourceUseCase.derivePublicKeys(
factorSource = factorSource,
input = proxyInput.request
)
}.mapCatching { factorInstances ->
finishWithSuccess(factorInstances)
}.toUnit()
}

private suspend fun onDismissCallback() {
// end the signing process and return the output (error)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.babylon.wallet.android.presentation.addfactorsource

import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptionsBuilder
import androidx.navigation.navigation
import com.babylon.wallet.android.presentation.addfactorsource.device.confirmseedphrase.confirmDeviceSeedPhrase
import com.babylon.wallet.android.presentation.addfactorsource.device.seedphrase.deviceSeedPhrase
import com.babylon.wallet.android.presentation.addfactorsource.intro.ROUTE_ADD_FACTOR_INTRO
import com.babylon.wallet.android.presentation.addfactorsource.intro.addFactorIntro
import com.babylon.wallet.android.presentation.addfactorsource.name.setFactorName

const val ROUTE_ADD_FACTOR_SOURCE_GRAPH = "add_factor_source_graph"

fun NavController.addFactorSource(
navOptionsBuilder: NavOptionsBuilder.() -> Unit = {}
) {
navigate(ROUTE_ADD_FACTOR_SOURCE_GRAPH, navOptionsBuilder)
}

fun NavGraphBuilder.addFactorSource(
navController: NavController
) {
navigation(
startDestination = ROUTE_ADD_FACTOR_INTRO,
route = ROUTE_ADD_FACTOR_SOURCE_GRAPH
) {
addFactorIntro(navController)

deviceSeedPhrase(navController)

confirmDeviceSeedPhrase(navController)

setFactorName(navController)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.babylon.wallet.android.presentation.addfactorsource

import com.radixdlt.sargon.FactorSourceId
import com.radixdlt.sargon.FactorSourceKind

interface AddFactorSourceProxy {

suspend fun addFactorSource(kind: FactorSourceKind): AddFactorSourceOutput.Id
}

interface AddFactorSourceIOHandler {

fun getInput(): AddFactorSourceInput

suspend fun setOutput(output: AddFactorSourceOutput)
}

sealed interface AddFactorSourceInput {

data class WithKind(val kind: FactorSourceKind) : AddFactorSourceInput

data object Init : AddFactorSourceInput
}

sealed interface AddFactorSourceOutput {

data class Id(val value: FactorSourceId) : AddFactorSourceOutput

data object Init : AddFactorSourceOutput
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.babylon.wallet.android.presentation.addfactorsource

import com.babylon.wallet.android.utils.AppEvent
import com.babylon.wallet.android.utils.AppEventBus
import com.radixdlt.sargon.FactorSourceKind
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.first
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class AddFactorSourceProxyImpl @Inject constructor(
private val appEventBus: AppEventBus
) : AddFactorSourceProxy, AddFactorSourceIOHandler {

private var input: AddFactorSourceInput = AddFactorSourceInput.Init
private val _output = MutableSharedFlow<AddFactorSourceOutput>()

override suspend fun addFactorSource(kind: FactorSourceKind): AddFactorSourceOutput.Id {
input = AddFactorSourceInput.WithKind(kind)

appEventBus.sendEvent(AppEvent.AddFactorSource)
val result = _output.first()

return result as AddFactorSourceOutput.Id
}

override fun getInput(): AddFactorSourceInput {
return input
}

override suspend fun setOutput(output: AddFactorSourceOutput) {
_output.emit(output)
reset()
}

private suspend fun reset() {
input = AddFactorSourceInput.Init
_output.emit(AddFactorSourceOutput.Init)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.babylon.wallet.android.presentation.addfactorsource.device.confirmseedphrase

import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.babylon.wallet.android.presentation.addfactorsource.name.setFactorName

private const val ROUTE_CONFIRM_DEVICE_SEED_PHRASE = "confirm_device_seed_phrase"

fun NavController.confirmDeviceSeedPhrase() {
navigate(ROUTE_CONFIRM_DEVICE_SEED_PHRASE)
}

fun NavGraphBuilder.confirmDeviceSeedPhrase(
navController: NavController
) {
composable(
route = ROUTE_CONFIRM_DEVICE_SEED_PHRASE,
enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Left) },
exitTransition = { ExitTransition.None },
popEnterTransition = { EnterTransition.None },
popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Right) }
) {
ConfirmDeviceSeedPhraseScreen(
viewModel = hiltViewModel(),
onDismiss = { navController.popBackStack() },
onConfirmed = { factorSourceKind, mnemonicWithPassphrase ->
navController.setFactorName(
factorSourceKind,
mnemonicWithPassphrase
)
}
)
}
}
Loading

0 comments on commit b45823c

Please sign in to comment.