Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-22 11:52:10 +03:00
parent cb23c1dea3
commit 538757a7ce
54 changed files with 622 additions and 488 deletions

View file

@ -4,14 +4,12 @@ import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.core.*
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.common.extensions.toHexString
import com.tangem.common.map
import com.tangem.common.timemeasure.RealtimeMonotonicTimeSource
@ -46,7 +44,7 @@ import timber.log.Timber
import kotlin.coroutines.resume
import kotlin.time.measureTimedValue
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
class VisaCardActivationTask @AssistedInject constructor(
@Assisted private val mode: VisaCardActivationTaskMode,
@Assisted private val activationInput: VisaActivationInput,
@ -97,7 +95,23 @@ class VisaCardActivationTask @AssistedInject constructor(
context.signAuthorizationChallenge(mode.authorizationChallenge)
}
is VisaCardActivationTaskMode.SignOnly -> {
context.signData(mode.dataToSignByCardWallet)
val wallet =
card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val derivedPublicKey = when (val deriveKeyResult = context.deriveKey(wallet.publicKey)) {
is CompletionResult.Failure -> {
return CompletionResult.Failure(deriveKeyResult.error)
}
is CompletionResult.Success -> {
deriveKeyResult.data
}
}
context.signData(
mode.dataToSignByCardWallet,
derivedPublicKey,
)
}
}
}
@ -138,9 +152,35 @@ class VisaCardActivationTask @AssistedInject constructor(
private suspend fun SessionContext.processSignedAuthorizationChallenge(
signedChallenge: VisaAuthSignedChallenge,
): CompletionResult<VisaCardActivationResponse> {
when (val createWalletResult = createWallet()) {
is CompletionResult.Failure -> return CompletionResult.Failure(createWalletResult.error)
is CompletionResult.Success -> {}
}
val card =
session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val wallet =
card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val derivedPublicKey = when (val deriveKeyResult = deriveKey(wallet.publicKey)) {
is CompletionResult.Failure -> {
return CompletionResult.Failure(deriveKeyResult.error)
}
is CompletionResult.Success -> {
deriveKeyResult.data
}
}
val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(derivedPublicKey.publicKey)
.getOrElse { return CompletionResult.Failure(it.tangemError) }
.value
return coroutineScope {
val dataToSignDeferred = async { getDataToSign(signedChallenge) }
val otpTaskDeferred = async { createWallet() }
val dataToSignDeferred =
async { getDataToSign(signedChallenge = signedChallenge, cardWalletAddress = walletAddress) }
val otpTaskDeferred = async { createOTP() }
val dataToSign = dataToSignDeferred.await()
.getOrElse {
@ -150,12 +190,16 @@ class VisaCardActivationTask @AssistedInject constructor(
otpTaskDeferred.await()
signData(dataToSign)
signData(
dataToSign = dataToSign,
derivedPublicKey = derivedPublicKey,
)
}
}
private suspend fun SessionContext.getDataToSign(
signedChallenge: VisaAuthSignedChallenge,
cardWalletAddress: String,
): Either<TangemError, VisaDataToSignByCardWallet> = either {
catch(
block = {
@ -168,7 +212,12 @@ class VisaCardActivationTask @AssistedInject constructor(
raise(VisaActivationError.WrongRemoteState.tangemError)
}
visaActivationRepository.getCardWalletAcceptanceData(remoteState.request)
visaActivationRepository.getCardWalletAcceptanceData(
VisaCardWalletDataToSignRequest(
activationOrderInfo = remoteState.activationOrderInfo,
cardWalletAddress = cardWalletAddress,
),
)
},
catch = {
raise(VisaAuthorizationAPIError.tangemError)
@ -182,7 +231,7 @@ class VisaCardActivationTask @AssistedInject constructor(
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
return if (card.wallets.any { it.curve == EllipticCurve.Secp256k1 }) {
createOTP()
CompletionResult.Success(Unit)
} else {
val createWalletTask = CreateWalletTask(EllipticCurve.Secp256k1)
@ -199,7 +248,7 @@ class VisaCardActivationTask @AssistedInject constructor(
when (val result = timedResult.value) {
is CompletionResult.Success -> {
Timber.i("CreateWalletTask success")
createOTP()
CompletionResult.Success(Unit)
}
is CompletionResult.Failure -> {
Timber.e("CreateWalletTask failure ${result.error}")
@ -245,26 +294,15 @@ class VisaCardActivationTask @AssistedInject constructor(
private suspend fun SessionContext.signData(
dataToSign: VisaDataToSignByCardWallet,
derivedPublicKey: ExtendedPublicKey,
): CompletionResult<VisaCardActivationResponse> {
val card =
session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val wallet =
card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val derivedPublicKey = when (val deriveKeyResult = deriveKey(wallet.publicKey)) {
is CompletionResult.Failure -> {
return CompletionResult.Failure(deriveKeyResult.error)
}
is CompletionResult.Success -> {
deriveKeyResult.data
}
}
val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(derivedPublicKey.publicKey)
.getOrElse { return CompletionResult.Failure(it.tangemError) }
.value
val task = SignHashCommand(
hash = dataToSign.hashToSign.hexToBytes(),
walletPublicKey = wallet.publicKey,
@ -286,8 +324,8 @@ class VisaCardActivationTask @AssistedInject constructor(
Timber.i("SignHashCommand success")
handleSignedData(
dataToSign = dataToSign,
walletAddress = walletAddress,
response = result.data,
derivedPublicKey = derivedPublicKey,
)
}
is CompletionResult.Failure -> {
@ -313,7 +351,7 @@ class VisaCardActivationTask @AssistedInject constructor(
private suspend fun SessionContext.handleSignedData(
dataToSign: VisaDataToSignByCardWallet,
walletAddress: String,
derivedPublicKey: ExtendedPublicKey,
response: SignHashResponse,
): CompletionResult<VisaCardActivationResponse> {
val otp = otpStorage.getOTP(cardId) ?: run {
@ -321,11 +359,16 @@ class VisaCardActivationTask @AssistedInject constructor(
otpStorage.getOTP(cardId) ?: return CompletionResult.Failure(VisaActivationError.MissingRootOTP.tangemError)
}
val rsvSignature = EthereumUtils.prepareSignedMessageData(
signedHash = response.signature,
hashToSign = dataToSign.hashToSign.hexToBytes(),
publicKey = derivedPublicKey.publicKey.toDecompressedPublicKey(),
)
val signedActivationData = dataToSign.sign(
cardWalletAddress = walletAddress,
rootOTP = otp.rootOTP.toHexString(),
otpCounter = otp.counter,
signature = response.signature.toHexString(),
signature = rsvSignature,
)
return setupAccessCode().map {

View file

@ -1,6 +1,7 @@
package com.tangem.tap.domain.tasks.visa
import arrow.core.getOrElse
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.CardWallet
@ -10,7 +11,7 @@ import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.core.error.ext.tangemError
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
@ -129,6 +130,7 @@ class VisaCustomerWalletApproveTask(
targetWalletPublicKey = wallet.publicKey,
derivationPath = derivationPath,
session = session,
extendedPublicKey = extendedPublicKey,
callback = callback,
)
}
@ -149,6 +151,7 @@ class VisaCustomerWalletApproveTask(
signApproveData(
targetWalletPublicKey = publicKey,
derivationPath = null,
extendedPublicKey = null,
session = session,
callback = callback,
)
@ -157,11 +160,14 @@ class VisaCustomerWalletApproveTask(
private fun signApproveData(
targetWalletPublicKey: ByteArray,
derivationPath: DerivationPath?,
extendedPublicKey: ExtendedPublicKey?,
session: CardSession,
callback: CompletionCallback<VisaSignedDataByCustomerWallet>,
) {
val hashToSign = visaDataForApprove.dataToSign.hashToSign.hexToBytes()
val signTask = SignHashCommand(
hash = visaDataForApprove.dataToSign.hashToSign.hexToBytes(),
hash = hashToSign,
walletPublicKey = targetWalletPublicKey,
derivationPath = derivationPath,
)
@ -169,11 +175,18 @@ class VisaCustomerWalletApproveTask(
signTask.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val rsvSignature = EthereumUtils.prepareSignedMessageData(
signedHash = result.data.signature,
hashToSign = hashToSign,
publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey()
?: targetWalletPublicKey.toDecompressedPublicKey(),
)
scanCard(
session = session,
callback = callback,
signedData = visaDataForApprove.dataToSign.sign(
signature = result.data.signature.toHexString(),
signature = rsvSignature,
customerWalletAddress = visaDataForApprove.targetAddress,
),
)

View file

@ -128,6 +128,8 @@ internal class VisaCardScanHandler @Inject constructor(
Timber.i("Requesting challenge for wallet authorization")
val challengeResponse = runCatching {
// TODO [REDACTED_TASK_KEY]
error("sign and get specific error to switch to card_id flow")
visaAuthRepository.getCardWalletAuthChallenge(cardWalletAddress = walletAddress.value)
}.getOrElse {
Timber.i(
@ -180,6 +182,7 @@ internal class VisaCardScanHandler @Inject constructor(
return CompletionResult.Success(VisaCardActivationStatus.Activated(authorizationTokensResponse))
}
@Suppress("LongMethod")
private suspend fun SessionContext.handleCardAuthorization(
cardWalletAddress: String,
): CompletionResult<VisaCardActivationStatus> {
@ -232,11 +235,17 @@ internal class VisaCardScanHandler @Inject constructor(
tokens = authorizationTokensResponse,
)
val activationRemoteState = visaActivationRepository.getActivationRemoteState()
val activationRemoteState = runCatching {
visaActivationRepository.getActivationRemoteState()
}.getOrElse {
Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.message}")
return CompletionResult.Failure(VisaAuthorizationAPIError.tangemError)
}
val error = when (activationRemoteState) {
VisaActivationRemoteState.BlockedForActivation -> VisaActivationError.BlockedForActivation
VisaActivationRemoteState.Activated -> VisaActivationError.InvalidActivationState
VisaActivationRemoteState.Failed -> VisaActivationError.FailedRemoteState
else -> null
}