Updated on 2026-08-14

This commit is contained in:
Tangem 2020-12-02 16:38:06 +03:00
parent 78e1021a76
commit ec392c2cc8
10 changed files with 328 additions and 27 deletions

View file

@ -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?)

View file

@ -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
)
}

View file

@ -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 <T : CommandResponse> runTaskAsync(
suspend fun <T : CommandResponse> runTaskAsync(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null
): CompletionResult<T> =
withContext(Dispatchers.IO) {

View file

@ -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 {

View file

@ -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 {

View file

@ -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<ScanNoteResponse> {
@ -42,31 +50,74 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
return@run
}
if (card.cardData?.productMask?.contains(Product.TwinCard) == true) {
dealWithTwinCard(card, session, callback)
return@run
}
val walletManager = try {
WalletManagerFactory.makeWalletManager(card)
} catch (exception: Exception) {
return@run callback(CompletionResult.Success(ScanNoteResponse(null, card)))
}
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()))
}
}
}
verifyCard(walletManager, card, null, session, callback)
}
}
}
}
private fun verifyCard(
walletManager: WalletManager?, card: Card, publicKey: String? = null,
session: CardSession, callback: (result: CompletionResult<ScanNoteResponse>) -> 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<ScanNoteResponse>) -> 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<String>(TlvTag.FileName)
if (name != null && name == TwinsHelper.TWIN_FILE_NAME) {
val publicKey = decoder.decodeOptional<ByteArray>(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)) {

View file

@ -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<CreateWalletResponse> {
override val requiresPin2 = false
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> 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<CreateWalletResponse>) -> Unit) {
DeleteFilesTask().run(session) { deleteResponse ->
when (deleteResponse) {
is CompletionResult.Success ->
CreateWalletTask().run(session) { callback(it) }
is CompletionResult.Failure ->
callback(CompletionResult.Failure(deleteResponse.error))
}
}
}
}

View file

@ -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<CreateWalletResponse> {
override val requiresPin2 = true
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> 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<CreateWalletResponse>) -> 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))
}
}
}
}

View file

@ -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<ScanNoteResponse> {
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)
}
}
}

View file

@ -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
}
}