Updated on 2026-08-14
This commit is contained in:
parent
025fc498a3
commit
b05e10db56
132 changed files with 231 additions and 189 deletions
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.domain
|
||||
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed interface DomainDialog {
|
||||
|
||||
data class DialogError(val error: DomainModuleError) : DomainDialog
|
||||
|
||||
data class SelectTokenDialog(
|
||||
val items: List<CoinsResponse.Coin.Network>,
|
||||
val networkIdConverter: (String) -> String,
|
||||
val onSelect: (CoinsResponse.Coin.Network) -> Unit,
|
||||
val onClose: VoidCallback = {},
|
||||
) : DomainDialog
|
||||
}
|
||||
26
domain/legacy/src/main/java/com/tangem/domain/DomainLayer.kt
Normal file
26
domain/legacy/src/main/java/com/tangem/domain/DomainLayer.kt
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.domain
|
||||
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||
import com.tangem.domain.redux.state.ActionStateLoggerImpl
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object DomainLayer {
|
||||
internal val actionStateLogger = ActionStateLoggerImpl()
|
||||
|
||||
var onInitComplete: ((DomainModuleError?) -> Unit)? = null
|
||||
|
||||
fun init() {
|
||||
initActionStateLogger()
|
||||
|
||||
onInitComplete?.invoke(null)
|
||||
}
|
||||
|
||||
private fun initActionStateLogger() {
|
||||
val factory = actionStateLogger.actionStateConvertersFactory
|
||||
|
||||
factory.addConverter(AddCustomTokenAction::class.java, AddCustomTokenState.Converter())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
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]",
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.domain
|
||||
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
* Provides a temporary copies of the app module classes, data structures, etc.
|
||||
*/
|
||||
// TODO: refactoring: : after refactoring they should be unwrapped and moved
|
||||
// to appropriate parts of module
|
||||
sealed interface DomainWrapped {
|
||||
|
||||
// Mirror reflection ot the com.tangem.tap.features.wallet.redux.Currency
|
||||
sealed interface Currency {
|
||||
val blockchain: com.tangem.blockchain.common.Blockchain
|
||||
val currencySymbol: String
|
||||
val derivationPath: String?
|
||||
|
||||
data class Token(
|
||||
val token: com.tangem.blockchain.common.Token,
|
||||
override val blockchain: com.tangem.blockchain.common.Blockchain,
|
||||
override val derivationPath: String?,
|
||||
) : Currency {
|
||||
override val currencySymbol = token.symbol
|
||||
}
|
||||
|
||||
data class Blockchain(
|
||||
override val blockchain: com.tangem.blockchain.common.Blockchain,
|
||||
override val derivationPath: String?,
|
||||
) : Currency {
|
||||
override val currencySymbol: String = blockchain.currency
|
||||
}
|
||||
|
||||
fun isCustomCurrency(derivationStyle: DerivationStyle?): Boolean {
|
||||
if (derivationPath == null || derivationStyle == null) return false
|
||||
return derivationPath != blockchain.derivationPath(derivationStyle)?.rawPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.domain.common
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
|
||||
interface CardTypesResolver {
|
||||
fun isTangemNote(): Boolean
|
||||
fun isTangemWallet(): Boolean
|
||||
fun isWallet2(): Boolean
|
||||
fun isSaltPay(): Boolean
|
||||
fun isSaltPayVisa(): Boolean
|
||||
fun isSaltPayWallet(): Boolean
|
||||
fun isTangemTwins(): Boolean
|
||||
fun isStart2Coin(): Boolean
|
||||
|
||||
fun isMultiwalletAllowed(): Boolean
|
||||
|
||||
fun getBlockchain(): Blockchain
|
||||
fun getPrimaryToken(): Token?
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.domain.common
|
||||
|
||||
import com.tangem.domain.features.BuildConfig
|
||||
|
||||
object LogConfig {
|
||||
const val imageLoader: Boolean = false
|
||||
val storeAction: Boolean = BuildConfig.DEBUG
|
||||
const val zendesk: Boolean = false
|
||||
val network: NetworkLogConfig = NetworkLogConfig
|
||||
val analyticsHandlers: AnalyticsHandlersLogConfig = AnalyticsHandlersLogConfig
|
||||
}
|
||||
|
||||
object NetworkLogConfig {
|
||||
const val mercuryoService: Boolean = false
|
||||
const val moonPayService: Boolean = false
|
||||
const val utorgService: Boolean = false
|
||||
val tangemTechService: Boolean = BuildConfig.DEBUG
|
||||
val paymentologyApiService: Boolean = BuildConfig.DEBUG
|
||||
val blockchainSdkNetwork: Boolean = BuildConfig.DEBUG
|
||||
}
|
||||
|
||||
object AnalyticsHandlersLogConfig {
|
||||
const val firebase: Boolean = false
|
||||
const val appsFlyer: Boolean = false
|
||||
const val amplitude: Boolean = false
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
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
|
||||
|
||||
object SaltPayWorkaround {
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
val visaBatches = listOf(
|
||||
"AE02",
|
||||
"AE03",
|
||||
) + attachTestVisaBatches()
|
||||
|
||||
val walletCardIds = 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",
|
||||
) + attachTestWalletCardIds()
|
||||
|
||||
val walletCardIdRanges = listOf(
|
||||
CardIdRange("AC05000000000003", "AC05000000023997")!!,
|
||||
) + attachTestWalletCardIdRanges()
|
||||
|
||||
fun tokenFrom(blockchain: Blockchain): Token {
|
||||
return when (blockchain) {
|
||||
Blockchain.SaltPay -> Token(
|
||||
name = "WXDAI",
|
||||
symbol = "wxDAI",
|
||||
contractAddress = "0x4200000000000000000000000000000000000006",
|
||||
decimals = 18,
|
||||
id = "wrapped-xdai",
|
||||
)
|
||||
else -> error("It is not SaltPay")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun isSaltPayCardId(cardId: String): Boolean = isVisaBatchId(cardId.take(4)) || isWalletCardId(cardId)
|
||||
|
||||
fun isVisaBatchId(batchId: String): Boolean = visaBatches.contains(batchId)
|
||||
|
||||
fun isWalletCardId(cardId: String): Boolean {
|
||||
return if (walletCardIds.contains(cardId)) true else walletCardIdRanges.contains(cardId)
|
||||
}
|
||||
|
||||
private fun attachTestVisaBatches(): List<String> = listOf(
|
||||
"FF03",
|
||||
)
|
||||
|
||||
private fun attachTestWalletCardIds(): List<String> = listOf(
|
||||
"FF04000000000232",
|
||||
)
|
||||
|
||||
private fun attachTestWalletCardIdRanges(): List<CardIdRange> = listOf(
|
||||
CardIdRange("FF04000000000000", "FF04999999999999")!!,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package com.tangem.domain.common
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.common.card.WalletData
|
||||
import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain
|
||||
import com.tangem.domain.common.TapWorkarounds.isSaltPay
|
||||
import com.tangem.domain.common.TapWorkarounds.isSaltPayVisa
|
||||
import com.tangem.domain.common.TapWorkarounds.isSaltPayWallet
|
||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
|
||||
class TangemCardTypesResolver(
|
||||
private val card: CardDTO,
|
||||
private val productType: ProductType,
|
||||
private val walletData: WalletData?,
|
||||
) : CardTypesResolver {
|
||||
|
||||
override fun isTangemNote(): Boolean = productType == ProductType.Note
|
||||
override fun isTangemWallet(): Boolean = card.settings.isBackupAllowed &&
|
||||
card.settings.isHDWalletAllowed &&
|
||||
card.firmwareVersion >= FirmwareVersion.MultiWalletAvailable &&
|
||||
!card.isSaltPay
|
||||
|
||||
override fun isWallet2(): Boolean = card.firmwareVersion >= FirmwareVersion.KeysImportAvailable
|
||||
override fun isSaltPay(): Boolean = productType == ProductType.SaltPay
|
||||
override fun isSaltPayVisa(): Boolean = card.isSaltPayVisa
|
||||
override fun isSaltPayWallet(): Boolean = card.isSaltPayWallet
|
||||
override fun isTangemTwins(): Boolean = productType == ProductType.Twins
|
||||
override fun isStart2Coin(): Boolean = card.isStart2Coin
|
||||
|
||||
override fun isMultiwalletAllowed(): Boolean =
|
||||
!isTangemTwins() && !card.isStart2Coin && !isTangemNote() && !isSaltPay() &&
|
||||
(
|
||||
card.firmwareVersion >= FirmwareVersion.MultiWalletAvailable ||
|
||||
card.wallets.firstOrNull()?.curve == EllipticCurve.Secp256k1
|
||||
)
|
||||
|
||||
override fun getBlockchain(): Blockchain {
|
||||
return when (productType) {
|
||||
ProductType.Start2Coin -> if (card.isTestCard) Blockchain.BitcoinTestnet else Blockchain.Bitcoin
|
||||
ProductType.SaltPay -> Blockchain.SaltPay
|
||||
else -> {
|
||||
val blockchainName: String = walletData?.blockchain
|
||||
?: if (productType == ProductType.Note) {
|
||||
return card.getTangemNoteBlockchain() ?: Blockchain.Unknown
|
||||
} else {
|
||||
return Blockchain.Unknown
|
||||
}
|
||||
Blockchain.fromBlockchainName(blockchainName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPrimaryToken(): Token? {
|
||||
if (isSaltPay()) return SaltPayWorkaround.tokenFrom(getBlockchain())
|
||||
|
||||
val cardToken = walletData?.token ?: return null
|
||||
return Token(
|
||||
cardToken.name,
|
||||
cardToken.symbol,
|
||||
cardToken.contractAddress,
|
||||
cardToken.decimals,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Blockchain.Companion.fromBlockchainName(blockchainName: String): Blockchain {
|
||||
// workaround for BSC (BNB) notes cards
|
||||
return when (blockchainName) {
|
||||
"BINANCE" -> {
|
||||
Blockchain.BSC
|
||||
}
|
||||
"BINANCE/test" -> {
|
||||
Blockchain.BSCTestnet
|
||||
}
|
||||
else -> {
|
||||
Blockchain.fromId(blockchainName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package com.tangem.domain.common
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
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"
|
||||
|
||||
val CardDTO.isTangemTwins: Boolean
|
||||
get() = TwinsHelper.getTwinCardNumber(cardId) != null
|
||||
|
||||
// TODO: replace by reading files from a card
|
||||
val CardDTO.isTangemNote: Boolean
|
||||
get() = tangemNoteBatches.contains(batchId)
|
||||
|
||||
val CardDTO.isStart2Coin: Boolean
|
||||
get() = isStart2CoinIssuer(issuer.name)
|
||||
|
||||
val CardDTO.isSaltPay: Boolean
|
||||
get() = isSaltPayVisa || isSaltPayWallet
|
||||
|
||||
val CardDTO.isSaltPayVisa: Boolean
|
||||
get() = SaltPayWorkaround.isVisaBatchId(batchId)
|
||||
|
||||
val CardDTO.isSaltPayWallet: Boolean
|
||||
get() = SaltPayWorkaround.isWalletCardId(cardId)
|
||||
|
||||
val CardDTO.isTestCard: Boolean
|
||||
get() = batchId == TEST_CARD_BATCH && cardId.startsWith(TEST_CARD_ID_STARTS_WITH)
|
||||
|
||||
val CardDTO.useOldStyleDerivation: Boolean
|
||||
get() = batchId == "AC01" || batchId == "AC02" || batchId == "CB95"
|
||||
|
||||
val CardDTO.derivationStyle: DerivationStyle?
|
||||
get() = if (!settings.isHDWalletAllowed) {
|
||||
null
|
||||
} else if (useOldStyleDerivation) {
|
||||
DerivationStyle.LEGACY
|
||||
} else {
|
||||
DerivationStyle.NEW
|
||||
}
|
||||
val CardDTO.isExcluded: Boolean
|
||||
get() {
|
||||
val excludedBatch = excludedBatches.contains(batchId)
|
||||
val excludedIssuerName = excludedIssuers.contains(issuer.name.uppercase(Locale.ROOT))
|
||||
return excludedBatch || excludedIssuerName
|
||||
}
|
||||
|
||||
val CardDTO.isNotSupportedInThatRelease: Boolean
|
||||
get() = false
|
||||
|
||||
private val tangemNoteBatches = mapOf(
|
||||
"AB01" to Blockchain.Bitcoin,
|
||||
"AB02" to Blockchain.Ethereum,
|
||||
"AB03" to Blockchain.CardanoShelley,
|
||||
"AB04" to Blockchain.Dogecoin,
|
||||
"AB05" to Blockchain.BSC,
|
||||
"AB06" to Blockchain.XRP,
|
||||
"AB07" to Blockchain.Bitcoin,
|
||||
"AB08" to Blockchain.Ethereum,
|
||||
"AB09" to Blockchain.Bitcoin, // new batches for 3.34
|
||||
"AB10" to Blockchain.Ethereum,
|
||||
"AB11" to Blockchain.Bitcoin,
|
||||
"AB12" to Blockchain.Ethereum,
|
||||
)
|
||||
|
||||
private val excludedBatches = listOf("0027", "0030", "0031", "0035")
|
||||
|
||||
private val excludedIssuers = listOf("TTM BANK")
|
||||
|
||||
@Deprecated(
|
||||
"Now blockchain is read form files (CardTypesResolver.getBlockchain), " +
|
||||
"but for previously saved cards this method is still used",
|
||||
)
|
||||
fun CardDTO.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId]
|
||||
|
||||
fun isStart2CoinIssuer(cardIssuer: String?): Boolean {
|
||||
return cardIssuer?.lowercase(Locale.US) == START_2_COIN_ISSUER
|
||||
}
|
||||
|
||||
fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId] ?: null
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.tangem.domain.common
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
open class Throttler<T>(
|
||||
private val duration: Long,
|
||||
) : Throttle<T> {
|
||||
|
||||
private val items: MutableMap<T, Long> = mutableMapOf()
|
||||
|
||||
override fun isStillThrottled(item: T): Boolean {
|
||||
val inThrottlingUpTo = items[item] ?: return false
|
||||
val diff = System.currentTimeMillis() - inThrottlingUpTo
|
||||
return diff < 0
|
||||
}
|
||||
|
||||
override fun updateThrottlingTo(item: T): T {
|
||||
val now = System.currentTimeMillis()
|
||||
val throttledUpTo = items[item] ?: 0L
|
||||
if (throttledUpTo == 0L || throttledUpTo < now) {
|
||||
val newTime = now + duration
|
||||
items[item] = newTime
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
open fun clear() {
|
||||
items.clear()
|
||||
}
|
||||
}
|
||||
|
||||
class ThrottlerWithValues<T, V>(
|
||||
duration: Long,
|
||||
) : Throttler<T>(duration), ValuesHolder<T, V> {
|
||||
|
||||
private val valuesHolder: MutableMap<T, V?> = mutableMapOf()
|
||||
|
||||
override fun setValue(item: T, value: V) {
|
||||
valuesHolder[item] = value
|
||||
}
|
||||
|
||||
override fun geValue(item: T): V? = valuesHolder[item]
|
||||
|
||||
override fun remove(item: T) {
|
||||
valuesHolder.remove(item)
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
valuesHolder.clear()
|
||||
super.clear()
|
||||
}
|
||||
}
|
||||
|
||||
interface Throttle<T> {
|
||||
fun isStillThrottled(item: T): Boolean
|
||||
fun updateThrottlingTo(item: T): T
|
||||
}
|
||||
|
||||
interface ValuesHolder<K, V> {
|
||||
fun setValue(item: K, value: V)
|
||||
fun geValue(item: K): V?
|
||||
fun remove(item: K)
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.domain.common
|
||||
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
|
||||
object TwinsHelper {
|
||||
private val firstCardSeries = listOf("CB61", "CB64")
|
||||
private val secondCardSeries = listOf("CB62", "CB65")
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun verifyTwinPublicKey(issuerData: ByteArray, cardWalletPublicKey: ByteArray?): Boolean {
|
||||
if (issuerData.size < 65 || cardWalletPublicKey == null) return false
|
||||
|
||||
val publicKey = issuerData.sliceArray(0 until 65)
|
||||
val signedKey = issuerData.sliceArray(65 until issuerData.size)
|
||||
return CryptoUtils.verify(cardWalletPublicKey, publicKey, signedKey)
|
||||
}
|
||||
|
||||
fun getTwinCardNumber(cardId: String): TwinCardNumber? = when {
|
||||
firstCardSeries.any(cardId::startsWith) -> TwinCardNumber.First
|
||||
secondCardSeries.any(cardId::startsWith) -> TwinCardNumber.Second
|
||||
else -> null
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun getTwinCardIdForUser(cardId: String): String {
|
||||
if (cardId.length < 16) return cardId
|
||||
|
||||
val twinCardId = cardId.substring(11..14)
|
||||
val twinCardNumber = getTwinCardNumber(cardId)?.number ?: 1
|
||||
return "$twinCardId #$twinCardNumber"
|
||||
}
|
||||
}
|
||||
|
||||
enum class TwinCardNumber(val number: Int) {
|
||||
First(1), Second(2);
|
||||
|
||||
fun pairNumber(): TwinCardNumber = when (this) {
|
||||
First -> Second
|
||||
Second -> First
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Use ScanResponse.isTangemTwin")
|
||||
fun CardDTO.isTangemTwin(): Boolean {
|
||||
return TwinsHelper.getTwinCardNumber(cardId) != null
|
||||
}
|
||||
|
||||
fun CardDTO.getTwinCardNumber(): TwinCardNumber? {
|
||||
return TwinsHelper.getTwinCardNumber(this.cardId)
|
||||
}
|
||||
|
||||
fun CardDTO.getTwinCardIdForUser(): String {
|
||||
return TwinsHelper.getTwinCardIdForUser(this.cardId)
|
||||
}
|
||||
|
|
@ -0,0 +1,412 @@
|
|||
package com.tangem.domain.common.demo
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.features.BuildConfig
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class DemoConfig {
|
||||
|
||||
val demoBlockchains = listOf(
|
||||
Blockchain.Bitcoin,
|
||||
Blockchain.Ethereum,
|
||||
Blockchain.Dogecoin,
|
||||
Blockchain.Solana,
|
||||
)
|
||||
|
||||
val demoCardIds: List<String> by lazy {
|
||||
val demoIds = getReleaseIds().toMutableList()
|
||||
if (BuildConfig.DEBUG) demoIds.addAll(debugTestDemoCardIds)
|
||||
|
||||
return@lazy demoIds.distinct()
|
||||
}
|
||||
|
||||
private val walletBalances: Map<Blockchain, Amount> = mapOf(
|
||||
Blockchain.Bitcoin to Amount(0.028.toBigDecimal(), Blockchain.Bitcoin),
|
||||
Blockchain.Ethereum to Amount(0.2311.toBigDecimal(), Blockchain.Ethereum),
|
||||
Blockchain.Dogecoin to Amount(1450.025.toBigDecimal(), Blockchain.Dogecoin),
|
||||
Blockchain.Solana to Amount(13.246.toBigDecimal(), Blockchain.Solana),
|
||||
)
|
||||
|
||||
fun isDemoCardId(cardId: String): Boolean = demoCardIds.contains(cardId)
|
||||
|
||||
fun isTestDemoCardId(cardId: String): Boolean = testDemoCardIds.contains(cardId)
|
||||
|
||||
fun getBalance(blockchain: Blockchain): Amount = walletBalances[blockchain]?.copy()
|
||||
?: Amount(BigDecimal.ZERO, blockchain).copy()
|
||||
|
||||
private fun getReleaseIds(): List<String> {
|
||||
return (releaseDemoCardIds + testDemoCardIds).distinct()
|
||||
}
|
||||
|
||||
@Suppress("ClassOrdering")
|
||||
private val releaseDemoCardIds = mutableListOf(
|
||||
// === Not from the Google Sheet table ===
|
||||
"AC01000000041225",
|
||||
"AC01000000041472",
|
||||
"AB01000000046498",
|
||||
"AB01000000049608",
|
||||
"AB01000000049574",
|
||||
"AB01000000046704",
|
||||
"AB02000000051000",
|
||||
"AB02000000050911",
|
||||
|
||||
// === Mvideo ===
|
||||
// Wallet
|
||||
"AC01000000045754",
|
||||
"AC01000000041662",
|
||||
"AC01000000041647",
|
||||
"AC01000000041209",
|
||||
"AC01000000042462",
|
||||
"AC01000000041100",
|
||||
"AC01000000041621",
|
||||
"AC01000000045960",
|
||||
"AC01000000041092",
|
||||
"AC01000000041217",
|
||||
"AC01000000013489",
|
||||
"AC01000000028610",
|
||||
"AC01000000028701",
|
||||
"AC01000000028578",
|
||||
"AC01000000027281",
|
||||
"AC01000000027216",
|
||||
"AC01000000028594",
|
||||
"AC01000000028602",
|
||||
"AC01000000028636",
|
||||
"AC01000000013968",
|
||||
"AC01000000027208",
|
||||
"AC01000000013471",
|
||||
"AC01000000028586",
|
||||
"AC01000000013703",
|
||||
"AC01000000028628",
|
||||
"AC01000000028693",
|
||||
"AC01000000028685",
|
||||
"AC01000000013950",
|
||||
"AC01000000013828",
|
||||
"AC01000000013497",
|
||||
"AC01000000013836",
|
||||
"AC01000000013505",
|
||||
"AC03000000046693",
|
||||
"AC03000000046685",
|
||||
"AC03000000046677",
|
||||
"AC03000000046669",
|
||||
"AC03000000046651",
|
||||
"AC03000000046644",
|
||||
"AC03000000046636",
|
||||
"AC03000000046628",
|
||||
"AC03000000046610",
|
||||
"AC03000000046602",
|
||||
"AC03000000046594",
|
||||
"AC03000000046586",
|
||||
"AC03000000046578",
|
||||
"AC03000000046560",
|
||||
"AC03000000046552",
|
||||
"AC03000000046545",
|
||||
"AC03000000046537",
|
||||
"AC03000000046529",
|
||||
"AC03000000046511",
|
||||
"AC03000000046800",
|
||||
"AC03000000046792",
|
||||
"AC03000000046784",
|
||||
"AC03000000046776",
|
||||
"AC03000000046768",
|
||||
"AC03000000046750",
|
||||
"AC03000000046743",
|
||||
"AC03000000046735",
|
||||
"AC03000000046727",
|
||||
"AC03000000046446",
|
||||
"AC03000000046438",
|
||||
"AC03000000046412",
|
||||
"AC03000000046388",
|
||||
"AC03000000046370",
|
||||
"AC03000000046354",
|
||||
"AC03000000046347",
|
||||
"AC03000000046339",
|
||||
"AC03000000046321",
|
||||
"AC03000000046172",
|
||||
"AC03000000046396",
|
||||
"AC03000000046404",
|
||||
"AC03000000046701",
|
||||
"AC03000000046420",
|
||||
"AC03000000046719",
|
||||
"AC03000000046503",
|
||||
"AC03000000046495",
|
||||
"AC03000000046487",
|
||||
"AC03000000046362",
|
||||
"AC03000000046479",
|
||||
"AC03000000046461",
|
||||
"AC03000000046453",
|
||||
|
||||
// Note BTC
|
||||
"AB01000000059608",
|
||||
"AB01000000046647",
|
||||
"AB01000000046571",
|
||||
"AB01000000046746",
|
||||
"AB01000000059574",
|
||||
"AB01000000046753",
|
||||
"AB01000000046605",
|
||||
"AB01000000046761",
|
||||
"AB01000000046720",
|
||||
"AB01000000046530",
|
||||
"AB01000000016475",
|
||||
"AB01000000016483",
|
||||
"AB01000000016491",
|
||||
"AB01000000020709",
|
||||
"AB01000000020717",
|
||||
"AB01000000015550",
|
||||
"AB01000000015394",
|
||||
"AB01000000016079",
|
||||
"AB01000000016087",
|
||||
"AB01000000016095",
|
||||
"AB01000000020915",
|
||||
"AB01000000017184",
|
||||
"AB01000000020907",
|
||||
"AB01000000017192",
|
||||
"AB01000000016210",
|
||||
"AB01000000016111",
|
||||
"AB01000000016103",
|
||||
"AB01000000015766",
|
||||
"AB01000000015774",
|
||||
"AB01000000015782",
|
||||
"AB01000000022598",
|
||||
"AB01000000022580",
|
||||
"AB01000000005688",
|
||||
"AB07000000005696",
|
||||
"AB07000000005902",
|
||||
"AB07000000005910",
|
||||
"AB07000000005928",
|
||||
"AB07000000005936",
|
||||
"AB07000000005944",
|
||||
"AB07000000005993",
|
||||
"AB07000000005985",
|
||||
"AB07000000005977",
|
||||
"AB07000000005969",
|
||||
"AB07000000005951",
|
||||
"AB07000000005605",
|
||||
"AB07000000005803",
|
||||
"AB07000000005811",
|
||||
"AB07000000005829",
|
||||
"AB07000000005837",
|
||||
"AB07000000005845",
|
||||
"AB07000000005852",
|
||||
"AB07000000005860",
|
||||
"AB07000000005878",
|
||||
"AB07000000005886",
|
||||
"AB07000000005894",
|
||||
"AB07000000005704",
|
||||
"AB07000000005712",
|
||||
"AB07000000005720",
|
||||
"AB07000000005738",
|
||||
"AB07000000005746",
|
||||
"AB07000000005514",
|
||||
"AB07000000005522",
|
||||
"AB07000000005563",
|
||||
"AB07000000005571",
|
||||
"AB07000000005589",
|
||||
"AB07000000005597",
|
||||
"AB07000000005613",
|
||||
"AB07000000005621",
|
||||
"AB07000000005639",
|
||||
"AB07000000005647",
|
||||
"AB07000000005654",
|
||||
"AB07000000005662",
|
||||
"AB07000000005670",
|
||||
"AB07000000005530",
|
||||
"AB07000000005548",
|
||||
"AB07000000005555",
|
||||
"AB07000000005753",
|
||||
"AB07000000005761",
|
||||
"AB07000000005779",
|
||||
"AB07000000005787",
|
||||
"AB07000000005795",
|
||||
"AB07000000005506",
|
||||
|
||||
// Note ETH
|
||||
"AB02000000051083",
|
||||
"AB02000000051059",
|
||||
"AB02000000051158",
|
||||
"AB02000000050986",
|
||||
"AB02000000051026",
|
||||
"AB02000000050960",
|
||||
"AB02000000051042",
|
||||
"AB02000000051091",
|
||||
"AB02000000051034",
|
||||
"AB02000000051133",
|
||||
"AB02000000019924",
|
||||
"AB02000000019932",
|
||||
"AB02000000022092",
|
||||
"AB02000000022282",
|
||||
"AB02000000023983",
|
||||
"AB02000000023439",
|
||||
"AB02000000020328",
|
||||
"AB02000000020310",
|
||||
"AB02000000021565",
|
||||
"AB02000000022357",
|
||||
"AB02000000023355",
|
||||
"AB02000000022324",
|
||||
"AB02000000022100",
|
||||
"AB02000000019999",
|
||||
"AB02000000020013",
|
||||
"AB02000000020005",
|
||||
"AB02000000020021",
|
||||
"AB02000000020039",
|
||||
"AB02000000020278",
|
||||
"AB02000000020252",
|
||||
"AB02000000018652",
|
||||
"AB02000000018561",
|
||||
"AB08000000009481",
|
||||
"AB08000000009473",
|
||||
"AB08000000009705",
|
||||
"AB08000000009897",
|
||||
"AB08000000009689",
|
||||
"AB08000000009671",
|
||||
"AB08000000009465",
|
||||
"AB08000000009457",
|
||||
"AB08000000009440",
|
||||
"AB08000000009432",
|
||||
"AB08000000009424",
|
||||
"AB08000000009416",
|
||||
"AB08000000009408",
|
||||
"AB08000000009390",
|
||||
"AB08000000009374",
|
||||
"AB08000000009382",
|
||||
"AB08000000009267",
|
||||
"AB08000000009275",
|
||||
"AB08000000009283",
|
||||
"AB08000000009291",
|
||||
"AB08000000009309",
|
||||
"AB08000000009317",
|
||||
"AB08000000009325",
|
||||
"AB08000000009333",
|
||||
"AB08000000009341",
|
||||
"AB08000000009358",
|
||||
"AB08000000009366",
|
||||
"AB08000000009077",
|
||||
"AB08000000009143",
|
||||
"AB08000000009168",
|
||||
"AB08000000009184",
|
||||
"AB08000000009192",
|
||||
"AB08000000009200",
|
||||
"AB08000000009226",
|
||||
"AB08000000009218",
|
||||
"AB08000000009234",
|
||||
"AB08000000009242",
|
||||
"AB08000000008574",
|
||||
"AB08000000009069",
|
||||
"AB08000000008525",
|
||||
"AB08000000009051",
|
||||
"AB08000000009135",
|
||||
"AB08000000009150",
|
||||
"AB08000000009176",
|
||||
"AB08000000009085",
|
||||
"AB08000000009093",
|
||||
"AB08000000009101",
|
||||
"AB08000000009119",
|
||||
"AB08000000009127",
|
||||
"AB08000000009259",
|
||||
|
||||
// === Technopark ===
|
||||
// Wallet
|
||||
"AC01000000044120",
|
||||
"AC01000000044997",
|
||||
"AC01000000044989",
|
||||
"AC01000000043494",
|
||||
"AC01000000043486",
|
||||
"AC01000000044187",
|
||||
"AC01000000043148",
|
||||
"AC01000000044013",
|
||||
"AC01000000043973",
|
||||
"AC01000000044815",
|
||||
"AC01000000044807",
|
||||
"AC01000000043809",
|
||||
"AC01000000043833",
|
||||
"AC01000000043460",
|
||||
"AC01000000043064",
|
||||
"AC01000000044138",
|
||||
"AC01000000044500",
|
||||
"AC01000000044492",
|
||||
"AC01000000044260",
|
||||
"AC01000000044278",
|
||||
|
||||
// Note BTC
|
||||
"AB01000000049864",
|
||||
"AB01000000053239",
|
||||
"AB01000000053056",
|
||||
"AB01000000054237",
|
||||
"AB01000000054245",
|
||||
"AB01000000054211",
|
||||
"AB01000000054229",
|
||||
"AB01000000053189",
|
||||
"AB01000000054195",
|
||||
"AB01000000050797",
|
||||
"AB01000000053833",
|
||||
"AB01000000052124",
|
||||
"AB01000000051605",
|
||||
"AB01000000052223",
|
||||
"AB01000000052207",
|
||||
"AB01000000052199",
|
||||
"AB01000000047785",
|
||||
"AB01000000047850",
|
||||
"AB01000000047868",
|
||||
"AB01000000048288",
|
||||
|
||||
// Note ETH
|
||||
"AB02000000049715",
|
||||
"AB02000000049848",
|
||||
"AB02000000049814",
|
||||
"AB02000000049863",
|
||||
"AB02000000049871",
|
||||
"AB02000000049855",
|
||||
"AB02000000049285",
|
||||
"AB02000000049277",
|
||||
"AB02000000049558",
|
||||
"AB02000000049889",
|
||||
"AB02000000049988",
|
||||
"AB02000000049707",
|
||||
"AB02000000049699",
|
||||
"AB02000000049897",
|
||||
"AB02000000049905",
|
||||
"AB02000000049913",
|
||||
"AB02000000049251",
|
||||
"AB02000000049533",
|
||||
"AB02000000049541",
|
||||
"AB02000000049830",
|
||||
// === more cids ===
|
||||
"AC03000000091418",
|
||||
"AC03000000091400",
|
||||
"AC03000000099007",
|
||||
"AC03000000098991",
|
||||
"AC03000000098942",
|
||||
"AC03000000091715",
|
||||
"AC03000000091301",
|
||||
"AC03000000091343",
|
||||
"AB01000000055705",
|
||||
"AB01000000052918",
|
||||
"AB01000000047710",
|
||||
"AB01000000052306",
|
||||
"AB01000000047645",
|
||||
"AB01000000048957",
|
||||
"AB01000000052900",
|
||||
"AB01000000050391",
|
||||
"AB01000000047363",
|
||||
"AB02000000053998",
|
||||
"AB02000000019809",
|
||||
"AB02000000020872",
|
||||
"AB02000000022027",
|
||||
"AB02000000058955",
|
||||
"AB02000000053253",
|
||||
"AB02000000048063",
|
||||
"AB02000000023736",
|
||||
"AB02000000058187",
|
||||
)
|
||||
|
||||
@Suppress("ClassOrdering")
|
||||
private val testDemoCardIds = listOf(
|
||||
"FB20000000000186", // Note ETH
|
||||
"FB10000000000196", // Note BTC
|
||||
"FB30000000000176", // Wallet
|
||||
)
|
||||
|
||||
@Suppress("ClassOrdering")
|
||||
private val debugTestDemoCardIds = listOf<String>()
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
package com.tangem.domain.common.extensions
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
|
||||
return when (networkId) {
|
||||
"arbitrum-one" -> Blockchain.Arbitrum
|
||||
"arbitrum-one/test" -> Blockchain.ArbitrumTestnet
|
||||
"avalanche", "avalanche-2" -> Blockchain.Avalanche
|
||||
"avalanche/test", "avalanche-2/test" -> Blockchain.AvalancheTestnet
|
||||
"binancecoin" -> Blockchain.Binance
|
||||
"binancecoin/test" -> Blockchain.BinanceTestnet
|
||||
"binance-smart-chain" -> Blockchain.BSC
|
||||
"binance-smart-chain/test" -> Blockchain.BSCTestnet
|
||||
"ethereum" -> Blockchain.Ethereum
|
||||
"ethereum/test" -> Blockchain.EthereumTestnet
|
||||
"ethereum-classic" -> Blockchain.EthereumClassic
|
||||
"ethereum-classic/test" -> Blockchain.EthereumClassicTestnet
|
||||
"polygon-pos", "matic-network" -> Blockchain.Polygon
|
||||
"polygon-pos/test", "matic-network/test" -> Blockchain.PolygonTestnet
|
||||
"solana" -> Blockchain.Solana
|
||||
"solana/test" -> Blockchain.SolanaTestnet
|
||||
"fantom" -> Blockchain.Fantom
|
||||
"fantom/test" -> Blockchain.FantomTestnet
|
||||
"bitcoin" -> Blockchain.Bitcoin
|
||||
"bitcoin/test" -> Blockchain.BitcoinTestnet
|
||||
"bitcoin-cash" -> Blockchain.BitcoinCash
|
||||
"bitcoin-cash/test" -> Blockchain.BitcoinCashTestnet
|
||||
"cardano" -> Blockchain.CardanoShelley
|
||||
"dogecoin" -> Blockchain.Dogecoin
|
||||
"ducatus" -> Blockchain.Ducatus
|
||||
"litecoin" -> Blockchain.Litecoin
|
||||
"rootstock" -> Blockchain.RSK
|
||||
"stellar" -> Blockchain.Stellar
|
||||
"stellar/test" -> Blockchain.StellarTestnet
|
||||
"tezos" -> Blockchain.Tezos
|
||||
"tron" -> Blockchain.Tron
|
||||
"tron/test" -> Blockchain.TronTestnet
|
||||
"xrp", "ripple" -> Blockchain.XRP
|
||||
"xdai" -> Blockchain.Gnosis
|
||||
"ethereum-pow-iou" -> Blockchain.EthereumPow
|
||||
"ethereum-pow-iou/test" -> Blockchain.EthereumPowTestnet
|
||||
"ethereumfair" -> Blockchain.EthereumFair
|
||||
"polkadot" -> Blockchain.Polkadot
|
||||
"polkadot/test" -> Blockchain.PolkadotTestnet
|
||||
"kusama" -> Blockchain.Kusama
|
||||
"optimistic-ethereum" -> Blockchain.Optimism
|
||||
"optimistic-ethereum/test" -> Blockchain.OptimismTestnet
|
||||
"dash" -> Blockchain.Dash
|
||||
"sxdai" -> Blockchain.SaltPay
|
||||
"kaspa" -> Blockchain.Kaspa
|
||||
"the-open-network" -> Blockchain.TON
|
||||
"the-open-network/test" -> Blockchain.TONTestnet
|
||||
"kava" -> Blockchain.Kava
|
||||
"kava/test" -> Blockchain.KavaTestnet
|
||||
"ravencoin" -> Blockchain.Ravencoin
|
||||
"ravencoin/test" -> Blockchain.RavencoinTestnet
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
fun Blockchain.toNetworkId(): String {
|
||||
return when (this) {
|
||||
Blockchain.Unknown -> "unknown"
|
||||
Blockchain.Arbitrum -> "arbitrum-one"
|
||||
Blockchain.ArbitrumTestnet -> "arbitrum-one/test"
|
||||
Blockchain.Avalanche -> "avalanche"
|
||||
Blockchain.AvalancheTestnet -> "avalanche/test"
|
||||
Blockchain.Binance -> "binancecoin"
|
||||
Blockchain.BinanceTestnet -> "binancecoin/test"
|
||||
Blockchain.BSC -> "binance-smart-chain"
|
||||
Blockchain.BSCTestnet -> "binance-smart-chain/test"
|
||||
Blockchain.Bitcoin -> "bitcoin"
|
||||
Blockchain.BitcoinTestnet -> "bitcoin/test"
|
||||
Blockchain.BitcoinCash -> "bitcoin-cash"
|
||||
Blockchain.BitcoinCashTestnet -> "bitcoin-cash/test"
|
||||
Blockchain.Cardano -> "cardano"
|
||||
Blockchain.CardanoShelley -> "cardano"
|
||||
Blockchain.Dogecoin -> "dogecoin"
|
||||
Blockchain.Ducatus -> "ducatus"
|
||||
Blockchain.Ethereum -> "ethereum"
|
||||
Blockchain.EthereumTestnet -> "ethereum/test"
|
||||
Blockchain.EthereumClassic -> "ethereum-classic"
|
||||
Blockchain.EthereumClassicTestnet -> "ethereum-classic/test"
|
||||
Blockchain.Fantom -> "fantom"
|
||||
Blockchain.FantomTestnet -> "fantom/test"
|
||||
Blockchain.Litecoin -> "litecoin"
|
||||
Blockchain.Polygon -> "polygon-pos"
|
||||
Blockchain.PolygonTestnet -> "polygon-pos/test"
|
||||
Blockchain.RSK -> "rootstock"
|
||||
Blockchain.Stellar -> "stellar"
|
||||
Blockchain.StellarTestnet -> "stellar/test"
|
||||
Blockchain.Solana -> "solana"
|
||||
Blockchain.SolanaTestnet -> "solana/test"
|
||||
Blockchain.Tezos -> "tezos"
|
||||
Blockchain.XRP -> "xrp"
|
||||
Blockchain.Tron -> "tron"
|
||||
Blockchain.TronTestnet -> "tron/test"
|
||||
Blockchain.Gnosis -> "xdai"
|
||||
Blockchain.EthereumPow -> "ethereum-pow-iou"
|
||||
Blockchain.EthereumPowTestnet -> "ethereum-pow-iou/test"
|
||||
Blockchain.EthereumFair -> "ethereumfair"
|
||||
Blockchain.Polkadot -> "polkadot"
|
||||
Blockchain.PolkadotTestnet -> "polkadot/test"
|
||||
Blockchain.Kusama -> "kusama"
|
||||
Blockchain.Optimism -> "optimistic-ethereum"
|
||||
Blockchain.OptimismTestnet -> "optimistic-ethereum/test"
|
||||
Blockchain.Dash -> "dash"
|
||||
Blockchain.SaltPay -> "sxdai"
|
||||
Blockchain.Kaspa -> "kaspa"
|
||||
Blockchain.TON -> "the-open-network"
|
||||
Blockchain.TONTestnet -> "the-open-network/test"
|
||||
Blockchain.Kava -> "kava"
|
||||
Blockchain.KavaTestnet -> "kava/test"
|
||||
Blockchain.Ravencoin -> "ravencoin"
|
||||
Blockchain.RavencoinTestnet -> "ravencoin/test"
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
fun Blockchain.toCoinId(): String {
|
||||
return when (this) {
|
||||
Blockchain.Binance, Blockchain.BinanceTestnet, Blockchain.BSC, Blockchain.BSCTestnet -> "binancecoin"
|
||||
Blockchain.Bitcoin, Blockchain.BitcoinTestnet -> "bitcoin"
|
||||
Blockchain.BitcoinCash, Blockchain.BitcoinCashTestnet -> "bitcoin-cash"
|
||||
Blockchain.Ethereum, Blockchain.EthereumTestnet -> "ethereum"
|
||||
Blockchain.EthereumClassic, Blockchain.EthereumClassicTestnet -> "ethereum-classic"
|
||||
Blockchain.Stellar, Blockchain.StellarTestnet -> "stellar"
|
||||
Blockchain.Cardano, Blockchain.CardanoShelley -> "cardano"
|
||||
Blockchain.Polygon, Blockchain.PolygonTestnet -> "matic-network"
|
||||
Blockchain.Arbitrum, Blockchain.ArbitrumTestnet -> "ethereum"
|
||||
Blockchain.Avalanche, Blockchain.AvalancheTestnet -> "avalanche-2"
|
||||
Blockchain.Solana, Blockchain.SolanaTestnet -> "solana"
|
||||
Blockchain.Fantom, Blockchain.FantomTestnet -> "fantom"
|
||||
Blockchain.Tron, Blockchain.TronTestnet -> "tron"
|
||||
Blockchain.Polkadot, Blockchain.PolkadotTestnet -> "polkadot"
|
||||
Blockchain.Ducatus -> "ducatus"
|
||||
Blockchain.Litecoin -> "litecoin"
|
||||
Blockchain.RSK -> "rootstock"
|
||||
Blockchain.Tezos -> "tezos"
|
||||
Blockchain.XRP -> "ripple"
|
||||
Blockchain.Dogecoin -> "dogecoin"
|
||||
Blockchain.Gnosis -> "xdai"
|
||||
Blockchain.EthereumPow, Blockchain.EthereumPowTestnet -> "ethereum-pow-iou"
|
||||
Blockchain.EthereumFair -> "ethereumfair"
|
||||
Blockchain.Kusama -> "kusama"
|
||||
Blockchain.Optimism, Blockchain.OptimismTestnet -> "ethereum"
|
||||
Blockchain.Dash -> "dash"
|
||||
Blockchain.SaltPay -> "xdai"
|
||||
Blockchain.Kaspa -> "kaspa"
|
||||
Blockchain.TON, Blockchain.TONTestnet -> "the-open-network"
|
||||
Blockchain.Unknown -> "unknown"
|
||||
Blockchain.Kava, Blockchain.KavaTestnet -> "kava"
|
||||
Blockchain.Ravencoin, Blockchain.RavencoinTestnet -> "ravencoin"
|
||||
}
|
||||
}
|
||||
|
||||
fun Blockchain.isSupportedInApp(): Boolean {
|
||||
return !excludedBlockchains.contains(this)
|
||||
}
|
||||
|
||||
private val excludedBlockchains = listOf(
|
||||
Blockchain.SaltPay,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.domain.common.extensions
|
||||
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
fun ByteArray.calculateHmacSha256(key: ByteArray): ByteArray {
|
||||
val mac: Mac = Mac.getInstance("HmacSHA256")
|
||||
mac.init(SecretKeySpec(key, "HmacSHA256"))
|
||||
return mac.doFinal(this)
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.domain.common.extensions
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
val FirmwareVersion.Companion.SolanaTokensAvailable
|
||||
get() = FirmwareVersion(4, 52)
|
||||
|
||||
fun CardDTO.supportedBlockchains(): List<Blockchain> {
|
||||
val supportedBlockchains = if (firmwareVersion < FirmwareVersion.MultiWalletAvailable) {
|
||||
Blockchain.fromCurve(EllipticCurve.Secp256k1)
|
||||
} else {
|
||||
wallets.flatMap { Blockchain.fromCurve(it.curve) }.distinct()
|
||||
}
|
||||
|
||||
return supportedBlockchains
|
||||
.filter { isTestCard == it.isTestnet() }
|
||||
.filter { it.isSupportedInApp() }
|
||||
}
|
||||
|
||||
fun CardDTO.supportedTokens(): List<Blockchain> {
|
||||
val tokensSupportedByBlockchain = supportedBlockchains().filter { it.canHandleTokens() }.toMutableList()
|
||||
val tokensSupportedByCard = when {
|
||||
firmwareVersion >= FirmwareVersion.SolanaTokensAvailable -> tokensSupportedByBlockchain
|
||||
else -> {
|
||||
tokensSupportedByBlockchain.apply {
|
||||
remove(Blockchain.Solana)
|
||||
remove(Blockchain.SolanaTestnet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tokensSupportedByCard.filter { isTestCard == it.isTestnet() }
|
||||
}
|
||||
|
||||
fun CardDTO.canHandleToken(blockchain: Blockchain): Boolean {
|
||||
return this.supportedTokens().contains(blockchain)
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.domain.common.extensions
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
suspend fun <T> withMainContext(block: suspend CoroutineScope.() -> T): T = withContext(Dispatchers.Main, block)
|
||||
|
||||
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T): T = withContext(Dispatchers.IO, block)
|
||||
|
||||
fun <T> debounce(
|
||||
waitMs: Long = 300L,
|
||||
coroutineScope: CoroutineScope,
|
||||
runDestinationOn: CoroutineDispatcher = Dispatchers.Unconfined,
|
||||
destinationFunction: (T) -> Unit,
|
||||
): (T) -> Unit {
|
||||
var debounceJob: Job? = null
|
||||
return { param: T ->
|
||||
debounceJob?.cancel()
|
||||
debounceJob = coroutineScope.launch {
|
||||
delay(waitMs)
|
||||
withContext(runDestinationOn) { destinationFunction(param) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.domain.common.form
|
||||
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface DataConverterVisitor<Data, Result> {
|
||||
fun visit(data: Data?)
|
||||
fun getConvertedData(): Result
|
||||
}
|
||||
|
||||
interface FieldDataConverter<Result> : DataConverterVisitor<FieldData, Result>
|
||||
|
||||
abstract class BaseFieldDataConverter<Result> : FieldDataConverter<Result> {
|
||||
protected val collectIds: List<FieldId>
|
||||
get() = getIdToCollect()
|
||||
|
||||
protected val collectedData: MutableMap<FieldId, Any?> = mutableMapOf()
|
||||
|
||||
override fun visit(data: Pair<FieldId, Field.Data<*>>?) {
|
||||
val id = data?.first ?: return
|
||||
|
||||
if (collectIds.contains(id)) {
|
||||
collectedData[id] = data.second.value
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getIdToCollect(): List<FieldId>
|
||||
}
|
||||
|
||||
class FieldToJsonConverter(
|
||||
private val fieldsToConvert: List<FieldId> = listOf(),
|
||||
protected val jsonConverter: MoshiJsonConverter,
|
||||
) : BaseFieldDataConverter<String>() {
|
||||
|
||||
override fun getConvertedData(): String = jsonConverter.toJson(collectedData, " ")
|
||||
|
||||
override fun getIdToCollect(): List<FieldId> = fieldsToConvert
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
package com.tangem.domain.common.form
|
||||
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumAddressService
|
||||
import com.tangem.blockchain.blockchains.solana.SolanaAddressService
|
||||
import com.tangem.blockchain.blockchains.tron.TronAddressService
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.address.AddressService
|
||||
import com.tangem.common.Validator
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface CustomTokenValidator<T> : Validator<T, AddCustomTokenError>
|
||||
|
||||
class StringIsEmptyValidator : CustomTokenValidator<String> {
|
||||
override fun validate(data: String?): AddCustomTokenError? = when {
|
||||
data == null || data.isEmpty() -> null
|
||||
else -> AddCustomTokenError.FieldIsNotEmpty
|
||||
}
|
||||
}
|
||||
|
||||
class StringIsNotEmptyValidator : CustomTokenValidator<String> {
|
||||
override fun validate(data: String?): AddCustomTokenError? = when {
|
||||
data == null || data.isEmpty() -> AddCustomTokenError.FieldIsEmpty
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
class TokenContractAddressValidator : CustomTokenValidator<String> {
|
||||
|
||||
private var blockchain: Blockchain = Blockchain.Unknown
|
||||
|
||||
private val successAddressValidator = object : AddressService() {
|
||||
override fun makeAddress(walletPublicKey: ByteArray, curve: EllipticCurve?): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun validate(address: String): Boolean = true
|
||||
}
|
||||
|
||||
fun nextValidationFor(blockchain: Blockchain) {
|
||||
this.blockchain = blockchain
|
||||
}
|
||||
|
||||
override fun validate(data: String?): AddCustomTokenError? {
|
||||
if (data == null || data.isEmpty()) return AddCustomTokenError.FieldIsEmpty
|
||||
|
||||
return if (getAddressService().validate(data)) {
|
||||
null
|
||||
} else {
|
||||
AddCustomTokenError.InvalidContractAddress
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAddressService(): AddressService {
|
||||
return when (blockchain) {
|
||||
Blockchain.Unknown -> successAddressValidator
|
||||
Blockchain.Binance, Blockchain.BinanceTestnet -> successAddressValidator
|
||||
Blockchain.Solana, Blockchain.SolanaTestnet -> SolanaAddressService()
|
||||
Blockchain.Tron, Blockchain.TronTestnet -> TronAddressService()
|
||||
else -> {
|
||||
if (blockchain.isEvm()) {
|
||||
EthereumAddressService()
|
||||
} else {
|
||||
Timber.e("Throw for blockchain: ${blockchain.fullName}")
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TokenNetworkValidator : CustomTokenValidator<Blockchain> {
|
||||
override fun validate(data: Blockchain?): AddCustomTokenError? = when (data) {
|
||||
null, Blockchain.Unknown -> AddCustomTokenError.NetworkIsNotSelected
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
class TokenNameValidator : CustomTokenValidator<String> {
|
||||
override fun validate(data: String?): AddCustomTokenError? = StringIsNotEmptyValidator().validate(data)
|
||||
}
|
||||
|
||||
class TokenSymbolValidator : CustomTokenValidator<String> {
|
||||
override fun validate(data: String?): AddCustomTokenError? = StringIsNotEmptyValidator().validate(data)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
class TokenDecimalsValidator : CustomTokenValidator<String> {
|
||||
override fun validate(data: String?): AddCustomTokenError? {
|
||||
val decimal = data?.toIntOrNull() ?: return AddCustomTokenError.FieldIsEmpty
|
||||
|
||||
return when {
|
||||
decimal > 30 -> AddCustomTokenError.InvalidDecimalsCount
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.tangem.domain.common.form
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class Form(
|
||||
fieldList: List<DataField<*>>,
|
||||
) {
|
||||
private val _fieldList: MutableList<DataField<*>> = fieldList.toMutableList()
|
||||
|
||||
val fieldList: List<DataField<*>>
|
||||
get() = _fieldList.toList()
|
||||
|
||||
fun getField(id: FieldId): DataField<*>? = fieldList.firstOrNull { it.id == id }
|
||||
|
||||
fun getData(id: FieldId): Pair<FieldId, *>? = getField(id)?.getData()
|
||||
|
||||
fun setField(field: DataField<*>) {
|
||||
val oldField = getField(field.id) ?: return
|
||||
val oldIndexOfField = _fieldList.indexOf(oldField)
|
||||
if (oldIndexOfField == -1) return
|
||||
|
||||
_fieldList.removeAt(oldIndexOfField)
|
||||
_fieldList.add(oldIndexOfField, field)
|
||||
}
|
||||
|
||||
// convert this form data whatever you want
|
||||
fun visitDataConverter(converter: FieldDataConverter<*>) {
|
||||
fieldList.forEach { it.visitDataConverter(converter) }
|
||||
}
|
||||
}
|
||||
|
||||
interface FieldId
|
||||
|
||||
interface Field<T> {
|
||||
val id: FieldId
|
||||
var data: Data<T>
|
||||
|
||||
data class Data<Data>(
|
||||
val value: Data,
|
||||
val isUserInput: Boolean,
|
||||
)
|
||||
}
|
||||
|
||||
typealias FieldData = Pair<FieldId, Field.Data<*>>
|
||||
|
||||
interface DataField<T> : Field<T> {
|
||||
fun getData(): Pair<FieldId, Field.Data<T>>
|
||||
fun visitDataConverter(dataConverter: FieldDataConverter<*>)
|
||||
}
|
||||
|
||||
abstract class BaseDataField<T>(
|
||||
override val id: FieldId,
|
||||
override var data: Field.Data<T>,
|
||||
) : DataField<T> {
|
||||
|
||||
override fun getData(): Pair<FieldId, Field.Data<T>> = id to data
|
||||
|
||||
override fun visitDataConverter(dataConverter: FieldDataConverter<*>) {
|
||||
dataConverter.visit(getData())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.domain.common.util
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.domain.common.TangemCardTypesResolver
|
||||
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
|
||||
val ScanResponse.cardTypesResolver: CardTypesResolver
|
||||
get() = TangemCardTypesResolver(
|
||||
card = card,
|
||||
productType = productType,
|
||||
walletData = walletData,
|
||||
)
|
||||
|
||||
fun ScanResponse.twinsIsTwinned(): Boolean = card.isTangemTwins && walletData != null && secondTwinPublicKey != null
|
||||
fun ScanResponse.supportsHdWallet(): Boolean = card.settings.isHDWalletAllowed
|
||||
fun ScanResponse.supportsBackup(): Boolean = card.settings.isBackupAllowed
|
||||
|
||||
fun ScanResponse.hasDerivation(blockchain: Blockchain, rawDerivationPath: String): Boolean {
|
||||
return hasDerivation(blockchain, DerivationPath(rawDerivationPath))
|
||||
}
|
||||
|
||||
private fun ScanResponse.hasDerivation(blockchain: Blockchain, derivationPath: DerivationPath): Boolean {
|
||||
val isTestnet = card.isTestCard || blockchain.isTestnet()
|
||||
return when {
|
||||
Blockchain.secp256k1Blockchains(isTestnet).contains(blockchain) -> {
|
||||
hasDerivation(EllipticCurve.Secp256k1, derivationPath)
|
||||
}
|
||||
Blockchain.ed25519OnlyBlockchains(isTestnet).contains(blockchain) -> {
|
||||
hasDerivation(EllipticCurve.Ed25519, derivationPath)
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun ScanResponse.hasDerivation(curve: EllipticCurve, derivationPath: DerivationPath): Boolean {
|
||||
val foundWallet = card.wallets.firstOrNull { it.curve == curve }
|
||||
?: return false
|
||||
val extendedPublicKeysMap = derivedKeys[foundWallet.publicKey.toMapKey()] ?: return false
|
||||
val extendedPublicKey = extendedPublicKeysMap[derivationPath]
|
||||
return extendedPublicKey != null
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.domain.common.util
|
||||
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toHexString
|
||||
|
||||
class UserWalletId(
|
||||
val stringValue: String,
|
||||
) {
|
||||
val value = stringValue.hexToBytes()
|
||||
|
||||
constructor(value: ByteArray?) : this(
|
||||
stringValue = value?.toHexString() ?: "",
|
||||
)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is UserWalletId) return false
|
||||
|
||||
if (stringValue != other.stringValue) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return stringValue.hashCode()
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override fun toString(): String {
|
||||
return with(stringValue) {
|
||||
"UserWalletId(${take(3)}...${takeLast(3)})"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.tangem.domain.common.util
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
class ValueDebouncer<T>(
|
||||
private val initialValue: T,
|
||||
private val debounceDuration: Long = 400,
|
||||
private val onValueChanged: (T) -> Unit,
|
||||
private val onEmitValueReceived: (T) -> Unit = {},
|
||||
) {
|
||||
|
||||
var emittedValue: T = initialValue
|
||||
private set
|
||||
var emitsCountBeforeDebounce: Int = 0
|
||||
private set
|
||||
var debounced: T = initialValue
|
||||
private set
|
||||
|
||||
private val debounceScope: CoroutineScope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
private val flow = MutableStateFlow(debounced)
|
||||
|
||||
init {
|
||||
debounceScope.launch {
|
||||
flow.debounce(debounceDuration)
|
||||
.onEach {
|
||||
debounced = it
|
||||
onValueChanged(it)
|
||||
debounceScope.launch {
|
||||
delay(500)
|
||||
emitsCountBeforeDebounce = 0
|
||||
}
|
||||
}
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fun isDebounced(value: T): Boolean = this.debounced == value
|
||||
|
||||
fun emmit(emmitValue: T) {
|
||||
emitsCountBeforeDebounce++
|
||||
emittedValue = emmitValue
|
||||
onEmitValueReceived.invoke(emmitValue)
|
||||
debounceScope.launch { flow.emit(emmitValue) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.tangem.domain.features.addCustomToken
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class AddCustomTokenService(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val supportedTokenNetworkIds: List<String>,
|
||||
) {
|
||||
|
||||
suspend fun findToken(
|
||||
contractAddress: String,
|
||||
networkId: String? = null,
|
||||
active: Boolean? = null,
|
||||
): List<CoinsResponse.Coin> = withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
tangemTechApi.getCoins(
|
||||
contractAddress = contractAddress,
|
||||
networkIds = selectNetworksForSearch(networkId),
|
||||
active = active,
|
||||
)
|
||||
}
|
||||
.onSuccess { response ->
|
||||
var coinsList = mutableListOf<CoinsResponse.Coin>()
|
||||
response.coins.forEach { coin ->
|
||||
val networksWithTheSameAddress = coin.networks
|
||||
.filter { it.contractAddress != null || it.decimalCount != null }
|
||||
.filter { it.contractAddress?.equals(contractAddress, ignoreCase = true) == true }
|
||||
.filter { supportedTokenNetworkIds.contains(it.networkId) }
|
||||
if (networksWithTheSameAddress.isNotEmpty()) {
|
||||
val newToken = coin.copy(networks = networksWithTheSameAddress)
|
||||
coinsList.add(newToken)
|
||||
}
|
||||
}
|
||||
if (coinsList.size > 1) {
|
||||
// https://tangem.slack.com/archives/GMXC6PP71/p1649672562078679
|
||||
coinsList = mutableListOf(coinsList[0])
|
||||
}
|
||||
return@withContext coinsList
|
||||
}
|
||||
.onFailure {
|
||||
return@withContext emptyList()
|
||||
}
|
||||
|
||||
error("Unreachable code because runCatching must return result")
|
||||
}
|
||||
|
||||
private fun selectNetworksForSearch(networkId: String?): String {
|
||||
return networkId ?: supportedTokenNetworkIds.joinToString(",")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package com.tangem.domain.features.addCustomToken
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.common.form.BaseFieldDataConverter
|
||||
import com.tangem.domain.common.form.FieldId
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed class CustomCurrency(
|
||||
val network: Blockchain,
|
||||
val derivationPath: DerivationPath?,
|
||||
) {
|
||||
|
||||
class CustomBlockchain(
|
||||
network: Blockchain,
|
||||
derivationPath: DerivationPath?,
|
||||
) : CustomCurrency(network, derivationPath) {
|
||||
|
||||
class Converter(
|
||||
private val derivationStyle: DerivationStyle?,
|
||||
) : BaseFieldDataConverter<CustomBlockchain>() {
|
||||
override fun getConvertedData(): CustomBlockchain {
|
||||
val mainNetwork = collectedData[CustomTokenFieldId.Network] as Blockchain
|
||||
val derivationPathNetwork = collectedData[CustomTokenFieldId.DerivationPath] as Blockchain
|
||||
val derivationPath = AddCustomTokenState.getDerivationPath(
|
||||
mainNetwork,
|
||||
derivationPathNetwork,
|
||||
derivationStyle,
|
||||
)
|
||||
return CustomBlockchain(mainNetwork, derivationPath)
|
||||
}
|
||||
|
||||
override fun getIdToCollect(): List<FieldId> =
|
||||
listOf(CustomTokenFieldId.Network, CustomTokenFieldId.DerivationPath)
|
||||
}
|
||||
}
|
||||
|
||||
class CustomToken(
|
||||
val token: Token,
|
||||
network: Blockchain,
|
||||
derivationPath: DerivationPath?,
|
||||
) : CustomCurrency(network, derivationPath) {
|
||||
|
||||
class Converter(
|
||||
private val tokenId: String?,
|
||||
private val derivationStyle: DerivationStyle?,
|
||||
) : BaseFieldDataConverter<CustomToken>() {
|
||||
|
||||
override fun getConvertedData(): CustomToken {
|
||||
val mainNetwork = collectedData[CustomTokenFieldId.Network] as Blockchain
|
||||
val derivationPathNetwork = collectedData[CustomTokenFieldId.DerivationPath] as Blockchain
|
||||
val derivationPath = AddCustomTokenState.getDerivationPath(
|
||||
mainNetwork,
|
||||
derivationPathNetwork,
|
||||
derivationStyle,
|
||||
)
|
||||
|
||||
val token = Token(
|
||||
name = collectedData[CustomTokenFieldId.Name] as String,
|
||||
symbol = collectedData[CustomTokenFieldId.Symbol] as String,
|
||||
contractAddress = collectedData[CustomTokenFieldId.ContractAddress] as String,
|
||||
decimals = (collectedData[CustomTokenFieldId.Decimals] as String).toInt(),
|
||||
id = tokenId,
|
||||
)
|
||||
return CustomToken(
|
||||
token,
|
||||
collectedData[CustomTokenFieldId.Network] as Blockchain,
|
||||
derivationPath,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getIdToCollect(): List<FieldId> = CustomTokenFieldId.values().toList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.domain.features.addCustomToken
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.common.form.BaseDataField
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.domain.common.form.FieldId
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
enum class CustomTokenFieldId : FieldId {
|
||||
ContractAddress,
|
||||
Network,
|
||||
Name,
|
||||
Symbol,
|
||||
Decimals,
|
||||
DerivationPath,
|
||||
}
|
||||
|
||||
data class TokenField(
|
||||
override val id: FieldId,
|
||||
) : BaseDataField<String>(id, Field.Data("", false))
|
||||
|
||||
data class TokenBlockchainField(
|
||||
override val id: FieldId,
|
||||
val itemList: List<Blockchain>,
|
||||
) : BaseDataField<Blockchain>(id, Field.Data(Blockchain.Unknown, false))
|
||||
|
||||
data class TokenDerivationPathField(
|
||||
override val id: FieldId,
|
||||
val itemList: List<Blockchain>,
|
||||
) : BaseDataField<Blockchain>(id, Field.Data(Blockchain.Unknown, false))
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.domain.features.addCustomToken.redux
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import com.tangem.domain.DomainWrapped
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.domain.common.form.FieldId
|
||||
import com.tangem.domain.features.addCustomToken.CustomCurrency
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId
|
||||
import org.rekotlin.Action
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed class AddCustomTokenAction : Action {
|
||||
sealed class Init : AddCustomTokenAction() {
|
||||
data class SetAddedCurrencies(val addedCurrencies: List<DomainWrapped.Currency>) : AddCustomTokenAction()
|
||||
data class SetOnAddTokenCallback(val callback: (CustomCurrency) -> Unit) : AddCustomTokenAction()
|
||||
}
|
||||
|
||||
object OnCreate : AddCustomTokenAction() {
|
||||
data class SetDerivationStyle(val derivationStyle: DerivationStyle?) : AddCustomTokenAction()
|
||||
}
|
||||
|
||||
object OnDestroy : AddCustomTokenAction()
|
||||
|
||||
// from user, ui
|
||||
data class OnTokenContractAddressChanged(val contractAddress: Field.Data<String>) : AddCustomTokenAction()
|
||||
data class OnTokenNetworkChanged(val blockchainNetwork: Field.Data<Blockchain>) : AddCustomTokenAction()
|
||||
data class OnTokenNameChanged(val tokenName: Field.Data<String>) : AddCustomTokenAction()
|
||||
data class OnTokenSymbolChanged(val tokenSymbol: Field.Data<String>) : AddCustomTokenAction()
|
||||
data class OnTokenDerivationPathChanged(
|
||||
val blockchainDerivationPath: Field.Data<Blockchain>,
|
||||
) : AddCustomTokenAction()
|
||||
|
||||
data class OnTokenDecimalsChanged(val tokenDecimals: Field.Data<String>) : AddCustomTokenAction()
|
||||
object OnAddCustomTokenClicked : AddCustomTokenAction()
|
||||
|
||||
data class SetFoundTokenInfo(val foundToken: CoinsResponse.Coin?) : AddCustomTokenAction()
|
||||
|
||||
// form fields
|
||||
data class UpdateForm(val state: AddCustomTokenState) : AddCustomTokenAction()
|
||||
|
||||
sealed class FieldError : AddCustomTokenAction() {
|
||||
data class Add(val id: CustomTokenFieldId, val error: AddCustomTokenError) : FieldError()
|
||||
data class Remove(val id: CustomTokenFieldId) : FieldError()
|
||||
}
|
||||
|
||||
// warnings
|
||||
sealed class Warning : AddCustomTokenAction() {
|
||||
data class Add(val warnings: Set<AddCustomTokenError.Warning>) : Warning()
|
||||
data class Remove(val warnings: Set<AddCustomTokenError.Warning>) : Warning()
|
||||
data class Replace(
|
||||
val remove: Set<AddCustomTokenError.Warning>,
|
||||
val add: Set<AddCustomTokenError.Warning>,
|
||||
) : Warning()
|
||||
}
|
||||
|
||||
// To change the screenState
|
||||
sealed class Screen : AddCustomTokenAction() {
|
||||
data class UpdateTokenFields(val pairs: List<Pair<FieldId, ViewStates.TokenField>>) : Screen()
|
||||
data class UpdateAddButton(val addButton: ViewStates.AddButton) : Screen()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,742 @@
|
|||
package com.tangem.domain.features.addCustomToken.redux
|
||||
|
||||
import android.webkit.ValueCallback
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
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.DomainDialog
|
||||
import com.tangem.domain.DomainWrapped
|
||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
import com.tangem.domain.common.extensions.canHandleToken
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.domain.common.extensions.supportedBlockchains
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.domain.common.form.Form
|
||||
import com.tangem.domain.common.form.TokenContractAddressValidator
|
||||
import com.tangem.domain.common.form.TokenDecimalsValidator
|
||||
import com.tangem.domain.common.form.TokenNameValidator
|
||||
import com.tangem.domain.common.form.TokenNetworkValidator
|
||||
import com.tangem.domain.common.form.TokenSymbolValidator
|
||||
import com.tangem.domain.features.addCustomToken.AddCustomTokenService
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.ContractAddress
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Decimals
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.DerivationPath
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Name
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Network
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Symbol
|
||||
import com.tangem.domain.features.addCustomToken.TokenBlockchainField
|
||||
import com.tangem.domain.features.addCustomToken.TokenDerivationPathField
|
||||
import com.tangem.domain.features.addCustomToken.TokenField
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.FieldError
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.Init
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnAddCustomTokenClicked
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnCreate
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnDestroy
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenContractAddressChanged
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenDecimalsChanged
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenDerivationPathChanged
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenNameChanged
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenNetworkChanged
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenSymbolChanged
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.Screen
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.SetFoundTokenInfo
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.UpdateForm
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.Warning
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState.Companion.createInitialScreenState
|
||||
import com.tangem.domain.redux.BaseStoreHub
|
||||
import com.tangem.domain.redux.DomainState
|
||||
import com.tangem.domain.redux.ReStoreReducer
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.domain.redux.extensions.dispatchOnMain
|
||||
import com.tangem.domain.redux.global.DomainGlobalAction
|
||||
import com.tangem.domain.redux.global.DomainGlobalState
|
||||
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LargeClass")
|
||||
internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomTokenHub") {
|
||||
|
||||
private val hubState: AddCustomTokenState
|
||||
get() = domainStore.state.addCustomTokensState
|
||||
|
||||
override fun getReducer(): ReStoreReducer<AddCustomTokenState> = AddCustomTokenReducer(globalState)
|
||||
|
||||
override fun getHubState(storeState: DomainState): AddCustomTokenState = hubState
|
||||
|
||||
override fun updateStoreState(storeState: DomainState, newHubState: AddCustomTokenState): DomainState {
|
||||
return storeState.copy(addCustomTokensState = newHubState)
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
override suspend fun handleAction(action: Action, storeState: DomainState, cancel: ValueCallback<Action>) {
|
||||
if (action !is AddCustomTokenAction) return
|
||||
|
||||
when (action) {
|
||||
is OnCreate -> {
|
||||
hubState.appSavedCurrencies.guard {
|
||||
return throwUnAppropriateInitialization("addedTokens")
|
||||
}
|
||||
}
|
||||
is OnDestroy -> cancelAll()
|
||||
is OnTokenContractAddressChanged -> {
|
||||
validateContractAddressAndNotify(action.contractAddress.value)
|
||||
}
|
||||
is OnTokenNetworkChanged -> {
|
||||
if (!action.blockchainNetwork.isUserInput) return
|
||||
|
||||
validateContractAddressAndNotify(ContractAddress.getFieldValue())
|
||||
}
|
||||
is OnTokenDerivationPathChanged -> {
|
||||
updateAddButton()
|
||||
}
|
||||
is OnTokenNameChanged, is OnTokenSymbolChanged, is OnTokenDecimalsChanged -> {
|
||||
updateAddButton()
|
||||
}
|
||||
is OnAddCustomTokenClicked -> {
|
||||
val state = hubState
|
||||
val completeData = when {
|
||||
state.getCustomTokenType() == CustomTokenType.Token && state.networkIsSelected() -> {
|
||||
state.gatherUserToken()
|
||||
}
|
||||
state.getCustomTokenType() == CustomTokenType.Blockchain && state.networkIsSelected() -> {
|
||||
state.gatherBlockchain()
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (completeData == null) {
|
||||
// normally it can't be, because the AddButton must be blocked
|
||||
} else {
|
||||
hubScope.launch(Dispatchers.Main) {
|
||||
state.onTokenAddCallback?.invoke(completeData)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun validateContractAddressAndNotify(contractAddress: String) {
|
||||
val error = ContractAddress.validateValue(contractAddress)
|
||||
if (Network.isFilled()) {
|
||||
when (error) {
|
||||
null -> {
|
||||
// valid contract address
|
||||
ContractAddress.removeError()
|
||||
findTokenAndUpdateFields(contractAddress)
|
||||
}
|
||||
AddCustomTokenError.InvalidContractAddress -> {
|
||||
ContractAddress.addError(error)
|
||||
enableDisableTokenDetailFields(hubState.tokensAnyFieldsIsFilled())
|
||||
}
|
||||
AddCustomTokenError.FieldIsEmpty -> {
|
||||
ContractAddress.removeError()
|
||||
clearTokenDetailsFields()
|
||||
disableTokenDetailFields()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
} else {
|
||||
// is default selection (Blockchain.Unknown)
|
||||
when (error) {
|
||||
null -> {
|
||||
// Blockchain.Unknown has always valid contract address
|
||||
ContractAddress.removeError()
|
||||
findTokenAndUpdateFields(contractAddress)
|
||||
}
|
||||
else -> {
|
||||
ContractAddress.removeError()
|
||||
clearTokenDetailsFields()
|
||||
disableTokenDetailFields()
|
||||
}
|
||||
}
|
||||
}
|
||||
updateDerivationPath(Network.getFieldValue())
|
||||
updateWarnings()
|
||||
updateAddButton()
|
||||
}
|
||||
|
||||
private suspend fun findTokenAndUpdateFields(contractAddress: String) {
|
||||
val foundTokens = requestInfoAboutToken(contractAddress)
|
||||
if (foundTokens.isEmpty()) {
|
||||
// token not found - it's completely custom
|
||||
dispatchOnMain(SetFoundTokenInfo(null))
|
||||
enableTokenDetailFields()
|
||||
return
|
||||
}
|
||||
|
||||
// foundToken - contains all info about the token
|
||||
val foundToken = foundTokens[0]
|
||||
dispatchOnMain(SetFoundTokenInfo(foundToken))
|
||||
when {
|
||||
foundToken.networks.isEmpty() -> {
|
||||
Timber.e("Unexpected state -> throw to FB")
|
||||
}
|
||||
foundToken.networks.size == 1 -> {
|
||||
// token with single contract address
|
||||
val singleTokenContract = foundToken.networks[0]
|
||||
fillTokenFields(foundToken, singleTokenContract)
|
||||
disableTokenDetailFields()
|
||||
}
|
||||
else -> {
|
||||
val dialog = DomainDialog.SelectTokenDialog(
|
||||
items = foundToken.networks,
|
||||
networkIdConverter = { networkId ->
|
||||
val blockchain = Blockchain.fromNetworkId(networkId)
|
||||
if (blockchain == null || blockchain == Blockchain.Unknown) {
|
||||
throw AddCustomTokenError.SelectTokeNetworkError(networkId)
|
||||
}
|
||||
hubState.blockchainToName(blockchain) ?: ""
|
||||
},
|
||||
onSelect = { selectedContract ->
|
||||
hubScope.launch {
|
||||
// find how to connect to the upper coroutineContext and dispatch through them
|
||||
fillTokenFields(foundToken, selectedContract)
|
||||
disableTokenDetailFields()
|
||||
}
|
||||
},
|
||||
)
|
||||
dispatchOnMain(DomainGlobalAction.ShowDialog(dialog))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateDerivationPath(blockchainNetwork: Blockchain) {
|
||||
val state = hubState
|
||||
val derivationIsSupportedByNetwork = blockchainNetwork.isEvm() || blockchainNetwork == Blockchain.Unknown
|
||||
|
||||
if (DerivationPath.isFilled() && !derivationIsSupportedByNetwork) {
|
||||
// reset to default
|
||||
val derivationField = DerivationPath.getField<TokenDerivationPathField>()
|
||||
derivationField.data = derivationField.data.copy(
|
||||
value = Blockchain.Unknown,
|
||||
isUserInput = false,
|
||||
)
|
||||
state.setField(derivationField)
|
||||
dispatchOnMain(UpdateForm(hubState))
|
||||
}
|
||||
|
||||
if (state.screenState.derivationPath.isEnabled != derivationIsSupportedByNetwork) {
|
||||
val action = Screen.UpdateTokenFields(
|
||||
listOf(
|
||||
DerivationPath to state.screenState.derivationPath.copy(
|
||||
isEnabled = derivationIsSupportedByNetwork,
|
||||
),
|
||||
),
|
||||
)
|
||||
dispatchOnMain(action)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWarnings() {
|
||||
val state = hubState
|
||||
val warningsAdd = mutableSetOf<AddCustomTokenError.Warning>()
|
||||
val warningsRemove = mutableSetOf<AddCustomTokenError.Warning>()
|
||||
|
||||
val tokenIsSupported = tokenIsSupported(Network.getFieldValue())
|
||||
val alreadyAdded = isPersistIntoAppSavedTokensList()
|
||||
when (state.getCustomTokenType()) {
|
||||
CustomTokenType.Blockchain -> {
|
||||
warningsRemove.add(UnsupportedSolanaToken)
|
||||
if (alreadyAdded) {
|
||||
warningsAdd.add(TokenAlreadyAdded)
|
||||
} else {
|
||||
warningsRemove.add(TokenAlreadyAdded)
|
||||
}
|
||||
if (state.derivationPathIsSelected()) {
|
||||
warningsAdd.add(PotentialScamToken)
|
||||
} else {
|
||||
warningsRemove.add(PotentialScamToken)
|
||||
}
|
||||
}
|
||||
CustomTokenType.Token -> {
|
||||
if (tokenIsSupported) {
|
||||
warningsRemove.add(UnsupportedSolanaToken)
|
||||
} else {
|
||||
val error = ContractAddress.validateValue(ContractAddress.getFieldValue())
|
||||
when (error) {
|
||||
AddCustomTokenError.FieldIsEmpty -> warningsRemove.add(UnsupportedSolanaToken)
|
||||
else -> {
|
||||
warningsAdd.add(UnsupportedSolanaToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isPersistIntoAppSavedTokensList()) {
|
||||
warningsAdd.add(TokenAlreadyAdded)
|
||||
} else {
|
||||
warningsRemove.add(TokenAlreadyAdded)
|
||||
}
|
||||
|
||||
if (state.foundToken == null) {
|
||||
if (state.tokensAnyFieldsIsFilled()) {
|
||||
warningsAdd.add(PotentialScamToken)
|
||||
} else {
|
||||
warningsRemove.add(PotentialScamToken)
|
||||
}
|
||||
} else {
|
||||
if (state.foundToken.active) {
|
||||
warningsRemove.add(PotentialScamToken)
|
||||
} else {
|
||||
warningsAdd.add(PotentialScamToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchOnMain(
|
||||
Warning.Replace(
|
||||
remove = warningsRemove,
|
||||
add = warningsAdd,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun updateAddButton() {
|
||||
if (isPersistIntoAppSavedTokensList()) {
|
||||
TokenAlreadyAdded.add()
|
||||
disableAddButton()
|
||||
return
|
||||
} else {
|
||||
TokenAlreadyAdded.remove()
|
||||
}
|
||||
|
||||
val state = hubState
|
||||
when {
|
||||
// token
|
||||
state.tokensFieldsIsFilled() && state.networkIsSelected() -> {
|
||||
val error = ContractAddress.validateValue(ContractAddress.getFieldValue<String>())
|
||||
val tokenIsSupported = tokenIsSupported(Network.getFieldValue())
|
||||
enableDisableAddButton(tokenIsSupported && error == null)
|
||||
}
|
||||
// token
|
||||
state.tokensAnyFieldsIsFilled() -> {
|
||||
disableAddButton()
|
||||
}
|
||||
// blockchain
|
||||
else -> {
|
||||
if (state.networkIsSelected()) {
|
||||
val alreadyAdded = isBlockchainPersistIntoAppSavedTokensList()
|
||||
if (alreadyAdded) {
|
||||
disableAddButton()
|
||||
} else {
|
||||
enableAddButton()
|
||||
}
|
||||
} else {
|
||||
disableAddButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private suspend fun requestInfoAboutToken(contractAddress: String): List<CoinsResponse.Coin> {
|
||||
val tangemTechServiceManager = requireNotNull(hubState.tangemTechServiceManager)
|
||||
dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = true))))
|
||||
|
||||
val field = hubState.getField<TokenBlockchainField>(Network)
|
||||
val selectedNetworkId: String? = field.data.value.let {
|
||||
if (it == Blockchain.Unknown) null else it
|
||||
}?.toNetworkId()
|
||||
|
||||
// simulate loading effect. It would be better if the delay would only run if tokenManager.checkAddress()
|
||||
// got the result faster than 500ms and the delay would only be the difference between them.
|
||||
delay(500)
|
||||
|
||||
val result = tangemTechServiceManager.findToken(contractAddress, selectedNetworkId)
|
||||
|
||||
dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = false))))
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* These are helper functions.
|
||||
*/
|
||||
private fun isPersistIntoAppSavedTokensList(): Boolean = when (hubState.getCustomTokenType()) {
|
||||
CustomTokenType.Blockchain -> isBlockchainPersistIntoAppSavedTokensList()
|
||||
CustomTokenType.Token -> isTokenPersistIntoAppSavedTokensList()
|
||||
}
|
||||
|
||||
private fun isTokenPersistIntoAppSavedTokensList(
|
||||
tokenId: String? = hubState.foundToken?.id,
|
||||
tokenContractAddress: String = ContractAddress.getFieldValue(),
|
||||
tokenNetworkId: String = Network.getFieldValue<Blockchain>().toNetworkId(),
|
||||
selectedDerivation: Blockchain = DerivationPath.getFieldValue(),
|
||||
): Boolean {
|
||||
val savedCurrencies = hubState.appSavedCurrencies ?: return false
|
||||
|
||||
val derivationPath = getDerivationPathFromSelectedBlockchain(selectedDerivation)
|
||||
savedCurrencies.forEach { wrappedCurrency ->
|
||||
when (wrappedCurrency) {
|
||||
is DomainWrapped.Currency.Blockchain -> {}
|
||||
is DomainWrapped.Currency.Token -> {
|
||||
val sameId = tokenId == wrappedCurrency.token.id
|
||||
val sameAddress = tokenContractAddress == wrappedCurrency.token.contractAddress
|
||||
val sameBlockchain = Blockchain.fromNetworkId(tokenNetworkId) == wrappedCurrency.blockchain
|
||||
val sameDerivationPath = derivationPath?.rawPath == wrappedCurrency.derivationPath
|
||||
@Suppress("ComplexCondition")
|
||||
if (sameId && sameAddress && sameBlockchain && sameDerivationPath) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun isBlockchainPersistIntoAppSavedTokensList(
|
||||
selectedNetwork: Blockchain = Network.getFieldValue(),
|
||||
selectedDerivation: Blockchain = DerivationPath.getFieldValue(),
|
||||
): Boolean {
|
||||
val state = hubState
|
||||
val savedCurrencies = state.appSavedCurrencies ?: return false
|
||||
|
||||
val derivationPath = getDerivationPathFromSelectedBlockchain(selectedDerivation)
|
||||
savedCurrencies.forEach { wrappedCurrency ->
|
||||
when (wrappedCurrency) {
|
||||
is DomainWrapped.Currency.Blockchain -> {
|
||||
val isSameBlockchain = selectedNetwork == wrappedCurrency.blockchain
|
||||
val isSameDerivationPath = derivationPath?.rawPath == wrappedCurrency.derivationPath
|
||||
if (isSameBlockchain && isSameDerivationPath) return true
|
||||
}
|
||||
is DomainWrapped.Currency.Token -> {}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun getDerivationPathFromSelectedBlockchain(
|
||||
selectedDerivationBlockchain: Blockchain,
|
||||
): com.tangem.crypto.hdWallet.DerivationPath? = AddCustomTokenState.getDerivationPath(
|
||||
mainNetwork = Network.getFieldValue(),
|
||||
derivationNetwork = selectedDerivationBlockchain,
|
||||
derivationStyle = hubState.cardDerivationStyle,
|
||||
)
|
||||
|
||||
private suspend fun fillTokenFields(token: CoinsResponse.Coin, coinNetwork: CoinsResponse.Coin.Network) {
|
||||
val blockchain = Blockchain.fromNetworkId(coinNetwork.networkId) ?: Blockchain.Unknown
|
||||
Network.setFieldValue(Field.Data(blockchain, false))
|
||||
Name.setFieldValue(Field.Data(token.name, false))
|
||||
Symbol.setFieldValue(Field.Data(token.symbol, false))
|
||||
Decimals.setFieldValue(Field.Data(coinNetwork.decimalCount.toString(), false))
|
||||
dispatchOnMain(UpdateForm(hubState))
|
||||
}
|
||||
|
||||
private suspend fun clearTokenDetailsFields() {
|
||||
Name.setFieldValue(Field.Data("", false))
|
||||
Symbol.setFieldValue(Field.Data("", false))
|
||||
Decimals.setFieldValue(Field.Data("", false))
|
||||
dispatchOnMain(UpdateForm(hubState))
|
||||
}
|
||||
|
||||
private suspend fun enableTokenDetailFields() {
|
||||
enableDisableTokenDetailFields(true)
|
||||
}
|
||||
|
||||
private suspend fun disableTokenDetailFields() {
|
||||
enableDisableTokenDetailFields(false)
|
||||
}
|
||||
|
||||
private suspend fun enableDisableTokenDetailFields(isEnabled: Boolean = true) {
|
||||
val state = hubState
|
||||
val action = Screen.UpdateTokenFields(
|
||||
listOf(
|
||||
Name to state.screenState.name.copy(isEnabled = isEnabled),
|
||||
Symbol to state.screenState.symbol.copy(isEnabled = isEnabled),
|
||||
Decimals to state.screenState.decimals.copy(isEnabled = isEnabled),
|
||||
),
|
||||
)
|
||||
dispatchOnMain(action)
|
||||
}
|
||||
|
||||
private suspend fun enableAddButton() {
|
||||
enableDisableAddButton(true)
|
||||
}
|
||||
|
||||
private suspend fun disableAddButton() {
|
||||
enableDisableAddButton(false)
|
||||
}
|
||||
|
||||
private suspend fun enableDisableAddButton(isEnabled: Boolean) {
|
||||
dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(isEnabled)))
|
||||
}
|
||||
|
||||
private fun tokenIsSupported(blockchain: Blockchain): Boolean = when (blockchain) {
|
||||
Blockchain.Unknown -> true
|
||||
else -> globalState.scanResponse?.card?.canHandleToken(blockchain) ?: false
|
||||
}
|
||||
|
||||
@Throws
|
||||
private fun throwUnAppropriateInitialization(objName: String) {
|
||||
throw AddCustomTokenError.UnAppropriateInitialization(
|
||||
"AddCustomTokenHub",
|
||||
"$objName must be not NULL",
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun CustomTokenFieldId.addError(error: AddCustomTokenError) {
|
||||
dispatchOnMain(FieldError.Add(this, error))
|
||||
}
|
||||
|
||||
private suspend fun CustomTokenFieldId.removeError() {
|
||||
dispatchOnMain(FieldError.Remove(this))
|
||||
}
|
||||
|
||||
private inline fun <reified T> CustomTokenFieldId.getField(): T {
|
||||
val state = hubState
|
||||
val value = when (this) {
|
||||
ContractAddress -> state.getField<TokenField>(this)
|
||||
Network -> state.getField<TokenBlockchainField>(this)
|
||||
Name -> state.getField<TokenField>(this)
|
||||
Symbol -> state.getField<TokenField>(this)
|
||||
Decimals -> state.getField<TokenField>(this)
|
||||
DerivationPath -> state.getField<TokenDerivationPathField>(this)
|
||||
}
|
||||
return value as T
|
||||
}
|
||||
|
||||
private inline fun <reified T> CustomTokenFieldId.getFieldValue(): T {
|
||||
val value = when (this) {
|
||||
ContractAddress -> getField<TokenField>().data.value
|
||||
Network -> getField<TokenBlockchainField>().data.value
|
||||
Name -> getField<TokenField>().data.value
|
||||
Symbol -> getField<TokenField>().data.value
|
||||
Decimals -> getField<TokenField>().data.value
|
||||
DerivationPath -> getField<TokenDerivationPathField>().data.value
|
||||
}
|
||||
return value as T
|
||||
}
|
||||
|
||||
private fun CustomTokenFieldId.setFieldValue(fieldData: Field.Data<*>) {
|
||||
when (this) {
|
||||
ContractAddress -> getField<TokenField>().data = fieldData as Field.Data<String>
|
||||
Network -> getField<TokenBlockchainField>().data = fieldData as Field.Data<Blockchain>
|
||||
Name -> getField<TokenField>().data = fieldData as Field.Data<String>
|
||||
Symbol -> getField<TokenField>().data = fieldData as Field.Data<String>
|
||||
Decimals -> getField<TokenField>().data = fieldData as Field.Data<String>
|
||||
DerivationPath -> getField<TokenDerivationPathField>().data = fieldData as Field.Data<Blockchain>
|
||||
}
|
||||
}
|
||||
|
||||
private fun CustomTokenFieldId.validateValue(value: Any): AddCustomTokenError? {
|
||||
val state = hubState
|
||||
val contractAddressValidator: TokenContractAddressValidator = state.getValidator(ContractAddress)
|
||||
val nameValidator: TokenNameValidator = state.getValidator(Name)
|
||||
val symbolValidator: TokenSymbolValidator = state.getValidator(Symbol)
|
||||
val decimalsValidator: TokenDecimalsValidator = state.getValidator(Decimals)
|
||||
val networkValidator: TokenNetworkValidator = state.getValidator(Network)
|
||||
return when (this) {
|
||||
ContractAddress -> {
|
||||
contractAddressValidator.nextValidationFor(Network.getFieldValue())
|
||||
contractAddressValidator.validate(value as String)
|
||||
}
|
||||
Network, DerivationPath -> networkValidator.validate(value as Blockchain)
|
||||
Name -> nameValidator.validate(value as String)
|
||||
Symbol -> symbolValidator.validate(value as String)
|
||||
Decimals -> decimalsValidator.validate(value as String)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CustomTokenFieldId.isFilled(): Boolean {
|
||||
return when (this) {
|
||||
ContractAddress -> getFieldValue<String>().isNotEmpty()
|
||||
Network -> getFieldValue<Blockchain>() != Blockchain.Unknown
|
||||
Name -> getFieldValue<String>().isNotEmpty()
|
||||
Symbol -> getFieldValue<String>().isNotEmpty()
|
||||
Decimals -> getFieldValue<String>().isNotEmpty()
|
||||
DerivationPath -> getFieldValue<Blockchain>() != Blockchain.Unknown
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun AddCustomTokenError.Warning.add() {
|
||||
dispatchOnMain(Warning.Add(setOf(this)))
|
||||
}
|
||||
|
||||
private suspend fun AddCustomTokenError.Warning.remove() {
|
||||
dispatchOnMain(Warning.Remove(setOf(this)))
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
private class AddCustomTokenReducer(
|
||||
private val globalState: DomainGlobalState,
|
||||
) : ReStoreReducer<AddCustomTokenState> {
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override fun reduceAction(action: Action, state: AddCustomTokenState): AddCustomTokenState {
|
||||
return when (action) {
|
||||
is Init.SetAddedCurrencies -> {
|
||||
state.copy(appSavedCurrencies = action.addedCurrencies)
|
||||
}
|
||||
is Init.SetOnAddTokenCallback -> {
|
||||
state.copy(onTokenAddCallback = action.callback)
|
||||
}
|
||||
is OnCreate -> {
|
||||
val card = requireNotNull(globalState.scanResponse?.card)
|
||||
val supportedTokenNetworkIds = card.supportedBlockchains()
|
||||
.filter { it.canHandleTokens() }
|
||||
.map { it.toNetworkId() }
|
||||
val tangemTechServiceManager = AddCustomTokenService(
|
||||
tangemTechApi = globalState.networkServices.tangemTechService.api,
|
||||
dispatchers = AppCoroutineDispatcherProvider(),
|
||||
supportedTokenNetworkIds = supportedTokenNetworkIds,
|
||||
)
|
||||
val form = Form(AddCustomTokenState.createFormFields(card, CustomTokenType.Blockchain))
|
||||
state.copy(
|
||||
cardDerivationStyle = card.derivationStyle,
|
||||
form = form,
|
||||
tangemTechServiceManager = tangemTechServiceManager,
|
||||
screenState = createInitialScreenState(card.settings.isHDWalletAllowed),
|
||||
)
|
||||
}
|
||||
is OnDestroy -> {
|
||||
val card = requireNotNull(globalState.scanResponse?.card)
|
||||
state.reset(card)
|
||||
}
|
||||
is UpdateForm -> {
|
||||
updateFormState(action.state)
|
||||
}
|
||||
is OnTokenContractAddressChanged -> {
|
||||
val field: TokenField = state.getField(ContractAddress)
|
||||
field.data = action.contractAddress
|
||||
updateFormState(state)
|
||||
}
|
||||
is OnTokenNetworkChanged -> {
|
||||
val field: TokenBlockchainField = state.getField(Network)
|
||||
field.data = action.blockchainNetwork
|
||||
updateFormState(state)
|
||||
}
|
||||
is OnTokenNameChanged -> {
|
||||
val field: TokenField = state.getField(Name)
|
||||
field.data = action.tokenName
|
||||
updateFormState(state)
|
||||
}
|
||||
is OnTokenSymbolChanged -> {
|
||||
val field: TokenField = state.getField(Symbol)
|
||||
field.data = action.tokenSymbol
|
||||
updateFormState(state)
|
||||
}
|
||||
is OnTokenDecimalsChanged -> {
|
||||
val field: TokenField = state.getField(Decimals)
|
||||
field.data = action.tokenDecimals
|
||||
updateFormState(state)
|
||||
}
|
||||
is OnTokenDerivationPathChanged -> {
|
||||
val field: TokenDerivationPathField = state.getField(DerivationPath)
|
||||
field.data = action.blockchainDerivationPath
|
||||
updateFormState(state)
|
||||
}
|
||||
is FieldError.Add -> {
|
||||
val newMap = state.formErrors.toMutableMap().apply { this[action.id] = action.error }
|
||||
state.copy(formErrors = newMap)
|
||||
}
|
||||
is FieldError.Remove -> {
|
||||
val newMap = state.formErrors.toMutableMap().apply { remove(action.id) }
|
||||
state.copy(formErrors = newMap)
|
||||
}
|
||||
is SetFoundTokenInfo -> {
|
||||
state.copy(foundToken = action.foundToken)
|
||||
}
|
||||
is Warning.Add -> {
|
||||
val newList = state.warnings.toMutableSet().apply { addAll(action.warnings) }
|
||||
state.copy(warnings = newList.toSet())
|
||||
}
|
||||
is Warning.Remove -> {
|
||||
val newList = state.warnings.toMutableSet().apply { removeAll(action.warnings) }
|
||||
state.copy(warnings = newList.toSet())
|
||||
}
|
||||
is Warning.Replace -> {
|
||||
val newList = state.warnings.toMutableSet().apply {
|
||||
removeAll(action.remove)
|
||||
addAll(action.add)
|
||||
}
|
||||
state.copy(warnings = newList.toSet())
|
||||
}
|
||||
is Screen.UpdateTokenFields -> {
|
||||
var newScreenState = state.screenState
|
||||
action.pairs.forEach {
|
||||
newScreenState = when (it.first) {
|
||||
ContractAddress -> {
|
||||
if (state.screenState.contractAddressField == it.second) {
|
||||
newScreenState
|
||||
} else {
|
||||
newScreenState.copy(contractAddressField = it.second)
|
||||
}
|
||||
}
|
||||
Network -> {
|
||||
if (state.screenState.network == it.second) {
|
||||
newScreenState
|
||||
} else {
|
||||
newScreenState.copy(network = it.second)
|
||||
}
|
||||
}
|
||||
Name -> {
|
||||
if (state.screenState.name == it.second) {
|
||||
newScreenState
|
||||
} else {
|
||||
newScreenState.copy(name = it.second)
|
||||
}
|
||||
}
|
||||
Symbol -> {
|
||||
if (state.screenState.symbol == it.second) {
|
||||
newScreenState
|
||||
} else {
|
||||
newScreenState.copy(symbol = it.second)
|
||||
}
|
||||
}
|
||||
Decimals -> {
|
||||
if (state.screenState.decimals == it.second) {
|
||||
newScreenState
|
||||
} else {
|
||||
newScreenState.copy(decimals = it.second)
|
||||
}
|
||||
}
|
||||
DerivationPath -> {
|
||||
if (state.screenState.derivationPath == it.second) {
|
||||
newScreenState
|
||||
} else {
|
||||
newScreenState.copy(derivationPath = it.second)
|
||||
}
|
||||
}
|
||||
else -> newScreenState
|
||||
}
|
||||
}
|
||||
if (state.screenState == newScreenState) {
|
||||
state
|
||||
} else {
|
||||
state.copy(screenState = newScreenState)
|
||||
}
|
||||
}
|
||||
is Screen.UpdateAddButton -> {
|
||||
val newScreenState = if (state.screenState.addButton == action.addButton) {
|
||||
state.screenState
|
||||
} else {
|
||||
state.screenState.copy(addButton = action.addButton)
|
||||
}
|
||||
if (newScreenState == state.screenState) {
|
||||
state
|
||||
} else {
|
||||
state.copy(screenState = newScreenState)
|
||||
}
|
||||
}
|
||||
else -> state
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFormState(state: AddCustomTokenState): AddCustomTokenState {
|
||||
return state.copy(form = Form(state.form.fieldList))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,327 @@
|
|||
package com.tangem.domain.features.addCustomToken.redux
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import com.tangem.domain.DomainWrapped
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.common.extensions.isSupportedInApp
|
||||
import com.tangem.domain.common.extensions.supportedBlockchains
|
||||
import com.tangem.domain.common.extensions.supportedTokens
|
||||
import com.tangem.domain.common.form.CustomTokenValidator
|
||||
import com.tangem.domain.common.form.DataField
|
||||
import com.tangem.domain.common.form.FieldDataConverter
|
||||
import com.tangem.domain.common.form.FieldId
|
||||
import com.tangem.domain.common.form.FieldToJsonConverter
|
||||
import com.tangem.domain.common.form.Form
|
||||
import com.tangem.domain.common.form.StringIsEmptyValidator
|
||||
import com.tangem.domain.common.form.StringIsNotEmptyValidator
|
||||
import com.tangem.domain.common.form.TokenContractAddressValidator
|
||||
import com.tangem.domain.common.form.TokenDecimalsValidator
|
||||
import com.tangem.domain.common.form.TokenNameValidator
|
||||
import com.tangem.domain.common.form.TokenNetworkValidator
|
||||
import com.tangem.domain.common.form.TokenSymbolValidator
|
||||
import com.tangem.domain.features.addCustomToken.AddCustomTokenService
|
||||
import com.tangem.domain.features.addCustomToken.CustomCurrency
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.ContractAddress
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Decimals
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.DerivationPath
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Name
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Network
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Symbol
|
||||
import com.tangem.domain.features.addCustomToken.TokenBlockchainField
|
||||
import com.tangem.domain.features.addCustomToken.TokenDerivationPathField
|
||||
import com.tangem.domain.features.addCustomToken.TokenField
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.redux.DomainState
|
||||
import com.tangem.domain.redux.state.StringActionStateConverter
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.StateType
|
||||
|
||||
data class AddCustomTokenState(
|
||||
val appSavedCurrencies: List<DomainWrapped.Currency>? = null,
|
||||
val onTokenAddCallback: ((CustomCurrency) -> Unit)? = null,
|
||||
val cardDerivationStyle: DerivationStyle? = null,
|
||||
val form: Form = Form(listOf()),
|
||||
val formValidators: Map<CustomTokenFieldId, CustomTokenValidator<out Any>> = createFormValidators(),
|
||||
val formErrors: Map<CustomTokenFieldId, AddCustomTokenError> = emptyMap(),
|
||||
val foundToken: CoinsResponse.Coin? = null,
|
||||
val warnings: Set<AddCustomTokenError.Warning> = emptySet(),
|
||||
val screenState: ScreenState = createInitialScreenState(),
|
||||
val tangemTechServiceManager: AddCustomTokenService? = null,
|
||||
) : StateType {
|
||||
|
||||
inline fun <reified T> getField(id: FieldId): T = form.getField(id) as T
|
||||
|
||||
fun setField(field: DataField<*>) {
|
||||
form.setField(field)
|
||||
}
|
||||
|
||||
inline fun <reified T> getValidator(id: FieldId): T = formValidators[id] as T
|
||||
|
||||
fun getError(id: FieldId): AddCustomTokenError? = formErrors[id]
|
||||
|
||||
fun hasError(id: FieldId): Boolean = formErrors[id] != null
|
||||
|
||||
inline fun <reified T> visitDataConverter(converter: FieldDataConverter<T>): T {
|
||||
form.visitDataConverter(converter)
|
||||
return converter.getConvertedData()
|
||||
}
|
||||
|
||||
fun blockchainToName(blockchain: Blockchain, isDerivationPath: Boolean = false): String? {
|
||||
return when {
|
||||
isDerivationPath -> blockchain.derivationPath(DerivationStyle.LEGACY)?.rawPath
|
||||
else -> {
|
||||
when (blockchain) {
|
||||
Blockchain.Unknown -> null
|
||||
else -> blockchain.fullName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// except network
|
||||
fun tokensFieldsIsFilled(): Boolean {
|
||||
val idsToCheck = listOf(ContractAddress, Name, Symbol, Decimals)
|
||||
val fieldsToCheck = form.fieldList.filter { idsToCheck.contains(it.id) }
|
||||
val validator = StringIsNotEmptyValidator()
|
||||
fieldsToCheck.forEach { field ->
|
||||
val error = validator.validate(field.data.value?.toString())
|
||||
if (error != null) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// except network
|
||||
fun tokensAnyFieldsIsFilled(): Boolean {
|
||||
val idsToCheck = listOf(ContractAddress, Name, Symbol, Decimals)
|
||||
val fieldsToCheck = form.fieldList.filter { idsToCheck.contains(it.id) }
|
||||
val validator = StringIsEmptyValidator()
|
||||
val errorsList = fieldsToCheck.mapNotNull { field ->
|
||||
validator.validate(field.data.value?.toString())
|
||||
}
|
||||
return errorsList.isNotEmpty()
|
||||
}
|
||||
|
||||
fun networkIsSelected(): Boolean {
|
||||
val network = getField<TokenBlockchainField>(Network)
|
||||
return network.data.value != Blockchain.Unknown
|
||||
}
|
||||
|
||||
fun derivationPathIsSelected(): Boolean {
|
||||
val network = getField<TokenDerivationPathField>(DerivationPath)
|
||||
return network.data.value != Blockchain.Unknown
|
||||
}
|
||||
|
||||
fun getCustomTokenType(): CustomTokenType = when {
|
||||
tokensAnyFieldsIsFilled() || tokensFieldsIsFilled() -> CustomTokenType.Token
|
||||
else -> CustomTokenType.Blockchain
|
||||
}
|
||||
|
||||
fun gatherUserToken(): CustomCurrency.CustomToken? = try {
|
||||
getToken()
|
||||
} catch (ex: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
fun gatherBlockchain(): CustomCurrency.CustomBlockchain? = try {
|
||||
getBlockchain()
|
||||
} catch (ex: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
fun reset(card: CardDTO): AddCustomTokenState {
|
||||
return this.copy(
|
||||
appSavedCurrencies = null,
|
||||
onTokenAddCallback = null,
|
||||
cardDerivationStyle = null,
|
||||
form = Form(createFormFields(card, CustomTokenType.Blockchain)),
|
||||
formErrors = emptyMap(),
|
||||
foundToken = null,
|
||||
warnings = emptySet(),
|
||||
screenState = createInitialScreenState(card.settings.isHDWalletAllowed),
|
||||
tangemTechServiceManager = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getToken(): CustomCurrency.CustomToken {
|
||||
return CustomCurrency.CustomToken.Converter(foundToken?.id, cardDerivationStyle)
|
||||
.apply { visitDataConverter(this) }
|
||||
.getConvertedData()
|
||||
}
|
||||
|
||||
private fun getBlockchain(): CustomCurrency.CustomBlockchain {
|
||||
return CustomCurrency.CustomBlockchain.Converter(cardDerivationStyle)
|
||||
.apply { visitDataConverter(this) }
|
||||
.getConvertedData()
|
||||
}
|
||||
|
||||
fun getNetworks(card: CardDTO, type: CustomTokenType): List<Blockchain> {
|
||||
return getNetworksList(card, type)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* If an user select derivation path (derivationNetwork) as Blockchain.Unknown,
|
||||
* then we should use a blockchain from the mainNetwork to determine a DerivationPath
|
||||
*/
|
||||
internal fun getDerivationPath(
|
||||
mainNetwork: Blockchain,
|
||||
derivationNetwork: Blockchain,
|
||||
derivationStyle: DerivationStyle?,
|
||||
): com.tangem.crypto.hdWallet.DerivationPath? {
|
||||
// If we allow user to select derivations, we need to provide different derivations
|
||||
// (Legacy style derivations).
|
||||
// But the mainNetwork derivation depends on whether a user has a card
|
||||
// with legacy derivations or new style derivations.
|
||||
val derivationStyleToUse = if (derivationNetwork == Blockchain.Unknown) {
|
||||
derivationStyle
|
||||
} else {
|
||||
DerivationStyle.LEGACY
|
||||
}
|
||||
return when (derivationNetwork) {
|
||||
Blockchain.Unknown -> mainNetwork
|
||||
else -> derivationNetwork
|
||||
}.derivationPath(derivationStyleToUse)
|
||||
}
|
||||
|
||||
internal fun createFormFields(card: CardDTO, type: CustomTokenType): List<DataField<*>> {
|
||||
return listOf(
|
||||
TokenField(ContractAddress),
|
||||
TokenBlockchainField(Network, getNetworksList(card, type)),
|
||||
TokenField(Name),
|
||||
TokenField(Symbol),
|
||||
TokenField(Decimals),
|
||||
TokenDerivationPathField(DerivationPath, getSupportedDerivations(card)),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Serves to determine the networks (blockchains & tokens) that can be selected by Form.Networks.
|
||||
* Blockchain.Unknown - is the default selection
|
||||
*/
|
||||
private fun getNetworksList(card: CardDTO, type: CustomTokenType): List<Blockchain> {
|
||||
val evmBlockchains = Blockchain.values()
|
||||
.filter { it.isEvm() }
|
||||
.filter { card.isTestCard == it.isTestnet() }
|
||||
|
||||
val additionalBlockchains = listOf(
|
||||
Blockchain.Binance,
|
||||
Blockchain.BinanceTestnet,
|
||||
Blockchain.Solana,
|
||||
Blockchain.SolanaTestnet,
|
||||
Blockchain.Tron,
|
||||
Blockchain.TronTestnet,
|
||||
)
|
||||
|
||||
val supportedByCard = when (type) {
|
||||
CustomTokenType.Blockchain -> card.supportedBlockchains()
|
||||
CustomTokenType.Token -> card.supportedTokens()
|
||||
}
|
||||
val typedNetworksList = (evmBlockchains + additionalBlockchains)
|
||||
.filter { supportedByCard.contains(it) }
|
||||
.toMutableList()
|
||||
|
||||
val default = Blockchain.Unknown
|
||||
typedNetworksList.add(0, default)
|
||||
|
||||
return typedNetworksList.sortByName()
|
||||
}
|
||||
|
||||
private fun createFormValidators(): Map<CustomTokenFieldId, CustomTokenValidator<out Any>> {
|
||||
return mapOf(
|
||||
ContractAddress to TokenContractAddressValidator(),
|
||||
Network to TokenNetworkValidator(),
|
||||
Name to TokenNameValidator(),
|
||||
Symbol to TokenSymbolValidator(),
|
||||
Decimals to TokenDecimalsValidator(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSupportedDerivations(card: CardDTO): List<Blockchain> {
|
||||
val evmBlockchains = Blockchain.values()
|
||||
.filter { card.isTestCard == it.isTestnet() && it.isEvm() }
|
||||
.filter { it.isSupportedInApp() }
|
||||
|
||||
return (listOf(Blockchain.Unknown) + evmBlockchains).sortByName()
|
||||
}
|
||||
|
||||
internal fun createInitialScreenState(showDerivationPathField: Boolean = false): ScreenState {
|
||||
return ScreenState(
|
||||
contractAddressField = ViewStates.TokenField(),
|
||||
network = ViewStates.TokenField(),
|
||||
name = ViewStates.TokenField(isEnabled = false),
|
||||
symbol = ViewStates.TokenField(isEnabled = false),
|
||||
decimals = ViewStates.TokenField(isEnabled = false),
|
||||
derivationPath = ViewStates.TokenField(isVisible = showDerivationPathField),
|
||||
addButton = ViewStates.AddButton(isEnabled = false),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Converter : StringActionStateConverter<DomainState> {
|
||||
private val jsonConverter: MoshiJsonConverter = MoshiJsonConverter.INSTANCE
|
||||
private var builder: StringBuilder = StringBuilder()
|
||||
|
||||
override fun convert(action: Action, stateHolder: DomainState): String? {
|
||||
val action = action as? AddCustomTokenAction ?: return null
|
||||
|
||||
val state = stateHolder.addCustomTokensState
|
||||
val fieldConverter =
|
||||
FieldToJsonConverter(
|
||||
listOf(
|
||||
ContractAddress,
|
||||
Network,
|
||||
Name,
|
||||
Symbol,
|
||||
Decimals,
|
||||
DerivationPath,
|
||||
),
|
||||
jsonConverter,
|
||||
)
|
||||
state.visitDataConverter(fieldConverter)
|
||||
val errors = state.formErrors.map {
|
||||
"${it.key}: ${it.value::class.java.simpleName}"
|
||||
}
|
||||
val warnings = state.warnings.map { it::class.java.simpleName }
|
||||
|
||||
printAction(action, state)
|
||||
printStateValue("fields", fieldConverter.getConvertedData())
|
||||
printStateValue("fieldErrors", toJson(errors))
|
||||
printStateValue("warnings", toJson(warnings))
|
||||
printStateValue("screenState", toJson(state.screenState))
|
||||
printMessage("------------------------------------------------------")
|
||||
|
||||
val printed = builder.toString()
|
||||
builder = StringBuilder()
|
||||
|
||||
return printed
|
||||
}
|
||||
|
||||
private fun printStateValue(name: String, value: String) {
|
||||
printMessage("$name: $value")
|
||||
}
|
||||
|
||||
private fun printAction(action: AddCustomTokenAction, state: AddCustomTokenState) {
|
||||
printMessage("action: $action, state: ${state::class.java.simpleName}")
|
||||
}
|
||||
|
||||
private fun toJson(value: Any): String {
|
||||
return jsonConverter.prettyPrint(value)
|
||||
}
|
||||
|
||||
private fun printMessage(message: String) {
|
||||
builder.append("$message\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Blockchain>.sortByName(): List<Blockchain> = this.sortedBy { it.fullName }
|
||||
|
||||
enum class CustomTokenType {
|
||||
Token, Blockchain
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.domain.features.addCustomToken.redux
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
// describes state the screen, except the form fields
|
||||
data class ScreenState(
|
||||
val contractAddressField: ViewStates.TokenField,
|
||||
val network: ViewStates.TokenField,
|
||||
val name: ViewStates.TokenField,
|
||||
val symbol: ViewStates.TokenField,
|
||||
val decimals: ViewStates.TokenField,
|
||||
val derivationPath: ViewStates.TokenField,
|
||||
val addButton: ViewStates.AddButton,
|
||||
)
|
||||
|
||||
sealed class ViewStates {
|
||||
data class TokenField(
|
||||
val isLoading: Boolean = false,
|
||||
val isEnabled: Boolean = true,
|
||||
val isVisible: Boolean = true,
|
||||
) : ViewStates()
|
||||
|
||||
data class AddButton(
|
||||
val isEnabled: Boolean = true,
|
||||
) : ViewStates()
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.domain.redux
|
||||
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||
import com.tangem.domain.redux.global.DomainGlobalState
|
||||
import org.rekotlin.StateType
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
data class DomainState(
|
||||
val globalState: DomainGlobalState = DomainGlobalState(),
|
||||
val addCustomTokensState: AddCustomTokenState = AddCustomTokenState(),
|
||||
) : StateType
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.domain.redux
|
||||
|
||||
import com.tangem.domain.DomainLayer
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenHub
|
||||
import com.tangem.domain.redux.global.DomainGlobalHub
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Store
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
private val RE_STORE_HUBS: List<ReStoreHub<DomainState, *>> = listOf(
|
||||
DomainGlobalHub(),
|
||||
AddCustomTokenHub(),
|
||||
)
|
||||
|
||||
val domainStore = Store(
|
||||
state = DomainState(),
|
||||
middleware = RE_STORE_HUBS.map { it.getMiddleware() },
|
||||
reducer = { action, state -> reduce(action, state) },
|
||||
)
|
||||
|
||||
private fun reduce(action: Action, domainState: DomainState?): DomainState {
|
||||
requireNotNull(domainState)
|
||||
|
||||
// we can examine the store state after each change by reducer
|
||||
var assembleReducedDomainState: DomainState = domainState
|
||||
val reducedStatesByAction = mutableListOf<Pair<Action, DomainState>>()
|
||||
|
||||
RE_STORE_HUBS.forEach {
|
||||
val reducedState = it.reduce(action, assembleReducedDomainState)
|
||||
|
||||
assembleReducedDomainState = if (reducedState != assembleReducedDomainState) {
|
||||
reducedStatesByAction.add(action to assembleReducedDomainState)
|
||||
reducedState
|
||||
} else {
|
||||
assembleReducedDomainState
|
||||
}
|
||||
}
|
||||
DomainLayer.actionStateLogger.log(reducedStatesByAction)
|
||||
|
||||
return assembleReducedDomainState
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
package com.tangem.domain.redux
|
||||
|
||||
import android.webkit.ValueCallback
|
||||
import com.tangem.domain.redux.global.DomainGlobalState
|
||||
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.DispatchFunction
|
||||
import org.rekotlin.Middleware
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
* ReStoreHub's should not store the <StoreState> or the <State>, because this can lead to destabilization of
|
||||
* a state behavior.
|
||||
* All ReStoreHub's must be marked as internal
|
||||
*/
|
||||
internal interface ReStoreHub<StoreState, State> {
|
||||
fun getMiddleware(): Middleware<StoreState>
|
||||
fun reduce(action: Action, domainState: StoreState): StoreState
|
||||
}
|
||||
|
||||
internal interface ReStoreReducer<State> {
|
||||
fun reduceAction(action: Action, state: State): State
|
||||
}
|
||||
|
||||
/**
|
||||
* ReStoreHub is the entry point for actions. It processes it through middleware and reducer.
|
||||
* Actions handled by ReStoreHub go into coroutine scope, which can be canceled while the action is being processed.
|
||||
* All action went from the middleware must be dispatched through ReStoreHub.dispatchOnMain(Actions) to prevent
|
||||
* concurrent modification in the Store
|
||||
* Only the changed hub State will change its state in the DomainState
|
||||
* Do not implement other states like as DomainGlobalState. Because it can dilute the responsibility of
|
||||
* states.
|
||||
* @param name - name of the Hub
|
||||
* @param dispatcher - main coroutine dispatcher for actions
|
||||
* @property globalState - state witch produce accessibility to global variables
|
||||
*/
|
||||
internal abstract class BaseStoreHub<State>(
|
||||
private val name: String,
|
||||
private val dispatcher: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher(),
|
||||
) : ReStoreHub<DomainState, State> {
|
||||
|
||||
val globalState: DomainGlobalState
|
||||
get() = domainStore.state.globalState
|
||||
|
||||
val hubScope = CoroutineScope(
|
||||
Job() + dispatcher + CoroutineName(name) + FeatureCoroutineExceptionHandler.create(name),
|
||||
)
|
||||
|
||||
private val actionsAndJobs = mutableMapOf<Action, Job>()
|
||||
|
||||
override fun getMiddleware(): Middleware<DomainState> {
|
||||
return { dispatch, state ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
handle(state, action, dispatch)
|
||||
next(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches new coroutine and stores the action with it's coroutine job. (Coroutine can be cancelled
|
||||
* through invoking the cancelActionJob() function inside a middleware).
|
||||
* Removes the action when job is completed.
|
||||
*/
|
||||
protected open fun handle(storeStateHolder: () -> DomainState?, action: Action, dispatch: DispatchFunction) {
|
||||
val storeState = storeStateHolder()
|
||||
?: throw UnsupportedOperationException("StoreState for the $name can't be NULL")
|
||||
|
||||
hubScope.launch {
|
||||
actionsAndJobs[action] = this.coroutineContext.job
|
||||
actionsAndJobs[action]?.invokeOnCompletion { actionsAndJobs.remove(action) }
|
||||
|
||||
handleAction(action, storeState) {
|
||||
actionsAndJobs.remove(it)?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce the action and check it. If the action hasn't updated the hubState, then it doesn't need to update
|
||||
* storeState
|
||||
*/
|
||||
override fun reduce(action: Action, domainState: DomainState): DomainState {
|
||||
val hubOldState = getHubState(domainState)
|
||||
val hubNewState = getReducer().reduceAction(action, hubOldState)
|
||||
return if (hubOldState === hubNewState) {
|
||||
domainState
|
||||
} else {
|
||||
updateStoreState(domainState, hubNewState)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun cancelAll() {
|
||||
actionsAndJobs.forEach { (_, job) -> job.cancel() }
|
||||
}
|
||||
|
||||
protected abstract suspend fun handleAction(action: Action, storeState: DomainState, cancel: ValueCallback<Action>)
|
||||
protected abstract fun getReducer(): ReStoreReducer<State>
|
||||
|
||||
protected abstract fun getHubState(storeState: DomainState): State
|
||||
protected abstract fun updateStoreState(storeState: DomainState, newHubState: State): DomainState
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.domain.redux.extensions
|
||||
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import org.rekotlin.Action
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal suspend inline fun dispatchOnMain(vararg actions: Action) {
|
||||
withMainContext { actions.forEach { domainStore.dispatch(it) } }
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.domain.redux.global
|
||||
|
||||
import com.tangem.domain.DomainDialog
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import org.rekotlin.Action
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
// TODO: refactoring: is alias for the GlobalAction
|
||||
sealed class DomainGlobalAction : Action {
|
||||
data class SaveScanNoteResponse(val scanResponse: ScanResponse) : DomainGlobalAction()
|
||||
data class ShowDialog(val stateDialog: DomainDialog?) : DomainGlobalAction()
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.domain.redux.global
|
||||
|
||||
import android.webkit.ValueCallback
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.datasource.utils.RequestHeader
|
||||
import com.tangem.domain.redux.BaseStoreHub
|
||||
import com.tangem.domain.redux.DomainState
|
||||
import com.tangem.domain.redux.ReStoreReducer
|
||||
import com.tangem.lib.auth.AuthProvider
|
||||
import org.rekotlin.Action
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
// TODO: refactoring: is alias for the GlobalMiddleware and the GlobalReducer
|
||||
internal class DomainGlobalHub : BaseStoreHub<DomainGlobalState>("DomainGlobalHub") {
|
||||
|
||||
override fun getHubState(storeState: DomainState): DomainGlobalState {
|
||||
return storeState.globalState
|
||||
}
|
||||
|
||||
override fun updateStoreState(storeState: DomainState, newHubState: DomainGlobalState): DomainState {
|
||||
return storeState.copy(globalState = newHubState)
|
||||
}
|
||||
|
||||
override suspend fun handleAction(action: Action, storeState: DomainState, cancel: ValueCallback<Action>) {
|
||||
if (action !is DomainGlobalAction) return
|
||||
}
|
||||
|
||||
override fun getReducer(): ReStoreReducer<DomainGlobalState> = DomainGlobalReducer()
|
||||
}
|
||||
|
||||
private class DomainGlobalReducer : ReStoreReducer<DomainGlobalState> {
|
||||
|
||||
override fun reduceAction(action: Action, state: DomainGlobalState): DomainGlobalState {
|
||||
return when (action) {
|
||||
is DomainGlobalAction.SaveScanNoteResponse -> {
|
||||
val card = action.scanResponse.card
|
||||
state.networkServices.tangemTechService.addAuthenticationHeader(
|
||||
RequestHeader.AuthenticationHeader(
|
||||
object : AuthProvider {
|
||||
override fun getCardPublicKey(): String = card.cardPublicKey.toHexString()
|
||||
override fun getCardId(): String = card.cardId
|
||||
},
|
||||
),
|
||||
)
|
||||
state.copy(scanResponse = action.scanResponse)
|
||||
}
|
||||
is DomainGlobalAction.ShowDialog -> {
|
||||
state.copy(dialog = action.stateDialog)
|
||||
}
|
||||
else -> state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.domain.redux.global
|
||||
|
||||
import com.tangem.datasource.api.paymentology.PaymentologyApiService
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechService
|
||||
import com.tangem.domain.DomainDialog
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
data class DomainGlobalState(
|
||||
// there is a part of mirrors from the GlobalState.
|
||||
// It updates on GlobalAction.SaveScanNoteResponse -> DomainGlobalAction.SaveScanNoteResponse(scanResponse)
|
||||
val scanResponse: ScanResponse? = null,
|
||||
//
|
||||
val networkServices: NetworkServices = NetworkServices(),
|
||||
val dialog: DomainDialog? = null,
|
||||
)
|
||||
|
||||
data class NetworkServices(
|
||||
val tangemTechService: TangemTechService = TangemTechService,
|
||||
val paymentologyService: PaymentologyApiService = PaymentologyApiService,
|
||||
)
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.domain.redux.state
|
||||
|
||||
import com.tangem.domain.redux.DomainState
|
||||
import org.rekotlin.Action
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface StringStateConverter<StateHolder> {
|
||||
fun convert(stateHolder: StateHolder): String
|
||||
}
|
||||
|
||||
interface StringActionStateConverter<StateHolder> {
|
||||
fun convert(action: Action, stateHolder: StateHolder): String?
|
||||
}
|
||||
|
||||
class ActionStateConvertersFactory {
|
||||
private val stateConverters = mutableMapOf<Class<out Action>, StringActionStateConverter<DomainState>>()
|
||||
|
||||
fun addConverter(classOfAction: Class<out Action>, converter: StringActionStateConverter<DomainState>) {
|
||||
stateConverters[classOfAction] = converter
|
||||
}
|
||||
|
||||
fun getConverter(action: Action): StringActionStateConverter<DomainState>? {
|
||||
val converter = stateConverters.firstNotNullOfOrNull { (classOfAction, converter) ->
|
||||
if (classOfAction.isAssignableFrom(action::class.java)) {
|
||||
converter
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} ?: return null
|
||||
|
||||
return converter
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.domain.redux.state
|
||||
|
||||
import com.tangem.domain.features.BuildConfig
|
||||
import com.tangem.domain.redux.DomainState
|
||||
import org.rekotlin.Action
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
* Use it only in debug mode!
|
||||
*/
|
||||
internal interface ActionStateLogger {
|
||||
fun log(reducedSates: List<Pair<Action, DomainState>>)
|
||||
}
|
||||
|
||||
internal class ActionStateLoggerImpl : ActionStateLogger {
|
||||
|
||||
val actionStateConvertersFactory = ActionStateConvertersFactory()
|
||||
|
||||
override fun log(reducedSates: List<Pair<Action, DomainState>>) {
|
||||
if (!BuildConfig.LOG_ENABLED) return
|
||||
|
||||
logStates(reducedSates)
|
||||
}
|
||||
|
||||
private fun logStates(reducedSates: List<Pair<Action, DomainState>>) {
|
||||
reducedSates.forEach { (action, domainState) ->
|
||||
val messageToPrint = actionStateConvertersFactory.getConverter(action)
|
||||
?.convert(action, domainState)
|
||||
?: return@forEach
|
||||
|
||||
Timber.d(messageToPrint)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue