Updated on 2026-08-14
This commit is contained in:
parent
6111067060
commit
8ca71f0788
25 changed files with 554 additions and 20 deletions
|
|
@ -233,6 +233,7 @@ dependencies {
|
|||
implementation(deps.reownWeb3)
|
||||
implementation(deps.prettyLogger)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.moshi.adapters)
|
||||
|
||||
/** Testing libraries */
|
||||
testImplementation(deps.test.coroutine)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository
|
|||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
|
||||
import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -15,18 +16,23 @@ import javax.inject.Singleton
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class TangemSdkManagerModule {
|
||||
internal class TangemSdkManagerModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemSdkManager(
|
||||
@ApplicationContext context: Context,
|
||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
visaCardScanHandler: VisaCardScanHandler,
|
||||
): TangemSdkManager {
|
||||
return if (BuildConfig.MOCK_DATA_SOURCE) {
|
||||
MockTangemSdkManager(resources = context.resources)
|
||||
} else {
|
||||
DefaultTangemSdkManager(cardSdkConfigRepository = cardSdkConfigRepository, resources = context.resources)
|
||||
DefaultTangemSdkManager(
|
||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||
resources = context.resources,
|
||||
visaCardScanHandler = visaCardScanHandler,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -43,19 +43,18 @@ import com.tangem.tap.domain.tasks.product.ScanProductTask
|
|||
import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
|
||||
import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask
|
||||
import com.tangem.tap.domain.twins.FinalizeTwinTask
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@Suppress("TooManyFunctions", "LargeClass")
|
||||
class DefaultTangemSdkManager(
|
||||
internal class DefaultTangemSdkManager(
|
||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val resources: Resources,
|
||||
private val visaCardScanHandler: VisaCardScanHandler,
|
||||
) : TangemSdkManager {
|
||||
|
||||
private val awaitInitializationMutex = Mutex()
|
||||
|
|
@ -127,16 +126,20 @@ class DefaultTangemSdkManager(
|
|||
allowsRequestAccessCodeFromRepository: Boolean,
|
||||
): CompletionResult<ScanResponse> {
|
||||
val message = Message(resources.getStringSafe(messageRes ?: R.string.initial_message_scan_header))
|
||||
return runTaskAsyncReturnOnMain(
|
||||
return coroutineScope {
|
||||
runTaskAsyncReturnOnMain(
|
||||
runnable = ScanProductTask(
|
||||
card = null,
|
||||
derivationsFinder = derivationsFinder,
|
||||
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
|
||||
visaCardScanHandler = visaCardScanHandler,
|
||||
visaCoroutineScope = this,
|
||||
),
|
||||
cardId = cardId,
|
||||
initialMessage = message,
|
||||
).also { sendScanResultsToAnalytics(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createProductWallet(
|
||||
scanResponse: ScanResponse,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.tangem.common.core.TangemError
|
|||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.deserialization.WalletDataDeserializer
|
||||
import com.tangem.common.extensions.*
|
||||
import com.tangem.common.map
|
||||
import com.tangem.common.tlv.Tlv
|
||||
import com.tangem.common.tlv.TlvDecoder
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
|
|
@ -22,6 +23,7 @@ import com.tangem.domain.common.TapWorkarounds.isVisa
|
|||
import com.tangem.domain.common.TwinsHelper
|
||||
import com.tangem.domain.common.configs.CardConfig
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.common.visa.VisaUtilities
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS
|
||||
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX
|
||||
|
|
@ -35,16 +37,20 @@ import com.tangem.operations.files.ReadFilesTask
|
|||
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.domain.TapSdkError
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
import com.tangem.tap.mainScope
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.collections.set
|
||||
|
||||
internal class ScanProductTask(
|
||||
private val card: Card?,
|
||||
private val derivationsFinder: DerivationsFinder?,
|
||||
private val visaCardScanHandler: VisaCardScanHandler?,
|
||||
private val visaCoroutineScope: CoroutineScope?,
|
||||
override val allowsRequestAccessCodeFromRepository: Boolean = false,
|
||||
) : CardSessionRunnable<ScanResponse> {
|
||||
|
||||
|
|
@ -61,6 +67,16 @@ internal class ScanProductTask(
|
|||
return
|
||||
}
|
||||
|
||||
if (VisaUtilities.isVisaCard(cardDto)) {
|
||||
readVisaCard(
|
||||
session = session,
|
||||
cardDto = cardDto,
|
||||
scanWalletProcessor = ScanWalletProcessor(derivationsFinder),
|
||||
callback = callback,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val commandProcessor = when {
|
||||
cardDto.isTangemTwins -> ScanTwinProcessor()
|
||||
else -> ScanWalletProcessor(derivationsFinder)
|
||||
|
|
@ -96,6 +112,43 @@ internal class ScanProductTask(
|
|||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun readVisaCard(
|
||||
session: CardSession,
|
||||
cardDto: CardDTO,
|
||||
scanWalletProcessor: ScanWalletProcessor,
|
||||
callback: (result: CompletionResult<ScanResponse>) -> Unit,
|
||||
) {
|
||||
visaCardScanHandler ?: run {
|
||||
callback(CompletionResult.Failure(TangemSdkError.InsNotSupported()))
|
||||
return
|
||||
}
|
||||
|
||||
visaCoroutineScope ?: run {
|
||||
callback(CompletionResult.Failure(TangemSdkError.InsNotSupported()))
|
||||
return
|
||||
}
|
||||
|
||||
visaCoroutineScope.launch {
|
||||
when (val result = visaCardScanHandler.handleVisaCardScan(session = session)) {
|
||||
is CompletionResult.Success -> {
|
||||
scanWalletProcessor.proceed(
|
||||
card = cardDto,
|
||||
session = session,
|
||||
) { scanResponseResult ->
|
||||
callback(
|
||||
scanResponseResult.map { scanResponse ->
|
||||
scanResponse.copy(visaCardActivationStatus = result.data)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
callback(CompletionResult.Failure(result.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ScanWalletProcessor(
|
||||
|
|
|
|||
|
|
@ -23,8 +23,12 @@ class FinalizeTwinTask(
|
|||
PreflightReadTask(PreflightReadMode.FullCardRead).run(session) { readResult ->
|
||||
when (readResult) {
|
||||
is CompletionResult.Success ->
|
||||
ScanProductTask(readResult.data, derivationsFinder = null)
|
||||
.run(session, callback)
|
||||
ScanProductTask(
|
||||
readResult.data,
|
||||
derivationsFinder = null,
|
||||
visaCardScanHandler = null,
|
||||
visaCoroutineScope = null,
|
||||
).run(session, callback)
|
||||
is CompletionResult.Failure ->
|
||||
callback(CompletionResult.Failure(readResult.error))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.common.json.TangemSdkAdapter
|
|||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.models.scan.serialization.*
|
||||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.sdk.storage.AndroidSecureStorage
|
||||
import com.tangem.sdk.storage.createEncryptedSharedPreferences
|
||||
|
|
@ -60,6 +61,7 @@ internal object UserWalletsListManagerModule {
|
|||
.add(TangemSdkAdapter.DateAdapter())
|
||||
.add(TangemSdkAdapter.DerivationNodeAdapter())
|
||||
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
|
||||
.add(VisaCardActivationStatus.serializer)
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,256 @@
|
|||
package com.tangem.tap.domain.visa
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.CardWallet
|
||||
import com.tangem.common.core.CardSession
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.domain.common.visa.VisaUtilities
|
||||
import com.tangem.domain.visa.model.VisaActivationInput
|
||||
import com.tangem.domain.visa.model.VisaAuthSignedChallenge
|
||||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||
import com.tangem.domain.visa.model.toSignedChallenge
|
||||
import com.tangem.domain.visa.repository.VisaAuthRepository
|
||||
import com.tangem.operations.attestation.AttestCardKeyCommand
|
||||
import com.tangem.operations.attestation.AttestCardKeyResponse
|
||||
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
|
||||
import com.tangem.operations.sign.SignHashCommand
|
||||
import com.tangem.operations.sign.SignHashResponse
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
internal class VisaCardScanHandler @Inject constructor(
|
||||
private val visaAuthRepository: VisaAuthRepository,
|
||||
) {
|
||||
|
||||
suspend fun handleVisaCardScan(session: CardSession): CompletionResult<VisaCardActivationStatus> {
|
||||
Timber.i("Attempting to handle Visa card scan")
|
||||
|
||||
val card = session.environment.card ?: run {
|
||||
Timber.e("Card is null")
|
||||
return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
}
|
||||
|
||||
val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run {
|
||||
val activationInput =
|
||||
VisaActivationInput(card.cardId, card.cardPublicKey, card.isAccessCodeSet)
|
||||
val activationStatus = VisaCardActivationStatus.NotStartedActivation(activationInput)
|
||||
return CompletionResult.Success(activationStatus)
|
||||
}
|
||||
|
||||
return deriveKey(wallet, session)
|
||||
}
|
||||
|
||||
private suspend fun deriveKey(
|
||||
wallet: CardWallet,
|
||||
session: CardSession,
|
||||
): CompletionResult<VisaCardActivationStatus> {
|
||||
val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run {
|
||||
Timber.e("Failed to create derivation path while first scan")
|
||||
|
||||
return CompletionResult.Failure(
|
||||
TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToCreateDerivationPath.errorDescription),
|
||||
)
|
||||
}
|
||||
|
||||
val derivationTask = DeriveWalletPublicKeyTask(wallet.publicKey, derivationPath)
|
||||
val derivationTaskResult = suspendCancellableCoroutine { continuation ->
|
||||
derivationTask.run(session) { result ->
|
||||
continuation.resume(result)
|
||||
}
|
||||
}
|
||||
return handleDerivationResponse(derivationTaskResult, session)
|
||||
}
|
||||
|
||||
private suspend fun handleDerivationResponse(
|
||||
result: CompletionResult<ExtendedPublicKey>,
|
||||
session: CardSession,
|
||||
): CompletionResult<VisaCardActivationStatus> {
|
||||
return when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
Timber.i("Start task for loading challenge for Visa wallet")
|
||||
handleWalletAuthorization(session)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
CompletionResult.Failure(result.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleWalletAuthorization(session: CardSession): CompletionResult<VisaCardActivationStatus> {
|
||||
Timber.i("Started handling authorization using Visa wallet")
|
||||
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
|
||||
val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run {
|
||||
Timber.e("Failed to create derivation path while handling wallet authorization")
|
||||
return CompletionResult.Failure(
|
||||
TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToCreateDerivationPath.errorDescription),
|
||||
)
|
||||
}
|
||||
|
||||
val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run {
|
||||
Timber.e("Failed to find extended public key while handling wallet authorization")
|
||||
return CompletionResult.Failure(
|
||||
TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToFindDerivedWalletKey.errorDescription),
|
||||
)
|
||||
}
|
||||
|
||||
val extendedPublicKey = wallet.derivedKeys[derivationPath] ?: run {
|
||||
Timber.e("Failed to find extended public key while handling wallet authorization")
|
||||
return CompletionResult.Failure(
|
||||
TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToFindDerivedWalletKey.errorDescription),
|
||||
)
|
||||
}
|
||||
|
||||
Timber.i("Requesting challenge for wallet authorization")
|
||||
// Will be changed later after backend implementation
|
||||
val challengeResponse = runCatching {
|
||||
visaAuthRepository.getCustomerWalletAuthChallenge(
|
||||
cardId = card.cardId,
|
||||
walletPublicKey = extendedPublicKey.publicKey.toHexString(),
|
||||
)
|
||||
}.getOrElse {
|
||||
return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: "Unknown error"))
|
||||
}
|
||||
|
||||
val signChallengeResult = signChallengeWithWallet(
|
||||
publicKey = wallet.publicKey,
|
||||
derivationPath = derivationPath,
|
||||
nonce = challengeResponse.challenge,
|
||||
session = session,
|
||||
)
|
||||
|
||||
return when (signChallengeResult) {
|
||||
is CompletionResult.Success -> {
|
||||
Timber.i("Challenge signed with Wallet public key")
|
||||
handleWalletAuthorizationTokens(
|
||||
session = session,
|
||||
signedChallenge = challengeResponse
|
||||
.toSignedChallenge(signChallengeResult.data.signature.toHexString()),
|
||||
)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
Timber.e("Error during Wallet authorization process. Tangem Sdk Error: ${signChallengeResult.error}")
|
||||
CompletionResult.Failure(signChallengeResult.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleWalletAuthorizationTokens(
|
||||
session: CardSession,
|
||||
signedChallenge: VisaAuthSignedChallenge,
|
||||
): CompletionResult<VisaCardActivationStatus> {
|
||||
val authorizationTokensResponse = runCatching {
|
||||
visaAuthRepository.getAccessTokens(signedChallenge = signedChallenge)
|
||||
}.getOrElse {
|
||||
Timber.i(
|
||||
"Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key",
|
||||
)
|
||||
return handleCardAuthorization(session)
|
||||
}
|
||||
|
||||
Timber.i("Authorized using Wallet public key successfully")
|
||||
|
||||
return CompletionResult.Success(VisaCardActivationStatus.Activated(authorizationTokensResponse))
|
||||
}
|
||||
|
||||
private suspend fun handleCardAuthorization(session: CardSession): CompletionResult<VisaCardActivationStatus> {
|
||||
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
|
||||
Timber.i("Requesting authorization challenge to sign")
|
||||
|
||||
val challengeResponse = runCatching {
|
||||
visaAuthRepository.getCardAuthChallenge(
|
||||
cardId = card.cardId,
|
||||
cardPublicKey = card.cardPublicKey.toHexString(),
|
||||
)
|
||||
}.getOrElse {
|
||||
Timber.e("Failed to get challenge for Card authorization. Plain error: ${it.message}")
|
||||
return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: "Unknown error"))
|
||||
}
|
||||
|
||||
Timber.i("Received challenge to sign: ${challengeResponse.challenge}")
|
||||
|
||||
val signChallengeResult = signChallengeWithCard(session = session, challenge = challengeResponse.challenge)
|
||||
|
||||
val attestCardKeyResponse = when (signChallengeResult) {
|
||||
is CompletionResult.Success -> {
|
||||
Timber.i("Challenged signed.")
|
||||
signChallengeResult.data
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
Timber.e(
|
||||
"Failed to sign challenge with Card public key. Tangem Sdk Error: ${signChallengeResult.error}",
|
||||
)
|
||||
return CompletionResult.Failure(signChallengeResult.error)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
val authorizationTokensResponse = runCatching {
|
||||
visaAuthRepository.getAccessTokens(
|
||||
signedChallenge = challengeResponse.toSignedChallenge(
|
||||
signedChallenge = attestCardKeyResponse.cardSignature.toHexString(),
|
||||
salt = attestCardKeyResponse.salt.toHexString(),
|
||||
),
|
||||
)
|
||||
}.getOrElse {
|
||||
Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.message}")
|
||||
return CompletionResult.Failure(
|
||||
TangemSdkError.Underlying(
|
||||
customMessage = it.message ?: "Unknown error",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
TODO() // implement card activation status handling ([REDACTED_TASK_KEY])
|
||||
}
|
||||
|
||||
private suspend fun signChallengeWithWallet(
|
||||
publicKey: ByteArray,
|
||||
derivationPath: DerivationPath,
|
||||
nonce: String,
|
||||
session: CardSession,
|
||||
): CompletionResult<SignHashResponse> {
|
||||
val signHashCommand = SignHashCommand(publicKey, nonce.toByteArray(), derivationPath)
|
||||
val result = suspendCancellableCoroutine {
|
||||
signHashCommand.run(session) { result ->
|
||||
it.resume(result)
|
||||
}
|
||||
}
|
||||
|
||||
return when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
CompletionResult.Success(result.data)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
CompletionResult.Failure(result.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun signChallengeWithCard(
|
||||
session: CardSession,
|
||||
challenge: String,
|
||||
): CompletionResult<AttestCardKeyResponse> {
|
||||
val signHashCommand = AttestCardKeyCommand(challenge = challenge.toByteArray())
|
||||
val result = suspendCancellableCoroutine { continuation ->
|
||||
signHashCommand.run(session) { result ->
|
||||
continuation.resume(result)
|
||||
}
|
||||
}
|
||||
|
||||
return when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
CompletionResult.Success(result.data)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
CompletionResult.Failure(result.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.tap.domain.visa
|
||||
|
||||
private const val COMMON_DESCRIPTION = "Error occurred. Please contact support."
|
||||
|
||||
internal enum class VisaCardScanHandlerError(val errorDescription: String) {
|
||||
FailedToCreateDerivationPath(COMMON_DESCRIPTION),
|
||||
FailedToFindWallet(COMMON_DESCRIPTION),
|
||||
FailedToFindDerivedWalletKey(COMMON_DESCRIPTION),
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import com.tangem.common.card.WalletData
|
|||
import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain
|
||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.common.visa.VisaUtilities
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.operations.attestation.Attestation
|
||||
|
|
@ -66,7 +67,7 @@ internal class TangemCardTypesResolver(
|
|||
override fun getBlockchain(): Blockchain {
|
||||
return when (productType) {
|
||||
ProductType.Start2Coin -> if (card.isTestCard) Blockchain.BitcoinTestnet else Blockchain.Bitcoin
|
||||
ProductType.Visa -> Blockchain.PolygonTestnet
|
||||
ProductType.Visa -> VisaUtilities.visaBlockchain
|
||||
else -> {
|
||||
val blockchainName: String = walletData?.blockchain
|
||||
?: if (productType == ProductType.Note) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.domain.common
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.domain.common.visa.VisaUtilities
|
||||
import com.tangem.domain.demo.DemoConfig
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import java.util.Locale
|
||||
|
|
@ -23,7 +24,7 @@ object TapWorkarounds {
|
|||
get() = isStart2CoinIssuer(issuer.name)
|
||||
|
||||
val CardDTO.isVisa: Boolean
|
||||
get() = firmwareVersion.doubleValue in FirmwareVersion.visaRange
|
||||
get() = VisaUtilities.isVisaCard(this)
|
||||
|
||||
val CardDTO.isTestCard: Boolean
|
||||
get() = batchId == TEST_CARD_BATCH && cardId.startsWith(TEST_CARD_ID_STARTS_WITH)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.domain.common.visa
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.derivation.DerivationStyle
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
|
||||
private const val VISA_BATCH_START = "AE"
|
||||
|
||||
object VisaUtilities {
|
||||
|
||||
val mandatoryCurve = EllipticCurve.Secp256k1
|
||||
|
||||
const val tokenId = "tether"
|
||||
|
||||
val visaBlockchain = Blockchain.Polygon
|
||||
|
||||
val visaDefaultDerivationPath = visaBlockchain.derivationPath(DerivationStyle.V3)
|
||||
|
||||
fun isVisaCard(card: CardDTO): Boolean {
|
||||
return isVisaCard(card.firmwareVersion.doubleValue, card.batchId)
|
||||
}
|
||||
|
||||
fun isVisaCard(firmwareVersion: Double, batchId: String): Boolean {
|
||||
return FirmwareVersion.visaRange.contains(firmwareVersion) &&
|
||||
batchId.startsWith(VISA_BATCH_START)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,10 @@ plugins {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.domain.visa.models)
|
||||
|
||||
implementation(deps.tangem.card.core)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.moshi.adapters)
|
||||
implementation(deps.kotlin.serialization)
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.squareup.moshi.JsonClass
|
|||
import com.tangem.common.card.WalletData
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.domain.models.scan.serialization.ScanResponseAsStringSerializer
|
||||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||
import com.tangem.operations.backup.PrimaryCard
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import kotlinx.serialization.Serializable
|
||||
|
|
@ -20,6 +21,7 @@ data class ScanResponse(
|
|||
@Json(name = "productType") val productType: ProductType,
|
||||
@Json(name = "walletData") val walletData: WalletData?,
|
||||
@Json(name = "secondTwinPublicKey") val secondTwinPublicKey: String? = null,
|
||||
@Json(name = "visaCardActivationStatus") val visaCardActivationStatus: VisaCardActivationStatus? = null,
|
||||
@Json(name = "derivedKeys") val derivedKeys: Map<KeyWalletPublicKey, ExtendedPublicKeysMap> = mapOf(),
|
||||
@Json(name = "primaryCard") val primaryCard: PrimaryCard? = null,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.squareup.moshi.Moshi
|
|||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import com.tangem.common.json.TangemSdkAdapter
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
|
|
@ -22,6 +23,7 @@ internal object ScanResponseAsStringSerializer : KSerializer<ScanResponse> {
|
|||
.add(TangemSdkAdapter.DateAdapter())
|
||||
.add(TangemSdkAdapter.DerivationNodeAdapter())
|
||||
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
|
||||
.add(VisaCardActivationStatus.serializer)
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
/** Domain models */
|
||||
api(projects.domain.visa.models)
|
||||
|
||||
/** Project - Domain */
|
||||
implementation(projects.core.utils)
|
||||
|
|
|
|||
1
domain/visa/models/.gitignore
vendored
Normal file
1
domain/visa/models/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
11
domain/visa/models/build.gradle.kts
Normal file
11
domain/visa/models/build.gradle.kts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
alias(deps.plugins.kotlin.serialization)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.moshi.adapters)
|
||||
implementation(deps.kotlin.serialization)
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.domain.visa.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class VisaActivationInput(
|
||||
@Json(name = "cardId") val cardId: String,
|
||||
@Json(name = "cardPublicKey") val cardPublicKey: ByteArray,
|
||||
@Json(name = "isAccessCodeSet") val isAccessCodeSet: Boolean,
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as VisaActivationInput
|
||||
|
||||
if (cardId != other.cardId) return false
|
||||
if (!cardPublicKey.contentEquals(other.cardPublicKey)) return false
|
||||
if (isAccessCodeSet != other.isAccessCodeSet) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = cardId.hashCode()
|
||||
result = 31 * result + cardPublicKey.contentHashCode()
|
||||
result = 31 * result + isAccessCodeSet.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.domain.visa.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class VisaActivationRemoteState {
|
||||
CardWalletSignatureRequired,
|
||||
CustomerWalletSignatureRequired,
|
||||
PaymentAccountDeploying,
|
||||
WaitingPinCode,
|
||||
WaitingForActivationFinishing,
|
||||
Activated,
|
||||
BlockedForActivation,
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.domain.visa.model
|
||||
|
||||
sealed class VisaAuthChallenge {
|
||||
|
||||
abstract val session: VisaAuthSession
|
||||
|
||||
data class Card(
|
||||
val challenge: String,
|
||||
override val session: VisaAuthSession,
|
||||
) : VisaAuthChallenge()
|
||||
|
||||
data class Wallet(
|
||||
val challenge: String,
|
||||
override val session: VisaAuthSession,
|
||||
) : VisaAuthChallenge()
|
||||
}
|
||||
|
||||
fun VisaAuthChallenge.Card.toSignedChallenge(signedChallenge: String, salt: String): VisaAuthSignedChallenge {
|
||||
return VisaAuthSignedChallenge.ByCardPublicKey(
|
||||
challenge = this,
|
||||
signature = signedChallenge,
|
||||
salt = salt,
|
||||
)
|
||||
}
|
||||
|
||||
fun VisaAuthChallenge.Wallet.toSignedChallenge(signedChallenge: String): VisaAuthSignedChallenge {
|
||||
return VisaAuthSignedChallenge.ByWallet(
|
||||
challenge = this,
|
||||
signature = signedChallenge,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package com.tangem.domain.visa.model
|
||||
|
||||
@JvmInline
|
||||
value class VisaAuthSession(val sessionId: String)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.visa.model
|
||||
|
||||
sealed class VisaAuthSignedChallenge {
|
||||
|
||||
data class ByCardPublicKey(
|
||||
val challenge: VisaAuthChallenge.Card,
|
||||
val signature: String,
|
||||
val salt: String,
|
||||
) : VisaAuthSignedChallenge()
|
||||
|
||||
data class ByWallet(
|
||||
val challenge: VisaAuthChallenge.Wallet,
|
||||
val signature: String,
|
||||
) : VisaAuthSignedChallenge()
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.domain.visa.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class VisaAuthTokens(
|
||||
@Json(name = "accessToken") val accessToken: String,
|
||||
@Json(name = "refreshToken") val refreshToken: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.domain.visa.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed class VisaCardActivationStatus {
|
||||
|
||||
@Serializable
|
||||
data class Activated(
|
||||
@Json(name = "visaAuthTokens") val visaAuthTokens: VisaAuthTokens,
|
||||
) : VisaCardActivationStatus()
|
||||
|
||||
@Serializable
|
||||
data class ActivationStarted(
|
||||
@Json(name = "activationInput") val activationInput: VisaActivationInput,
|
||||
@Json(name = "authTokens") val authTokens: VisaAuthTokens,
|
||||
@Json(name = "remoteState") val remoteState: VisaActivationRemoteState,
|
||||
) : VisaCardActivationStatus()
|
||||
|
||||
@Serializable
|
||||
data class NotStartedActivation(
|
||||
@Json(name = "activationInput") val activationInput: VisaActivationInput,
|
||||
) : VisaCardActivationStatus()
|
||||
|
||||
@Serializable
|
||||
data object Blocked : VisaCardActivationStatus()
|
||||
|
||||
companion object {
|
||||
val serializer: PolymorphicJsonAdapterFactory<VisaCardActivationStatus>
|
||||
get() = PolymorphicJsonAdapterFactory.of(VisaCardActivationStatus::class.java, "type")
|
||||
.withSubtype(Activated::class.java, "Activated")
|
||||
.withSubtype(ActivationStarted::class.java, "ActivationStarted")
|
||||
.withSubtype(NotStartedActivation::class.java, "NotStartedActivation")
|
||||
.withDefaultValue(Blocked)
|
||||
}
|
||||
}
|
||||
|
|
@ -232,6 +232,7 @@ include(":domain:transaction")
|
|||
include(":domain:transaction:models")
|
||||
include(":domain:analytics")
|
||||
include(":domain:visa")
|
||||
include(":domain:visa:models")
|
||||
include(":domain:onboarding")
|
||||
include(":domain:feedback")
|
||||
include(":domain:qr-scanning")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue