Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-11 11:07:03 +03:00
commit f0d03e3648
286 changed files with 6353 additions and 5898 deletions

View file

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

View file

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

View file

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

View file

@ -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]",
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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(",")
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -47,9 +47,6 @@ private class DomainGlobalReducer : ReStoreReducer<DomainGlobalState> {
)
state.copy(scanResponse = action.scanResponse)
}
is DomainGlobalAction.ShowDialog -> {
state.copy(dialog = action.stateDialog)
}
else -> state
}
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,8 @@
package com.tangem.domain.redux.state
/**
[REDACTED_AUTHOR]
*/
interface StringStateConverter<StateHolder> {
fun convert(stateHolder: StateHolder): String
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,6 @@
package com.tangem.domain.tokens.error
sealed class GetCurrenciesError {
data class DataError(val cause: Throwable) : GetCurrenciesError()
}

View file

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

View file

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

View file

@ -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()
/**

View file

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

View file

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

View file

@ -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.
*

View file

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

View file

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

View file

@ -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'
*