Updated on 2026-08-14
This commit is contained in:
parent
78e1021a76
commit
ec392c2cc8
10 changed files with 328 additions and 27 deletions
|
|
@ -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?)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
65
app/src/main/java/com/tangem/tap/domain/twins/TwinsHelper.kt
Normal file
65
app/src/main/java/com/tangem/tap/domain/twins/TwinsHelper.kt
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue