Updated on 2026-08-14
This commit is contained in:
commit
f0d03e3648
286 changed files with 6353 additions and 5898 deletions
|
|
@ -26,5 +26,10 @@ enum class AppThemeMode {
|
|||
* The default [AppThemeMode].
|
||||
*/
|
||||
val DEFAULT: AppThemeMode = FORCE_LIGHT
|
||||
|
||||
/**
|
||||
* List of available [AppThemeMode]s.
|
||||
* */
|
||||
val available: List<AppThemeMode> = values().toList()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
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())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
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
|
||||
|
|
@ -36,32 +35,14 @@ sealed class AddCustomTokenError(
|
|||
) {
|
||||
|
||||
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]",
|
||||
)
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package com.tangem.domain
|
||||
|
||||
/**
|
||||
[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
|
||||
@Deprecated("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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.domain.common
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.domain.demo.DemoConfig
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
|
|
@ -71,6 +70,4 @@ object TapWorkarounds {
|
|||
fun isStart2CoinIssuer(cardIssuer: String?): Boolean {
|
||||
return cardIssuer?.lowercase(Locale.US) == START_2_COIN_ISSUER
|
||||
}
|
||||
|
||||
fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId] ?: null
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
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> {
|
||||
private 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(),
|
||||
private val jsonConverter: MoshiJsonConverter,
|
||||
) : BaseFieldDataConverter<String>() {
|
||||
|
||||
override fun getConvertedData(): String = jsonConverter.toJson(collectedData, " ")
|
||||
|
||||
override fun getIdToCollect(): List<FieldId> = fieldsToConvert
|
||||
}
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
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? {
|
||||
return if (data.isNullOrEmpty()) null else AddCustomTokenError.FieldIsNotEmpty
|
||||
}
|
||||
}
|
||||
|
||||
class StringIsNotEmptyValidator : CustomTokenValidator<String> {
|
||||
override fun validate(data: String?): AddCustomTokenError? {
|
||||
return if (data.isNullOrEmpty()) 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? {
|
||||
return when {
|
||||
data.isNullOrEmpty() -> AddCustomTokenError.FieldIsEmpty
|
||||
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? {
|
||||
return 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)
|
||||
}
|
||||
|
||||
class TokenDecimalsValidator : CustomTokenValidator<String> {
|
||||
override fun validate(data: String?): AddCustomTokenError? {
|
||||
val decimal = data?.toIntOrNull() ?: return AddCustomTokenError.FieldIsEmpty
|
||||
|
||||
return if (decimal > INVALID_DECIMALS_COUNT) AddCustomTokenError.InvalidDecimalsCount else null
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val INVALID_DECIMALS_COUNT = 30
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
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,11 @@
|
|||
package com.tangem.domain.exchange
|
||||
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
|
||||
/**
|
||||
* Manager that holds info about available actions as Sell and Buy
|
||||
*/
|
||||
interface RampStateManager {
|
||||
fun availableForBuy(cryptoCurrency: CryptoCurrency): Boolean
|
||||
fun availableForSell(cryptoCurrency: CryptoCurrency): Boolean
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
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?): List<CoinsResponse.Coin> {
|
||||
return withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
tangemTechApi.getCoins(
|
||||
contractAddress = contractAddress,
|
||||
networkIds = selectNetworksForSearch(networkId),
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
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])
|
||||
}
|
||||
coinsList
|
||||
},
|
||||
onFailure = { emptyList() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectNetworksForSearch(networkId: String?): String {
|
||||
return networkId ?: supportedTokenNetworkIds.joinToString(",")
|
||||
}
|
||||
}
|
||||
|
|
@ -2,11 +2,7 @@ package com.tangem.domain.features.addCustomToken
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.derivation.DerivationStyle
|
||||
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]
|
||||
|
|
@ -16,65 +12,16 @@ sealed class CustomCurrency(
|
|||
val derivationPath: DerivationPath?,
|
||||
) {
|
||||
|
||||
@Deprecated("It will be removed in next releases")
|
||||
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)
|
||||
}
|
||||
}
|
||||
) : CustomCurrency(network, derivationPath)
|
||||
|
||||
@Deprecated("It will be removed in next releases")
|
||||
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()
|
||||
}
|
||||
}
|
||||
) : CustomCurrency(network, derivationPath)
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
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))
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
package com.tangem.domain.features.addCustomToken.redux
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
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()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,720 +0,0 @@
|
|||
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.*
|
||||
import com.tangem.domain.DomainDialog
|
||||
import com.tangem.domain.DomainWrapped
|
||||
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.*
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.features.addCustomToken.*
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.*
|
||||
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 -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
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 validationResult = ContractAddress.validateValue(ContractAddress.getFieldValue())
|
||||
if (validationResult == 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()) {
|
||||
if (isBlockchainPersistIntoAppSavedTokensList()) disableAddButton() else enableAddButton()
|
||||
} else {
|
||||
disableAddButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(timeMillis = 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(): Boolean {
|
||||
val savedCurrencies = hubState.appSavedCurrencies ?: return false
|
||||
|
||||
val tokenId = hubState.foundToken?.id
|
||||
val tokenContractAddress = ContractAddress.getFieldValue<String>()
|
||||
val tokenNetworkId = Network.getFieldValue<Blockchain>().toNetworkId()
|
||||
val selectedDerivation = DerivationPath.getFieldValue<Blockchain>()
|
||||
|
||||
val derivationPath = getDerivationPathFromSelectedBlockchain(selectedDerivation)
|
||||
savedCurrencies.forEach { wrappedCurrency ->
|
||||
when (wrappedCurrency) {
|
||||
is DomainWrapped.Currency.Blockchain -> Unit
|
||||
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(): Boolean {
|
||||
val savedCurrencies = hubState.appSavedCurrencies ?: return false
|
||||
val selectedNetwork = Network.getFieldValue<Blockchain>()
|
||||
val selectedDerivation = DerivationPath.getFieldValue<Blockchain>()
|
||||
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 -> Unit
|
||||
}
|
||||
}
|
||||
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) {
|
||||
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 -> {
|
||||
val scanResponse = globalState.scanResponse
|
||||
scanResponse?.card?.canHandleToken(
|
||||
blockchain = blockchain,
|
||||
cardTypesResolver = scanResponse.cardTypesResolver,
|
||||
) ?: 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? {
|
||||
return when (this) {
|
||||
ContractAddress -> {
|
||||
val contractAddressValidator: TokenContractAddressValidator = hubState.getValidator(ContractAddress)
|
||||
contractAddressValidator.nextValidationFor(Network.getFieldValue())
|
||||
contractAddressValidator.validate(value as String)
|
||||
}
|
||||
Network, DerivationPath -> {
|
||||
hubState.getValidator<TokenNetworkValidator>(Network).validate(value as Blockchain)
|
||||
}
|
||||
Name -> {
|
||||
hubState.getValidator<TokenNameValidator>(Name).validate(value as String)
|
||||
}
|
||||
Symbol -> {
|
||||
hubState.getValidator<TokenSymbolValidator>(Symbol).validate(value as String)
|
||||
}
|
||||
Decimals -> {
|
||||
hubState.getValidator<TokenDecimalsValidator>(Decimals).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 scanResponse = requireNotNull(globalState.scanResponse)
|
||||
val card = globalState.scanResponse.card
|
||||
val supportedTokenNetworkIds = card.supportedBlockchains(scanResponse.cardTypesResolver)
|
||||
.filter(Blockchain::canHandleTokens)
|
||||
.map(Blockchain::toNetworkId)
|
||||
|
||||
val tangemTechServiceManager = AddCustomTokenService(
|
||||
tangemTechApi = globalState.networkServices.tangemTechService.api,
|
||||
dispatchers = AppCoroutineDispatcherProvider(),
|
||||
supportedTokenNetworkIds = supportedTokenNetworkIds,
|
||||
)
|
||||
|
||||
state.copy(
|
||||
cardDerivationStyle = globalState.scanResponse.derivationStyleProvider.getDerivationStyle(),
|
||||
form = Form(
|
||||
AddCustomTokenState.createFormFields(
|
||||
cardTypesResolver = globalState.scanResponse.cardTypesResolver,
|
||||
card = card,
|
||||
type = CustomTokenType.Blockchain,
|
||||
),
|
||||
),
|
||||
tangemTechServiceManager = tangemTechServiceManager,
|
||||
screenState = createInitialScreenState(card.settings.isHDWalletAllowed),
|
||||
)
|
||||
}
|
||||
is OnDestroy -> {
|
||||
val scanResponse = requireNotNull(globalState.scanResponse)
|
||||
val card = scanResponse.card
|
||||
state.reset(scanResponse.cardTypesResolver, 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))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,311 +0,0 @@
|
|||
package com.tangem.domain.features.addCustomToken.redux
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.derivation.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.CardTypesResolver
|
||||
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.*
|
||||
import com.tangem.domain.features.addCustomToken.*
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
|
||||
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]
|
||||
|
||||
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 {
|
||||
return if (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(cardTypesResolver: CardTypesResolver, card: CardDTO): AddCustomTokenState {
|
||||
return this.copy(
|
||||
appSavedCurrencies = null,
|
||||
onTokenAddCallback = null,
|
||||
cardDerivationStyle = null,
|
||||
form = Form(createFormFields(cardTypesResolver, 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()
|
||||
}
|
||||
|
||||
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(
|
||||
cardTypesResolver: CardTypesResolver,
|
||||
card: CardDTO,
|
||||
type: CustomTokenType,
|
||||
): List<DataField<*>> {
|
||||
return listOf(
|
||||
TokenField(ContractAddress),
|
||||
TokenBlockchainField(Network, getNetworksList(cardTypesResolver, 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(
|
||||
cardTypesResolver: CardTypesResolver,
|
||||
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(cardTypesResolver)
|
||||
CustomTokenType.Token -> card.supportedTokens(cardTypesResolver)
|
||||
}
|
||||
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? {
|
||||
if (action !is 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
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
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()
|
||||
}
|
||||
|
|
@ -1,13 +1,9 @@
|
|||
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
|
||||
data class DomainState(val globalState: DomainGlobalState = DomainGlobalState()) : StateType
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
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
|
||||
|
|
@ -9,10 +7,7 @@ import org.rekotlin.Store
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
private val RE_STORE_HUBS: List<ReStoreHub<DomainState, *>> = listOf(
|
||||
DomainGlobalHub(),
|
||||
AddCustomTokenHub(),
|
||||
)
|
||||
private val RE_STORE_HUBS: List<ReStoreHub<DomainState, *>> = listOf(DomainGlobalHub())
|
||||
|
||||
val domainStore = Store(
|
||||
state = DomainState(),
|
||||
|
|
@ -37,7 +32,6 @@ private fun reduce(action: Action, domainState: DomainState?): DomainState {
|
|||
assembleReducedDomainState
|
||||
}
|
||||
}
|
||||
DomainLayer.actionStateLogger.log(reducedStatesByAction)
|
||||
|
||||
return assembleReducedDomainState
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.domain.redux.global
|
||||
|
||||
import com.tangem.domain.DomainDialog
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import org.rekotlin.Action
|
||||
|
||||
|
|
@ -10,5 +9,4 @@ import org.rekotlin.Action
|
|||
// 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()
|
||||
}
|
||||
|
|
@ -47,9 +47,6 @@ private class DomainGlobalReducer : ReStoreReducer<DomainGlobalState> {
|
|||
)
|
||||
state.copy(scanResponse = action.scanResponse)
|
||||
}
|
||||
is DomainGlobalAction.ShowDialog -> {
|
||||
state.copy(dialog = action.stateDialog)
|
||||
}
|
||||
else -> state
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ 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
|
||||
|
||||
/**
|
||||
|
|
@ -14,7 +13,6 @@ data class DomainGlobalState(
|
|||
val scanResponse: ScanResponse? = null,
|
||||
//
|
||||
val networkServices: NetworkServices = NetworkServices(),
|
||||
val dialog: DomainDialog? = null,
|
||||
)
|
||||
|
||||
data class NetworkServices(
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.redux.state
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface StringStateConverter<StateHolder> {
|
||||
fun convert(stateHolder: StateHolder): String
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import org.rekotlin.Action
|
||||
|
||||
sealed interface TokensAction : Action {
|
||||
|
||||
/** Single way to pass data to the screen */
|
||||
sealed interface SetArgs : TokensAction {
|
||||
object ManageAccess : SetArgs
|
||||
object ReadAccess : SetArgs
|
||||
}
|
||||
|
||||
@Deprecated("Action is used for saving data by old way. It will be removed after deleting of legacy wallet screen")
|
||||
data class LegacySaveChanges(
|
||||
val currentTokens: List<TokenWithBlockchain>,
|
||||
val currentBlockchains: List<Blockchain>,
|
||||
val changedTokens: List<TokenWithBlockchain>,
|
||||
val changedBlockchains: List<Blockchain>,
|
||||
val scanResponse: ScanResponse,
|
||||
) : TokensAction
|
||||
|
||||
data class NewSaveChanges(
|
||||
val currentTokens: List<CryptoCurrency.Token>,
|
||||
val currentCoins: List<CryptoCurrency.Coin>,
|
||||
val changedTokens: List<CryptoCurrency.Token>,
|
||||
val changedCoins: List<CryptoCurrency.Coin>,
|
||||
val userWallet: UserWallet,
|
||||
) : TokensAction
|
||||
}
|
||||
|
||||
data class TokenWithBlockchain(val token: Token, val blockchain: Blockchain)
|
||||
|
|
@ -1,28 +1,17 @@
|
|||
package com.tangem.domain.walletmanager.model
|
||||
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
|
||||
// TODO: [REDACTED_JIRA] move to txhistory module
|
||||
sealed class CryptoCurrencyTransaction {
|
||||
|
||||
abstract val amount: BigDecimal
|
||||
abstract val fromAddress: String?
|
||||
abstract val toAddress: String?
|
||||
abstract val sentAt: DateTime
|
||||
abstract val txHistoryItem: TxHistoryItem
|
||||
|
||||
data class Coin(
|
||||
override val amount: BigDecimal,
|
||||
override val fromAddress: String?,
|
||||
override val toAddress: String?,
|
||||
override val sentAt: DateTime,
|
||||
) : CryptoCurrencyTransaction()
|
||||
data class Coin(override val txHistoryItem: TxHistoryItem) : CryptoCurrencyTransaction()
|
||||
|
||||
data class Token(
|
||||
val tokenId: String?,
|
||||
val tokenContractAddress: String,
|
||||
override val amount: BigDecimal,
|
||||
override val fromAddress: String?,
|
||||
override val toAddress: String?,
|
||||
override val sentAt: DateTime,
|
||||
override val txHistoryItem: TxHistoryItem,
|
||||
) : CryptoCurrencyTransaction()
|
||||
}
|
||||
|
|
@ -3,37 +3,37 @@ package com.tangem.domain.walletmanager.utils
|
|||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.domain.common.extensions.amountToCreateAccount
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount
|
||||
import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction
|
||||
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import org.joda.time.Instant
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import java.util.Calendar
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
internal class UpdateWalletManagerResultFactory {
|
||||
|
||||
fun getResult(walletManager: WalletManager): UpdateWalletManagerResult.Verified {
|
||||
val wallet = walletManager.wallet
|
||||
val addresses = getAvailableAddresses(wallet.addresses)
|
||||
|
||||
return UpdateWalletManagerResult.Verified(
|
||||
defaultAddress = wallet.address,
|
||||
addresses = getAvailableAddresses(wallet.addresses),
|
||||
addresses = addresses,
|
||||
currenciesAmounts = getTokensAmounts(wallet.amounts.values.toSet()),
|
||||
currentTransactions = getCurrentTransactions(wallet.recentTransactions.toSet()),
|
||||
currentTransactions = getCurrentTransactions(addresses, wallet.recentTransactions.toSet()),
|
||||
)
|
||||
}
|
||||
|
||||
fun getDemoResult(walletManager: WalletManager, demoAmount: Amount): UpdateWalletManagerResult.Verified {
|
||||
val wallet = walletManager.wallet
|
||||
val addresses = getAvailableAddresses(wallet.addresses)
|
||||
|
||||
return UpdateWalletManagerResult.Verified(
|
||||
defaultAddress = wallet.address,
|
||||
addresses = getAvailableAddresses(wallet.addresses),
|
||||
addresses = addresses,
|
||||
currenciesAmounts = getDemoTokensAmounts(demoAmount, walletManager.cardTokens),
|
||||
currentTransactions = getCurrentTransactions(wallet.recentTransactions.toSet()),
|
||||
currentTransactions = getCurrentTransactions(addresses, wallet.recentTransactions.toSet()),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -70,12 +70,15 @@ internal class UpdateWalletManagerResultFactory {
|
|||
}
|
||||
}
|
||||
|
||||
private fun getCurrentTransactions(recentTransactions: Set<TransactionData>): Set<CryptoCurrencyTransaction> {
|
||||
private fun getCurrentTransactions(
|
||||
walletAddresses: Set<String>,
|
||||
recentTransactions: Set<TransactionData>,
|
||||
): Set<CryptoCurrencyTransaction> {
|
||||
val unconfirmedTransactions = recentTransactions.filter {
|
||||
it.status == TransactionStatus.Unconfirmed
|
||||
}
|
||||
|
||||
return unconfirmedTransactions.mapNotNullTo(hashSetOf(), ::createCurrencyTransaction)
|
||||
return unconfirmedTransactions.mapNotNullTo(hashSetOf()) { createCurrencyTransaction(walletAddresses, it) }
|
||||
}
|
||||
|
||||
private fun createCurrencyAmount(amount: Amount): CryptoCurrencyAmount? {
|
||||
|
|
@ -92,31 +95,78 @@ internal class UpdateWalletManagerResultFactory {
|
|||
}
|
||||
}
|
||||
|
||||
private fun createCurrencyTransaction(data: TransactionData): CryptoCurrencyTransaction? {
|
||||
val fromAddress = takeAddressIfNotUnknown(data.sourceAddress)
|
||||
val toAddress = takeAddressIfNotUnknown(data.destinationAddress)
|
||||
val amount = getTransactionAmountValue(data.amount) ?: return null
|
||||
val sentAt = getTransactionSentTime(data.date) ?: return null
|
||||
|
||||
private fun createCurrencyTransaction(
|
||||
walletAddresses: Set<String>,
|
||||
data: TransactionData,
|
||||
): CryptoCurrencyTransaction? {
|
||||
return when (val type = data.amount.type) {
|
||||
is AmountType.Coin -> CryptoCurrencyTransaction.Coin(
|
||||
amount = amount,
|
||||
fromAddress = fromAddress,
|
||||
toAddress = toAddress,
|
||||
sentAt = sentAt,
|
||||
)
|
||||
is AmountType.Token -> CryptoCurrencyTransaction.Token(
|
||||
tokenId = type.token.id,
|
||||
tokenContractAddress = type.token.contractAddress,
|
||||
amount = amount,
|
||||
fromAddress = fromAddress,
|
||||
toAddress = toAddress,
|
||||
sentAt = sentAt,
|
||||
)
|
||||
is AmountType.Coin -> {
|
||||
val txHistoryItem = createTxHistoryItem(walletAddresses, data) ?: return null
|
||||
CryptoCurrencyTransaction.Coin(txHistoryItem)
|
||||
}
|
||||
is AmountType.Token -> {
|
||||
val txHistoryItem = createTxHistoryItem(walletAddresses, data) ?: return null
|
||||
CryptoCurrencyTransaction.Token(
|
||||
tokenId = type.token.id,
|
||||
tokenContractAddress = type.token.contractAddress,
|
||||
txHistoryItem = txHistoryItem,
|
||||
)
|
||||
}
|
||||
is AmountType.Reserve -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTxHistoryItem(walletAddresses: Set<String>, data: TransactionData): TxHistoryItem? {
|
||||
val direction = extractDirection(walletAddresses, data) ?: run {
|
||||
Timber.w("Can not determine address for $data")
|
||||
return null
|
||||
}
|
||||
val hash = data.hash ?: return null
|
||||
val millis = data.date?.timeInMillis ?: return null
|
||||
val amount = getTransactionAmountValue(data.amount) ?: return null
|
||||
|
||||
return TxHistoryItem(
|
||||
txHash = hash,
|
||||
timestampInMillis = TimeUnit.SECONDS.toMillis(millis),
|
||||
direction = direction,
|
||||
status = when (data.status) {
|
||||
TransactionStatus.Confirmed -> TxHistoryItem.TxStatus.Confirmed
|
||||
TransactionStatus.Unconfirmed -> TxHistoryItem.TxStatus.Unconfirmed
|
||||
},
|
||||
type = TxHistoryItem.TransactionType.Transfer,
|
||||
amount = amount,
|
||||
)
|
||||
}
|
||||
|
||||
private fun extractDirection(
|
||||
walletAddresses: Set<String>,
|
||||
data: TransactionData,
|
||||
): TxHistoryItem.TransactionDirection? {
|
||||
val fromAddress = data.sourceAddress
|
||||
val toAddress = data.destinationAddress
|
||||
|
||||
return when {
|
||||
toAddress in walletAddresses -> {
|
||||
TxHistoryItem.TransactionDirection.Incoming(TxHistoryItem.Address.Single(fromAddress))
|
||||
}
|
||||
fromAddress in walletAddresses -> {
|
||||
TxHistoryItem.TransactionDirection.Outgoing(TxHistoryItem.Address.Single(toAddress))
|
||||
}
|
||||
else -> {
|
||||
Timber.e(
|
||||
"""
|
||||
Unable to find transaction direction
|
||||
|- To address: ${data.destinationAddress}
|
||||
|- From address: ${data.sourceAddress}
|
||||
|- Network addresses: $walletAddresses
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAvailableAddresses(addresses: Set<Address>): Set<String> {
|
||||
return addresses.mapTo(hashSetOf()) { it.value }
|
||||
}
|
||||
|
|
@ -140,24 +190,4 @@ internal class UpdateWalletManagerResultFactory {
|
|||
|
||||
return value
|
||||
}
|
||||
|
||||
private fun getTransactionSentTime(date: Calendar?): DateTime? {
|
||||
if (date == null) {
|
||||
Timber.e("Transaction date must not be null")
|
||||
return null
|
||||
}
|
||||
|
||||
val instant = Instant.ofEpochMilli(date.timeInMillis)
|
||||
val timeZone = DateTimeZone.forTimeZone(date.timeZone)
|
||||
|
||||
return instant.toDateTime(timeZone)
|
||||
}
|
||||
|
||||
private fun takeAddressIfNotUnknown(address: String): String? {
|
||||
return address.takeIf { it.isNotBlank() && it != UNKNOWN_TRANSACTION_ADDRESS }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val UNKNOWN_TRANSACTION_ADDRESS = "unknown"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,21 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.domain.tokens"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Project - Domain */
|
||||
implementation(projects.domain.core)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
|
||||
|
|
|
|||
|
|
@ -93,6 +93,8 @@ sealed class CryptoCurrency : Serializable {
|
|||
|
||||
val rawCurrencyId: String? = (suffix as? Suffix.RawID)?.rawId
|
||||
|
||||
val rawNetworkId: String = networkId.value
|
||||
|
||||
/**
|
||||
* Represents the different types of prefixes that can be associated with a cryptocurrency ID.
|
||||
* These prefixes can help in quickly categorizing the type of cryptocurrency.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
package com.tangem.domain.tokens.models.remove
|
||||
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
|
||||
sealed class RemoveCurrencyError : Throwable() {
|
||||
data class HasLinkedTokens(val currency: CryptoCurrency) : RemoveCurrencyError()
|
||||
data class DataError(override val cause: Throwable) : RemoveCurrencyError()
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.domain.tokens.error.GetCurrenciesError
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
class GetCryptoCurrenciesUseCase(private val currenciesRepository: CurrenciesRepository) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
refresh: Boolean = false,
|
||||
): Either<GetCurrenciesError, List<CryptoCurrency>> {
|
||||
return either {
|
||||
catch(
|
||||
block = { currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId, refresh) },
|
||||
catch = { raise(GetCurrenciesError.DataError(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +1,84 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.tokens.model.TokenActionsState
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
|
||||
/**
|
||||
* Use case to determine which TokenActions are available for a [CryptoCurrency]
|
||||
*
|
||||
* @property rampManager Ramp manager to check ramp availability
|
||||
*/
|
||||
class GetCryptoCurrencyActionsUseCase(
|
||||
private val rampManager: RampStateManager,
|
||||
private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
operator fun invoke(userWalletId: UserWalletId, tokenId: String): Flow<TokenActionsState> {
|
||||
operator fun invoke(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Flow<TokenActionsState> {
|
||||
return flow {
|
||||
emit(getMockState(userWalletId, tokenId))
|
||||
val actionStates = createTokenActionsState(userWalletId, cryptoCurrency)
|
||||
emit(actionStates)
|
||||
}.flowOn(dispatchers.io)
|
||||
}
|
||||
|
||||
// TODO replace by real data
|
||||
private fun getMockState(userWalletId: UserWalletId, tokenId: String): TokenActionsState {
|
||||
private suspend fun createTokenActionsState(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): TokenActionsState {
|
||||
return TokenActionsState(
|
||||
walletId = userWalletId,
|
||||
tokenId = tokenId,
|
||||
states = listOf(
|
||||
TokenActionsState.ActionState.Buy(true),
|
||||
TokenActionsState.ActionState.Send(true),
|
||||
TokenActionsState.ActionState.Receive(true),
|
||||
TokenActionsState.ActionState.Sell(true),
|
||||
TokenActionsState.ActionState.Swap(true),
|
||||
),
|
||||
cryptoCurrencyId = cryptoCurrency.id,
|
||||
states = createListOfActions(userWalletId, cryptoCurrency),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates list of action for expected order
|
||||
* Actions priority: [Buy Send Receive Sell Swap]
|
||||
*/
|
||||
private suspend fun createListOfActions(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): List<TokenActionsState.ActionState> {
|
||||
return buildList {
|
||||
// todo add check available in swap 1inch etc if backend doen't handle it
|
||||
if (marketCryptoCurrencyRepository.isExchangeable(userWalletId, cryptoCurrency.id) &&
|
||||
!isCustomToken(cryptoCurrency)
|
||||
) {
|
||||
addFirst(TokenActionsState.ActionState.Swap(true))
|
||||
} else {
|
||||
add(TokenActionsState.ActionState.Swap(false))
|
||||
}
|
||||
|
||||
if (rampManager.availableForSell(cryptoCurrency)) {
|
||||
addFirst(TokenActionsState.ActionState.Sell(true))
|
||||
} else {
|
||||
add(TokenActionsState.ActionState.Sell(false))
|
||||
}
|
||||
|
||||
addFirst(TokenActionsState.ActionState.Receive(true))
|
||||
addFirst(TokenActionsState.ActionState.Send(true))
|
||||
|
||||
if (rampManager.availableForBuy(cryptoCurrency)) {
|
||||
addFirst(TokenActionsState.ActionState.Buy(true))
|
||||
} else {
|
||||
add(TokenActionsState.ActionState.Buy(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isCustomToken(currency: CryptoCurrency): Boolean {
|
||||
return currency is CryptoCurrency.Token && currency.isCustom
|
||||
}
|
||||
|
||||
private fun <T> MutableList<T>.addFirst(item: T) {
|
||||
this.add(0, item)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.error.mapper.mapToCurrencyError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.models.Network
|
||||
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
class GetNetworkCoinStatusUseCase(
|
||||
private val currenciesRepository: CurrenciesRepository,
|
||||
private val quotesRepository: QuotesRepository,
|
||||
private val networksRepository: NetworksRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
|
||||
return flow {
|
||||
emitAll(
|
||||
flow = getCurrency(
|
||||
userWalletId = userWalletId,
|
||||
networkId = networkId,
|
||||
),
|
||||
)
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
}
|
||||
|
||||
private suspend fun getCurrency(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
|
||||
val operations = CurrenciesStatusesOperations(
|
||||
currenciesRepository = currenciesRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
networksRepository = networksRepository,
|
||||
userWalletId = userWalletId,
|
||||
)
|
||||
|
||||
return operations.getNetworkCoinFlow(networkId).map { maybeCurrency ->
|
||||
maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ package com.tangem.domain.tokens
|
|||
import arrow.core.Either
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.ensure
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.domain.tokens.models.remove.RemoveCurrencyError
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
|
|
@ -20,10 +19,6 @@ class RemoveCurrencyUseCase(
|
|||
currency: CryptoCurrency,
|
||||
): Either<RemoveCurrencyError, Unit> {
|
||||
return either {
|
||||
ensure(
|
||||
condition = !currency.hasLinkedTokens(userWalletId),
|
||||
raise = { RemoveCurrencyError.HasLinkedTokens(currency) },
|
||||
)
|
||||
catch(
|
||||
block = { currenciesRepository.removeCurrency(userWalletId, currency) },
|
||||
catch = { raise(RemoveCurrencyError.DataError(it)) },
|
||||
|
|
@ -31,10 +26,11 @@ class RemoveCurrencyUseCase(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun CryptoCurrency.hasLinkedTokens(userWalletId: UserWalletId): Boolean {
|
||||
suspend fun hasLinkedTokens(userWalletId: UserWalletId, currency: CryptoCurrency): Boolean {
|
||||
val walletCurrencies = currenciesRepository
|
||||
.getMultiCurrencyWalletCurrenciesSync(userWalletId = userWalletId, refresh = false)
|
||||
|
||||
return this is CryptoCurrency.Coin && walletCurrencies.any { it != this && it.network.id == this.network.id }
|
||||
return currency is CryptoCurrency.Coin &&
|
||||
walletCurrencies.any { it != currency && it.network.id == currency.network.id }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.domain.tokens.error
|
||||
|
||||
sealed class GetCurrenciesError {
|
||||
|
||||
data class DataError(val cause: Throwable) : GetCurrenciesError()
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
|||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import org.rekotlin.Action
|
||||
import java.math.BigDecimal
|
||||
|
||||
sealed class TradeCryptoAction : Action {
|
||||
|
||||
|
|
@ -36,7 +37,13 @@ sealed class TradeCryptoAction : Action {
|
|||
val appCurrencyCode: String,
|
||||
) : New()
|
||||
|
||||
object Send : New()
|
||||
data class SendToken(
|
||||
val userWallet: UserWallet,
|
||||
val tokenStatus: CryptoCurrencyStatus,
|
||||
val coinFiatRate: BigDecimal?,
|
||||
) : New()
|
||||
|
||||
data class SendCoin(val userWallet: UserWallet, val coinStatus: CryptoCurrencyStatus) : New()
|
||||
|
||||
data class Swap(val cryptoCurrency: CryptoCurrency) : New()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.domain.tokens.model
|
||||
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
|
|
@ -20,8 +21,10 @@ data class CryptoCurrencyStatus(
|
|||
|
||||
/**
|
||||
* Represents the various states a token can have, encapsulating different information based on the state.
|
||||
*
|
||||
* @property isError Indicates whether this status represents an error status.
|
||||
*/
|
||||
sealed class Status {
|
||||
sealed class Status(val isError: Boolean) {
|
||||
|
||||
/** The amount of the token. */
|
||||
open val amount: BigDecimal? = null
|
||||
|
|
@ -39,23 +42,23 @@ data class CryptoCurrencyStatus(
|
|||
open val hasCurrentNetworkTransactions: Boolean = false
|
||||
|
||||
/** The pending cryptocurrency transactions. */
|
||||
open val pendingTransactions: Set<PendingTransaction> = emptySet()
|
||||
open val pendingTransactions: Set<TxHistoryItem> = emptySet()
|
||||
|
||||
/** The network address */
|
||||
open val networkAddress: NetworkAddress? = null
|
||||
}
|
||||
|
||||
/** Represents the Loading state of a token, typically while fetching its details. */
|
||||
object Loading : Status()
|
||||
object Loading : Status(isError = false)
|
||||
|
||||
/** Represents a state where the token is not reachable. */
|
||||
object Unreachable : Status()
|
||||
object Unreachable : Status(isError = true)
|
||||
|
||||
/** Represents a state where the token's derivation is missed. */
|
||||
object MissedDerivation : Status()
|
||||
object MissedDerivation : Status(isError = true)
|
||||
|
||||
/** Represents a state where there is no account associated with the token. */
|
||||
object NoAccount : Status()
|
||||
object NoAccount : Status(isError = false)
|
||||
|
||||
/**
|
||||
* Represents a Loaded state of a token with complete information.
|
||||
|
|
@ -74,9 +77,9 @@ data class CryptoCurrencyStatus(
|
|||
override val fiatRate: BigDecimal,
|
||||
override val priceChange: BigDecimal,
|
||||
override val hasCurrentNetworkTransactions: Boolean,
|
||||
override val pendingTransactions: Set<PendingTransaction>,
|
||||
override val pendingTransactions: Set<TxHistoryItem>,
|
||||
override val networkAddress: NetworkAddress?,
|
||||
) : Status()
|
||||
) : Status(isError = false)
|
||||
|
||||
/**
|
||||
* Represents a Custom state of a token, typically used for user-defined tokens.
|
||||
|
|
@ -95,9 +98,9 @@ data class CryptoCurrencyStatus(
|
|||
override val fiatRate: BigDecimal?,
|
||||
override val priceChange: BigDecimal?,
|
||||
override val hasCurrentNetworkTransactions: Boolean,
|
||||
override val pendingTransactions: Set<PendingTransaction>,
|
||||
override val pendingTransactions: Set<TxHistoryItem>,
|
||||
override val networkAddress: NetworkAddress?,
|
||||
) : Status()
|
||||
) : Status(isError = false)
|
||||
|
||||
/**
|
||||
* Represents a state where the token is available, but there is no current quote available for it.
|
||||
|
|
@ -110,7 +113,7 @@ data class CryptoCurrencyStatus(
|
|||
data class NoQuote(
|
||||
override val amount: BigDecimal,
|
||||
override val hasCurrentNetworkTransactions: Boolean,
|
||||
override val pendingTransactions: Set<PendingTransaction>,
|
||||
override val pendingTransactions: Set<TxHistoryItem>,
|
||||
override val networkAddress: NetworkAddress?,
|
||||
) : Status()
|
||||
) : Status(isError = false)
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.domain.tokens.model
|
|||
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.domain.tokens.models.Network
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
|
|
@ -44,7 +45,7 @@ data class NetworkStatus(
|
|||
data class Verified(
|
||||
val address: NetworkAddress,
|
||||
val amounts: Map<CryptoCurrency.ID, BigDecimal>,
|
||||
val pendingTransactions: Map<CryptoCurrency.ID, Set<PendingTransaction>>,
|
||||
val pendingTransactions: Map<CryptoCurrency.ID, Set<TxHistoryItem>>,
|
||||
) : Status()
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
package com.tangem.domain.tokens.model
|
||||
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
data class TokenActionsState(
|
||||
val walletId: UserWalletId,
|
||||
val tokenId: String,
|
||||
val cryptoCurrencyId: CryptoCurrency.ID,
|
||||
val states: List<ActionState>,
|
||||
) {
|
||||
|
||||
|
|
|
|||
|
|
@ -80,6 +80,15 @@ internal class CurrenciesStatusesOperations(
|
|||
return getCurrencyStatusFlow(currency)
|
||||
}
|
||||
|
||||
suspend fun getNetworkCoinFlow(networkId: Network.ID): Flow<Either<Error, CryptoCurrencyStatus>> {
|
||||
val currency = recover(
|
||||
block = { getNetworkCoin(networkId) },
|
||||
recover = { return flowOf(it.left()) },
|
||||
)
|
||||
|
||||
return getCurrencyStatusFlow(currency)
|
||||
}
|
||||
|
||||
suspend fun getPrimaryCurrencyStatusFlow(): Flow<Either<Error, CryptoCurrencyStatus>> {
|
||||
val currency = recover(
|
||||
block = { getPrimaryCurrency() },
|
||||
|
|
@ -177,6 +186,12 @@ internal class CurrenciesStatusesOperations(
|
|||
.bind()
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.getNetworkCoin(networkId: Network.ID): CryptoCurrency {
|
||||
return Either.catch { currenciesRepository.getNetworkCoin(userWalletId, networkId) }
|
||||
.mapLeft { Error.DataError(it) }
|
||||
.bind()
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.getPrimaryCurrency(): CryptoCurrency {
|
||||
return catch(
|
||||
block = { currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) },
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.domain.tokens.repository
|
||||
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.domain.tokens.models.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
|
@ -27,6 +28,16 @@ interface CurrenciesRepository {
|
|||
isSortedByBalance: Boolean,
|
||||
)
|
||||
|
||||
/**
|
||||
* Add currencies to a specific user wallet.
|
||||
*
|
||||
* @param userWalletId The unique identifier of the user wallet.
|
||||
* @param currencies The currencies which must be added.
|
||||
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet
|
||||
* ID provided.
|
||||
*/
|
||||
suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>)
|
||||
|
||||
/**
|
||||
* Removes currency from a specific user wallet.
|
||||
*
|
||||
|
|
@ -37,6 +48,16 @@ interface CurrenciesRepository {
|
|||
*/
|
||||
suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency)
|
||||
|
||||
/**
|
||||
* Removes currencies from a specific user wallet.
|
||||
*
|
||||
* @param userWalletId The unique identifier of the user wallet.
|
||||
* @param currencies The currencies which must be removed.
|
||||
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet
|
||||
* ID provided.
|
||||
*/
|
||||
suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>)
|
||||
|
||||
/**
|
||||
* Retrieves the primary cryptocurrency for a specific single-currency user wallet.
|
||||
*
|
||||
|
|
@ -83,6 +104,14 @@ interface CurrenciesRepository {
|
|||
*/
|
||||
suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: CryptoCurrency.ID): CryptoCurrency
|
||||
|
||||
/**
|
||||
* Get the coin for a specific network.
|
||||
*
|
||||
* @param userWalletId The unique identifier of the user wallet.
|
||||
* @param networkId The unique identifier of the network.
|
||||
*/
|
||||
suspend fun getNetworkCoin(userWalletId: UserWalletId, networkId: Network.ID): CryptoCurrency.Coin
|
||||
|
||||
/**
|
||||
* Determines whether the tokens within a specific multi-currency user wallet are grouped.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.domain.tokens.repository
|
||||
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
/**
|
||||
* MarketCryptoCurrencyRepository works with data from Tangem coins backend, CoinMarketCap etc
|
||||
*/
|
||||
interface MarketCryptoCurrencyRepository {
|
||||
|
||||
suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Boolean
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import arrow.core.Either
|
|||
import arrow.core.getOrElse
|
||||
import com.tangem.domain.core.error.DataError
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.domain.tokens.models.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
|
@ -40,10 +41,14 @@ internal class MockCurrenciesRepository(
|
|||
isTokensSortedByBalanceAfterSortingApply = isSortedByBalance
|
||||
}
|
||||
|
||||
override suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) = Unit
|
||||
|
||||
override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) {
|
||||
removeCurrencyResult.onLeft { throw it }
|
||||
}
|
||||
|
||||
override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) = Unit
|
||||
|
||||
override suspend fun getMultiCurrencyWalletCurrenciesSync(
|
||||
userWalletId: UserWalletId,
|
||||
refresh: Boolean,
|
||||
|
|
@ -70,6 +75,10 @@ internal class MockCurrenciesRepository(
|
|||
return token
|
||||
}
|
||||
|
||||
override suspend fun getNetworkCoin(userWalletId: UserWalletId, networkId: Network.ID): CryptoCurrency.Coin {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isTokensGrouped(userWalletId: UserWalletId): Flow<Boolean> {
|
||||
return isGrouped.map { it.getOrElse { e -> throw e } }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import com.tangem.domain.wallets.models.GetSelectedWalletError
|
|||
import com.tangem.domain.wallets.models.UserWallet
|
||||
|
||||
/**
|
||||
* Use case for getting selected wallet
|
||||
* Use case for getting selected wallet.
|
||||
* Important! If all wallets is locked, use case returns a error.
|
||||
*
|
||||
* @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager'
|
||||
*
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue