diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt index 8057bcae9e..9dddeee4f2 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt @@ -1,6 +1,7 @@ package com.tangem.tap.common.analytics -import com.tangem.commands.Card +import com.tangem.commands.common.card.Card + interface AnalyticsHandler { fun triggerEvent(event: AnalyticsEvent, card: Card?) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt index 6e7f460773..5b468233ea 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt @@ -4,7 +4,7 @@ import android.os.Bundle import androidx.core.os.bundleOf import com.google.firebase.analytics.ktx.analytics import com.google.firebase.ktx.Firebase -import com.tangem.commands.Card +import com.tangem.commands.common.card.Card object FirebaseAnalyticsHandler: AnalyticsHandler { override fun triggerEvent(event: AnalyticsEvent, card: Card?) { @@ -17,7 +17,7 @@ object FirebaseAnalyticsHandler: AnalyticsHandler { return bundleOf( AnalyticsParam.BLOCKCHAIN.param to card.cardData?.blockchainName, AnalyticsParam.BATCH_ID.param to card.cardData?.batchId, - AnalyticsParam.FIRMWARE.param to card.firmwareVersion + AnalyticsParam.FIRMWARE.param to card.firmwareVersion.version ) } diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index a1971bad03..e34ddfe8c7 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -3,8 +3,8 @@ package com.tangem.tap.domain import androidx.activity.ComponentActivity import com.tangem.* import com.tangem.commands.* +import com.tangem.commands.common.card.CardType import com.tangem.common.CompletionResult -import com.tangem.common.extensions.CardType import com.tangem.common.extensions.calculateSha256 import com.tangem.tangem_sdk_new.extensions.init import com.tangem.tap.common.analytics.AnalyticsEvent @@ -64,7 +64,7 @@ class TangemSdkManager(val activity: ComponentActivity) { ), cardId, initialMessage = Message(activity.getString(R.string.initial_message_tap_header))) } - private suspend fun runTaskAsync( + suspend fun runTaskAsync( runnable: CardSessionRunnable, cardId: String? = null, initialMessage: Message? = null ): CompletionResult = withContext(Dispatchers.IO) { diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index c6e9bae5c8..f9da8875d4 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -3,7 +3,8 @@ package com.tangem.tap.domain import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager -import com.tangem.commands.CardStatus +import com.tangem.commands.common.card.CardStatus +import com.tangem.commands.common.card.masks.Product import com.tangem.commands.common.network.Result import com.tangem.common.extensions.toHexString import com.tangem.tap.common.analytics.AnalyticsEvent @@ -93,7 +94,15 @@ class TapWalletManager { withContext(Dispatchers.Main) { store.dispatch(WalletAction.ResetState) store.dispatch(GlobalAction.SaveScanNoteResponse(data)) - store.dispatch(AddressPayIdActionUi.ChangePayIdState(configManager?.config?.isWalletPayIdEnabled ?: false)) + store.dispatch(AddressPayIdActionUi.ChangePayIdState(configManager?.config?.isWalletPayIdEnabled + ?: false)) + if (data.card.cardData?.productMask?.contains(Product.TwinCard) == true) { + val secondCardId = TwinsHelper.getTwinsCardId(data.card.cardId) + val cardNumber = TwinsHelper.getTwinCardNumber(data.card.cardId) + if (secondCardId != null && cardNumber != null) { + store.dispatch(WalletAction.TwinsAction.SetTwinCard(secondCardId, cardNumber)) + } + } loadData(data) } } @@ -114,7 +123,8 @@ class TapWalletManager { store.dispatch(WalletAction.LoadArtwork(data.card, artworkId)) store.dispatch(WalletAction.LoadFiatRate) store.dispatch(WalletAction.LoadPayId) - } else if (data.card.status == CardStatus.Empty) { + } else if (data.card.status == CardStatus.Empty || + data.card.cardData?.productMask?.contains(Product.TwinCard) == true) { store.dispatch(WalletAction.EmptyWallet) store.dispatch(WalletAction.LoadArtwork(data.card, artworkId)) } else { diff --git a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt b/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt index bb5e44f015..1e18167a52 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt @@ -1,6 +1,6 @@ package com.tangem.tap.domain -import com.tangem.commands.Card +import com.tangem.commands.common.card.Card import java.util.* object TapWorkarounds { diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt index 93a2617258..1f0c9bcd06 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt @@ -6,20 +6,28 @@ import com.tangem.TangemError import com.tangem.TangemSdkError import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.WalletManagerFactory -import com.tangem.commands.Card -import com.tangem.commands.CardStatus import com.tangem.commands.CommandResponse -import com.tangem.commands.Product +import com.tangem.commands.common.card.Card +import com.tangem.commands.common.card.CardStatus +import com.tangem.commands.common.card.masks.Product +import com.tangem.commands.file.ReadFileDataCommand import com.tangem.commands.verifycard.VerifyCardCommand import com.tangem.commands.verifycard.VerifyCardResponse import com.tangem.common.CompletionResult +import com.tangem.common.extensions.hexToBytes +import com.tangem.common.extensions.toHexString +import com.tangem.common.tlv.Tlv +import com.tangem.common.tlv.TlvDecoder +import com.tangem.common.tlv.TlvTag import com.tangem.tap.domain.TapSdkError +import com.tangem.tap.domain.TwinsHelper import com.tangem.tasks.ScanTask data class ScanNoteResponse( val walletManager: WalletManager?, val card: Card, - val verifyResponse: VerifyCardResponse? = null + val verifyResponse: VerifyCardResponse? = null, + val secondTwinPublicKey: String? = null ) : CommandResponse class ScanNoteTask(val card: Card? = null) : CardSessionRunnable { @@ -42,31 +50,74 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable - when (verifyResult) { - is CompletionResult.Success -> { - callback(CompletionResult.Success(ScanNoteResponse( - walletManager, card, verifyResult.data - ))) - } - is CompletionResult.Failure -> { - callback(CompletionResult.Failure(TangemSdkError.VerificationFailed())) - } - } - } + verifyCard(walletManager, card, null, session, callback) } } } } + private fun verifyCard( + walletManager: WalletManager?, card: Card, publicKey: String? = null, + session: CardSession, callback: (result: CompletionResult) -> Unit + ) { + + VerifyCardCommand(true).run(session) { verifyResult -> + when (verifyResult) { + is CompletionResult.Success -> { + callback(CompletionResult.Success(ScanNoteResponse( + walletManager, card, verifyResult.data + ))) + } + is CompletionResult.Failure -> { + callback(CompletionResult.Failure(TangemSdkError.VerificationFailed())) + } + } + } + } + + private fun dealWithTwinCard( + card: Card, session: CardSession, + callback: (result: CompletionResult) -> Unit + ) { + ReadFileDataCommand(readPrivateFiles = true).run(session) { filesResult -> + when (filesResult) { + is CompletionResult.Success -> { + val decoder = Tlv.deserialize(filesResult.data.fileData)?.let { TlvDecoder(it) } + val name = decoder?.decodeOptional(TlvTag.FileName) + if (name != null && name == TwinsHelper.TWIN_FILE_NAME) { + val publicKey = decoder.decodeOptional(TlvTag.FileData)?.toHexString() + val walletManager = try { + WalletManagerFactory.makeMultisigWalletManager(card, publicKey!!.hexToBytes()) + } catch (exception: Exception) { + callback(CompletionResult.Success(ScanNoteResponse(null, card))) + return@run + } + verifyCard(walletManager, card, publicKey, session, callback) + return@run + } else { + callback(CompletionResult.Success(ScanNoteResponse(null, card))) + } + } + is CompletionResult.Failure -> + callback(CompletionResult.Success(ScanNoteResponse(null, card))) + } + + } + } + private fun getErrorIfExcludedCard(card: Card): TangemError? { - if (card.cardData?.productMask != null && - card.cardData?.productMask?.contains(Product.Note) != true) { + val productMask = card.cardData?.productMask ?: return TangemSdkError.CardError() + if (!productMask.contains(Product.Note) && !productMask.contains(Product.TwinCard)) { return TapSdkError.CardForDifferentApp } if (excludedBatches.contains(card.cardData?.batchId)) { diff --git a/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt new file mode 100644 index 0000000000..4b008408e6 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt @@ -0,0 +1,41 @@ +package com.tangem.tap.domain.twins + +import com.tangem.CardSession +import com.tangem.CardSessionRunnable +import com.tangem.commands.CreateWalletResponse +import com.tangem.commands.PurgeWalletCommand +import com.tangem.commands.common.card.CardStatus +import com.tangem.common.CompletionResult +import com.tangem.tasks.CreateWalletTask +import com.tangem.tasks.file.DeleteFilesTask + +class CreateFirstTwinWalletTask : CardSessionRunnable { + override val requiresPin2 = false + + override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { + if (session.environment.card?.walletPublicKey != null) { + PurgeWalletCommand().run(session) { response -> + when (response) { + is CompletionResult.Success -> { + session.environment.card = session.environment.card?.copy(status = CardStatus.Empty) + finishTask(session, callback) + } + is CompletionResult.Failure -> callback(CompletionResult.Failure(response.error)) + } + } + } else { + finishTask(session, callback) + } + } + + private fun finishTask(session: CardSession, callback: (result: CompletionResult) -> Unit) { + DeleteFilesTask().run(session) { deleteResponse -> + when (deleteResponse) { + is CompletionResult.Success -> + CreateWalletTask().run(session) { callback(it) } + is CompletionResult.Failure -> + callback(CompletionResult.Failure(deleteResponse.error)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt new file mode 100644 index 0000000000..4885e78f38 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt @@ -0,0 +1,50 @@ +package com.tangem.tap.domain.twins + +import com.tangem.CardSession +import com.tangem.CardSessionRunnable +import com.tangem.commands.CreateWalletResponse +import com.tangem.commands.PurgeWalletCommand +import com.tangem.commands.common.card.CardStatus +import com.tangem.commands.file.WriteFileDataCommand +import com.tangem.common.CompletionResult +import com.tangem.tasks.CreateWalletTask +import com.tangem.tasks.file.DeleteFilesTask + +class CreateSecondTwinWalletTask(private val firstPublicKey: String) : CardSessionRunnable { + override val requiresPin2 = true + + override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { + if (session.environment.card?.walletPublicKey != null) { + PurgeWalletCommand().run(session) { response -> + when (response) { + is CompletionResult.Success -> { + session.environment.card = + session.environment.card?.copy(status = CardStatus.Empty) + finishTask(session, callback) + } + is CompletionResult.Failure -> callback(CompletionResult.Failure(response.error)) + } + } + } else { + finishTask(session, callback) + } + } + + private fun finishTask(session: CardSession, callback: (result: CompletionResult) -> Unit) { + DeleteFilesTask().run(session) { deleteResponse -> + when (deleteResponse) { + is CompletionResult.Success -> { + val file = TwinCardsManager.createFileWithPublicKey(firstPublicKey) + WriteFileDataCommand(file).run(session) { result -> + when (result) { + is CompletionResult.Success -> CreateWalletTask().run(session, callback) + is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) + } + } + } + is CompletionResult.Failure -> + callback(CompletionResult.Failure(deleteResponse.error)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt new file mode 100644 index 0000000000..0255ed6f5a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt @@ -0,0 +1,83 @@ +package com.tangem.tap.domain.twins + +import com.tangem.Message +import com.tangem.blockchain.common.WalletManagerFactory +import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.commands.file.FileData +import com.tangem.commands.file.WriteFileDataCommand +import com.tangem.common.CompletionResult +import com.tangem.common.extensions.hexToBytes +import com.tangem.common.extensions.toHexString +import com.tangem.common.tlv.TlvEncoder +import com.tangem.common.tlv.TlvTag +import com.tangem.common.tlv.serialize +import com.tangem.tap.domain.TwinsHelper +import com.tangem.tap.domain.tasks.ScanNoteResponse +import com.tangem.tap.tangemSdkManager + +class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse) { + + private val currentCardId: String = scanNoteResponse.card.cardId + private val secondCardId: String? = TwinsHelper.getTwinsCardId(currentCardId) + + private var currentCardPublicKey: String? = null + private var secondCardPublicKey: String? = null + + suspend fun createFirstWallet(message: Message): SimpleResult { + val response = tangemSdkManager.runTaskAsync( + CreateFirstTwinWalletTask(), currentCardId, message + ) + when (response) { + is CompletionResult.Success -> { + currentCardPublicKey = response.data.walletPublicKey.toHexString() + return SimpleResult.Success + } + is CompletionResult.Failure -> return SimpleResult.failure(response.error) + } + + } + + + suspend fun createSecondWallet(message: Message): SimpleResult { + val response = tangemSdkManager.runTaskAsync( + CreateSecondTwinWalletTask(currentCardPublicKey!!), secondCardId, message + ) + when (response) { + is CompletionResult.Success -> { + secondCardPublicKey = response.data.walletPublicKey.toHexString() + return SimpleResult.Success + } + is CompletionResult.Failure -> return SimpleResult.failure(response.error) + } + + } + + suspend fun complete(message: Message): Result { + val response = tangemSdkManager.runTaskAsync( + WriteFileDataCommand(createFileWithPublicKey(secondCardPublicKey!!)), + currentCardId, message + ) + return when (response) { + is CompletionResult.Success -> { + val walletManager = WalletManagerFactory.makeMultisigWalletManager( + scanNoteResponse.card, secondCardPublicKey!!.hexToBytes() + ) + return Result.Success(scanNoteResponse.copy( + walletManager = walletManager, + secondTwinPublicKey = secondCardPublicKey + )) + } + is CompletionResult.Failure -> Result.failure(response.error) + } + } + + companion object { + fun createFileWithPublicKey(publicKey: String): FileData { + val nameTlv = TlvEncoder().encode(TlvTag.FileName, "TwinPublicKey") + val pubKey = TlvEncoder().encode(TlvTag.FileData, publicKey.hexToBytes()) + val tlvs = listOf(nameTlv, pubKey).serialize() + return FileData.DataProtectedByPasscode(tlvs) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinsHelper.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinsHelper.kt new file mode 100644 index 0000000000..ddb6d6854b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinsHelper.kt @@ -0,0 +1,65 @@ +package com.tangem.tap.domain + +import com.tangem.tap.common.extensions.isEven + +class TwinsHelper { + companion object { + const val TWIN_FILE_NAME = "TwinPublicKey" + + fun getTwinCardNumber(cardId: String): TwinCardNumber? { + return when { + firstCardBatches.map { cardId.startsWith(it) }.contains(true) -> { + TwinCardNumber.First + } + secondCardBatches.map { cardId.startsWith(it) }.contains(true) -> { + TwinCardNumber.Second + } + else -> { + null + } + } + } + + fun getTwinsCardId(cardId: String): String? { + val cardIdWithNewBatch = when (getTwinCardNumber(cardId) ?: return null) { + TwinCardNumber.First -> { + val batchIndex = if (cardId.startsWith(firstCardBatches[1])) 1 else 2 + cardId.replace(firstCardBatches[batchIndex], secondCardBatches[batchIndex]) + } + TwinCardNumber.Second -> { + val batchIndex = if (cardId.startsWith(secondCardBatches[1])) 1 else 2 + cardId.replace(secondCardBatches[batchIndex], firstCardBatches[batchIndex]) + } + } + val cardIdWithoutChecksum = cardIdWithNewBatch.dropLast(1) + val checkSum = cardIdWithoutChecksum.calculateLuhn() + return cardIdWithoutChecksum + checkSum + } + + private val firstCardBatches = listOf("CB61", "CB64") + private val secondCardBatches = listOf("CB62", "CB65") + } +} + +private fun String.calculateLuhn(): Int { + return 10 - this.reversed() + .mapIndexed { index, c -> + val digit = if (c in '0'..'9') c - '0' else c - 'A' + if (!index.isEven()) { + digit + } else { + val newDigit = digit * 2 + if (newDigit >= 10) newDigit - 9 else newDigit + } + } + .sum() % 10 +} + +enum class TwinCardNumber(val number: Int) { + First(1), Second(2); + + fun pairNumber(): TwinCardNumber = when (this) { + First -> Second + Second -> First + } +} \ No newline at end of file