From 5c3a3d753330939a39449da3da65d0695b65becf Mon Sep 17 00:00:00 2001 From: sdlaver <103003665+sdlaver@users.noreply.github.com> Date: Wed, 4 Dec 2024 07:07:13 -1000 Subject: [PATCH] Complete refactor of SeedVaultSimulator to use Hilt dependency injection (#376) --- .../ApplicationDependencyContainer.kt | 26 ------- .../seedvaultimpl/SeedVaultImplApplication.kt | 15 +--- .../seedvaultimpl/SodiumAndroidProvider.kt | 22 ++++++ .../contentprovider/WalletContentProvider.kt | 33 ++++---- .../seedvaultimpl/data/SeedRepository.kt | 2 +- .../seedvaultimpl/ui/AuthorizeActivity.kt | 36 ++++----- ...ewModel.kt => AuthorizeCommonViewModel.kt} | 4 +- .../ui/authorize/AuthorizeViewModel.kt | 77 +++++++++---------- .../ui/seeddetail/SeedDetailViewModel.kt | 9 +-- .../ui/selectseed/SelectSeedViewModel.kt | 33 ++++---- .../usecase/BipDerivationUseCase.kt | 14 ++-- .../usecase/Ed25519Slip10UseCase.kt | 19 +++-- .../PrepopulateKnownAccountsUseCase.kt | 14 +++- .../usecase/SignPayloadUseCase.kt | 20 +++-- .../data/tests/ShowSeedSettingsTestCase.kt | 1 - 15 files changed, 162 insertions(+), 163 deletions(-) delete mode 100644 SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ApplicationDependencyContainer.kt create mode 100644 SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/SodiumAndroidProvider.kt rename SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/{AuthorizeViewModel.kt => AuthorizeCommonViewModel.kt} (98%) diff --git a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ApplicationDependencyContainer.kt b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ApplicationDependencyContainer.kt deleted file mode 100644 index 70b8e4a3..00000000 --- a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ApplicationDependencyContainer.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2022 Solana Mobile Inc. - */ - -package com.solanamobile.seedvaultimpl - -import com.solanamobile.seedvaultimpl.data.SeedRepository -import com.goterl.lazysodium.LazySodiumAndroid -import com.goterl.lazysodium.SodiumAndroid -import java.nio.charset.StandardCharsets - -/** - * Dependency injection container scoped to the Application. Contents will be lazily initialized - * where possible. - */ -class ApplicationDependencyContainer(val seedRepository: SeedRepository) { - - // The companion object contains dependencies which should be globally available, but logically - // are still application dependencies - companion object { - val sodium: LazySodiumAndroid by lazy { - val sodiumAndroid = SodiumAndroid() - LazySodiumAndroid(sodiumAndroid, StandardCharsets.UTF_8) - } - } -} \ No newline at end of file diff --git a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/SeedVaultImplApplication.kt b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/SeedVaultImplApplication.kt index aaf15170..58cf466c 100644 --- a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/SeedVaultImplApplication.kt +++ b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/SeedVaultImplApplication.kt @@ -5,20 +5,7 @@ package com.solanamobile.seedvaultimpl import android.app.Application -import com.solanamobile.seedvaultimpl.data.SeedRepository import dagger.hilt.android.HiltAndroidApp -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import javax.inject.Inject @HiltAndroidApp -class SeedVaultImplApplication : Application() { - @Inject - lateinit var seedRepository: SeedRepository - - // TODO: remove dependencyContainer in favor of hilt/dagger injection - val dependencyContainer: ApplicationDependencyContainer by lazy { - ApplicationDependencyContainer(seedRepository) - } -} \ No newline at end of file +class SeedVaultImplApplication : Application() \ No newline at end of file diff --git a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/SodiumAndroidProvider.kt b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/SodiumAndroidProvider.kt new file mode 100644 index 00000000..76b0b7c8 --- /dev/null +++ b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/SodiumAndroidProvider.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 Solana Mobile Inc. + */ + +package com.solanamobile.seedvaultimpl + +import com.goterl.lazysodium.LazySodiumAndroid +import com.goterl.lazysodium.SodiumAndroid +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import java.nio.charset.StandardCharsets + +@Module +@InstallIn(SingletonComponent::class) +class SodiumAndroidProvider { + @Provides + fun provideLazySodiumAndroid() : LazySodiumAndroid { + return LazySodiumAndroid(SodiumAndroid(), StandardCharsets.UTF_8) + } +} diff --git a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/contentprovider/WalletContentProvider.kt b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/contentprovider/WalletContentProvider.kt index b8c319c5..dba237fd 100644 --- a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/contentprovider/WalletContentProvider.kt +++ b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/contentprovider/WalletContentProvider.kt @@ -16,14 +16,16 @@ import android.util.Log import com.solanamobile.seedvault.BipDerivationPath import com.solanamobile.seedvault.WalletContractV1 import com.solanamobile.seedvault.WalletContractV1.AUTHORITY_WALLET_PROVIDER -import com.solanamobile.seedvaultimpl.ApplicationDependencyContainer -import com.solanamobile.seedvaultimpl.SeedVaultImplApplication import com.solanamobile.seedvaultimpl.data.SeedRepository import com.solanamobile.seedvaultimpl.model.Authorization import com.solanamobile.seedvaultimpl.usecase.Base58EncodeUseCase import com.solanamobile.seedvaultimpl.usecase.RequestLimitsUseCase import com.solanamobile.seedvaultimpl.usecase.normalize import com.solanamobile.seedvaultimpl.usecase.toBip32DerivationPath +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -31,7 +33,13 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking class WalletContentProvider : ContentProvider() { - private lateinit var dependencyContainer: ApplicationDependencyContainer + @EntryPoint + @InstallIn(SingletonComponent::class) + interface WalletContentProviderHiltEntryPoint { + fun provideSeedRepository(): SeedRepository + } + + private lateinit var seedRepository: SeedRepository override fun onCreate(): Boolean { // NOTE: this occurs before the Application instance is created, so we can't do our @@ -91,7 +99,6 @@ class WalletContentProvider : ContentProvider() { // NOTE: A real Seed Vault implementation should NOT provide this method private fun callResetSeedVaultSimulator(): Bundle? { - val seedRepository = dependencyContainer.seedRepository runBlocking { seedRepository.delayUntilDataValid() seedRepository.deleteAllSeeds() @@ -185,7 +192,6 @@ class WalletContentProvider : ContentProvider() { val filteredProjection = projection?.intersect(defaultProjection) ?: defaultProjection val cursor = MatrixCursor(filteredProjection.toTypedArray()) - val seedRepository = dependencyContainer.seedRepository runBlocking { seedRepository.delayUntilDataValid() @@ -235,7 +241,6 @@ class WalletContentProvider : ContentProvider() { val filteredProjection = projection?.intersect(defaultProjection) ?: defaultProjection val cursor = MatrixCursor(filteredProjection.toTypedArray()) - val seedRepository = dependencyContainer.seedRepository runBlocking { seedRepository.delayUntilDataValid() if (callerIsPrivileged) { @@ -290,7 +295,6 @@ class WalletContentProvider : ContentProvider() { val filteredProjection = projection?.intersect(defaultProjection) ?: defaultProjection val cursor = MatrixCursor(filteredProjection.toTypedArray()) - val seedRepository = dependencyContainer.seedRepository runBlocking { seedRepository.delayUntilDataValid() } @@ -405,7 +409,6 @@ class WalletContentProvider : ContentProvider() { uid: Int, @WalletContractV1.AuthToken authToken: Long ): Int { - val seedRepository = dependencyContainer.seedRepository runBlocking { seedRepository.delayUntilDataValid() } @@ -464,7 +467,6 @@ class WalletContentProvider : ContentProvider() { @WalletContractV1.AccountId accountId: Long, values: ContentValues? ): Int { - val seedRepository = dependencyContainer.seedRepository runBlocking { seedRepository.delayUntilDataValid() } @@ -512,12 +514,11 @@ class WalletContentProvider : ContentProvider() { private fun checkDependencyInjection() { // Note: this can be executed in an arbitrary thread context. Use double-checked locking // pattern to safely initialize it. - if (!this::dependencyContainer.isInitialized) { - val didInitialization = synchronized(this::dependencyContainer) { - if (!this::dependencyContainer.isInitialized) { - dependencyContainer = - (requireContext().applicationContext as SeedVaultImplApplication) - .dependencyContainer + if (!this::seedRepository.isInitialized) { + val didInitialization = synchronized(this::seedRepository) { + if (!this::seedRepository.isInitialized) { + val hiltEntryPoint = EntryPointAccessors.fromApplication(requireContext().applicationContext, WalletContentProviderHiltEntryPoint::class.java) + seedRepository = hiltEntryPoint.provideSeedRepository() true } else { false @@ -533,7 +534,7 @@ class WalletContentProvider : ContentProvider() { private fun observeSeedRepositoryChanges() { val repositoryOwnerScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) repositoryOwnerScope.launch { - dependencyContainer.seedRepository.changes.collect { change -> + seedRepository.changes.collect { change -> // NOTE: this is overeager; we aren't checking, e.g., if deleting a particular seed // will affect an observer watching a particular account. val uris: List = when (change.category) { diff --git a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/data/SeedRepository.kt b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/data/SeedRepository.kt index 939e9299..d7a8ff31 100644 --- a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/data/SeedRepository.kt +++ b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/data/SeedRepository.kt @@ -390,7 +390,7 @@ class SeedRepository @Inject constructor( @WalletContractV1.AccountId suspend fun addKnownAccountForSeed(id: Long, account: Account): Long { - require(account.id == Account.INVALID_ACCOUNT_ID) { "Accound ID must be invalid" } + require(account.id == Account.INVALID_ACCOUNT_ID) { "Account ID must be invalid" } Log.d(TAG, "ENTER addKnownAccountForSeed") @WalletContractV1.AccountId val accountId: Long diff --git a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/AuthorizeActivity.kt b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/AuthorizeActivity.kt index 373f04dc..e7175fd4 100644 --- a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/AuthorizeActivity.kt +++ b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/AuthorizeActivity.kt @@ -25,29 +25,29 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.solanamobile.seedvault.WalletContractV1 -import com.solanamobile.seedvaultimpl.SeedVaultImplApplication import com.solanamobile.seedvaultimpl.ui.authorize.AuthorizeContents +import com.solanamobile.seedvaultimpl.ui.authorize.AuthorizeViewModel import com.solanamobile.seedvaultimpl.ui.authorizeinfo.AuthorizeInfoContents import com.solanamobile.seedvaultimpl.ui.selectseed.SelectSeedContents import com.solanamobile.seedvaultimpl.ui.selectseed.SelectSeedViewModel import com.solanamobile.ui.apptheme.SolanaTheme +import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.lifecycle.withCreationCallback import kotlinx.coroutines.launch +@AndroidEntryPoint class AuthorizeActivity : ComponentActivity() { - private val activityViewModel: AuthorizeViewModel by viewModels() - private val authorizeViewModel: com.solanamobile.seedvaultimpl.ui.authorize.AuthorizeViewModel by viewModels { - com.solanamobile.seedvaultimpl.ui.authorize.AuthorizeViewModel.provideFactory( - (application as SeedVaultImplApplication).dependencyContainer.seedRepository, - activityViewModel, - application - ) - } - private val selectSeedViewModel: SelectSeedViewModel by viewModels { - SelectSeedViewModel.provideFactory( - (application as SeedVaultImplApplication).dependencyContainer.seedRepository, - activityViewModel - ) - } + private val authorizeCommonViewModel: AuthorizeCommonViewModel by viewModels() + private val authorizeViewModel: AuthorizeViewModel by viewModels(extrasProducer = { + defaultViewModelCreationExtras.withCreationCallback { factory -> + factory.create(authorizeCommonViewModel) + } + }) + private val selectSeedViewModel: SelectSeedViewModel by viewModels(extrasProducer = { + defaultViewModelCreationExtras.withCreationCallback { factory -> + factory.create(authorizeCommonViewModel) + } + }) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -106,7 +106,7 @@ class AuthorizeActivity : ComponentActivity() { navController.navigateUp() }, completeAuthorizationWithError = { - activityViewModel.completeAuthorizationWithError( + authorizeCommonViewModel.completeAuthorizationWithError( WalletContractV1.RESULT_UNSPECIFIED_ERROR ) }, @@ -134,10 +134,10 @@ class AuthorizeActivity : ComponentActivity() { null } - activityViewModel.setRequest(callingActivity, uid, intent) + authorizeCommonViewModel.setRequest(callingActivity, uid, intent) lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { - activityViewModel.events.collect { event -> + authorizeCommonViewModel.events.collect { event -> when (event.event) { AuthorizeEventType.COMPLETE -> { Log.i(TAG, "Returning result=${event.resultCode}/intent=${event.data} from AuthorizeActivity") diff --git a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/AuthorizeViewModel.kt b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/AuthorizeCommonViewModel.kt similarity index 98% rename from SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/AuthorizeViewModel.kt rename to SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/AuthorizeCommonViewModel.kt index d5ade234..cd6c5b1b 100644 --- a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/AuthorizeViewModel.kt +++ b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/AuthorizeCommonViewModel.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @HiltViewModel -class AuthorizeViewModel @Inject constructor() : ViewModel() { +class AuthorizeCommonViewModel @Inject constructor() : ViewModel() { private val _requests = MutableSharedFlow(replay = 1) val requests = _requests.asSharedFlow() @@ -187,7 +187,7 @@ class AuthorizeViewModel @Inject constructor() : ViewModel() { } companion object { - private val TAG = AuthorizeViewModel::class.simpleName + private val TAG = AuthorizeCommonViewModel::class.simpleName } } diff --git a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/authorize/AuthorizeViewModel.kt b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/authorize/AuthorizeViewModel.kt index 2969bb33..7131500d 100644 --- a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/authorize/AuthorizeViewModel.kt +++ b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/authorize/AuthorizeViewModel.kt @@ -10,9 +10,7 @@ import android.content.pm.PackageManager import android.graphics.drawable.Drawable import android.net.Uri import android.util.Log -import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.solanamobile.seedvault.* import com.solanamobile.seedvaultimpl.R @@ -20,9 +18,14 @@ import com.solanamobile.seedvaultimpl.data.SeedRepository import com.solanamobile.seedvaultimpl.model.Account import com.solanamobile.seedvaultimpl.model.Authorization import com.solanamobile.seedvaultimpl.model.Seed +import com.solanamobile.seedvaultimpl.ui.AuthorizeCommonViewModel import com.solanamobile.seedvaultimpl.ui.AuthorizeRequest import com.solanamobile.seedvaultimpl.ui.AuthorizeRequestType import com.solanamobile.seedvaultimpl.usecase.* +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -30,11 +33,15 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class AuthorizeViewModel private constructor( +@HiltViewModel(assistedFactory = AuthorizeViewModel.Factory::class) +class AuthorizeViewModel @AssistedInject constructor( + private val application: Application, private val seedRepository: SeedRepository, - private val activityViewModel: com.solanamobile.seedvaultimpl.ui.AuthorizeViewModel, - private val application: Application -) : AndroidViewModel(application) { + @Assisted private val authorizeCommonViewModel: AuthorizeCommonViewModel, + private val signPayloadUseCase: SignPayloadUseCase, + private val bipDerivationUseCase: BipDerivationUseCase, + private val prepopulateKnownAccountsUseCase: PrepopulateKnownAccountsUseCase, +) : ViewModel() { private val _uiState = MutableStateFlow(AuthorizeUiState()) val uiState = _uiState.asStateFlow() @@ -48,7 +55,7 @@ class AuthorizeViewModel private constructor( init { viewModelScope.launch { - activityViewModel.requests.collect { request -> + authorizeCommonViewModel.requests.collect { request -> if (request.type !is AuthorizeRequestType.Seed && request.type !is AuthorizeRequestType.Signature && request.type !is AuthorizeRequestType.PublicKey) { @@ -71,7 +78,7 @@ class AuthorizeViewModel private constructor( when (request.type) { is AuthorizeRequestType.Seed -> { if (isPrivilegedPermissionGranted()) { - activityViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_NO_AVAILABLE_SEEDS) + authorizeCommonViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_NO_AVAILABLE_SEEDS) return@collect } @@ -85,7 +92,7 @@ class AuthorizeViewModel private constructor( } if (seed == null) { Log.e(TAG, "No non-authorized seeds remaining for ${request.requestorUid}; aborting...") - activityViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_NO_AVAILABLE_SEEDS) + authorizeCommonViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_NO_AVAILABLE_SEEDS) return@collect } this@AuthorizeViewModel.seed = seed @@ -106,7 +113,7 @@ class AuthorizeViewModel private constructor( seed = seedRepository.authorizations.value[authKey] if (seed == null) { Log.e(TAG, "No seed found for ${authKey.authToken}/${authKey.uid}; aborting...") - activityViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_INVALID_AUTH_TOKEN) + authorizeCommonViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_INVALID_AUTH_TOKEN) return@collect } this@AuthorizeViewModel.seed = seed @@ -116,19 +123,19 @@ class AuthorizeViewModel private constructor( this@AuthorizeViewModel.purpose = purpose if (request.type.transactions.any { t -> t.payload.isEmpty() }) { Log.e(TAG, "Only non-empty transaction payloads can be signed") - activityViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_INVALID_PAYLOAD) + authorizeCommonViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_INVALID_PAYLOAD) return@collect } val numTransactions = request.type.transactions.size if (numTransactions > RequestLimitsUseCase.MAX_SIGNING_REQUESTS) { Log.e(TAG, "Too many transactions provided: actual=$numTransactions, max=${RequestLimitsUseCase.MAX_SIGNING_REQUESTS}") - activityViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_IMPLEMENTATION_LIMIT_EXCEEDED) + authorizeCommonViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_IMPLEMENTATION_LIMIT_EXCEEDED) return@collect } val maxRequestedSignatures = request.type.transactions.maxOf { t -> t.requestedSignatures.size } if (maxRequestedSignatures > RequestLimitsUseCase.MAX_REQUESTED_SIGNATURES) { Log.e(TAG, "Too many signatures requested: actual=$maxRequestedSignatures, max=${RequestLimitsUseCase.MAX_REQUESTED_SIGNATURES}") - activityViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_IMPLEMENTATION_LIMIT_EXCEEDED) + authorizeCommonViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_IMPLEMENTATION_LIMIT_EXCEEDED) return@collect } try { @@ -139,7 +146,7 @@ class AuthorizeViewModel private constructor( this@AuthorizeViewModel.normalizedDerivationPaths = normalizedDerivationPaths } catch (e: Exception) { Log.e(TAG, "Failed normalizing BIP derivation paths", e) - activityViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_INVALID_DERIVATION_PATH) + authorizeCommonViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_INVALID_DERIVATION_PATH) return@collect } } @@ -153,7 +160,7 @@ class AuthorizeViewModel private constructor( seed = seedRepository.authorizations.value[authKey] if (seed == null) { Log.e(TAG, "No seed found for ${authKey.authToken}/${authKey.uid}; aborting...") - activityViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_INVALID_AUTH_TOKEN) + authorizeCommonViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_INVALID_AUTH_TOKEN) return@collect } this@AuthorizeViewModel.seed = seed @@ -164,14 +171,14 @@ class AuthorizeViewModel private constructor( val numDerivationPaths = request.type.derivationPaths.size if (numDerivationPaths > RequestLimitsUseCase.MAX_REQUESTED_PUBLIC_KEYS) { Log.e(TAG, "Too many public keys requested: actual=$numDerivationPaths, max=${RequestLimitsUseCase.MAX_REQUESTED_PUBLIC_KEYS}") - activityViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_IMPLEMENTATION_LIMIT_EXCEEDED) + authorizeCommonViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_IMPLEMENTATION_LIMIT_EXCEEDED) return@collect } val normalizedDerivationPaths = try { listOf(normalizeDerivationPaths(request.type.derivationPaths)) } catch (e: Exception) { Log.e(TAG, "Failed normalizing BIP derivation paths", e) - activityViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_INVALID_DERIVATION_PATH) + authorizeCommonViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_INVALID_DERIVATION_PATH) return@collect } doesRequireAuthentication = isAuthRequired(normalizedPaths = normalizedDerivationPaths) @@ -214,7 +221,7 @@ class AuthorizeViewModel private constructor( } fun cancel() { - activityViewModel.completeAuthorizationWithError(Activity.RESULT_CANCELED) + authorizeCommonViewModel.completeAuthorizationWithError(Activity.RESULT_CANCELED) } fun checkEnteredPIN(pin: String) { @@ -224,11 +231,11 @@ class AuthorizeViewModel private constructor( pinFailureCount++ if (pinFailureCount >= MAX_PIN_ATTEMPTS) { Log.e(TAG, "Max PIN attempts reached; aborting...") - activityViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_AUTHENTICATION_FAILED) + authorizeCommonViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_AUTHENTICATION_FAILED) } else { val remaining = MAX_PIN_ATTEMPTS - pinFailureCount Log.w(TAG, "PIN attempt $pinFailureCount failed; $remaining attempts remaining") - showMessage(getApplication().getString(R.string.error_incorrect_pin, remaining)) + showMessage(application.getString(R.string.error_incorrect_pin, remaining)) } return } @@ -280,9 +287,9 @@ class AuthorizeViewModel private constructor( val authToken = seedRepository.authorizeSeedForUid(seed.id, request.requestorUid, purpose) // Ensure that the seed vault contains appropriate known accounts for this authorization purpose - PrepopulateKnownAccountsUseCase(seedRepository).populateKnownAccounts(seed, purpose) + prepopulateKnownAccountsUseCase.populateKnownAccounts(seed, purpose) - activityViewModel.completeAuthorizationWithAuthToken(authToken) + authorizeCommonViewModel.completeAuthorizationWithAuthToken(authToken) } } @@ -291,7 +298,6 @@ class AuthorizeViewModel private constructor( viewModelScope.launch { val signatures = ArrayList(request.type.transactions.size) - val bipDerivationUseCase = BipDerivationUseCase(seedRepository) request.type.transactions.mapIndexedTo(signatures) { i, sr -> val requestNormalizedDerivationPaths = normalizedDerivationPaths[i] val sigs = requestNormalizedDerivationPaths.map { path -> @@ -299,13 +305,13 @@ class AuthorizeViewModel private constructor( withContext(Dispatchers.Default) { val privateKey = bipDerivationUseCase.derivePrivateKey(purpose, seed, path) if (request.type.type == AuthorizeRequestType.Signature.Type.Transaction) - SignPayloadUseCase.signTransaction( + signPayloadUseCase.signTransaction( purpose, privateKey, sr.payload ) else - SignPayloadUseCase.signMessage( + signPayloadUseCase.signMessage( purpose, privateKey, sr.payload @@ -317,13 +323,13 @@ class AuthorizeViewModel private constructor( // However, the caller should have verified the account was valid before // using it, and discovered it was invalid then. Thus, the use of this // derivation path can be considered invalid. - activityViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_INVALID_DERIVATION_PATH) + authorizeCommonViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_INVALID_DERIVATION_PATH) return@launch } } SigningResponse(sigs, requestNormalizedDerivationPaths.map { path -> path.toUri() }) } - activityViewModel.completeAuthorizationWithSignatures(signatures) + authorizeCommonViewModel.completeAuthorizationWithSignatures(signatures) } } @@ -332,7 +338,6 @@ class AuthorizeViewModel private constructor( viewModelScope.launch { val publicKeys = ArrayList(normalizedDerivationPaths.size) - val bipDerivationUseCase = BipDerivationUseCase(seedRepository) normalizedDerivationPaths.mapTo(publicKeys) { path -> val pathUri = path.toUri() val publicKey = publicKeyForPath(pathUri) ?: run { @@ -350,7 +355,7 @@ class AuthorizeViewModel private constructor( } PublicKeyResponse(publicKey, publicKey?.let { Base58EncodeUseCase(it) }, pathUri) } - activityViewModel.completeAuthorizationWithPublicKeys(publicKeys) + authorizeCommonViewModel.completeAuthorizationWithPublicKeys(publicKeys) } } } @@ -387,17 +392,11 @@ class AuthorizeViewModel private constructor( private val TAG = AuthorizeViewModel::class.simpleName private const val SHOW_PIN_ENTRY_AFTER_NUM_BIOMETRIC_FAILURES = 3 private const val MAX_PIN_ATTEMPTS = 5 + } - fun provideFactory( - seedRepository: SeedRepository, - activityViewModel: com.solanamobile.seedvaultimpl.ui.AuthorizeViewModel, - application: Application - ): ViewModelProvider.Factory = object : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return AuthorizeViewModel(seedRepository, activityViewModel, application) as T - } - } + @AssistedFactory + interface Factory { + fun create(activityViewModel: AuthorizeCommonViewModel): AuthorizeViewModel } } diff --git a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/seeddetail/SeedDetailViewModel.kt b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/seeddetail/SeedDetailViewModel.kt index e5f7e348..e5eed1a2 100644 --- a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/seeddetail/SeedDetailViewModel.kt +++ b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/seeddetail/SeedDetailViewModel.kt @@ -24,7 +24,8 @@ import kotlin.random.Random @HiltViewModel class SeedDetailViewModel @Inject constructor( - private val seedRepository: SeedRepository + private val seedRepository: SeedRepository, + private val prepopulateKnownAccountsUseCase: PrepopulateKnownAccountsUseCase, ) : ViewModel() { private val _seedDetailUiState: MutableStateFlow = MutableStateFlow(SeedDetailUiState()) val seedDetailUiState = _seedDetailUiState.asStateFlow() @@ -149,10 +150,8 @@ class SeedDetailViewModel @Inject constructor( // Pre-populate known accounts for all purposes val seed = seedRepository.seeds.value[seedId]!! - PrepopulateKnownAccountsUseCase(seedRepository).apply { - for (purpose in Authorization.Purpose.entries) { - populateKnownAccounts(seed, purpose) - } + for (purpose in Authorization.Purpose.entries) { + prepopulateKnownAccountsUseCase.populateKnownAccounts(seed, purpose) } mode.authorize?.let { authorize -> diff --git a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/selectseed/SelectSeedViewModel.kt b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/selectseed/SelectSeedViewModel.kt index 1a2b36ec..f3f62645 100644 --- a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/selectseed/SelectSeedViewModel.kt +++ b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/selectseed/SelectSeedViewModel.kt @@ -6,21 +6,25 @@ package com.solanamobile.seedvaultimpl.ui.selectseed import android.util.Log import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.solanamobile.seedvault.WalletContractV1 import com.solanamobile.seedvaultimpl.data.SeedRepository import com.solanamobile.seedvaultimpl.model.Authorization import com.solanamobile.seedvaultimpl.model.Seed import com.solanamobile.seedvaultimpl.ui.AuthorizeRequestType -import com.solanamobile.seedvaultimpl.ui.AuthorizeViewModel +import com.solanamobile.seedvaultimpl.ui.AuthorizeCommonViewModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -class SelectSeedViewModel private constructor( +@HiltViewModel(assistedFactory = SelectSeedViewModel.Factory::class) +class SelectSeedViewModel @AssistedInject constructor( private val seedRepository: SeedRepository, - private val activityViewModel: AuthorizeViewModel + @Assisted private val authorizeCommonViewModel: AuthorizeCommonViewModel ) : ViewModel() { private val _selectSeedUiState: MutableStateFlow = MutableStateFlow(SelectSeedUiState()) val selectSeedUiState = _selectSeedUiState.asStateFlow() @@ -30,7 +34,7 @@ class SelectSeedViewModel private constructor( init { viewModelScope.launch { - activityViewModel.requests.collect { request -> + authorizeCommonViewModel.requests.collect { request -> if (request.type !is AuthorizeRequestType.Seed) { // Any other request types should only be observed transiently, whilst the // activity state is being updated. @@ -48,7 +52,7 @@ class SelectSeedViewModel private constructor( if (uid == Authorization.INVALID_UID) { Log.e(TAG, "No UID provided; canceling authorization") - activityViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_UNSPECIFIED_ERROR) + authorizeCommonViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_UNSPECIFIED_ERROR) return } else if (this.uid == uid) { return // no change in UID, return without (re-)creating the relevant coroutine job @@ -72,7 +76,7 @@ class SelectSeedViewModel private constructor( } if (seeds.isEmpty()) { Log.w(TAG, "No non-authorized seeds remaining for UID $uid; aborting...") - activityViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_NO_AVAILABLE_SEEDS) + authorizeCommonViewModel.completeAuthorizationWithError(WalletContractV1.RESULT_NO_AVAILABLE_SEEDS) return@collect } val selectedSeedId = seeds.find { seed -> seed.id == currentSeedId }?.id ?: seeds[0].id @@ -98,21 +102,16 @@ class SelectSeedViewModel private constructor( Log.d(TAG, "setSelectedSeed for seedId=$selectedSeedId/uid=$uid") - activityViewModel.updateAuthorizeSeedRequestWithSeedId(selectedSeedId) + authorizeCommonViewModel.updateAuthorizeSeedRequestWithSeedId(selectedSeedId) } companion object { private val TAG = SelectSeedViewModel::class.simpleName + } - fun provideFactory( - seedRepository: SeedRepository, - activityViewModel: AuthorizeViewModel - ): ViewModelProvider.Factory = object : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return SelectSeedViewModel(seedRepository, activityViewModel) as T - } - } + @AssistedFactory + interface Factory { + fun create(activityViewModel: AuthorizeCommonViewModel): SelectSeedViewModel } } diff --git a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/usecase/BipDerivationUseCase.kt b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/usecase/BipDerivationUseCase.kt index e6509dda..0baad995 100644 --- a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/usecase/BipDerivationUseCase.kt +++ b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/usecase/BipDerivationUseCase.kt @@ -8,11 +8,15 @@ import com.solanamobile.seedvault.Bip32DerivationPath import com.solanamobile.seedvault.BipLevel import com.solanamobile.seedvault.Bip44DerivationPath import com.solanamobile.seedvault.BipDerivationPath -import com.solanamobile.seedvaultimpl.data.SeedRepository import com.solanamobile.seedvaultimpl.model.Authorization import com.solanamobile.seedvaultimpl.model.Seed +import javax.inject.Inject +import javax.inject.Singleton -class BipDerivationUseCase(private val seedRepository: SeedRepository) { +@Singleton +class BipDerivationUseCase @Inject constructor( + private val ed25519Slip10UseCase: Ed25519Slip10UseCase +) { // TODO: also support Ed25519Bip32 derivations // Opaque object representing a partial derivation from a BIP32 path. Further derivations can @@ -29,7 +33,7 @@ class BipDerivationUseCase(private val seedRepository: SeedRepository) { derivationPath: Bip32DerivationPath ): ByteArray { return when (purpose) { - Authorization.Purpose.SIGN_SOLANA_TRANSACTIONS -> Ed25519Slip10UseCase.derivePrivateKey( + Authorization.Purpose.SIGN_SOLANA_TRANSACTIONS -> ed25519Slip10UseCase.derivePrivateKey( seed.details, derivationPath) } } @@ -42,7 +46,7 @@ class BipDerivationUseCase(private val seedRepository: SeedRepository) { ): ByteArray { return when (purpose) { Authorization.Purpose.SIGN_SOLANA_TRANSACTIONS -> - Ed25519Slip10UseCase.derivePublicKey(seed.details, derivationPath, partialPublicDerivation) + ed25519Slip10UseCase.derivePublicKey(seed.details, derivationPath, partialPublicDerivation) } } @@ -53,7 +57,7 @@ class BipDerivationUseCase(private val seedRepository: SeedRepository) { ): PartialPublicDerivation { return when (purpose) { Authorization.Purpose.SIGN_SOLANA_TRANSACTIONS -> - Ed25519Slip10UseCase.derivePublicKeyPartialDerivation(seed.details, derivationPath) + ed25519Slip10UseCase.derivePublicKeyPartialDerivation(seed.details, derivationPath) } } } diff --git a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/usecase/Ed25519Slip10UseCase.kt b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/usecase/Ed25519Slip10UseCase.kt index c9f0ae67..dabedfc7 100644 --- a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/usecase/Ed25519Slip10UseCase.kt +++ b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/usecase/Ed25519Slip10UseCase.kt @@ -6,15 +6,18 @@ package com.solanamobile.seedvaultimpl.usecase import android.util.Log import androidx.annotation.Size +import com.goterl.lazysodium.LazySodiumAndroid import com.goterl.lazysodium.exceptions.SodiumException import com.solanamobile.seedvault.Bip32DerivationPath import com.solanamobile.seedvault.WalletContractV1 -import com.solanamobile.seedvaultimpl.ApplicationDependencyContainer import com.solanamobile.seedvaultimpl.model.SeedDetails import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec +import javax.inject.Inject +import javax.inject.Singleton -object Ed25519Slip10UseCase { +@Singleton +class Ed25519Slip10UseCase @Inject constructor(private val sodium: LazySodiumAndroid) { private data class KeyDerivationMaterial( @Size(SignPayloadUseCase.ED25519_SECRET_KEY_SIZE) val k: ByteArray, @Size(SignPayloadUseCase.ED25519_SECRET_KEY_SIZE) val c: ByteArray, @@ -38,10 +41,12 @@ object Ed25519Slip10UseCase { } } - private val TAG = Ed25519Slip10UseCase::class.simpleName + companion object { + private val TAG = Ed25519Slip10UseCase::class.simpleName - private const val MASTER_SECRET_MAC_KEY = "ed25519 seed" - private const val MAC = "HmacSHA512" + private const val MASTER_SECRET_MAC_KEY = "ed25519 seed" + private const val MAC = "HmacSHA512" + } @Size(SignPayloadUseCase.ED25519_SECRET_KEY_SIZE) fun derivePrivateKey( @@ -51,7 +56,7 @@ object Ed25519Slip10UseCase { Log.d(TAG, "Deriving private key from root") val kdm = deriveSecretKey(seed, bip32DerivationPath) val keyPair = try { - ApplicationDependencyContainer.sodium.cryptoSignSeedKeypair(kdm.k) + sodium.cryptoSignSeedKeypair(kdm.k) } catch (_: SodiumException) { throw BipDerivationUseCase.KeyDoesNotExistException("Key does not exist for $bip32DerivationPath") } @@ -67,7 +72,7 @@ object Ed25519Slip10UseCase { Log.d(TAG, "Deriving public key from derivationRoot=${if (derivationRoot != null) "partial" else "root"}") val kdm = deriveSecretKey(seed, bip32DerivationPath, derivationRoot as KeyDerivationMaterial?) val keyPair = try { - ApplicationDependencyContainer.sodium.cryptoSignSeedKeypair(kdm.k) + sodium.cryptoSignSeedKeypair(kdm.k) } catch (_: SodiumException) { throw BipDerivationUseCase.KeyDoesNotExistException("Key does not exist for $bip32DerivationPath") } diff --git a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/usecase/PrepopulateKnownAccountsUseCase.kt b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/usecase/PrepopulateKnownAccountsUseCase.kt index 4da57a5d..1cf8aaae 100644 --- a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/usecase/PrepopulateKnownAccountsUseCase.kt +++ b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/usecase/PrepopulateKnownAccountsUseCase.kt @@ -11,8 +11,14 @@ import com.solanamobile.seedvaultimpl.data.SeedRepository import com.solanamobile.seedvaultimpl.model.Account import com.solanamobile.seedvaultimpl.model.Authorization import com.solanamobile.seedvaultimpl.model.Seed +import javax.inject.Inject +import javax.inject.Singleton -class PrepopulateKnownAccountsUseCase(private val seedRepository: SeedRepository) { +@Singleton +class PrepopulateKnownAccountsUseCase @Inject constructor( + private val seedRepository: SeedRepository, + private val ed25519Slip10UseCase: Ed25519Slip10UseCase +) { suspend fun populateKnownAccounts( seed: Seed, purpose: Authorization.Purpose @@ -23,7 +29,7 @@ class PrepopulateKnownAccountsUseCase(private val seedRepository: SeedRepository .appendLevel(BipLevel(BIP44_PURPOSE, true)) .appendLevel(BipLevel(BIP44_COIN_TYPE_SOLANA, true)) .build().normalize(purpose) - val derivationRoot = Ed25519Slip10UseCase.derivePublicKeyPartialDerivation( + val derivationRoot = ed25519Slip10UseCase.derivePublicKeyPartialDerivation( seed.details, derivationRootPath ) val knownAccounts = mutableListOf() @@ -47,7 +53,7 @@ class PrepopulateKnownAccountsUseCase(private val seedRepository: SeedRepository .normalize(purpose) try { - val publicKey = Ed25519Slip10UseCase.derivePublicKey( + val publicKey = ed25519Slip10UseCase.derivePublicKey( seed.details, partialPath, derivationRoot @@ -78,7 +84,7 @@ class PrepopulateKnownAccountsUseCase(private val seedRepository: SeedRepository .appendLevels(type2Levels) .build() try { - val publicKey = Ed25519Slip10UseCase.derivePublicKey( + val publicKey = ed25519Slip10UseCase.derivePublicKey( seed.details, partialPath, derivationRoot diff --git a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/usecase/SignPayloadUseCase.kt b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/usecase/SignPayloadUseCase.kt index 6ac72053..28e843cb 100644 --- a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/usecase/SignPayloadUseCase.kt +++ b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/usecase/SignPayloadUseCase.kt @@ -5,14 +5,19 @@ package com.solanamobile.seedvaultimpl.usecase import androidx.annotation.Size -import com.solanamobile.seedvaultimpl.ApplicationDependencyContainer +import com.goterl.lazysodium.LazySodiumAndroid import com.solanamobile.seedvaultimpl.model.Authorization import com.goterl.lazysodium.interfaces.Sign - -object SignPayloadUseCase { - const val ED25519_SECRET_KEY_SIZE = Sign.ED25519_SECRETKEYBYTES.toLong() - const val ED25519_PUBLIC_KEY_SIZE = Sign.ED25519_PUBLICKEYBYTES.toLong() - const val ED25519_SIGNATURE_SIZE = Sign.ED25519_BYTES.toLong() +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SignPayloadUseCase @Inject constructor(private val sodium: LazySodiumAndroid) { + companion object { + const val ED25519_SECRET_KEY_SIZE = Sign.ED25519_SECRETKEYBYTES.toLong() + const val ED25519_PUBLIC_KEY_SIZE = Sign.ED25519_PUBLICKEYBYTES.toLong() + const val ED25519_SIGNATURE_SIZE = Sign.ED25519_BYTES.toLong() + } fun signTransaction( purpose: Authorization.Purpose, @@ -52,8 +57,7 @@ object SignPayloadUseCase { @Size(min=1) transaction: ByteArray ): ByteArray { val signature = ByteArray(Sign.ED25519_BYTES) - ApplicationDependencyContainer.sodium.cryptoSignDetached(signature, transaction, - transaction.size.toLong(), key) + sodium.cryptoSignDetached(signature, transaction, transaction.size.toLong(), key) return signature } } \ No newline at end of file diff --git a/cts/src/main/java/com/solanamobile/seedvault/cts/data/tests/ShowSeedSettingsTestCase.kt b/cts/src/main/java/com/solanamobile/seedvault/cts/data/tests/ShowSeedSettingsTestCase.kt index 574201ab..a3dcb172 100644 --- a/cts/src/main/java/com/solanamobile/seedvault/cts/data/tests/ShowSeedSettingsTestCase.kt +++ b/cts/src/main/java/com/solanamobile/seedvault/cts/data/tests/ShowSeedSettingsTestCase.kt @@ -19,7 +19,6 @@ import com.solanamobile.seedvault.cts.data.TestSessionLogger import com.solanamobile.seedvault.cts.data.conditioncheckers.HasSeedVaultPermissionChecker import com.solanamobile.seedvault.cts.data.conditioncheckers.KnownSeed12AuthorizedChecker import com.solanamobile.seedvault.cts.data.testdata.KnownSeed12 -import com.solanamobile.seedvault.cts.data.tests.ShowSeedSettingsTestCase.ShowSeedSettingsIntentContract import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.time.withTimeout import java.time.Duration