Updated on 2026-08-14

This commit is contained in:
Tangem 2022-10-17 19:46:06 +05:00
parent 84e5496689
commit 123f8b2e34
25 changed files with 411 additions and 296 deletions

View file

@ -8,7 +8,7 @@ import com.tangem.network.api.tangemTech.CoinsResponse
*/
sealed interface DomainDialog {
data class DialogError(val error: DomainError) : DomainDialog
data class DialogError(val error: DomainModuleError) : DomainDialog
data class SelectTokenDialog(
val items: List<CoinsResponse.Coin.Network>,

View file

@ -1,47 +0,0 @@
package com.tangem.domain
import com.tangem.common.module.ModuleError
import com.tangem.common.module.ModuleMessage
/**
[REDACTED_AUTHOR]
* All DomainError descendants must use their own range of codes, but no more than 999 error codes for each.
*/
sealed interface DomainModuleMessage : ModuleMessage
sealed class DomainError(
subCode: Int,
override val message: String,
override val data: Any?,
) : DomainModuleMessage, ModuleError {
override val code: Int = ERROR_CODE_DOMAIN + subCode
companion object {
// base code used for all error in the module
const val ERROR_CODE_DOMAIN = 10000
const val ERROR_CODE_ADD_CUSTOM_TOKEN = 100
// const val CODE_ANY_OTHER = 200..299
}
}
sealed class AddCustomTokenError(
subCode: Int = 0
) : DomainError(ERROR_CODE_ADD_CUSTOM_TOKEN + subCode, this::class.java.simpleName, null) {
object FieldIsEmpty : AddCustomTokenError()
object FieldIsNotEmpty : AddCustomTokenError()
object InvalidContractAddress : AddCustomTokenError()
object NetworkIsNotSelected : AddCustomTokenError()
object InvalidDecimalsCount : AddCustomTokenError()
object InvalidDerivationPath : AddCustomTokenError()
sealed class Network : AddCustomTokenError() {
object CheckAddressRequestError : Network()
}
sealed class Warning : AddCustomTokenError() {
object PotentialScamToken : Warning()
object TokenAlreadyAdded : Warning()
object UnsupportedSolanaToken: Warning()
}
}

View file

@ -1,21 +0,0 @@
package com.tangem.domain
import com.tangem.common.module.FbConsumeException
import com.tangem.common.module.ModuleException
/**
[REDACTED_AUTHOR]
*/
sealed class DomainException(override val message: String) : Throwable(message), ModuleException
sealed class AddCustomTokenException(message: String) : DomainException(message) {
data class SelectTokeNetworkException(val networkId: String) : AddCustomTokenException(
"Unknown network [$networkId] should not be included in the network selection dialog."
), FbConsumeException
data class UnAppropriateInitializationException(
val of: String,
val info: String? = null
) : AddCustomTokenException("The [$of], must be properly initialized. Info [$info]")
}

View file

@ -10,7 +10,7 @@ import com.tangem.domain.redux.state.ActionStateLoggerImpl
object DomainLayer {
internal val actionStateLogger = ActionStateLoggerImpl()
var onInitComplete: ((DomainException?) -> Unit)? = null
var onInitComplete: ((DomainModuleError?) -> Unit)? = null
fun init(data: Map<String, Any?>? = null) {
initActionStateLogger()

View file

@ -0,0 +1,65 @@
package com.tangem.domain
import com.tangem.common.module.FbConsumeException
import com.tangem.common.module.ModuleError
import com.tangem.common.module.ModuleErrorCode
import com.tangem.common.module.ModuleMessage
/**
[REDACTED_AUTHOR]
* All DomainError descendants must use their own range of codes, but no more than 999 error codes for each.
*/
sealed interface DomainModuleMessage : ModuleMessage
sealed class DomainModuleError(
subCode: Int,
override val message: String,
override val data: Any?,
) : DomainModuleMessage, ModuleError() {
override val code: Int = ModuleErrorCode.DOMAIN + subCode
companion object {
// base code used for all errors in the module
internal const val ERROR_CODE_ADD_CUSTOM_TOKEN = 100
// const val CODE_ANY_OTHER = 200..299, 300..399 etc
}
}
sealed class AddCustomTokenError(
subCode: Int = 0,
message: String? = null,
data: Any? = null,
) : DomainModuleError(
subCode = ERROR_CODE_ADD_CUSTOM_TOKEN + subCode,
message = message ?: this::class.java.simpleName,
data = data,
) {
object FieldIsEmpty : AddCustomTokenError()
object FieldIsNotEmpty : AddCustomTokenError()
object InvalidContractAddress : AddCustomTokenError()
object NetworkIsNotSelected : AddCustomTokenError()
object InvalidDecimalsCount : AddCustomTokenError()
object InvalidDerivationPath : AddCustomTokenError()
sealed class Network : AddCustomTokenError() {
object CheckAddressRequestError : Network()
}
sealed class Warning : AddCustomTokenError() {
object PotentialScamToken : Warning()
object TokenAlreadyAdded : Warning()
object UnsupportedSolanaToken : Warning()
}
data class SelectTokeNetworkError(val networkId: String) : AddCustomTokenError(
message = "Unknown network [$networkId] should not be included in the network selection dialog.",
), FbConsumeException
data class UnAppropriateInitialization(
val of: String,
val info: String? = null,
) : AddCustomTokenError(
message = "The [$of], must be properly initialized. Info [$info]",
)
}

View file

@ -0,0 +1,93 @@
package com.tangem.domain.common
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.common.CardIdRange
import com.tangem.common.contains
import com.tangem.domain.features.BuildConfig
object SaltPayWorkaround {
fun tokenFrom(blockchain: Blockchain): Token {
return when (blockchain) {
Blockchain.SaltPay -> Token(
name = "WXDAI",
symbol = "WXDAI",
contractAddress = "0x4346186e7461cB4DF06bCFCB4cD591423022e417",
decimals = 18,
id = "xdai",
)
Blockchain.SaltPayTestnet -> Token(
name = "WXDAI Test",
symbol = "MyERC20",
contractAddress = "0x69cca8D8295de046C7c14019D9029Ccc77987A48",
decimals = 0,
id = "xdai",
)
else -> throw IllegalArgumentException()
}
}
val visaBatches = listOf(
"AE02",
"AE03",
) + attachDebugVisaBatches()
val tangemWalletCardIds = listOf(
"AC01000000033503",
"AC01000000033594",
"AC01000000033586",
"AC01000000034477",
"AC01000000032760",
"AC01000000033867",
"AC01000000032653",
"AC01000000032752",
"AC01000000034485",
"AC01000000033644",
"AC01000000037454",
"AC01000000037462",
"AC03000000076070",
"AC03000000076088",
"AC03000000076096",
"AC03000000076104",
"AC03000000076112",
"AC03000000076120",
"AC03000000076138",
"AC03000000076146",
"AC03000000076153",
"AC03000000076161",
"AC03000000076179",
"AC03000000076187",
"AC03000000076195",
"AC03000000076203",
"AC03000000076211",
"AC03000000076229",
) + attachDebugTangemCardIds()
val tangemWalletCardIdRanges = listOf(
CardIdRange("AC05000000000003", "AC05000000023997")!!,
) + attachDebugTangemCardIdRanges()
fun isVisaBatchId(batchId: String): Boolean = visaBatches.contains(batchId)
fun isTangemWalletCardId(cardId: String): Boolean {
return if (tangemWalletCardIds.contains(cardId)) true else tangemWalletCardIdRanges.contains(cardId)
}
private fun attachDebugVisaBatches(): List<String> {
return if (BuildConfig.DEBUG) listOf(
"FF03",
) else listOf()
}
private fun attachDebugTangemCardIds(): List<String> {
return if (BuildConfig.DEBUG) listOf(
"FF04000000000232",
) else listOf()
}
private fun attachDebugTangemCardIdRanges(): List<CardIdRange> {
return if (BuildConfig.DEBUG) listOf(
CardIdRange("FF04000000000000", "FF04999999999999")!!,
) else listOf()
}
}

View file

@ -8,10 +8,10 @@ import com.tangem.common.card.WalletData
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.domain.common.TapWorkarounds.getSaltPayBlockchain
import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.domain.common.TapWorkarounds.isTangemNote
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.operations.CommandResponse
import com.tangem.operations.backup.PrimaryCard
@ -30,12 +30,8 @@ data class ScanResponse(
) : CommandResponse {
fun getBlockchain(): Blockchain {
return when (productType) {
ProductType.SaltPay -> {
card.getSaltPayBlockchain()
}
ProductType.Note -> {
card.getTangemNoteBlockchain() ?: Blockchain.Unknown
}
ProductType.SaltPay -> if (card.isTestCard) Blockchain.SaltPayTestnet else Blockchain.SaltPay
ProductType.Note -> card.getTangemNoteBlockchain() ?: Blockchain.Unknown
else -> {
val blockchainName: String = walletData?.blockchain ?: return Blockchain.Unknown
Blockchain.fromId(blockchainName)
@ -44,7 +40,7 @@ data class ScanResponse(
}
fun getPrimaryToken(): Token? {
if (card.isSaltPay) return TapWorkarounds.saltPayToken
if (card.isSaltPay) return SaltPayWorkaround.tokenFrom(getBlockchain())
val cardToken = walletData?.token ?: return null
return Token(
@ -57,11 +53,11 @@ data class ScanResponse(
fun isTangemNote(): Boolean = productType == ProductType.Note
fun isTangemWallet(): Boolean = productType == ProductType.Wallet
fun isSaltPay(): Boolean = productType == ProductType.SaltPay
fun isTangemTwins(): Boolean = productType == ProductType.Twins
fun twinsIsTwinned(): Boolean = card.isTangemTwins && walletData != null && secondTwinPublicKey != null
fun supportsHdWallet(): Boolean = card.settings.isHDWalletAllowed
fun supportsBackup(): Boolean = card.settings.isBackupAllowed
fun twinsIsTwinned(): Boolean =
card.isTangemTwins() && walletData != null && secondTwinPublicKey != null
fun hasDerivation(blockchain: Blockchain, rawDerivationPath: String): Boolean {
return hasDerivation(blockchain, DerivationPath(rawDerivationPath))
@ -95,12 +91,10 @@ enum class ProductType {
val Card.productType: ProductType
get() = when {
isTangemTwins() -> ProductType.Twins
isTangemNote() -> ProductType.Note
isTangemTwins -> ProductType.Twins
isTangemNote -> ProductType.Note
isSaltPay -> ProductType.SaltPay
else -> ProductType.Wallet
}
typealias KeyWalletPublicKey = ByteArrayKey
fun Card.isTangemTwins(): Boolean = TwinsHelper.getTwinCardNumber(cardId) != null
typealias KeyWalletPublicKey = ByteArrayKey

View file

@ -2,7 +2,6 @@ package com.tangem.domain.common
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Token
import com.tangem.common.card.Card
import java.util.*
@ -10,6 +9,10 @@ import java.util.*
[REDACTED_AUTHOR]
*/
object TapWorkarounds {
private const val START_2_COIN_ISSUER = "start2coin"
private const val TEST_CARD_BATCH = "99FF"
private const val TEST_CARD_ID_STARTS_WITH = "FF99"
fun isStart2CoinIssuer(cardIssuer: String?): Boolean {
return cardIssuer?.lowercase(Locale.US) == START_2_COIN_ISSUER
}
@ -17,21 +20,15 @@ object TapWorkarounds {
val Card.isStart2Coin: Boolean
get() = isStart2CoinIssuer(issuer.name)
val Card.isSaltPay: Boolean
get() = isSaltPayVisa || isSaltPayTangem
val Card.isSaltPayVisa: Boolean
get() = saltPayVisaBatches.contains(batchId)
val Card.isSaltPayTangem: Boolean
get() = saltPayTangemCardIds.contains(cardId)
val Card.isTestCard: Boolean
get() = batchId == TEST_CARD_BATCH && cardId.startsWith(TEST_CARD_ID_STARTS_WITH)
val Card.useOldStyleDerivation: Boolean
get() = batchId == "AC01" || batchId == "AC02" || batchId == "CB95"
val Card.derivationStyle: DerivationStyle?
get() = if (!settings.isHDWalletAllowed) {
Int.MAX_VALUE
null
} else if (useOldStyleDerivation) {
DerivationStyle.LEGACY
@ -39,38 +36,15 @@ object TapWorkarounds {
DerivationStyle.NEW
}
fun Card.isExcluded(): Boolean {
val excludedBatch = excludedBatches.contains(batchId)
val excludedIssuerName = excludedIssuers.contains(issuer.name.uppercase(Locale.ROOT))
return excludedBatch || excludedIssuerName
}
val Card.isNotSupportedInThatRelease: Boolean
get() = false
fun Card.isNotSupportedInThatRelease(): Boolean {
return false
}
val Card.isTangemTwins: Boolean
get() = TwinsHelper.getTwinCardNumber(cardId) != null
fun Card.isTangemNote(): Boolean = tangemNoteBatches.contains(batchId)
fun isTangemWalletBatch(card: Card): Boolean = tangemWalletBatches.contains(card.batchId)
fun Card.getTangemNoteBlockchain(): Blockchain? =
tangemNoteBatches[batchId] ?: null
fun Card.getSaltPayBlockchain(): Blockchain = Blockchain.SaltPay
private const val START_2_COIN_ISSUER = "start2coin"
private const val TEST_CARD_BATCH = "99FF"
private const val TEST_CARD_ID_STARTS_WITH = "FF99"
private val excludedBatches = listOf(
"0027",
"0030",
"0031",
"0035",
)
private val excludedIssuers = listOf(
"TTM BANK",
)
private val tangemWalletBatches = listOf("AC01")
//TODO: replace by reading files from a card
val Card.isTangemNote: Boolean
get() = tangemNoteBatches.contains(batchId)
private val tangemNoteBatches = mapOf(
"AB01" to Blockchain.Bitcoin,
@ -87,55 +61,34 @@ object TapWorkarounds {
"AB12" to Blockchain.Ethereum,
)
private val tangemWalletBatchesWithStandardDerivationType = listOf(
"AC01", "AC02", "CB95",
fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId] ?: null
val Card.isSaltPay: Boolean
get() = isSaltPayVisa || isSaltPayTangem
val Card.isSaltPayVisa: Boolean
get() = SaltPayWorkaround.isVisaBatchId(batchId)
val Card.isSaltPayTangem: Boolean
get() = SaltPayWorkaround.isTangemWalletCardId(cardId)
val Card.isExcluded: Boolean
get() {
val excludedBatch = excludedBatches.contains(batchId)
val excludedIssuerName = excludedIssuers.contains(issuer.name.uppercase(Locale.ROOT))
return excludedBatch || excludedIssuerName
}
private val excludedBatches = listOf(
"0027",
"0030",
"0031",
"0035",
)
val saltPayTangemCardIds = listOf(
"AC01000000000015", // TODO: remove testing CIDs
"AC03000000000088", // TODO: remove testing CIDs
"AC03000000076070",
"AC03000000076088",
"AC03000000076096",
"AC03000000076104",
"AC03000000076112",
"AC03000000076120",
"AC03000000076138",
"AC03000000076146",
"AC03000000076153",
"AC03000000076161",
"AC03000000076179",
"AC03000000076187",
"AC03000000076195",
"AC03000000076203",
"AC03000000076211",
"AC03000000076229",
// added 01.10.2022
"AC01000000033503",
"AC01000000033594",
"AC01000000033586",
"AC01000000034477",
"AC01000000032760",
"AC01000000033867",
"AC01000000032653",
"AC01000000032752",
"AC01000000034485",
"AC01000000033644",
"AC01000000037454",
"AC01000000037462",
private val excludedIssuers = listOf(
"TTM BANK",
)
private val saltPayVisaBatches = listOf(
"AE02",
"AE03",
)
val saltPayToken: Token
get() = Token(
name = "Wrapped xDAI",
symbol = "WxDAI",
contractAddress = "0x4346186e7461cB4DF06bCFCB4cD591423022e417",
decimals = 18,
id = "xdai",
)
}

View file

@ -1,36 +0,0 @@
package com.tangem.domain.common.card
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
class CardIdRange(
val cardIdStart: String,
val cardIdEnd: String,
) {
val range: LongRange
init {
checkCardId(cardIdStart)
checkCardId(cardIdEnd)
range = toLong(cardIdStart)..toLong(cardIdEnd)
}
fun contains(cardId: String): Boolean = try {
checkCardId(cardId)
range.contains(toLong(cardId))
} catch (ex: Exception) {
Timber.e(ex, "CardIdRange: check for the cardId: [$cardId] in range failed.")
false
}
private fun stripBatchPrefix(cardId: String): String = cardId.drop(4)
@Throws(NumberFormatException::class)
private fun toLong(cardId: String): Long = stripBatchPrefix(cardId).toLong()
private fun checkCardId(cardId: String) {
if (cardId.length != 16) throw IllegalArgumentException("CardId must be 16 characters long.")
}
}

View file

@ -48,6 +48,7 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
"optimistic-ethereum/test" -> Blockchain.OptimismTestnet
"dash" -> Blockchain.Dash
"wxdai" -> Blockchain.SaltPay
"wxdai/test" -> Blockchain.SaltPayTestnet
else -> null
}
}
@ -100,6 +101,7 @@ fun Blockchain.toNetworkId(): String {
Blockchain.OptimismTestnet -> "optimistic-ethereum/test"
Blockchain.Dash -> "dash"
Blockchain.SaltPay -> "wxdai"
Blockchain.SaltPayTestnet -> "wxdai/test"
}
}
@ -131,7 +133,7 @@ fun Blockchain.toCoinId(): String {
Blockchain.Kusama -> "kusama"
Blockchain.Optimism, Blockchain.OptimismTestnet -> "ethereum"
Blockchain.Dash -> "dash"
Blockchain.SaltPay -> "xdai"
Blockchain.SaltPay, Blockchain.SaltPayTestnet -> "xdai"
Blockchain.Unknown -> "unknown"
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.domain.common.extensions
import com.tangem.common.CompletionResult
import com.tangem.common.services.Result
/**
[REDACTED_AUTHOR]
*/
inline fun <T> Result<T>.successOr(failureClause: (Result.Failure) -> T): T {
return when (this) {
is Result.Success -> this.data
is Result.Failure -> failureClause(this)
}
}
inline fun <T> CompletionResult<T>.successOr(failureClause: (CompletionResult.Failure<T>) -> T): T {
return when (this) {
is CompletionResult.Success -> this.data
is CompletionResult.Failure -> failureClause(this)
}
}

View file

@ -8,7 +8,6 @@ import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.AddCustomTokenError.Warning.PotentialScamToken
import com.tangem.domain.AddCustomTokenError.Warning.TokenAlreadyAdded
import com.tangem.domain.AddCustomTokenError.Warning.UnsupportedSolanaToken
import com.tangem.domain.AddCustomTokenException
import com.tangem.domain.DomainDialog
import com.tangem.domain.DomainWrapped
import com.tangem.domain.common.TapWorkarounds.derivationStyle
@ -200,7 +199,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
networkIdConverter = { networkId ->
val blockchain = Blockchain.fromNetworkId(networkId)
if (blockchain == null || blockchain == Blockchain.Unknown) {
throw AddCustomTokenException.SelectTokeNetworkException(networkId)
throw AddCustomTokenError.SelectTokeNetworkError(networkId)
}
hubState.blockchainToName(blockchain) ?: ""
},
@ -488,7 +487,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
@Throws
private fun throwUnAppropriateInitialization(objName: String) {
throw AddCustomTokenException.UnAppropriateInitializationException(
throw AddCustomTokenError.UnAppropriateInitialization(
"AddCustomTokenHub", "$objName must be not NULL"
)
}

View file

@ -3,6 +3,7 @@ package com.tangem.domain.redux.global
import com.tangem.domain.DomainDialog
import com.tangem.domain.common.LogConfig
import com.tangem.domain.common.ScanResponse
import com.tangem.network.api.paymentology.PaymentologyApiService
import com.tangem.network.api.tangemTech.TangemTechService
/**
@ -18,5 +19,6 @@ data class DomainGlobalState(
)
data class NetworkServices(
val tangemTechService: TangemTechService = TangemTechService(LogConfig.network.tangemTechService)
val tangemTechService: TangemTechService = TangemTechService(LogConfig.network.tangemTechService),
val paymentologyService: PaymentologyApiService = PaymentologyApiService(LogConfig.network.paymentologyApiService),
)