Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-22 10:09:11 +02:00
commit e4754fed3b
483 changed files with 9315 additions and 4084 deletions

View file

@ -13,6 +13,8 @@ import com.tangem.domain.models.account.*
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
/**
[REDACTED_AUTHOR]
@ -37,23 +39,52 @@ internal class DefaultAccountsCRUDRepository(
}
override suspend fun getArchivedAccount(accountId: AccountId): Option<ArchivedAccount> = option {
ArchivedAccount(
accountId = accountId,
name = AccountName("Archived Account").getOrNull()!!,
icon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
derivationIndex = DerivationIndex(value = 1000).getOrNull()!!,
tokensCount = 2,
networksCount = 1,
createMockArchivedAccount(userWalletId = accountId.userWalletId)
}
override suspend fun getArchivedAccountsSync(userWalletId: UserWalletId): Option<List<ArchivedAccount>> = option {
listOf(
createMockArchivedAccount(userWalletId),
)
}
override fun getArchivedAccounts(userWalletId: UserWalletId): Flow<List<ArchivedAccount>> {
return flow {
getArchivedAccountsSync(userWalletId).getOrNull().orEmpty()
}
}
override suspend fun fetchArchivedAccounts(userWalletId: UserWalletId) = Unit
override suspend fun saveAccounts(accountList: AccountList) {
runtimeStore.update(emptyList()) {
it.addOrReplace(accountList) { it.userWallet.walletId == accountList.userWallet.walletId }
}
}
override suspend fun getTotalAccountsCount(userWalletId: UserWalletId): Int {
val activeAccountsCount = runtimeStore.getSyncOrNull()?.size ?: 1
return activeAccountsCount + 1
}
override fun getUserWallet(userWalletId: UserWalletId): UserWallet {
return userWalletsStore.getSyncStrict(userWalletId)
}
private fun createMockArchivedAccount(userWalletId: UserWalletId): ArchivedAccount {
val derivationIndex = DerivationIndex(value = 1000).getOrNull()!!
return ArchivedAccount(
accountId = AccountId.forCryptoPortfolio(
userWalletId = userWalletId,
derivationIndex = derivationIndex,
),
name = AccountName("Archived Account").getOrNull()!!,
icon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
derivationIndex = derivationIndex,
tokensCount = 2,
networksCount = 1,
)
}
}

View file

@ -3,7 +3,7 @@ package com.tangem.data.blockaid
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.domain.blockaid.models.transaction.*
import com.domain.blockaid.models.transaction.simultation.AmountInfo
import com.domain.blockaid.models.transaction.simultation.ApprovedAmount
import com.domain.blockaid.models.transaction.simultation.ApproveInfo
import com.domain.blockaid.models.transaction.simultation.SimulationData
import com.domain.blockaid.models.transaction.simultation.TokenInfo
import com.tangem.blockchain.extensions.hexToBigDecimal
@ -17,6 +17,9 @@ private const val SUCCESS_STATUS = "Success"
private const val DOMAIN_CHECKED_STATUS = "hit"
private const val VALIDATION_SAFE_STATUS = "Benign"
private const val VALIDATION_WARNING_STATUS = "Warning"
private const val VALIDATION_MALICIOUS_STATUS = "Malicious"
private const val SOL_ASSET_SYMBOL = "SOL"
internal object BlockAidMapper {
@ -28,6 +31,26 @@ internal object BlockAidMapper {
}
}
fun mapToDomain(from: SolanaTransactionResponse): CheckTransactionResult {
val validation = when (from.result.validation.resultType) {
VALIDATION_SAFE_STATUS -> ValidationResult.SAFE
VALIDATION_WARNING_STATUS -> ValidationResult.WARNING
VALIDATION_MALICIOUS_STATUS -> ValidationResult.UNSAFE
else -> ValidationResult.FAILED_TO_VALIDATE
}
val simulationResponse = from.result.simulation
val simulation = if (simulationResponse == null) {
SimulationResult.FailedToSimulate
} else {
mapToSolanaAssetsDiffs(simulationResponse.accountSummary.accountAssetsDiff)
}
return CheckTransactionResult(
validation = validation,
description = from.result.validation.description,
simulation = simulation,
)
}
fun mapToDomain(from: TransactionScanResponse): CheckTransactionResult {
return CheckTransactionResult(
validation = when {
@ -68,7 +91,7 @@ internal object BlockAidMapper {
fun mapToSolanaRequest(from: TransactionData): SolanaTransactionScanRequest {
return SolanaTransactionScanRequest(
chain = from.chain.lowercase(),
blockchain = from.chain.lowercase(),
accountAddress = from.accountAddress,
metadata = TransactionMetadata(from.domainUrl),
method = from.method,
@ -78,35 +101,61 @@ internal object BlockAidMapper {
private fun mapSimulationSuccessResult(from: AccountSummaryResponse): SimulationResult {
return when {
!from.assetsDiffs.isNullOrEmpty() -> mapSendReceiveTransaction(
from.assetsDiffs,
)
!from.exposures.isNullOrEmpty() -> mapApproveTransaction(
from.exposures,
)
!from.traces.isNullOrEmpty() -> mapNftSendReceiveTransaction(from.traces)
!from.exposures.isNullOrEmpty() -> mapApproveTransaction(from.exposures)
!from.assetsDiffs.isNullOrEmpty() -> mapSendReceiveTransaction(from.assetsDiffs)
else -> SimulationResult.Success(data = SimulationData.NoWalletChangesDetected)
}
}
private fun mapApproveTransaction(exposures: List<Exposure>?): SimulationResult {
val amounts = exposures?.flatMap { exposure ->
val tokenInfo = TokenInfo(
chainId = exposure.asset.chainId,
logoUrl = exposure.asset.logoUrl,
symbol = exposure.asset.symbol ?: "",
decimals = exposure.asset.decimals ?: 0,
private fun mapToSolanaAssetsDiffs(assetsDiffs: List<SolanaTransactionAssetDiff>): SimulationResult {
val sendInfo = assetsDiffs.mapNotNull { assetDiff ->
val outTransfer = assetDiff.outTransfer ?: return@mapNotNull null
val amount = outTransfer.amount?.toBigDecimalOrNull() ?: return@mapNotNull null
AmountInfo.FungibleTokens(
amount = amount,
token = TokenInfo(
chainId = null,
logoUrl = assetDiff.asset.logoUrl,
symbol = assetDiff.asset.assetSymbol(),
decimals = assetDiff.asset.decimals ?: 0,
),
)
exposure.spenders.flatMap { (_, spender) ->
val isUnlimited = spender.isApprovedForAll == true
val approval = spender.approval?.hexToBigDecimal()
spender.exposure.map { detail ->
ApprovedAmount(
approvedAmount = detail.value?.toBigDecimalOrNull() ?: approval ?: 1.toBigDecimal(),
isUnlimited = isUnlimited,
tokenInfo = tokenInfo,
)
}
}
val receiveInfo = assetsDiffs.mapNotNull { assetDiff ->
val inTransfer = assetDiff.inTransfer ?: return@mapNotNull null
val amount = inTransfer.amount?.toBigDecimalOrNull() ?: return@mapNotNull null
AmountInfo.FungibleTokens(
amount = amount,
token = TokenInfo(
chainId = null,
logoUrl = assetDiff.asset.logoUrl,
symbol = assetDiff.asset.assetSymbol(),
decimals = assetDiff.asset.decimals ?: 0,
),
)
}
return if (sendInfo.isNotEmpty() || receiveInfo.isNotEmpty()) {
SimulationResult.Success(SimulationData.SendAndReceive(send = sendInfo, receive = receiveInfo))
} else {
SimulationResult.Success(SimulationData.NoWalletChangesDetected)
}
}
private fun SolanaTransactionAsset.assetSymbol(): String {
return if (type?.lowercase().equals(SOL_ASSET_SYMBOL, ignoreCase = true)) {
symbol ?: SOL_ASSET_SYMBOL
} else {
symbol.orEmpty()
}
}
private fun mapApproveTransaction(exposures: List<Exposure>?): SimulationResult {
val amounts: List<ApproveInfo>? = exposures?.flatMap { exposure ->
if (exposure.assetType.isNFT()) {
listOf(mapApproveNftTransaction(exposure))
} else {
mapTransaction(exposure)
}
}
return if (!amounts.isNullOrEmpty()) {
@ -116,6 +165,34 @@ internal object BlockAidMapper {
}
}
private fun mapTransaction(exposure: Exposure): List<ApproveInfo.Amount> {
val tokenInfo = TokenInfo(
chainId = exposure.asset.chainId,
logoUrl = exposure.asset.logoUrl,
symbol = exposure.asset.symbol ?: "",
decimals = exposure.asset.decimals ?: 0,
)
return exposure.spenders.flatMap { (_, spender) ->
val isUnlimited = spender.isApprovedForAll == true
val approval = spender.approval?.hexToBigDecimal()
spender.exposure.map { detail ->
ApproveInfo.Amount(
approvedAmount = detail.value?.toBigDecimalOrNull() ?: approval ?: 1.toBigDecimal(),
isUnlimited = isUnlimited,
tokenInfo = tokenInfo,
)
}
}
}
private fun mapApproveNftTransaction(exposure: Exposure): ApproveInfo.NonFungibleToken {
return ApproveInfo.NonFungibleToken(
name = exposure.asset.name.orEmpty(),
logoUrl = exposure.spenders.values.firstOrNull()?.exposure?.firstOrNull()?.logoUrl
?: exposure.asset.logoUrl,
)
}
private fun mapSendReceiveTransaction(assetDiffs: List<AssetDiff>?): SimulationResult {
val sendInfo = arrayListOf<AmountInfo>()
val receiveInfo = arrayListOf<AmountInfo>()
@ -128,13 +205,31 @@ internal object BlockAidMapper {
decimals = diff.asset.decimals ?: 0,
)
diff.outTransfer.orEmpty().forEach { transfer ->
transfer.value?.toBigDecimalOrNull()?.let { amount ->
sendInfo.add(AmountInfo.FungibleTokens(amount = amount, token = token))
if (diff.assetType.isNFT()) {
sendInfo.add(
AmountInfo.NonFungibleTokens(
name = diff.asset.name.orEmpty(),
logoUrl = token.logoUrl,
),
)
} else {
transfer.value?.toBigDecimalOrNull()?.let { amount ->
sendInfo.add(AmountInfo.FungibleTokens(amount = amount, token = token))
}
}
}
diff.inTransfer.orEmpty().forEach { transfer ->
transfer.value?.toBigDecimalOrNull()?.let { amount ->
receiveInfo.add(AmountInfo.FungibleTokens(amount = amount, token = token))
if (diff.assetType.isNFT()) {
receiveInfo.add(
AmountInfo.NonFungibleTokens(
name = diff.asset.name.orEmpty(),
logoUrl = token.logoUrl,
),
)
} else {
transfer.value?.toBigDecimalOrNull()?.let { amount ->
receiveInfo.add(AmountInfo.FungibleTokens(amount = amount, token = token))
}
}
}
}
@ -146,17 +241,7 @@ internal object BlockAidMapper {
}
}
private fun mapNftSendReceiveTransaction(traces: List<Trace>?): SimulationResult {
val sendInfo = traces?.mapNotNull {
it.exposed?.let { exposed ->
AmountInfo.NonFungibleTokens(name = "${it.asset.name} #${exposed.tokenId}", logoUrl = exposed.logoUrl)
}
}
return if (!sendInfo.isNullOrEmpty()) {
SimulationResult.Success(SimulationData.SendAndReceive(send = sendInfo, receive = listOf()))
} else {
SimulationResult.Success(SimulationData.NoWalletChangesDetected)
}
private fun String.isNFT(): Boolean {
return this.lowercase() == "erc721" || this.lowercase() == "erc1155" || this.lowercase() == "nft"
}
}

View file

@ -24,16 +24,21 @@ internal class DefaultBlockAidRepository(
}
override suspend fun verifyTransaction(data: TransactionData): CheckTransactionResult {
val response = withContext(dispatchers.io) {
when (data.params) {
is TransactionParams.Evm -> {
api.scanJsonRpc(mapper.mapToEvmRequest(data))
}
is TransactionParams.Solana -> {
api.scanSolanaMessage(mapper.mapToSolanaRequest(data))
}
}
return when (data.params) {
is TransactionParams.Evm -> scanEvmTransaction(data = data)
is TransactionParams.Solana -> scanSolanaTransaction(data = data)
}
return mapper.mapToDomain(response)
}
private suspend fun scanEvmTransaction(data: TransactionData): CheckTransactionResult =
withContext(dispatchers.io) {
val response = api.scanJsonRpc(mapper.mapToEvmRequest(data))
mapper.mapToDomain(response)
}
private suspend fun scanSolanaTransaction(data: TransactionData): CheckTransactionResult =
withContext(dispatchers.io) {
val response = api.scanSolanaMessage(mapper.mapToSolanaRequest(data))
mapper.mapToDomain(response)
}
}

View file

@ -4,6 +4,7 @@ import com.domain.blockaid.models.dapp.CheckDAppResult
import com.domain.blockaid.models.transaction.SimulationResult
import com.domain.blockaid.models.transaction.ValidationResult
import com.domain.blockaid.models.transaction.simultation.AmountInfo
import com.domain.blockaid.models.transaction.simultation.ApproveInfo
import com.domain.blockaid.models.transaction.simultation.SimulationData
import com.google.common.truth.Truth
import com.tangem.datasource.api.common.blockaid.models.response.*
@ -44,6 +45,7 @@ class BlockAidMapperTest {
val exposure = Exposure(
asset = Asset(chainId = 1, logoUrl = "logo", symbol = "PEPE", decimals = 8),
spenders = mapOf("spender" to spenderDetails),
assetType = "native",
)
val response = TransactionScanResponse(
validation = ValidationResponse(status = "Success", resultType = "Benign", description = ""),
@ -65,9 +67,10 @@ class BlockAidMapperTest {
val approve = simulation?.data as? SimulationData.Approve
Truth.assertThat(approve).isNotNull()
Truth.assertThat(approve?.approvedAmounts?.size).isEqualTo(1)
Truth.assertThat(approve?.approvedAmounts?.first()?.approvedAmount).isEqualTo(BigDecimal("1000.0"))
Truth.assertThat(approve?.approvedAmounts?.first()?.isUnlimited).isTrue()
Truth.assertThat(approve?.items?.size).isEqualTo(1)
Truth.assertThat((approve?.items?.first() as? ApproveInfo.Amount)?.approvedAmount)
.isEqualTo(BigDecimal("1000.0"))
Truth.assertThat((approve?.items?.first() as? ApproveInfo.Amount)?.isUnlimited).isTrue()
}
@Test

View file

@ -11,6 +11,7 @@ import com.tangem.datasource.api.common.blockaid.models.request.DomainScanReques
import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionScanRequest
import com.tangem.datasource.api.common.blockaid.models.request.SolanaTransactionScanRequest
import com.tangem.datasource.api.common.blockaid.models.response.DomainScanResponse
import com.tangem.datasource.api.common.blockaid.models.response.SolanaTransactionResponse
import com.tangem.datasource.api.common.blockaid.models.response.TransactionScanResponse
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
@ -91,7 +92,7 @@ class DefaultBlockAidRepositoryTest {
)
val request = mockk<SolanaTransactionScanRequest>()
val response = mockk<TransactionScanResponse>()
val response = mockk<SolanaTransactionResponse>()
val expectedResult = mockk<CheckTransactionResult>()
every { mapper.mapToSolanaRequest(data) } returns request

View file

@ -9,6 +9,7 @@ import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.UserTokensResponseFactory
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.data.common.network.NetworkFactory
import com.tangem.data.common.utils.retryOnError
import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher
@ -42,6 +43,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
internal class DefaultManageTokensRepository(
private val tangemTechApi: TangemTechApi,
private val userWalletsStore: UserWalletsStore,
private val userTokenSaver: UserTokensSaver,
private val manageTokensUpdateFetcher: ManageTokensUpdateFetcher,
private val userTokensResponseStore: UserTokensResponseStore,
private val testnetTokensStorage: TestnetTokensStorage,
@ -127,7 +129,8 @@ internal class DefaultManageTokensRepository(
val tokensResponse = request.params.userWalletId?.let { userWalletId ->
if (loadUserTokensFromRemote && userWallet != null) {
safeApiCall({ tangemTechApi.getUserTokens(userWalletId.stringValue).bind() }) {
createDefaultUserTokensResponse(userWallet)
// save tokens response only if loadUserTokensFromRemote is true and it means onboarding call
createAndSaveDefaultUserTokensResponse(userWallet = userWallet)
}
} else {
getSavedUserTokensResponseSync(userWalletId)
@ -158,6 +161,12 @@ internal class DefaultManageTokensRepository(
)
}
private suspend fun createAndSaveDefaultUserTokensResponse(userWallet: UserWallet): UserTokensResponse {
val userTokensResponse = createDefaultUserTokensResponse(userWallet)
userTokenSaver.store(userWallet.walletId, userTokensResponse, useEnricher = false)
return userTokensResponse
}
private suspend fun fetchTestnetCurrencies(
userWallet: UserWallet,
request: Request<ManageTokensListConfig>,

View file

@ -32,6 +32,7 @@ internal object ManageTokensDataModule {
userWalletsStore: UserWalletsStore,
manageTokensUpdateFetcher: ManageTokensUpdateFetcher,
userTokensResponseStore: UserTokensResponseStore,
userTokensSaver: UserTokensSaver,
testnetTokensStorage: TestnetTokensStorage,
dispatchers: CoroutineDispatcherProvider,
excludedBlockchains: ExcludedBlockchains,
@ -43,6 +44,7 @@ internal object ManageTokensDataModule {
userWalletsStore = userWalletsStore,
manageTokensUpdateFetcher = manageTokensUpdateFetcher,
userTokensResponseStore = userTokensResponseStore,
userTokenSaver = userTokensSaver,
testnetTokensStorage = testnetTokensStorage,
excludedBlockchains = excludedBlockchains,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,

View file

@ -381,6 +381,26 @@ internal class DefaultTransactionRepository(
preparer.prepareForSendMultiple(transactionData, signer)
}
override suspend fun prepareAndSign(
transactionData: TransactionData,
signer: TransactionSigner,
userWalletId: UserWalletId,
network: Network,
) = withContext(dispatchers.io) {
val preparer = getPreparer(network, userWalletId)
preparer.prepareAndSign(transactionData, signer)
}
override suspend fun prepareAndSignMultiple(
transactionData: List<TransactionData>,
signer: TransactionSigner,
userWalletId: UserWalletId,
network: Network,
) = withContext(dispatchers.io) {
val preparer = getPreparer(network, userWalletId)
preparer.prepareAndSignMultiple(transactionData, signer)
}
private suspend fun getPreparer(network: Network, userWalletId: UserWalletId): TransactionPreparer {
val blockchain = network.toBlockchain()
val walletManager = walletManagersFacade.getOrCreateWalletManager(

View file

@ -50,6 +50,7 @@ dependencies {
/** Libs - Tangem */
implementation(tangemDeps.blockchain)
implementation(tangemDeps.card.core)
implementation(projects.libs.tangemSdkApi)
/** DI */
implementation(deps.hilt.core)

View file

@ -2,39 +2,52 @@ package com.tangem.data.pay
import arrow.core.Either
import com.squareup.moshi.Moshi
import com.tangem.common.map
import com.tangem.core.error.UniversalError
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.KycStartInfo
import com.tangem.domain.pay.repository.KycRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.model.VisaDataToSignByCustomerWallet
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.sdk.api.TangemSdkManager
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.withContext
@Suppress("UnusedPrivateMember")
class DefaultKycRepository @AssistedInject constructor(
@Assisted userWalletId: UserWalletId,
@NetworkMoshi moshi: Moshi,
private val tangemPayApi: TangemPayApi,
private val dispatcherProvider: CoroutineDispatcherProvider,
private val visaAuthRepository: VisaAuthRepository,
private val tangemSdkManager: TangemSdkManager,
) : KycRepository {
private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi)
override suspend fun getKycStartInfo(): Either<UniversalError, KycStartInfo> = withContext(dispatcherProvider.io) {
val authTokenForSpecificWallet = "get from userWalletId"
request {
tangemPayApi.getKycAccess(
authHeader = authTokenForSpecificWallet,
).getOrThrow().result
override suspend fun getKycStartInfo(address: String, cardId: String): Either<UniversalError, KycStartInfo> {
var authHeader = ""
visaAuthRepository.getCustomerWalletAuthChallenge(address).getOrNull()?.let { result ->
tangemSdkManager.visaCustomerWalletApprove(
VisaDataForApprove(
customerWalletCardId = cardId,
targetAddress = address,
dataToSign = VisaDataToSignByCustomerWallet(hashToSign = result.challenge),
),
).map { signResult ->
visaAuthRepository.getTokenWithCustomerWallet(
sessionId = result.session.sessionId,
signature = signResult.signature,
nonce = signResult.dataToSign.hashToSign,
).getOrNull()?.let { authHeader = it }
}
}
return request {
authHeader.ifEmpty { error("Cannot get auth header for KYC") }
tangemPayApi.getKycAccess(authHeader = authHeader).getOrThrow().result
}.map {
KycStartInfo(
token = it.token,
@ -65,6 +78,6 @@ class DefaultKycRepository @AssistedInject constructor(
@AssistedFactory
interface Factory : KycRepository.Factory {
override fun create(userWalletId: UserWalletId): DefaultKycRepository
override fun create(): DefaultKycRepository
}
}

View file

@ -131,16 +131,18 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
val authTokens =
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
visaApi.activateByCustomerWallet(
authHeader = authTokens.getAuthHeader(),
body = ActivationByCustomerWalletRequest(
orderId = signedData.dataToSign.request.orderId,
customerWallet = ActivationByCustomerWalletRequest.CustomerWallet(
deployAcceptanceSignature = signedData.signature,
customerWalletAddress = signedData.customerWalletAddress,
signedData.dataToSign.request?.orderId?.let { orderId ->
visaApi.activateByCustomerWallet(
authHeader = authTokens.getAuthHeader(),
body = ActivationByCustomerWalletRequest(
orderId = orderId,
customerWallet = ActivationByCustomerWalletRequest.CustomerWallet(
deployAcceptanceSignature = signedData.signature,
customerWalletAddress = signedData.customerWalletAddress,
),
),
),
).getOrThrow()
).getOrThrow()
} ?: error("Order Id cannot be null")
}
}

View file

@ -65,6 +65,39 @@ internal class DefaultVisaAuthRepository @Inject constructor(
}
}
override suspend fun getCustomerWalletAuthChallenge(
customerWalletAddress: String,
): Either<VisaApiError, VisaAuthChallenge.Wallet> = withContext(dispatchers.io) {
request {
visaAuthApi.generateNonceByCustomerWallet(
GenerateNonceByCustomerWalletRequest(customerWalletAddress = customerWalletAddress),
).getOrThrow()
}.map { response ->
VisaAuthChallenge.Wallet(
challenge = response.result.nonce,
session = VisaAuthSession(response.result.sessionId),
)
}
}
override suspend fun getTokenWithCustomerWallet(
sessionId: String,
signature: String,
nonce: String,
): Either<VisaApiError, String> = withContext(dispatchers.io) {
request {
visaAuthApi.getTokenByCustomerWallet(
GetTokenByCustomerWalletRequest(
sessionId = sessionId,
signature = signature,
messageFormat = "Tangem Pay wants to sign in with your account. Nonce: $nonce",
),
).getOrThrow()
}.map { response ->
"Bearer ${response.result.accessToken}"
}
}
override suspend fun getAccessTokens(
signedChallenge: VisaAuthSignedChallenge,
): Either<VisaApiError, VisaAuthTokens> = withContext(dispatchers.io) {

View file

@ -1,9 +1,21 @@
package com.tangem.data.walletconnect.network.ethereum
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.Wallet.Model
import com.reown.walletkit.client.WalletKit
import com.tangem.blockchain.extensions.hexToInt
import com.tangem.data.walletconnect.model.CAIP10
import com.tangem.data.walletconnect.model.CAIP2
import com.tangem.data.walletconnect.network.ethereum.WcEthNetwork.NamespaceConverter.Companion.ETH_NAMESPACE_KEY
import com.tangem.data.walletconnect.respond.WcRespondService
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.data.walletconnect.utils.WcNetworksConverter
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.walletconnect.model.HandleMethodError
import com.tangem.domain.walletconnect.model.WcEthMethod
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.WcSession
@ -13,9 +25,13 @@ import com.tangem.domain.walletconnect.usecase.method.WcNetworkDerivationState
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
internal class WcEthAddNetworkUseCase @AssistedInject constructor(
private val respondService: WcRespondService,
private val networksConverter: WcNetworksConverter,
addSwitchCommonDelegateFactory: WcEthAddSwitchCommonDelegate.Factory,
@Assisted val context: WcMethodUseCaseContext,
@Assisted override val method: WcEthMethod.AddEthereumChain,
) : WcAddNetworkUseCase {
@ -31,10 +47,63 @@ internal class WcEthAddNetworkUseCase @AssistedInject constructor(
else -> WcNetworkDerivationState.Single
}
private val addSwitchCommonDelegate = addSwitchCommonDelegateFactory.create(context)
override suspend fun invoke(): Either<HandleMethodError, WcAddNetworkUseCase.AddNetwork> {
return addSwitchCommonDelegate
.commonChecks(method.rawChain.chainId)
.map { addedNetwork ->
WcAddNetworkUseCase.AddNetwork(
network = addedNetwork,
isExistInWcSession = addSwitchCommonDelegate.existInWcSession(addedNetwork),
)
}
}
override suspend fun approve(): Either<WcRequestError, String> {
fun illegalState() = WcRequestError.UnknownError(IllegalStateException("IllegalStateException")).left()
val requestedNetworkCAIP2 = CAIP2.fromRaw(rawSdkRequest.chainId.orEmpty()) ?: return illegalState()
val networkToAddCAIP2 = addSwitchCommonDelegate.hexChainIdToCAIP2(method.rawChain.chainId)
?: return illegalState()
val namespaces = session.sdkModel.namespaces[requestedNetworkCAIP2.namespace]
?: return illegalState()
// find and add all derivation
val networkToAddCAIP10 = networksConverter
.allAddressForChain(networkToAddCAIP2.raw, wallet)
.map { address -> CAIP10(networkToAddCAIP2, address).raw }
val newNamespaces = namespaces.copy(
chains = namespaces.chains.plus(networkToAddCAIP2.raw),
accounts = namespaces.accounts.plus(networkToAddCAIP10),
)
val sdkNewNamespaces = session.sdkModel.namespaces
.plus(requestedNetworkCAIP2.namespace to newNamespaces)
.mapValues { (_, session) ->
Model.Namespace.Session(
chains = session.chains,
accounts = session.accounts,
methods = session.methods,
events = session.events,
)
}
val sessionUpdate = Wallet.Params.SessionUpdate(
sessionTopic = context.session.sdkModel.topic,
namespaces = sdkNewNamespaces,
)
sdkUpdateSession(sessionUpdate) // ignore result for now
return respondService.respond(rawSdkRequest, "")
}
private suspend fun sdkUpdateSession(sessionUpdate: Wallet.Params.SessionUpdate): Either<Throwable, Unit> {
return suspendCancellableCoroutine { continuation ->
WalletKit.updateSession(
params = sessionUpdate,
onSuccess = { if (continuation.isActive) continuation.resume(Unit.right()) },
onError = { if (continuation.isActive) continuation.resume(it.throwable.left()) },
)
}
}
override fun reject() {
respondService.rejectRequestNonBlock(rawSdkRequest)
}
@ -43,4 +112,37 @@ internal class WcEthAddNetworkUseCase @AssistedInject constructor(
interface Factory {
fun create(context: WcMethodUseCaseContext, method: WcEthMethod.AddEthereumChain): WcEthAddNetworkUseCase
}
}
internal class WcEthAddSwitchCommonDelegate @AssistedInject constructor(
private val networksConverter: WcNetworksConverter,
@Assisted val context: WcMethodUseCaseContext,
) {
private val wallet: UserWallet get() = context.session.wallet
fun hexChainIdToCAIP2(hexChainId: String): CAIP2? = CAIP2.fromRaw("$ETH_NAMESPACE_KEY:${hexChainId.hexToInt()}")
fun existInWcSession(network: Network): Boolean {
return context.session.networks.any { it.rawId == network.rawId }
}
suspend fun commonChecks(hexChainId: String): Either<HandleMethodError, Network> {
val caip2 = hexChainIdToCAIP2(hexChainId)
?: return HandleMethodError.UnknownError("Failed to parse CAIP2").left()
val generalNetwork = networksConverter.createNetwork(caip2.raw, wallet)
if (generalNetwork == null) {
return HandleMethodError.TangemUnsupportedNetwork(caip2.raw).left()
}
val addedNetwork = networksConverter.mainOrAnyWalletNetworkForRequest(caip2.raw, wallet)
if (addedNetwork == null) {
return HandleMethodError.NotAddedNetwork(generalNetwork.name).left()
}
return addedNetwork.right()
}
@AssistedFactory
interface Factory {
fun create(context: WcMethodUseCaseContext): WcEthAddSwitchCommonDelegate
}
}

View file

@ -14,7 +14,6 @@ import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter.Compani
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.data.walletconnect.utils.WcNetworksConverter
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.walletconnect.model.*
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.repository.WcSessionsManager
@ -44,7 +43,7 @@ internal class WcEthNetwork(
?: return HandleMethodError.UnknownSession.left()
val wallet = session.wallet
val chainId = request.chainId.orEmpty()
val method: WcEthMethod = name.toMethod(request, wallet)
val method: WcEthMethod = name.toMethod(request)
.getOrElse { return error(it.message.orEmpty()) }
?: return error("Failed to parse $name")
suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, wallet)
@ -54,7 +53,9 @@ internal class WcEthNetwork(
is WcEthMethod.SendTransaction -> method.transaction.from
is WcEthMethod.SignTransaction -> method.transaction.from
is WcEthMethod.SignTypedData -> method.account
is WcEthMethod.AddEthereumChain ->
is WcEthMethod.AddEthereumChain,
is WcEthMethod.SwitchEthereumChain,
->
anyExistNetwork()
?.let { network -> walletManagersFacade.getDefaultAddress(wallet.walletId, network).orEmpty() }
.orEmpty()
@ -65,7 +66,9 @@ internal class WcEthNetwork(
is WcEthMethod.SendTransaction,
is WcEthMethod.SignTransaction,
-> networksConverter.findWalletNetworkForRequest(request, session, accountAddress)
is WcEthMethod.AddEthereumChain -> anyExistNetwork()
is WcEthMethod.AddEthereumChain,
is WcEthMethod.SwitchEthereumChain,
-> anyExistNetwork()
} ?: return error("Failed to find walletNetwork for accountAddress $accountAddress")
val context = WcMethodUseCaseContext(
@ -81,13 +84,11 @@ internal class WcEthNetwork(
is WcEthMethod.SignTransaction -> factories.signTransaction.create(context, method)
is WcEthMethod.SignTypedData -> factories.signTypedData.create(context, method)
is WcEthMethod.AddEthereumChain -> factories.addNetwork.create(context, method)
is WcEthMethod.SwitchEthereumChain -> factories.switchNetwork.create(context, method)
}.right()
}
private suspend fun WcEthMethodName.toMethod(
request: WcSdkSessionRequest,
wallet: UserWallet,
): Either<Throwable, WcEthMethod?> {
private fun WcEthMethodName.toMethod(request: WcSdkSessionRequest): Either<Throwable, WcEthMethod?> {
val rawParams = request.request.params
return when (this) {
WcEthMethodName.EthSign,
@ -109,14 +110,17 @@ internal class WcEthNetwork(
}
}
?: return null.right()
WcEthMethodName.AddEthereumChain -> moshi.fromJson<List<WcEthAddChain>>(rawParams)
WcEthMethodName.AddEthereumChain,
WcEthMethodName.SwitchEthereumChain,
-> moshi.fromJson<List<WcEthAddChain>>(rawParams)
.getOrElse { return it.left() }
?.firstOrNull()
?.let {
val newNetwork = networksConverter
.mainOrAnyWalletNetworkForRequest(it.chainId, wallet)
?: return null.right()
WcEthMethod.AddEthereumChain(rawChain = it, network = newNetwork).right()
if (this == WcEthMethodName.AddEthereumChain) {
WcEthMethod.AddEthereumChain(rawChain = it).right()
} else {
WcEthMethod.SwitchEthereumChain(rawChain = it).right()
}
}
?: null.right()
}
@ -147,13 +151,17 @@ internal class WcEthNetwork(
override val excludedBlockchains: ExcludedBlockchains,
) : WcNamespaceConverter {
override val namespaceKey: NamespaceKey = NamespaceKey("eip155")
override val namespaceKey: NamespaceKey = NamespaceKey(ETH_NAMESPACE_KEY)
override fun toBlockchain(chainId: CAIP2): Blockchain? {
if (chainId.namespace != namespaceKey.key) return null
val ethChainId = chainId.reference.toIntOrNull() ?: return null
return Blockchain.fromChainId(ethChainId)
}
companion object {
const val ETH_NAMESPACE_KEY = "eip155"
}
}
internal class Factories @Inject constructor(
@ -162,5 +170,6 @@ internal class WcEthNetwork(
val sendTransaction: WcEthSendTransactionUseCase.Factory,
val signTransaction: WcEthSignTransactionUseCase.Factory,
val addNetwork: WcEthAddNetworkUseCase.Factory,
val switchNetwork: WcEthSwitchNetworkUseCase.Factory,
)
}

View file

@ -7,6 +7,9 @@ import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.formatHex
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam.TxSentFrom
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.Basic.TransactionSent.MemoType
import com.tangem.data.walletconnect.respond.WcRespondService
import com.tangem.data.walletconnect.sign.BaseWcSignUseCase
import com.tangem.data.walletconnect.sign.SignCollector
@ -86,6 +89,16 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
emit(state.toResult(parseSendError(error).left()))
}
.getOrNull() ?: return
analytics.send(
Basic.TransactionSent(
sentFrom = TxSentFrom.WalletConnect(
blockchain = network.name,
token = network.currencySymbol,
feeType = null,
),
memoType = MemoType.Null,
),
)
val respondResult = respondService.respond(rawSdkRequest, hash.formatHex())
emit(state.toResult(respondResult))
}

View file

@ -30,7 +30,7 @@ import com.tangem.blockchain.common.Amount as BlockchainAmount
internal class WcEthSignTransactionUseCase @AssistedInject constructor(
override val respondService: WcRespondService,
override val analytics: AnalyticsEventHandler,
private val prepareForSend: PrepareForSendUseCase,
private val prepareForSend: PrepareForSendUseCase, // TODO: TODO("[REDACTED_JIRA]")
private val ethTxHelper: WcEthTxHelper,
@Assisted override val context: WcMethodUseCaseContext,
@Assisted override val method: WcEthMethod.SignTransaction,

View file

@ -0,0 +1,56 @@
package com.tangem.data.walletconnect.network.ethereum
import arrow.core.Either
import com.tangem.data.walletconnect.respond.WcRespondService
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.domain.models.network.Network
import com.tangem.domain.walletconnect.model.HandleMethodError
import com.tangem.domain.walletconnect.model.WcEthMethod
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.usecase.method.WcNetworkDerivationState
import com.tangem.domain.walletconnect.usecase.method.WcSwitchNetworkUseCase
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class WcEthSwitchNetworkUseCase @AssistedInject constructor(
private val respondService: WcRespondService,
@Assisted val context: WcMethodUseCaseContext,
@Assisted override val method: WcEthMethod.SwitchEthereumChain,
addSwitchCommonDelegateFactory: WcEthAddSwitchCommonDelegate.Factory,
) : WcSwitchNetworkUseCase {
override val session: WcSession
get() = context.session
override val rawSdkRequest: WcSdkSessionRequest
get() = context.rawSdkRequest
override val network: Network
get() = context.network
override val derivationState: WcNetworkDerivationState = when {
context.networkDerivationsCount > 1 -> WcNetworkDerivationState.Multiple(walletAddress = context.accountAddress)
else -> WcNetworkDerivationState.Single
}
private val addSwitchCommonDelegate = addSwitchCommonDelegateFactory.create(context)
override suspend fun invoke(): Either<HandleMethodError, WcSwitchNetworkUseCase.SwitchNetwork> {
return addSwitchCommonDelegate
.commonChecks(method.rawChain.chainId)
.map { addedNetwork ->
WcSwitchNetworkUseCase.SwitchNetwork(
network = addedNetwork,
isExistInWcSession = addSwitchCommonDelegate.existInWcSession(addedNetwork),
)
}
}
override fun reject() {
respondService.rejectRequestNonBlock(rawSdkRequest)
}
@AssistedFactory
interface Factory {
fun create(context: WcMethodUseCaseContext, method: WcEthMethod.SwitchEthereumChain): WcEthSwitchNetworkUseCase
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.data.walletconnect.network.ethereum
import com.domain.blockaid.models.transaction.CheckTransactionResult
import com.domain.blockaid.models.transaction.SimulationResult
import com.domain.blockaid.models.transaction.simultation.ApprovedAmount
import com.domain.blockaid.models.transaction.simultation.ApproveInfo
import com.domain.blockaid.models.transaction.simultation.SimulationData
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.tokenmethods.ApprovalERC20TokenCallData
@ -68,13 +68,15 @@ internal class WcEthTxHelper @Inject constructor(
)
}
fun getApprovedAmount(txData: String?, result: CheckTransactionResult): ApprovedAmount? {
fun getApprovedAmount(txData: String?, result: CheckTransactionResult): ApproveInfo.Amount? {
val approvalMethodId = ApprovalERC20TokenCallData("", null).methodId
val isApprovalWcMethod = txData?.startsWith(approvalMethodId)
if (isApprovalWcMethod != true) return null
val simulation = result.simulation as? SimulationResult.Success
?: return null
val approves = (simulation.data as? SimulationData.Approve)?.approvedAmounts
val approves = (simulation.data as? SimulationData.Approve)
?.items
?.filterIsInstance<ApproveInfo.Amount>()
?: return null
if (approves.isEmpty()) return null
val amount = approves.first()

View file

@ -10,7 +10,7 @@ import com.tangem.data.walletconnect.sign.SignCollector
import com.tangem.data.walletconnect.sign.SignStateConverter.toResult
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate
import com.tangem.domain.transaction.usecase.PrepareForSendUseCase
import com.tangem.domain.transaction.usecase.PrepareAndSignUseCase
import com.tangem.domain.walletconnect.error.parseSendError
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck
@ -29,7 +29,7 @@ import org.json.JSONObject
internal class WcSolanaSignAllTransactionUseCase @AssistedInject constructor(
override val respondService: WcRespondService,
override val analytics: AnalyticsEventHandler,
private val prepareForSend: PrepareForSendUseCase,
private val prepareAndSign: PrepareAndSignUseCase,
@Assisted override val context: WcMethodUseCaseContext,
@Assisted override val method: WcSolanaMethod.SignAllTransaction,
blockAidDelegate: BlockAidVerificationDelegate,
@ -46,7 +46,7 @@ internal class WcSolanaSignAllTransactionUseCase @AssistedInject constructor(
).map { lce -> lce.map { result -> BlockAidTransactionCheck.Result.Plain(result) } }
override suspend fun SignCollector<List<TransactionData>>.onSign(state: WcSignState<List<TransactionData>>) {
val hash = prepareForSend.invoke(transactionData = state.signModel, userWallet = wallet, network = network)
val hash = prepareAndSign.invoke(transactionData = state.signModel, userWallet = wallet, network = network)
.onLeft { error ->
emit(state.toResult(parseSendError(error).left()))
}

View file

@ -10,7 +10,7 @@ import com.tangem.data.walletconnect.sign.SignCollector
import com.tangem.data.walletconnect.sign.SignStateConverter.toResult
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate
import com.tangem.domain.transaction.usecase.PrepareForSendUseCase
import com.tangem.domain.transaction.usecase.PrepareAndSignUseCase
import com.tangem.domain.walletconnect.error.parseSendError
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck
@ -27,7 +27,7 @@ import okio.ByteString.Companion.decodeBase64
internal class WcSolanaSignTransactionUseCase @AssistedInject constructor(
override val respondService: WcRespondService,
override val analytics: AnalyticsEventHandler,
private val prepareForSend: PrepareForSendUseCase,
private val prepareAndSign: PrepareAndSignUseCase,
@Assisted override val context: WcMethodUseCaseContext,
@Assisted override val method: WcSolanaMethod.SignTransaction,
blockAidDelegate: BlockAidVerificationDelegate,
@ -44,7 +44,7 @@ internal class WcSolanaSignTransactionUseCase @AssistedInject constructor(
).map { lce -> lce.map { result -> BlockAidTransactionCheck.Result.Plain(result) } }
override suspend fun SignCollector<TransactionData>.onSign(state: WcSignState<TransactionData>) {
val hash = prepareForSend.invoke(transactionData = state.signModel, userWallet = wallet, network = network)
val hash = prepareAndSign.invoke(transactionData = state.signModel, userWallet = wallet, network = network)
.onLeft { error ->
emit(state.toResult(parseSendError(error).left()))
}

View file

@ -20,10 +20,12 @@ import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import org.joda.time.DateTime
import timber.log.Timber
import java.net.URI
@Suppress("LongParameterList")
internal class DefaultWcPairUseCase @AssistedInject constructor(
@ -63,6 +65,12 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
return@flow
}
val dAppUri = URI(sdkSessionProposal.url)
if (dAppUri.host.isNullOrEmpty()) {
emit(WcPairState.Error(WcPairError.InvalidDomainURL))
return@flow
}
val proposalState = buildProposalState(sdkSessionProposal, sdkVerifyContext)
.onLeft {
analytics.send(WcAnalyticEvents.PairFailed(it.code))
@ -112,14 +120,21 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
Timber.tag(WC_TAG).e(it, "Failed to approve session ${sdkSessionProposal.name}")
}
emit(WcPairState.Approving.Result(sessionForApprove, either))
}.onCompletion {
if (it != null) {
Timber.tag(WC_TAG).e(it, "Completed with error $pairRequest")
emit(WcPairState.Error(WcPairError.Unknown(it.message.orEmpty())))
} else {
Timber.tag(WC_TAG).i("Completed successfully $pairRequest")
}
}
.catch {
val pairError: WcPairError = when (it) {
is TimeoutCancellationException -> WcPairError.TimeoutException(it.message.orEmpty())
else -> WcPairError.Unknown(it.message.orEmpty())
}
emit(WcPairState.Error(pairError))
}
.onCompletion {
if (it != null) {
Timber.tag(WC_TAG).e(it, "Completed with error $pairRequest")
} else {
Timber.tag(WC_TAG).i("Completed successfully $pairRequest")
}
}
}
override fun approve(sessionForApprove: WcSessionApprove) {

View file

@ -150,7 +150,7 @@ internal class WcPairSdkDelegate : WcSdkObserver {
private fun Throwable.toApproveError() = WcPairError.ApprovalFailed(this.localizedMessage.orEmpty()).left()
companion object {
private const val CALLBACK_TIMEOUT = 60
private const val CALLBACK_TIMEOUT = 15
// com.reown.android.pairing.engine.domain.PairingEngine.pair
private val pairingExpiredMessages = listOf(
"Pairing URI expired",

View file

@ -33,6 +33,7 @@ internal class DefaultWcRequestService(
Timber.tag(WC_TAG).i("handle request name $name")
if (name is WcMethodName.Unsupported) {
respondService.rejectRequestNonBlock(sr)
if (name.raw.startsWith("wallet_")) return
}
_wcRequest.trySend(name to sr)
}

View file

@ -4,31 +4,32 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.domain.models.network.Network
import com.tangem.utils.converter.Converter
import javax.inject.Inject
internal class BlockAidChainNameConverter @Inject constructor() : Converter<Network, String> {
internal object BlockAidChainNameConverter : Converter<Network, String?> {
@Suppress("CyclomaticComplexMethod")
override fun convert(value: Network): String {
override fun convert(value: Network): String? {
return when (Blockchain.fromNetworkId(value.backendId)) {
Blockchain.Arbitrum -> "arbitrum"
Blockchain.Avalanche -> "avalanche"
Blockchain.AvalancheTestnet -> "avalanche-fuji"
Blockchain.Binance, Blockchain.BSC -> "bsc"
Blockchain.Ethereum -> "ethereum"
Blockchain.EthereumTestnet -> "ethereum-sepolia"
Blockchain.Polygon -> "polygon"
Blockchain.Solana -> "mainnet"
Blockchain.Gnosis -> "gnosis"
Blockchain.Optimism -> "optimism"
Blockchain.ZkSyncEra -> "zksync"
Blockchain.ZkSyncEraTestnet -> "zksync-sepolia"
Blockchain.Base -> "base"
Blockchain.BaseTestnet -> "base-sepolia"
Blockchain.Binance, Blockchain.BSC -> "bsc"
Blockchain.Ethereum -> "ethereum"
Blockchain.Optimism -> "optimism"
Blockchain.Polygon -> "polygon"
Blockchain.ZkSyncEra -> "zksync"
Blockchain.ZkSyncEraTestnet -> "zksync-sepolia"
Blockchain.Blast, Blockchain.BlastTestnet -> "blast"
Blockchain.ApeChain, Blockchain.ApeChainTestnet -> "apechain"
Blockchain.Scroll -> "scroll"
else -> value.name
Blockchain.EthereumTestnet -> "ethereum-sepolia"
Blockchain.Gnosis -> "gnosis"
Blockchain.ApeChain, Blockchain.ApeChainTestnet -> "apechain"
Blockchain.Solana -> "mainnet"
else -> null
}
}
}

View file

@ -17,7 +17,6 @@ import javax.inject.Inject
internal class BlockAidVerificationDelegate @Inject constructor(
private val blockAidVerifier: BlockAidVerifier,
private val blockAidChainNameConverter: BlockAidChainNameConverter,
) {
fun getSecurityStatus(
@ -27,7 +26,6 @@ internal class BlockAidVerificationDelegate @Inject constructor(
session: WcSession,
accountAddress: String?,
): LceFlow<Throwable, CheckTransactionResult> = flow {
emit(Lce.Loading(partialContent = null))
val failedResult = CheckTransactionResult(
validation = ValidationResult.FAILED_TO_VALIDATE,
simulation = SimulationResult.FailedToSimulate,
@ -36,7 +34,21 @@ internal class BlockAidVerificationDelegate @Inject constructor(
emit(Lce.Content(failedResult))
return@flow
}
when (method) {
val chain = BlockAidChainNameConverter.convert(network)
if (chain == null) {
emit(Lce.Content(failedResult))
return@flow
}
emit(Lce.Loading(partialContent = null))
val methodName = when (method) {
is WcEthMethod -> rawSdkRequest.request.method
is WcSolanaMethod -> method.trimmedPrefixMethodName
is WcMethod.Unsupported -> {
emit(Lce.Content(failedResult))
return@flow
}
}
val params = when (method) {
is WcEthMethod -> TransactionParams.Evm(rawSdkRequest.request.params)
is WcSolanaMethod.SignAllTransaction -> TransactionParams.Solana(method.transaction)
is WcSolanaMethod.SignTransaction -> TransactionParams.Solana(listOf(method.transaction))
@ -45,25 +57,28 @@ internal class BlockAidVerificationDelegate @Inject constructor(
emit(Lce.Content(failedResult))
return@flow
}
else -> null
}?.let { params ->
blockAidVerifier.verifyTransaction(
TransactionData(
chain = blockAidChainNameConverter.convert(network),
accountAddress = accountAddress,
method = rawSdkRequest.request.method,
domainUrl = session.sdkModel.appMetaData.url,
params = params,
),
).fold(
ifLeft = {
Timber.e("Failed to verify transaction: ${it.localizedMessage}")
emit(Lce.Error(it))
},
ifRight = {
emit(Lce.Content(it))
},
)
} ?: emit(Lce.Content(failedResult))
else -> {
emit(Lce.Content(failedResult))
return@flow
}
}
blockAidVerifier.verifyTransaction(
data = TransactionData(
chain = chain,
accountAddress = accountAddress,
method = methodName,
domainUrl = session.sdkModel.appMetaData.url,
params = params,
),
).fold(
ifLeft = {
Timber.e("Failed to verify transaction: ${it.localizedMessage}")
emit(Lce.Error(it))
},
ifRight = {
emit(Lce.Content(it))
},
)
}
}

View file

@ -25,6 +25,11 @@ internal class WcNetworksConverter @Inject constructor(
private val tokensFeatureToggles: TokensFeatureToggles,
) {
fun createNetwork(chainId: String, wallet: UserWallet): Network? {
return namespaceConverters
.firstNotNullOfOrNull { it.toNetwork(chainId, wallet) }
}
suspend fun findWalletNetworkForRequest(
request: WcSdkSessionRequest,
session: WcSession,
@ -48,6 +53,11 @@ internal class WcNetworksConverter @Inject constructor(
return networks.firstOrNull { !isCustomCoin(it) } ?: networks.firstOrNull()
}
suspend fun allAddressForChain(rawChainId: String, wallet: UserWallet): List<String> {
return filterWalletNetworkForRequest(rawChainId, wallet)
.mapNotNull { walletManagersFacade.getDefaultAddress(wallet.walletId, it)?.lowercase() }
}
/**
* return all exist derivation networks
*/

View file

@ -10,6 +10,14 @@ internal object WcSdkSessionConverter : Converter<Wallet.Model.Session, WcSdkSes
return WcSdkSession(
topic = value.topic,
appMetaData = value.metaData?.let { WcAppMetaDataConverter.convert(it) } ?: WcAppMetaDataConverter.empty,
namespaces = value.namespaces.mapValues { (_, session) ->
WcSdkSession.Session(
chains = session.chains ?: listOf(),
accounts = session.accounts,
methods = session.methods,
events = session.events,
)
},
)
}
}

View file

@ -47,7 +47,7 @@ internal class DefaultWcPairUseCaseTest {
pairingTopic = "",
name = "",
description = "",
url = "",
url = "https://react-app.walletconnect.com/",
icons = listOf(),
redirect = "",
requiredNamespaces = mapOf(),

View file

@ -62,6 +62,7 @@ internal class WcSignUseCaseDelegateTest {
connectingTime = 0L,
sdkModel = WcSdkSession(
topic = "",
namespaces = mapOf(),
appMetaData = WcAppMetaData(
name = "",
description = "",

View file

@ -10,6 +10,7 @@ import com.tangem.data.walletmanager.extensions.makeWalletManagerForApp
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.config.curvesConfig
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import timber.log.Timber
@ -41,7 +42,7 @@ internal class WalletManagerFactory(
blockchain: Blockchain,
derivationPath: DerivationPath?,
): WalletManager? {
val curve = blockchain.getSupportedCurves().first()
val curve = hotWallet.curvesConfig.primaryCurve(blockchain)
val selectedWallet = hotWallet.wallets.orEmpty().firstOrNull { it.curve == curve }
?: return null
return try {

View file

@ -17,6 +17,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.SEED_FIRST_NOTIFI
import com.tangem.datasource.local.preferences.utils.get
import com.tangem.datasource.local.preferences.utils.getObjectMap
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.models.wallet.UserWallet
@ -47,14 +48,78 @@ internal class DefaultWalletsRepository(
return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false)
}
@Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method")
override fun shouldSaveUserWallets(): Flow<Boolean> {
return appPreferencesStore.get(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false)
}
@Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method")
override suspend fun saveShouldSaveUserWallets(item: Boolean) {
appPreferencesStore.store(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, value = item)
}
override suspend fun useBiometricAuthentication(): Boolean {
val useBiometricAuthentication = appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY,
)
if (useBiometricAuthentication != null) {
return useBiometricAuthentication
}
val legacySaveWalletsInTheApp = appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.SAVE_USER_WALLETS_KEY,
)
if (legacySaveWalletsInTheApp != null) {
// Migrate legacy setting to new one
appPreferencesStore.store(
key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY,
value = legacySaveWalletsInTheApp,
)
return legacySaveWalletsInTheApp
} else {
// Default value for new users
setUseBiometricAuthentication(false)
return false
}
}
override suspend fun setUseBiometricAuthentication(value: Boolean) {
appPreferencesStore.store(key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY, value = value)
}
override suspend fun requireAccessCode(): Boolean {
val requireAccessCode = appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY,
)
if (requireAccessCode != null) {
return requireAccessCode
}
val legacyShouldSaveAccessCode = appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY,
)
if (legacyShouldSaveAccessCode != null) {
// Migrate legacy setting to new one
appPreferencesStore.store(
key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY,
value = legacyShouldSaveAccessCode.not(),
)
return legacyShouldSaveAccessCode.not()
} else {
// Default value for new users
setRequireAccessCode(true)
return true
}
}
override suspend fun setRequireAccessCode(value: Boolean) {
appPreferencesStore.store(key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY, value = value)
}
override suspend fun isWalletWithRing(userWalletId: UserWalletId): Boolean {
return appPreferencesStore
.getSyncOrDefault(key = PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY, default = emptySet())

View file

@ -7,12 +7,11 @@ 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.config.curvesConfig
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import kotlin.collections.forEach
@ -51,13 +50,9 @@ internal class MissedDerivationsFinder(private val userWallet: UserWallet) {
}
private fun List<Network>.mapToNewDerivations(): List<DerivationData> {
val config = when (userWallet) {
is UserWallet.Cold -> CardConfig.createConfig(userWallet.scanResponse.card)
is UserWallet.Hot -> Wallet2CardConfig // TODO [REDACTED_TASK_KEY] [Hot Wallet] Derivation config for hot wallet
}
return mapNotNull { network ->
val blockchain = network.toBlockchain()
val curve = config.primaryCurve(blockchain) ?: return@mapNotNull null
val curve = userWallet.curvesConfig.primaryCurve(blockchain) ?: return@mapNotNull null
val walletPublicKey = when (userWallet) {
is UserWallet.Cold -> {

View file

@ -5,6 +5,7 @@ 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.data.wallets.hot.DefaultHotWalletAccessCodeAttemptsRepository
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.datastore.RuntimeStateStore
@ -13,6 +14,7 @@ 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.hot.HotWalletAccessCodeAttemptsRepository
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -68,4 +70,10 @@ internal interface WalletsDataBindsModule {
@Binds
@Singleton
fun bindColdMapDerivationsRepository(impl: DefaultColdMapDerivationsRepository): ColdMapDerivationsRepository
@Binds
@Singleton
fun bindHotWalletAccessCodeAttemptsRepository(
impl: DefaultHotWalletAccessCodeAttemptsRepository,
): HotWalletAccessCodeAttemptsRepository
}

View file

@ -0,0 +1,138 @@
package com.tangem.data.wallets.hot
import android.content.Context
import android.os.SystemClock
import android.provider.Settings
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Attempts
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.ATTEMPTS_BEFORE_DELETION
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.COOLDOWN_SECONDS
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.MAX_ATTEMPTS_BEFORE_DELETION
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.MAX_FAST_FORWARD_ATTEMPTS
import com.tangem.hot.sdk.model.HotWalletId
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@Suppress("MagicNumber")
class DefaultHotWalletAccessCodeAttemptsRepository @Inject constructor(
@ApplicationContext private val context: Context,
private val appPreferencesStore: AppPreferencesStore,
) : HotWalletAccessCodeAttemptsRepository {
override suspend fun incrementAttempts(id: HotWalletAccessCodeAttemptsRepository.AttemptId) {
val attemptsKey = PreferencesKeys.getHotWalletUnlockAttemptsKey(id.attemptIdKey())
appPreferencesStore.editData { preferences ->
val currentAttempts = preferences[attemptsKey] ?: 0
val newAttempts = currentAttempts + 1
preferences[attemptsKey] = newAttempts
val currentBootCount = currentBootCount()
preferences[PreferencesKeys.getHotWalletUnlockBootKey(id.attemptIdKey())] = currentBootCount
if (newAttempts >= MAX_FAST_FORWARD_ATTEMPTS) {
val currentDeadline = SystemClock.elapsedRealtime() + COOLDOWN_SECONDS * 1000
preferences[PreferencesKeys.getHotWalletUnlockDeadlineKey(id.attemptIdKey())] = currentDeadline
}
}
}
override suspend fun resetAttempts(hotWalletId: HotWalletId) {
val authAttemptId = HotWalletAccessCodeAttemptsRepository.AttemptId(
hotWalletId = hotWalletId,
auth = true,
)
val noAuthAttemptId = HotWalletAccessCodeAttemptsRepository.AttemptId(
hotWalletId = hotWalletId,
auth = false,
)
appPreferencesStore.editData {
it.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(authAttemptId.attemptIdKey()))
it.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(noAuthAttemptId.attemptIdKey()))
it.remove(PreferencesKeys.getHotWalletUnlockBootKey(authAttemptId.attemptIdKey()))
it.remove(PreferencesKeys.getHotWalletUnlockBootKey(noAuthAttemptId.attemptIdKey()))
it.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(authAttemptId.attemptIdKey()))
it.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(noAuthAttemptId.attemptIdKey()))
}
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun getAttempts(id: HotWalletAccessCodeAttemptsRepository.AttemptId): Flow<Attempts> {
val flow = appPreferencesStore.data.map {
AttemptsPersistentData(
attempts = it[PreferencesKeys.getHotWalletUnlockAttemptsKey(id.attemptIdKey())] ?: 0,
bootCount = it[PreferencesKeys.getHotWalletUnlockBootKey(id.attemptIdKey())] ?: 0,
deadline = it[PreferencesKeys.getHotWalletUnlockDeadlineKey(id.attemptIdKey())] ?: 0L,
)
}.distinctUntilChanged()
return flow.transformLatest {
while (true) {
emit(toState(id, it.attempts, it.deadline, it.bootCount))
val remaining = remainingSeconds(it.deadline, it.bootCount)
if (remaining <= 0) break
delay(timeMillis = 1000)
}
}.distinctUntilChanged()
}
override suspend fun getAttemptsSync(id: HotWalletAccessCodeAttemptsRepository.AttemptId): Attempts {
val prefs = appPreferencesStore.data.first()
val count = prefs[PreferencesKeys.getHotWalletUnlockAttemptsKey(id.attemptIdKey())] ?: 0
val boot = prefs[PreferencesKeys.getHotWalletUnlockBootKey(id.attemptIdKey())] ?: 0
val deadline = prefs[PreferencesKeys.getHotWalletUnlockDeadlineKey(id.attemptIdKey())] ?: 0L
return toState(id, count, deadline, boot)
}
private fun remainingSeconds(deadline: Long, bootStored: Int): Int {
val now = SystemClock.elapsedRealtime()
val bootNow = currentBootCount()
if (bootNow != bootStored) {
// If the boot happened after the last attempt, we consider timer to start from the beginning
return maxOf(0, COOLDOWN_SECONDS - (now / 1000).toInt())
}
return maxOf(0, ((deadline - now) / 1000).toInt())
}
private fun toState(
id: HotWalletAccessCodeAttemptsRepository.AttemptId,
count: Int,
deadlineElapsed: Long,
bootStored: Int,
): Attempts {
val fast = MAX_FAST_FORWARD_ATTEMPTS
val attention = ATTEMPTS_BEFORE_DELETION
val deletion = MAX_ATTEMPTS_BEFORE_DELETION
return when {
count < fast -> Attempts.FastForward(count)
id.auth && count >= deletion -> Attempts.Deletion
id.auth && count >= attention -> {
val remaining = remainingSeconds(deadlineElapsed, bootStored)
Attempts.BeforeDeletion(count, remaining, deletion - count)
}
else -> {
val remaining = remainingSeconds(deadlineElapsed, bootStored)
Attempts.WithDelay(count, remaining)
}
}
}
private fun HotWalletAccessCodeAttemptsRepository.AttemptId.attemptIdKey(): String {
return "${hotWalletId.value}_$auth"
}
private fun currentBootCount(): Int = Settings.Global.getInt(context.contentResolver, Settings.Global.BOOT_COUNT, 0)
private data class AttemptsPersistentData(
val attempts: Int,
val bootCount: Int,
val deadline: Long,
)
}

View file

@ -1,7 +1,11 @@
package com.tangem.data.wallets.hot
import com.tangem.common.core.TangemSdkError
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.copy
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.exception.WrongPasswordException
import com.tangem.hot.sdk.model.*
@ -9,7 +13,9 @@ import javax.inject.Inject
class HotWalletAccessor @Inject constructor(
private val tangemHotSdk: TangemHotSdk,
private val userWalletsListRepository: UserWalletsListRepository,
private val hotWalletPasswordRequester: HotWalletPasswordRequester,
private val walletsRepository: WalletsRepository,
) {
suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List<DataToSign>): List<SignedData> =
@ -23,10 +29,24 @@ class HotWalletAccessor @Inject constructor(
}
private suspend fun <T> hotSdkRequest(hotWalletId: HotWalletId, block: suspend (unlock: UnlockHotWallet) -> T): T {
val isAccessCodeRequired = walletsRepository.requireAccessCode()
val auth = when (hotWalletId.authType) {
HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth
HotWalletId.AuthType.Password -> requestPassword(false)
HotWalletId.AuthType.Biometry -> HotAuth.Biometry
HotWalletId.AuthType.Password -> requestPassword(
hotWalletId = hotWalletId,
hasBiometry = false,
)
HotWalletId.AuthType.Biometry -> {
if (isAccessCodeRequired) {
requestPassword(
hotWalletId = hotWalletId,
hasBiometry = false,
)
} else {
HotAuth.Biometry
}
}
}
return runCatchingSdkErrors(hotWalletId, auth) {
@ -42,27 +62,49 @@ class HotWalletAccessor @Inject constructor(
block: suspend (auth: HotAuth) -> T,
): T {
return runCatchingWrongPassInternal(
hotWalletId = hotWalletId,
originalAuth = auth,
auth = auth,
block = { blockAuth ->
block(blockAuth).also {
// TODO [REDACTED_TASK_KEY] [Hot Wallet] Authorization by access code
// 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,
)
}
// Update biometry auth if the original auth was password
updateBiometryAuthIfNeeded(
hotWalletId = hotWalletId,
originalAuth = blockAuth,
)
}
},
)
}
private suspend fun updateBiometryAuthIfNeeded(hotWalletId: HotWalletId, originalAuth: HotAuth) {
val isAccessCodeRequired = walletsRepository.requireAccessCode()
if (originalAuth is HotAuth.Password && isAccessCodeRequired.not()) {
val userWallet = userWalletsListRepository.userWalletsSync()
.find { it is UserWallet.Hot && it.hotWalletId == hotWalletId }
as? UserWallet.Hot
?: return
val newHotWalletId = tangemHotSdk.changeAuth(
unlockHotWallet = UnlockHotWallet(
walletId = hotWalletId,
auth = originalAuth,
),
auth = HotAuth.Biometry,
)
userWalletsListRepository.saveWithoutLock(
userWallet = userWallet.copy(
hotWalletId = newHotWalletId,
),
canOverride = true,
)
}
}
private suspend fun <T> runCatchingWrongPassInternal(
hotWalletId: HotWalletId,
originalAuth: HotAuth,
auth: HotAuth,
block: suspend (auth: HotAuth) -> T,
@ -71,9 +113,13 @@ class HotWalletAccessor @Inject constructor(
}.getOrElse { exception ->
if (auth is HotAuth.Biometry && exception.isBiometryError()) {
// fallback to password if biometry fails
val passAuth = requestPassword(true)
val passAuth = requestPassword(
hotWalletId = hotWalletId,
hasBiometry = true,
)
return@getOrElse runCatchingWrongPassInternal(
hotWalletId = hotWalletId,
originalAuth = originalAuth,
auth = passAuth,
block = block,
@ -87,17 +133,28 @@ class HotWalletAccessor @Inject constructor(
// If the exception is a wrong password, we need to request the password again
hotWalletPasswordRequester.wrongPassword()
val passResult = requestPassword(originalAuth is HotAuth.Biometry)
val passResult = requestPassword(
hotWalletId = hotWalletId,
hasBiometry = originalAuth is HotAuth.Biometry,
)
runCatchingWrongPassInternal(
hotWalletId = hotWalletId,
originalAuth = originalAuth,
auth = passResult,
block = block,
)
}
private suspend fun requestPassword(hasBiometry: Boolean): HotAuth {
return hotWalletPasswordRequester.requestPassword(hasBiometry).toAuth() ?: throw TangemSdkError.UserCancelled()
private suspend fun requestPassword(hotWalletId: HotWalletId, hasBiometry: Boolean): HotAuth {
val attemptRequest = HotWalletPasswordRequester.AttemptRequest(
hotWalletId = hotWalletId,
authMode = false,
hasBiometry = hasBiometry,
)
return hotWalletPasswordRequester.requestPassword(attemptRequest).toAuth()
?: throw TangemSdkError.UserCancelled()
}
private fun Throwable.isBiometryError(): Boolean {

View file

@ -195,7 +195,7 @@ class DefaultWalletsRepositoryTest {
)
val authProvider = mockk<AuthProvider> {
every { getCardsPublicKeys() } returns publicKeys
coEvery { getCardsPublicKeys() } returns publicKeys
}
repository = DefaultWalletsRepository(