Updated on 2026-08-14
This commit is contained in:
parent
03f4f51fc7
commit
2f91afa922
67 changed files with 623 additions and 371 deletions
|
|
@ -11,9 +11,14 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.data.common)
|
||||
|
||||
/** Tangem libraries */
|
||||
implementation(tangemDeps.blockchain) // android-library
|
||||
implementation(tangemDeps.blockchain)
|
||||
implementation(tangemDeps.card.core)
|
||||
implementation(tangemDeps.hot.core)
|
||||
implementation(projects.libs.tangemSdkApi)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
|
||||
/** Core */
|
||||
implementation(projects.core.datasource)
|
||||
|
|
@ -21,6 +26,7 @@ dependencies {
|
|||
|
||||
/** Domain */
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.card)
|
||||
api(projects.domain.models)
|
||||
|
||||
/** Domain models */
|
||||
|
|
@ -29,15 +35,17 @@ dependencies {
|
|||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
implementation(project(":domain:legacy"))
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Other deps */
|
||||
implementation(deps.androidx.datastore)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.timber)
|
||||
|
||||
/** tests */
|
||||
testImplementation(projects.domain.models)
|
||||
testImplementation(projects.common.test)
|
||||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.truth)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
package com.tangem.data.wallets.cold
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.data.wallets.derivations.Derivations
|
||||
import com.tangem.data.wallets.derivations.MissedDerivationsFinder
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository
|
||||
import com.tangem.domain.wallets.usecase.BackendId
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private typealias DerivedKeys = Map<ByteArrayKey, ExtendedPublicKeysMap>
|
||||
|
||||
internal class DefaultColdMapDerivationsRepository @Inject constructor(
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val networkFactory: NetworkFactory,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : ColdMapDerivationsRepository {
|
||||
|
||||
override suspend fun derivePublicKeys(
|
||||
userWallet: UserWallet.Cold,
|
||||
currencies: List<CryptoCurrency>,
|
||||
): UserWallet.Cold = withContext(dispatchers.io) {
|
||||
derivePublicKeysByNetworks(userWallet = userWallet, networks = currencies.map(CryptoCurrency::network))
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeysByNetworkIds(
|
||||
userWallet: UserWallet.Cold,
|
||||
networkIds: List<Network.RawID>,
|
||||
): UserWallet.Cold = withContext(dispatchers.io) {
|
||||
derivePublicKeysByNetworks(
|
||||
userWallet = userWallet,
|
||||
networks = networkIds.mapNotNull {
|
||||
networkFactory.create(
|
||||
blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null,
|
||||
extraDerivationPath = null,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeysByNetworks(
|
||||
userWallet: UserWallet.Cold,
|
||||
networks: List<Network>,
|
||||
): UserWallet.Cold = withContext(dispatchers.io) {
|
||||
if (!userWallet.scanResponse.card.settings.isHDWalletAllowed) {
|
||||
Timber.d("Nothing to derive")
|
||||
return@withContext userWallet
|
||||
}
|
||||
|
||||
val derivations = MissedDerivationsFinder(userWallet)
|
||||
.findByNetworks(networks)
|
||||
.ifEmpty {
|
||||
Timber.d("Nothing to derive")
|
||||
return@withContext userWallet
|
||||
}
|
||||
|
||||
return@withContext derivePublicKeys(userWallet = userWallet, derivations = derivations).first
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeys(
|
||||
userWallet: UserWallet.Cold,
|
||||
derivations: Map<ByteArrayKey, List<DerivationPath>>,
|
||||
): Pair<UserWallet.Cold, Map<ByteArrayKey, ExtendedPublicKeysMap>> = withContext(dispatchers.io) {
|
||||
// todo replace it in task [REDACTED_JIRA]
|
||||
val preflightReadFilter = UserWalletIdPreflightReadFilter(userWallet.walletId)
|
||||
val result = tangemSdkManager.derivePublicKeys(
|
||||
cardId = null,
|
||||
derivations = derivations,
|
||||
preflightReadFilter = preflightReadFilter,
|
||||
)
|
||||
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
userWallet.updateDerivedKeys(result.data.entries).also {
|
||||
validateDerivations(scanResponse = it.scanResponse, derivations = derivations)
|
||||
} to result.data.entries
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
throw result.error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun hasMissedDerivations(
|
||||
userWallet: UserWallet.Cold,
|
||||
networksWithDerivationPath: Map<BackendId, String?>,
|
||||
): Boolean = withContext(dispatchers.io) {
|
||||
val derivations =
|
||||
MissedDerivationsFinder(userWallet)
|
||||
.findByNetworks(
|
||||
networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) ->
|
||||
networkFactory.create(
|
||||
blockchain = Blockchain.fromNetworkId(backendId) ?: return@mapNotNull null,
|
||||
extraDerivationPath = extraDerivationPath,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
derivations.isNotEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* It throws an exception if any of the provided derivations are invalid
|
||||
* Validation for NonHardened moved to application layer, to avoid fails when derive multiple paths
|
||||
* It needs to be called after success [derivePublicKeys] or in same flows
|
||||
*/
|
||||
private fun validateDerivations(scanResponse: ScanResponse, derivations: Derivations) {
|
||||
derivations.entries.forEach { derivationForKey ->
|
||||
val wallet = scanResponse.card.wallets.firstOrNull { it.publicKey.toMapKey() == derivationForKey.key }
|
||||
if (wallet == null) return@forEach
|
||||
val hasHardenedNodes = derivationForKey.value.any { path -> path.nodes.any { node -> !node.isHardened } }
|
||||
if (wallet.curve == EllipticCurve.Ed25519Slip0010 && hasHardenedNodes) {
|
||||
throw TangemSdkError.NonHardenedDerivationNotSupported()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun UserWallet.Cold.updateDerivedKeys(keys: DerivedKeys): UserWallet.Cold {
|
||||
return copy(
|
||||
scanResponse = scanResponse.copy(
|
||||
derivedKeys = getUpdatedDerivedKeys(oldKeys = scanResponse.derivedKeys, newKeys = keys),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getUpdatedDerivedKeys(oldKeys: DerivedKeys, newKeys: DerivedKeys): DerivedKeys {
|
||||
return (oldKeys.keys + newKeys.keys).toSet()
|
||||
.associateWith { walletKey ->
|
||||
val oldDerivations = ExtendedPublicKeysMap(oldKeys[walletKey] ?: emptyMap())
|
||||
val newDerivations = newKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
|
||||
ExtendedPublicKeysMap(oldDerivations + newDerivations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.data.wallets.cold
|
||||
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.core.SessionEnvironment
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.operations.preflightread.PreflightReadFilter
|
||||
|
||||
/**
|
||||
* [PreflightReadFilter] for checking if card has expected user wallet id
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class UserWalletIdPreflightReadFilter(private val expectedUserWalletId: UserWalletId) : PreflightReadFilter {
|
||||
|
||||
override fun onCardRead(card: Card, environment: SessionEnvironment) = Unit
|
||||
|
||||
override fun onFullCardRead(card: Card, environment: SessionEnvironment) {
|
||||
val actualUserWalletId = UserWalletIdBuilder.card(card = CardDTO(card)).build() ?: return
|
||||
|
||||
if (expectedUserWalletId != actualUserWalletId) throw TangemSdkError.WalletNotFound()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package com.tangem.data.wallets.derivations
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.map
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository
|
||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||
import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.BackendId
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultDerivationsRepository @Inject constructor(
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val hotDerivationsRepository: HotMapDerivationsRepository,
|
||||
private val coldDerivationsRepository: ColdMapDerivationsRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : DerivationsRepository {
|
||||
|
||||
override suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
|
||||
derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network))
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeysByNetworkIds(userWalletId: UserWalletId, networkIds: List<Network.RawID>) {
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
when (userWallet) {
|
||||
is UserWallet.Cold -> coldDerivationsRepository.derivePublicKeysByNetworkIds(userWallet, networkIds)
|
||||
is UserWallet.Hot -> hotDerivationsRepository.derivePublicKeysByNetworkIds(userWallet, networkIds)
|
||||
}.also {
|
||||
userWallet.update(it)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List<Network>) {
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
when (userWallet) {
|
||||
is UserWallet.Cold -> coldDerivationsRepository.derivePublicKeysByNetworks(userWallet, networks)
|
||||
is UserWallet.Hot -> hotDerivationsRepository.derivePublicKeysByNetworks(userWallet, networks)
|
||||
}.also {
|
||||
userWallet.update(it)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeys(
|
||||
userWalletId: UserWalletId,
|
||||
derivations: Map<ByteArrayKey, List<DerivationPath>>,
|
||||
): Map<ByteArrayKey, ExtendedPublicKeysMap> {
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
return when (userWallet) {
|
||||
is UserWallet.Cold -> coldDerivationsRepository.derivePublicKeys(userWallet, derivations)
|
||||
is UserWallet.Hot -> hotDerivationsRepository.derivePublicKeys(userWallet, derivations)
|
||||
}.let {
|
||||
userWallet.update(it.first)
|
||||
it.second
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun hasMissedDerivations(
|
||||
userWalletId: UserWalletId,
|
||||
networksWithDerivationPath: Map<BackendId, String?>,
|
||||
): Boolean {
|
||||
return when (val userWallet = userWalletsStore.getSyncStrict(userWalletId)) {
|
||||
is UserWallet.Cold -> coldDerivationsRepository.hasMissedDerivations(userWallet, networksWithDerivationPath)
|
||||
is UserWallet.Hot -> hotDerivationsRepository.hasMissedDerivations(userWallet, networksWithDerivationPath)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun UserWallet.update(newUserWallet: UserWallet) = withContext(dispatchers.io) {
|
||||
check(this@update.walletId == newUserWallet.walletId) {
|
||||
"Cannot update UserWallet with different walletId: ${newUserWallet.walletId}"
|
||||
}
|
||||
|
||||
if (this@update == newUserWallet) {
|
||||
return@withContext // No update needed
|
||||
}
|
||||
|
||||
val updateResult = userWalletsStore.update(
|
||||
userWalletId = newUserWallet.walletId,
|
||||
update = { userWalletToUpdate -> newUserWallet },
|
||||
)
|
||||
|
||||
when (updateResult) {
|
||||
is CompletionResult.Failure -> throw updateResult.error
|
||||
is CompletionResult.Success -> updateResult.data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
package com.tangem.data.wallets.derivations
|
||||
|
||||
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.card.configs.CardConfig
|
||||
import com.tangem.domain.card.configs.Wallet2CardConfig
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.scan.KeyWalletPublicKey
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import kotlin.collections.forEach
|
||||
|
||||
private typealias DerivationData = Pair<ByteArrayKey, List<DerivationPath>>
|
||||
internal typealias Derivations = Map<ByteArrayKey, List<DerivationPath>>
|
||||
|
||||
/**
|
||||
* Finder of missed derivations
|
||||
*
|
||||
* @property userWallet User wallet to find derivations for
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class MissedDerivationsFinder(private val userWallet: UserWallet) {
|
||||
|
||||
/** Find missed derivations for given currencies [currencies] */
|
||||
fun find(currencies: List<CryptoCurrency>): Derivations {
|
||||
return currencies.map { it.network }.let(::findByNetworks)
|
||||
}
|
||||
|
||||
fun findByNetworks(networks: List<Network>): Derivations {
|
||||
return buildMap<ByteArrayKey, MutableList<DerivationPath>> {
|
||||
networks
|
||||
.mapToNewDerivations()
|
||||
.forEach { data ->
|
||||
val current = this[data.first]
|
||||
if (current != null) {
|
||||
current.addAll(data.second)
|
||||
current.distinct()
|
||||
} else {
|
||||
this[data.first] = data.second.toMutableList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Network>.mapToNewDerivations(): List<DerivationData> {
|
||||
val config = when (userWallet) {
|
||||
is UserWallet.Cold -> CardConfig.createConfig(userWallet.scanResponse.card)
|
||||
is UserWallet.Hot -> Wallet2CardConfig // TODO create config [REDACTED_TASK_KEY]
|
||||
}
|
||||
return mapNotNull { network ->
|
||||
val blockchain = network.toBlockchain()
|
||||
val curve = config.primaryCurve(blockchain) ?: return@mapNotNull null
|
||||
|
||||
val walletPublicKey = when (userWallet) {
|
||||
is UserWallet.Cold -> {
|
||||
val wallet = userWallet.scanResponse.card.wallets.firstOrNull { it.curve == curve }
|
||||
wallet?.publicKey
|
||||
}
|
||||
is UserWallet.Hot -> {
|
||||
val wallet = userWallet.wallets?.firstOrNull { it.curve == curve }
|
||||
wallet?.publicKey
|
||||
}
|
||||
}
|
||||
|
||||
walletPublicKey?.let {
|
||||
findNewDerivations(curve = curve, publicKey = it, network = network)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun findNewDerivations(curve: EllipticCurve, publicKey: ByteArray, network: Network): DerivationData? {
|
||||
val derivationCandidates = network
|
||||
.getDerivationCandidates(curve)
|
||||
.ifEmpty { return null }
|
||||
.filterAlreadyDerivedKeys(publicKey.toMapKey())
|
||||
.ifEmpty { return null }
|
||||
|
||||
return publicKey.toMapKey() to derivationCandidates
|
||||
}
|
||||
|
||||
private fun Network.getDerivationCandidates(curve: EllipticCurve): List<DerivationPath> {
|
||||
val blockchain = this.toBlockchain()
|
||||
|
||||
return buildList {
|
||||
add(blockchain.getDerivationPath(curve = curve))
|
||||
add(blockchain.getCustomDerivationPath(curve = curve, network = this@getDerivationCandidates))
|
||||
add(blockchain.getCardanoDerivationPathIfNeeded(network = this@getDerivationCandidates))
|
||||
}
|
||||
.filterNotNull()
|
||||
.distinct()
|
||||
}
|
||||
|
||||
private fun Blockchain.getDerivationPath(curve: EllipticCurve): DerivationPath? {
|
||||
return if (getSupportedCurves().contains(curve)) {
|
||||
derivationPath(style = userWallet.derivationStyleProvider.getDerivationStyle())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Blockchain.getCustomDerivationPath(curve: EllipticCurve, network: Network): DerivationPath? {
|
||||
return if (getSupportedCurves().contains(curve)) {
|
||||
network.derivationPath.value?.let(::DerivationPath)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Blockchain.getCardanoDerivationPathIfNeeded(network: Network): DerivationPath? {
|
||||
return if (this == Blockchain.Cardano) {
|
||||
network.derivationPath.value?.let {
|
||||
CardanoUtils.extendedDerivationPath(derivationPath = DerivationPath(it))
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<DerivationPath>.filterAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List<DerivationPath> {
|
||||
val alreadyDerivedPaths = getAlreadyDerivedKeys(publicKey)
|
||||
return filterNot(alreadyDerivedPaths::contains)
|
||||
}
|
||||
|
||||
private fun getAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List<DerivationPath> {
|
||||
val extendedPublicKeysMap = when (userWallet) {
|
||||
is UserWallet.Cold -> userWallet.scanResponse.derivedKeys[publicKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
is UserWallet.Hot -> {
|
||||
val wallets = userWallet.wallets ?: return emptyList()
|
||||
wallets.firstOrNull { it.publicKey.contentEquals(publicKey.bytes) }?.derivedKeys
|
||||
?: ExtendedPublicKeysMap(emptyMap())
|
||||
}
|
||||
}
|
||||
|
||||
return extendedPublicKeysMap.keys.toList()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,14 +2,21 @@ package com.tangem.data.wallets.di
|
|||
|
||||
import com.tangem.data.wallets.DefaultWalletNamesMigrationRepository
|
||||
import com.tangem.data.wallets.DefaultWalletsRepository
|
||||
import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository
|
||||
import com.tangem.data.wallets.derivations.DefaultDerivationsRepository
|
||||
import com.tangem.data.wallets.hot.DefaultHotMapDerivationsRepository
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository
|
||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||
import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository
|
||||
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -44,4 +51,21 @@ internal object WalletsDataModule {
|
|||
fun provideMigrateNamesRepository(appPreferencesStore: AppPreferencesStore): WalletNamesMigrationRepository {
|
||||
return DefaultWalletNamesMigrationRepository(appPreferencesStore)
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface WalletsDataBindsModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindDerivationsRepository(impl: DefaultDerivationsRepository): DerivationsRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindHotMapDerivationsRepository(impl: DefaultHotMapDerivationsRepository): HotMapDerivationsRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindColdMapDerivationsRepository(impl: DefaultColdMapDerivationsRepository): ColdMapDerivationsRepository
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
package com.tangem.data.wallets.hot
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.data.wallets.derivations.MissedDerivationsFinder
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository
|
||||
import com.tangem.domain.wallets.usecase.BackendId
|
||||
import com.tangem.hot.sdk.model.DeriveWalletRequest
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultHotMapDerivationsRepository @Inject constructor(
|
||||
private val networkFactory: NetworkFactory,
|
||||
private val hotWalletAccessor: HotWalletAccessor,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : HotMapDerivationsRepository {
|
||||
|
||||
override suspend fun derivePublicKeys(
|
||||
userWallet: UserWallet.Hot,
|
||||
currencies: List<CryptoCurrency>,
|
||||
): UserWallet.Hot {
|
||||
return derivePublicKeysByNetworks(userWallet = userWallet, networks = currencies.map(CryptoCurrency::network))
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeysByNetworkIds(
|
||||
userWallet: UserWallet.Hot,
|
||||
networkIds: List<Network.RawID>,
|
||||
): UserWallet.Hot {
|
||||
return derivePublicKeysByNetworks(
|
||||
userWallet = userWallet,
|
||||
networks = networkIds.mapNotNull {
|
||||
networkFactory.create(
|
||||
blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null,
|
||||
extraDerivationPath = null,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeysByNetworks(
|
||||
userWallet: UserWallet.Hot,
|
||||
networks: List<Network>,
|
||||
): UserWallet.Hot = withContext(dispatchers.default) {
|
||||
val derivations = MissedDerivationsFinder(userWallet)
|
||||
.findByNetworks(networks)
|
||||
.ifEmpty {
|
||||
Timber.d("Nothing to derive")
|
||||
return@withContext userWallet
|
||||
}
|
||||
|
||||
derivePublicKeys(userWallet, derivations).first
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeys(
|
||||
userWallet: UserWallet.Hot,
|
||||
derivations: Map<ByteArrayKey, List<DerivationPath>>,
|
||||
): Pair<UserWallet.Hot, Map<ByteArrayKey, ExtendedPublicKeysMap>> {
|
||||
val wallets = userWallet.wallets ?: return userWallet to emptyMap()
|
||||
|
||||
val request = DeriveWalletRequest(
|
||||
derivations.map { entry ->
|
||||
val wallet = wallets.first { it.publicKey.contentEquals(entry.key.bytes) }
|
||||
DeriveWalletRequest.Request(
|
||||
curve = wallet.curve,
|
||||
paths = entry.value,
|
||||
)
|
||||
},
|
||||
)
|
||||
val result = hotWalletAccessor.derivePublicKeys(
|
||||
hotWalletId = userWallet.hotWalletId,
|
||||
request = request,
|
||||
)
|
||||
val newKeys =
|
||||
result.responses.associate { ByteArrayKey(it.seedKey.publicKey) to ExtendedPublicKeysMap(it.publicKeys) }
|
||||
|
||||
return userWallet.updateWithNewKeys(newKeys) to newKeys
|
||||
}
|
||||
|
||||
override suspend fun hasMissedDerivations(
|
||||
userWallet: UserWallet.Hot,
|
||||
networksWithDerivationPath: Map<BackendId, String?>,
|
||||
): Boolean = withContext(dispatchers.default) {
|
||||
val derivations = MissedDerivationsFinder(userWallet)
|
||||
.findByNetworks(
|
||||
networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) ->
|
||||
networkFactory.create(
|
||||
blockchain = Blockchain.fromNetworkId(backendId) ?: return@mapNotNull null,
|
||||
extraDerivationPath = extraDerivationPath,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
derivations.isNotEmpty()
|
||||
}
|
||||
|
||||
private fun UserWallet.Hot.updateWithNewKeys(newKeys: Map<ByteArrayKey, ExtendedPublicKeysMap>): UserWallet.Hot {
|
||||
val wallets = this.wallets ?: return this
|
||||
val derivedKeys = wallets.associate {
|
||||
it.publicKey.toMapKey() to ExtendedPublicKeysMap(it.derivedKeys)
|
||||
}
|
||||
val updatedKeys = getUpdatedDerivedKeys(
|
||||
oldKeys = derivedKeys,
|
||||
newKeys = newKeys,
|
||||
)
|
||||
|
||||
return copy(
|
||||
wallets = wallets.map { wallet ->
|
||||
wallet.copy(
|
||||
derivedKeys = updatedKeys[wallet.publicKey.toMapKey()] ?: ExtendedPublicKeysMap(emptyMap()),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getUpdatedDerivedKeys(
|
||||
oldKeys: Map<ByteArrayKey, ExtendedPublicKeysMap>,
|
||||
newKeys: Map<ByteArrayKey, ExtendedPublicKeysMap>,
|
||||
): Map<ByteArrayKey, ExtendedPublicKeysMap> {
|
||||
return (oldKeys.keys + newKeys.keys).toSet()
|
||||
.associateWith { walletKey ->
|
||||
val oldDerivations = ExtendedPublicKeysMap(oldKeys[walletKey] ?: emptyMap())
|
||||
val newDerivations = newKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
|
||||
ExtendedPublicKeysMap(oldDerivations + newDerivations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
package com.tangem.data.wallets.hot
|
||||
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.hot.sdk.exception.WrongPasswordException
|
||||
import com.tangem.hot.sdk.model.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class HotWalletAccessor @Inject constructor(
|
||||
private val tangemHotSdk: TangemHotSdk,
|
||||
private val hotWalletPasswordRequester: HotWalletPasswordRequester,
|
||||
) {
|
||||
|
||||
suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List<DataToSign>): List<SignedData> =
|
||||
hotSdkRequest(hotWalletId) { unlock ->
|
||||
tangemHotSdk.signHashes(unlockHotWallet = unlock, dataToSign = dataToSign)
|
||||
}
|
||||
|
||||
suspend fun derivePublicKeys(hotWalletId: HotWalletId, request: DeriveWalletRequest): DerivedPublicKeyResponse =
|
||||
hotSdkRequest(hotWalletId) { unlock ->
|
||||
tangemHotSdk.derivePublicKey(unlockHotWallet = unlock, request = request)
|
||||
}
|
||||
|
||||
private suspend fun <T> hotSdkRequest(hotWalletId: HotWalletId, block: suspend (unlock: UnlockHotWallet) -> T): T {
|
||||
val auth = when (hotWalletId.authType) {
|
||||
HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth
|
||||
HotWalletId.AuthType.Password -> requestPassword(false)
|
||||
HotWalletId.AuthType.Biometry -> HotAuth.Biometry
|
||||
}
|
||||
|
||||
return runCatchingSdkErrors(hotWalletId, auth) {
|
||||
block(UnlockHotWallet(hotWalletId, it)).also {
|
||||
hotWalletPasswordRequester.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> runCatchingSdkErrors(
|
||||
hotWalletId: HotWalletId,
|
||||
auth: HotAuth,
|
||||
block: suspend (auth: HotAuth) -> T,
|
||||
): T {
|
||||
return runCatchingWrongPassInternal(
|
||||
originalAuth = auth,
|
||||
auth = auth,
|
||||
block = { blockAuth ->
|
||||
block(blockAuth).also {
|
||||
// TODO [REDACTED_TASK_KEY] if user has biometry enabled, we set it as the new auth method
|
||||
if (blockAuth is HotAuth.Password /*&& has biometry enabled */) {
|
||||
tangemHotSdk.changeAuth(
|
||||
unlockHotWallet = UnlockHotWallet(
|
||||
walletId = hotWalletId,
|
||||
auth = blockAuth,
|
||||
),
|
||||
auth = HotAuth.Biometry,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun <T> runCatchingWrongPassInternal(
|
||||
originalAuth: HotAuth,
|
||||
auth: HotAuth,
|
||||
block: suspend (auth: HotAuth) -> T,
|
||||
): T = runCatching {
|
||||
block(auth)
|
||||
}.getOrElse { exception ->
|
||||
if (auth is HotAuth.Biometry && exception.isBiometryError()) {
|
||||
// fallback to password if biometry fails
|
||||
val passAuth = requestPassword(true)
|
||||
|
||||
return@getOrElse runCatchingWrongPassInternal(
|
||||
originalAuth = originalAuth,
|
||||
auth = passAuth,
|
||||
block = block,
|
||||
)
|
||||
}
|
||||
|
||||
if (exception !is WrongPasswordException) {
|
||||
throw exception
|
||||
}
|
||||
|
||||
// If the exception is a wrong password, we need to request the password again
|
||||
|
||||
hotWalletPasswordRequester.wrongPassword()
|
||||
val passResult = requestPassword(originalAuth is HotAuth.Biometry)
|
||||
|
||||
runCatchingWrongPassInternal(
|
||||
originalAuth = originalAuth,
|
||||
auth = passResult,
|
||||
block = block,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun requestPassword(hasBiometry: Boolean): HotAuth {
|
||||
return hotWalletPasswordRequester.requestPassword(hasBiometry).toAuth() ?: throw TangemSdkError.UserCancelled()
|
||||
}
|
||||
|
||||
private fun Throwable.isBiometryError(): Boolean {
|
||||
return this is TangemSdkError.AuthenticationFailed ||
|
||||
this is TangemSdkError.AuthenticationCanceled ||
|
||||
this is TangemSdkError.AuthenticationLockout ||
|
||||
this is TangemSdkError.AuthenticationUnavailable ||
|
||||
this is TangemSdkError.AuthenticationAlreadyInProgress ||
|
||||
this is TangemSdkError.AuthenticationNotInitialized ||
|
||||
this is TangemSdkError.AuthenticationPermanentLockout
|
||||
}
|
||||
|
||||
private fun HotWalletPasswordRequester.Result.toAuth() = when (this) {
|
||||
HotWalletPasswordRequester.Result.UseBiometry -> HotAuth.Biometry
|
||||
HotWalletPasswordRequester.Result.Dismiss -> null
|
||||
is HotWalletPasswordRequester.Result.EnteredPassword -> this.password
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
package com.tangem.data.wallets.hot
|
||||
|
||||
import com.tangem.blockchain.common.TransactionSigner
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.map
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.hot.sdk.model.DataToSign
|
||||
import com.tangem.operations.sign.SignData
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import timber.log.Timber
|
||||
|
||||
class TangemHotWalletSigner @AssistedInject constructor(
|
||||
@Assisted private val userWallet: UserWallet.Hot,
|
||||
private val hotWalletAccessor: HotWalletAccessor,
|
||||
) : TransactionSigner {
|
||||
|
||||
override suspend fun sign(hash: ByteArray, publicKey: Wallet.PublicKey): CompletionResult<ByteArray> {
|
||||
return sign(listOf(hash), publicKey).map { it.first() }
|
||||
}
|
||||
|
||||
override suspend fun sign(
|
||||
hashes: List<ByteArray>,
|
||||
publicKey: Wallet.PublicKey,
|
||||
): CompletionResult<List<ByteArray>> {
|
||||
val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey.contentEquals(publicKey.seedKey) }
|
||||
?: return CompletionResult.Failure(
|
||||
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
|
||||
)
|
||||
|
||||
val result = runCatching {
|
||||
hotWalletAccessor.signHashes(
|
||||
hotWalletId = userWallet.hotWalletId,
|
||||
dataToSign = listOf(
|
||||
DataToSign(
|
||||
curve = wallet.curve,
|
||||
hashes = hashes,
|
||||
derivationPath = publicKey.derivationPath,
|
||||
),
|
||||
),
|
||||
)
|
||||
}.getOrElse {
|
||||
Timber.e(it)
|
||||
return if (it is TangemSdkError) {
|
||||
CompletionResult.Failure(it)
|
||||
} else {
|
||||
CompletionResult.Failure(TangemSdkError.ExceptionError(it))
|
||||
}
|
||||
}
|
||||
|
||||
return CompletionResult.Success(result.map { it.signatures }.flatten())
|
||||
}
|
||||
|
||||
override suspend fun multiSign(
|
||||
dataToSign: List<SignData>,
|
||||
publicKey: Wallet.PublicKey,
|
||||
): CompletionResult<Map<ByteArray, ByteArray>> {
|
||||
val result = runCatching {
|
||||
hotWalletAccessor.signHashes(
|
||||
hotWalletId = userWallet.hotWalletId,
|
||||
dataToSign = dataToSign.map { signData ->
|
||||
val wallet =
|
||||
userWallet.wallets.orEmpty().firstOrNull { it.publicKey.contentEquals(signData.publicKey) }
|
||||
?: return CompletionResult.Failure(
|
||||
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
|
||||
)
|
||||
|
||||
DataToSign(
|
||||
curve = wallet.curve,
|
||||
hashes = listOf(signData.hash),
|
||||
derivationPath = signData.derivationPath,
|
||||
)
|
||||
},
|
||||
)
|
||||
}.getOrElse {
|
||||
Timber.e(it)
|
||||
return if (it is TangemSdkError) {
|
||||
CompletionResult.Failure(it)
|
||||
} else {
|
||||
CompletionResult.Failure(TangemSdkError.ExceptionError(it))
|
||||
}
|
||||
}
|
||||
|
||||
return CompletionResult.Success(
|
||||
result.mapIndexed { index, data ->
|
||||
dataToSign[index].publicKey to data.signatures.first()
|
||||
}.toMap(),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(@Assisted userWallet: UserWallet.Hot): TangemHotWalletSigner
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
package com.tangem.data.wallets.derivations
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.test.domain.card.MockScanResponseFactory
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.card.ScanCardException
|
||||
import com.tangem.domain.card.configs.GenericCardConfig
|
||||
import com.tangem.domain.card.configs.MultiWalletCardConfig
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.operations.derivation.DerivationTaskResponse
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultDerivationsRepositoryTest {
|
||||
|
||||
private val tangemSdkManager = mockk<TangemSdkManager>()
|
||||
private val userWalletsStore = mockk<UserWalletsStore>()
|
||||
private val repository = DefaultDerivationsRepository(
|
||||
userWalletsStore = userWalletsStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
hotDerivationsRepository = mockk(),
|
||||
coldDerivationsRepository = DefaultColdMapDerivationsRepository(
|
||||
tangemSdkManager = tangemSdkManager,
|
||||
networkFactory = NetworkFactory(excludedBlockchains = ExcludedBlockchains()),
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
),
|
||||
)
|
||||
|
||||
private val defaultUserWalletId = UserWalletId("011")
|
||||
private val defaultUserWallet = UserWallet.Cold(
|
||||
name = "",
|
||||
walletId = defaultUserWalletId,
|
||||
cardsInWallet = setOf(),
|
||||
isMultiCurrency = false,
|
||||
scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()),
|
||||
hasBackupError = false,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `error if userWalletId not found`() = runTest {
|
||||
coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } throws IllegalStateException()
|
||||
|
||||
runCatching {
|
||||
repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList())
|
||||
}
|
||||
.onSuccess { error("Should throws exception") }
|
||||
.onFailure { Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java) }
|
||||
|
||||
coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) }
|
||||
coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) }
|
||||
coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) }
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
@Test
|
||||
fun `success if card is not supported derivations`() = runTest {
|
||||
coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns defaultUserWallet
|
||||
|
||||
repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList())
|
||||
|
||||
runCatching { }
|
||||
.onSuccess { Truth.assertThat(it) }
|
||||
.onFailure {
|
||||
error("Should returns success")
|
||||
}
|
||||
|
||||
coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) }
|
||||
coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) }
|
||||
coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) }
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
@Test
|
||||
fun `success if currencies is empty`() = runTest {
|
||||
val userWallet = defaultUserWallet.copy(
|
||||
scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()),
|
||||
)
|
||||
coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet
|
||||
|
||||
runCatching { repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList()) }
|
||||
.onSuccess { Truth.assertThat(it) }
|
||||
.onFailure { error("Should returns success") }
|
||||
|
||||
coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) }
|
||||
coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) }
|
||||
coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) }
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
@Test
|
||||
fun `success if card already has derivations`() = runTest {
|
||||
val userWallet = defaultUserWallet.copy(
|
||||
scanResponse = MockScanResponseFactory.create(
|
||||
cardConfig = MultiWalletCardConfig,
|
||||
derivedKeys = DerivedKeysMocks.ethereumDerivedKeys,
|
||||
),
|
||||
)
|
||||
|
||||
coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet
|
||||
|
||||
runCatching {
|
||||
repository.derivePublicKeys(
|
||||
userWalletId = defaultUserWalletId,
|
||||
currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf),
|
||||
)
|
||||
}
|
||||
.onSuccess { Truth.assertThat(it) }
|
||||
.onFailure { error("Should returns success") }
|
||||
|
||||
coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) }
|
||||
coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) }
|
||||
coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `error if tangemSdkManager throws exception`() = runTest {
|
||||
val userWallet = defaultUserWallet.copy(
|
||||
scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()),
|
||||
)
|
||||
coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet
|
||||
coEvery { tangemSdkManager.derivePublicKeys(null, any(), any()) } throws ScanCardException.UserCancelled
|
||||
|
||||
runCatching {
|
||||
repository.derivePublicKeys(
|
||||
userWalletId = defaultUserWalletId,
|
||||
currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf),
|
||||
)
|
||||
}
|
||||
.onSuccess { error("Should throws exception") }
|
||||
.onFailure { Truth.assertThat(it).isInstanceOf(ScanCardException.UserCancelled::class.java) }
|
||||
|
||||
coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) }
|
||||
coVerify(exactly = 1) { tangemSdkManager.derivePublicKeys(null, any(), any()) }
|
||||
coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) }
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
@Test
|
||||
fun `success case`() = runTest {
|
||||
val userWallet = defaultUserWallet.copy(
|
||||
scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()),
|
||||
)
|
||||
coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet
|
||||
coEvery { tangemSdkManager.derivePublicKeys(null, any(), any()) } returns CompletionResult.Success(
|
||||
DerivationTaskResponse(DerivedKeysMocks.ethereumDerivedKeys),
|
||||
)
|
||||
coEvery { userWalletsStore.update(defaultUserWalletId, any()) } returns CompletionResult.Success(userWallet)
|
||||
|
||||
runCatching {
|
||||
repository.derivePublicKeys(
|
||||
userWalletId = defaultUserWalletId,
|
||||
currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf),
|
||||
)
|
||||
}
|
||||
.onSuccess { Truth.assertThat(it) }
|
||||
.onFailure { error("Should returns success but $it") }
|
||||
|
||||
coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) }
|
||||
coVerify(exactly = 1) { tangemSdkManager.derivePublicKeys(null, any(), any()) }
|
||||
coVerify(exactly = 1) { userWalletsStore.update(defaultUserWalletId, any()) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.data.wallets.derivations
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.derivation.DerivationConfigV2
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object DerivedKeysMocks {
|
||||
|
||||
val ethereumDerivedKeys = mapOf(
|
||||
EllipticCurve.Secp256k1.name.toByteArray().toMapKey() to ExtendedPublicKeysMap(
|
||||
mapOf(
|
||||
DerivationConfigV2.derivations(Blockchain.Ethereum).values.first() to ExtendedPublicKey(
|
||||
publicKey = ByteArray(0),
|
||||
chainCode = ByteArray(0),
|
||||
depth = 2646,
|
||||
parentFingerprint = ByteArray(0),
|
||||
childNumber = 1142,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
package com.tangem.data.wallets.derivations
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.derivation.DerivationConfigV2
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.test.domain.card.MockScanResponseFactory
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.card.configs.GenericCardConfig
|
||||
import com.tangem.domain.card.configs.MultiWalletCardConfig
|
||||
import com.tangem.domain.card.configs.Wallet2CardConfig
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class MissedDerivationsFinderTest {
|
||||
|
||||
@Test
|
||||
fun `empty derivations for empty currencies`() {
|
||||
val scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap())
|
||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||
val finder = MissedDerivationsFinder(userWallet)
|
||||
|
||||
val actual = finder.find(emptyList())
|
||||
|
||||
Truth.assertThat(actual).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `empty derivations for non supported blockchains`() {
|
||||
// Bls is not supported
|
||||
val scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap())
|
||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||
val finder = MissedDerivationsFinder(userWallet)
|
||||
|
||||
val currencies = MockCryptoCurrencyFactory(userWallet).chia.let(::listOf)
|
||||
val actual = finder.find(currencies)
|
||||
|
||||
Truth.assertThat(actual).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `derivations ONLY for supported blockchains`() {
|
||||
// Bls is not supported
|
||||
val scanResponse = MockScanResponseFactory.create(
|
||||
cardConfig = GenericCardConfig(2),
|
||||
derivedKeys = emptyMap(),
|
||||
).let {
|
||||
it.copy(
|
||||
card = it.card.copy(
|
||||
settings = it.card.settings.copy(isHDWalletAllowed = true, isBackupAllowed = true),
|
||||
),
|
||||
)
|
||||
}
|
||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||
val finder = MissedDerivationsFinder(userWallet)
|
||||
|
||||
val currencies = MockCryptoCurrencyFactory(userWallet).chiaAndEthereum
|
||||
val actual = finder.find(currencies)
|
||||
|
||||
Truth.assertThat(actual).containsExactly(
|
||||
ByteArrayKey(EllipticCurve.Secp256k1.name.toByteArray()),
|
||||
listOf(DerivationConfigV2.derivations(Blockchain.Ethereum).values.first()),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `derivations for custom token`() {
|
||||
val scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap())
|
||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||
val finder = MissedDerivationsFinder(userWallet)
|
||||
|
||||
val currencies = MockCryptoCurrencyFactory(userWallet).ethereumTokenWithBinanceDerivation
|
||||
val actual = finder.find(currencies)
|
||||
|
||||
Truth.assertThat(actual).containsExactly(
|
||||
ByteArrayKey(EllipticCurve.Secp256k1.name.toByteArray()),
|
||||
listOf(
|
||||
DerivationConfigV2.derivations(Blockchain.Ethereum).values.first(),
|
||||
DerivationConfigV2.derivations(Blockchain.Binance).values.first(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `derivations for cardano`() {
|
||||
val scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap())
|
||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||
val finder = MissedDerivationsFinder(userWallet)
|
||||
|
||||
val currencies = MockCryptoCurrencyFactory(userWallet).cardano.let(::listOf)
|
||||
val actual = finder.find(currencies)
|
||||
|
||||
Truth.assertThat(actual).containsExactly(
|
||||
ByteArrayKey(EllipticCurve.Ed25519.name.toByteArray()),
|
||||
listOf(
|
||||
DerivationConfigV2.derivations(Blockchain.Cardano).values.first(),
|
||||
CardanoUtils.extendedDerivationPath(
|
||||
derivationPath = DerivationPath(
|
||||
Blockchain.Cardano.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle())!!
|
||||
.rawPath,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `empty derivations for already derived currencies`() {
|
||||
val scanResponse = MockScanResponseFactory.create(
|
||||
cardConfig = Wallet2CardConfig,
|
||||
derivedKeys = DerivedKeysMocks.ethereumDerivedKeys,
|
||||
)
|
||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||
val finder = MissedDerivationsFinder(userWallet)
|
||||
|
||||
val currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf)
|
||||
val actual = finder.find(currencies)
|
||||
|
||||
Truth.assertThat(actual).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `derivations ONLY for never derived currencies`() {
|
||||
val scanResponse = MockScanResponseFactory.create(
|
||||
cardConfig = MultiWalletCardConfig,
|
||||
derivedKeys = DerivedKeysMocks.ethereumDerivedKeys,
|
||||
)
|
||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||
val finder = MissedDerivationsFinder(userWallet)
|
||||
|
||||
val currencies = MockCryptoCurrencyFactory(userWallet).ethereumAndStellar
|
||||
val actual = finder.find(currencies)
|
||||
|
||||
Truth.assertThat(actual).containsExactly(
|
||||
ByteArrayKey(EllipticCurve.Ed25519.name.toByteArray()),
|
||||
listOf(DerivationConfigV2.derivations(Blockchain.Stellar).values.first()),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue