Updated on 2026-08-14
This commit is contained in:
commit
f784d9b33f
47 changed files with 599 additions and 600 deletions
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.tap.common
|
||||
|
||||
import timber.log.Timber
|
||||
|
||||
class CompositionCounter(
|
||||
val id: String,
|
||||
count: Int = 0
|
||||
) {
|
||||
var count: Int = count
|
||||
private set
|
||||
|
||||
fun increase(id: String): CompositionCounter {
|
||||
if (this.id != id) return this
|
||||
|
||||
count += 1
|
||||
return CompositionCounter(id, count)
|
||||
}
|
||||
}
|
||||
|
||||
class CompositionLogger(
|
||||
private val recomposeViewId: String,
|
||||
private val tag: String = recomposeViewId,
|
||||
private var turnOnForIds: List<String> = listOf(recomposeViewId)
|
||||
) {
|
||||
val count: Int
|
||||
get() = compositionCounter.count
|
||||
|
||||
private var compositionCounter: CompositionCounter = CompositionCounter(recomposeViewId)
|
||||
|
||||
fun nextComposition() {
|
||||
compositionCounter = compositionCounter.increase(recomposeViewId)
|
||||
log("")
|
||||
}
|
||||
|
||||
fun log(message: String) {
|
||||
if (!turnOnForIds.contains(recomposeViewId)) return
|
||||
|
||||
Timber.d("$tag[$recomposeViewId]:[${compositionCounter.count}]: $message")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
package com.tangem.tap.common.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.tangem.domain.common.util.ValueDebouncer
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
* This is an empty compose view. It just remember the ValueDebouncer inside of itself.
|
||||
*/
|
||||
@Composable
|
||||
fun <T> valueDebouncerAsState(
|
||||
debounce: Long = 400,
|
||||
onValueChanged: (T) -> Unit
|
||||
): ValueDebouncer<T> {
|
||||
return remember {
|
||||
ValueDebouncer(
|
||||
debounce = debounce,
|
||||
onValueChanged = { changedValue ->
|
||||
changedValue?.let { onValueChanged(it) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.tap.common.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.tangem.domain.common.util.ValueDebouncer
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
* This is an empty compose view. It just remember the ValueDebouncer inside of itself.
|
||||
*/
|
||||
@Composable
|
||||
fun <T> valueDebouncerAsState(
|
||||
initialValue: T,
|
||||
debounce: Long = 400,
|
||||
onEmitValueReceived: (T) -> Unit = {},
|
||||
onValueChanged: (T) -> Unit,
|
||||
): ValueDebouncer<T> {
|
||||
return remember {
|
||||
ValueDebouncer<T>(
|
||||
initialValue = initialValue,
|
||||
debounceDuration = debounce,
|
||||
onEmitValueReceived = { emitValue ->
|
||||
emitValue?.let { onEmitValueReceived(it) }
|
||||
},
|
||||
onValueChanged = { changedValue ->
|
||||
changedValue?.let { onValueChanged(it) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> valueDebouncerNullableAsState(
|
||||
initialValue: T?,
|
||||
debounce: Long = 400,
|
||||
onEmitValueReceived: (T?) -> Unit = {},
|
||||
onValueChanged: (T?) -> Unit,
|
||||
): ValueDebouncer<T?> {
|
||||
return remember {
|
||||
ValueDebouncer(
|
||||
initialValue = initialValue,
|
||||
debounceDuration = debounce,
|
||||
onEmitValueReceived = onEmitValueReceived,
|
||||
onValueChanged = onValueChanged,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.tap.common.extensions.ValueCallback
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -28,24 +27,14 @@ fun <T> OutlinedSpinner(
|
|||
isEnabled: Boolean = true,
|
||||
onClose: VoidCallback = {}
|
||||
) {
|
||||
val compositionCounter = remember { CompositionCounter(label) }
|
||||
val counter = compositionCounter.increase(label)
|
||||
|
||||
val rIsExpanded = remember { mutableStateOf(false) }
|
||||
val stateSelectedItem = remember { mutableStateOf(selectedItem.value) }
|
||||
osLog(label, "recompose ------------------------------------", counter)
|
||||
osLog(label, "selectedItem: $selectedItem", counter)
|
||||
osLog(label, "stateSelectedItem: ${stateSelectedItem.value}", counter)
|
||||
if (!selectedItem.isUserInput) {
|
||||
osLog(label, "stateSelectedItem.value: update = selectedItem.value", counter)
|
||||
stateSelectedItem.value = selectedItem.value
|
||||
}
|
||||
osLog(label, "stateSelectedItem: ${stateSelectedItem.value}", counter)
|
||||
|
||||
val onDropDownItemSelectedInternal: (T) -> Unit = {
|
||||
osLog(label, "onDropDownItemSelectedInternal: value: [${it.toString()}]", counter)
|
||||
stateSelectedItem.value = it
|
||||
osLog(label, "onDropDownItemSelectedInternal: stateSelectedItem: ${stateSelectedItem.value}", counter)
|
||||
rIsExpanded.value = false
|
||||
onItemSelected(it)
|
||||
}
|
||||
|
|
@ -85,27 +74,6 @@ fun <T> OutlinedSpinner(
|
|||
}
|
||||
}
|
||||
|
||||
private fun osLog(id: String, log: String, counter: CompositionCounter) {
|
||||
if (id == "Сеть") Timber.d(
|
||||
"OutlinedSpinner[$id]:[${counter.count}] - $log"
|
||||
)
|
||||
}
|
||||
|
||||
class CompositionCounter(
|
||||
val id: String,
|
||||
count: Int = 0
|
||||
) {
|
||||
var count: Int = count
|
||||
private set
|
||||
|
||||
fun increase(id: String): CompositionCounter {
|
||||
if (this.id != id) return this
|
||||
|
||||
count += 1
|
||||
return CompositionCounter(id, count)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun TestSpinnerPreview() {
|
||||
|
|
@ -113,7 +81,7 @@ fun TestSpinnerPreview() {
|
|||
OutlinedSpinner(
|
||||
label = "Blockchain name",
|
||||
itemList = listOf(Blockchain.values()),
|
||||
selectedItem = Field.Data(Blockchain.Avalanche),
|
||||
selectedItem = Field.Data(Blockchain.Avalanche, false),
|
||||
onItemSelected = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.common.module.ModuleError
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.tap.common.CompositionLogger
|
||||
import com.tangem.tap.common.compose.extensions.stringResourceDefault
|
||||
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
|
||||
|
||||
|
|
@ -33,7 +34,7 @@ import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
|
|||
@Composable
|
||||
fun OutlinedTextFieldWidget(
|
||||
modifier: Modifier = Modifier,
|
||||
textFieldData: Field.Data<String>,
|
||||
fieldData: Field.Data<String>,
|
||||
labelId: Int? = null,
|
||||
label: String = "",
|
||||
placeholderId: Int? = null,
|
||||
|
|
@ -56,7 +57,7 @@ fun OutlinedTextFieldWidget(
|
|||
) {
|
||||
OutlinedProgressTextField(
|
||||
modifier = modifier,
|
||||
textFieldData = textFieldData,
|
||||
fieldData = fieldData,
|
||||
label = stringResourceDefault(labelId, label),
|
||||
placeholder = stringResourceDefault(placeholderId, placeholder),
|
||||
trailingIcon = trailingIcon,
|
||||
|
|
@ -75,7 +76,7 @@ fun OutlinedTextFieldWidget(
|
|||
@Composable
|
||||
private fun OutlinedProgressTextField(
|
||||
modifier: Modifier = Modifier,
|
||||
textFieldData: Field.Data<String>,
|
||||
fieldData: Field.Data<String>,
|
||||
label: String = "",
|
||||
placeholder: String = "",
|
||||
isEnabled: Boolean = true,
|
||||
|
|
@ -87,24 +88,70 @@ private fun OutlinedProgressTextField(
|
|||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
onTextChanged: (String) -> Unit,
|
||||
) {
|
||||
val rTextDebouncer = valueDebouncerAsState(debounce, onTextChanged)
|
||||
val rText = remember { mutableStateOf(textFieldData.value) }
|
||||
val logger = remember {
|
||||
CompositionLogger(label, "OutlinedProgressTextField", listOf("Адрес контракта"))
|
||||
}
|
||||
logger.nextComposition()
|
||||
|
||||
fun updateFieldValueAndEmmit(value: String) {
|
||||
rText.value = value
|
||||
rTextDebouncer.emmit(value)
|
||||
}
|
||||
// This action came from redux. Update the field value and send a new event as if from the user
|
||||
if (!textFieldData.isUserInput) {
|
||||
updateFieldValueAndEmmit(textFieldData.value)
|
||||
val textValueState = remember { mutableStateOf(fieldData.value) }
|
||||
val textDebouncer = valueDebouncerAsState(
|
||||
initialValue = fieldData.value,
|
||||
debounce = debounce,
|
||||
onEmitValueReceived = {
|
||||
logger.log("DEBOUNCER: onEmitValueReceived: [$it]")
|
||||
logger.log("DEBOUNCER: start RECOMPOSE by new value for textValueState.value = [$it]")
|
||||
textValueState.value = it
|
||||
},
|
||||
onValueChanged = {
|
||||
logger.log("DEBOUNCER: onValueChanged: >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> dispatch.toStore([$it])")
|
||||
onTextChanged(it)
|
||||
})
|
||||
|
||||
logger.log("RECOMPOSE ---------------------------------------------------------------START [${logger.count}]")
|
||||
logger.log("RECOMPOSE --data: fieldData.value: [${fieldData}]")
|
||||
logger.log("RECOMPOSE --data: textValueState.value: [${textValueState.value}]")
|
||||
logger.log("RECOMPOSE --data: textDebouncer.emittedValue = [${textDebouncer.emittedValue}]")
|
||||
logger.log("RECOMPOSE --data: textDebouncer.debounced = [${textDebouncer.debounced}]")
|
||||
|
||||
if (!fieldData.isUserInput) {
|
||||
// initial value is not from an user
|
||||
val isNotUserInput = "-- IS NOT USER INPUT"
|
||||
logger.log("recompose $isNotUserInput")
|
||||
if (textValueState.value == fieldData.value) {
|
||||
logger.log("$isNotUserInput: внешние данные ОДИНАКОВЫ с данными в поле")
|
||||
} else {
|
||||
logger.log("$isNotUserInput: внешние данные РАЗЛИЧАЮТСЯ с данными в поле")
|
||||
if (textDebouncer.emittedValue != textDebouncer.debounced) {
|
||||
logger.log("$isNotUserInput: пользователь ВВОДИТ данные -> внешние данные игнорируем, ждем RECOMPOSE")
|
||||
} else {
|
||||
logger.log("$isNotUserInput: пользователь НЕ вводит данные -> пытаемся обработать внешние данные")
|
||||
if (textValueState.value != textDebouncer.emittedValue || textValueState.value != textDebouncer.debounced) {
|
||||
logger.log("$isNotUserInput: даннные в поле не соответствуют данным из textDebouncer")
|
||||
if (textDebouncer.emittedValue.isEmpty() && textDebouncer.debounced.isEmpty()) {
|
||||
logger.log("$isNotUserInput: даннные в textDebouncer ПУСТЫ -> start RECOMPOSE новые данные для textValueState.value = [${fieldData.value}]")
|
||||
textValueState.value = fieldData.value
|
||||
} else {
|
||||
logger.log("$isNotUserInput: даннные в textDebouncer НЕ ПУСТЫ -> start RECOMPOSE новые данные для textValueState.value = [${fieldData.value}]")
|
||||
textValueState.value = fieldData.value
|
||||
}
|
||||
} else {
|
||||
logger.log("$isNotUserInput: UNKNOWN -> start RECOMPOSE by new value for textValueState.value = [${fieldData.value}]")
|
||||
textValueState.value = fieldData.value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.log("recompose --------------------------------------------------------------FINISH [${logger.count}]")
|
||||
|
||||
Box {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
value = rText.value,
|
||||
onValueChange = ::updateFieldValueAndEmmit,
|
||||
value = textValueState.value,
|
||||
onValueChange = {
|
||||
logger.log("WIDGET: textDebouncer.emmit([$it])")
|
||||
textDebouncer.emmit(it)
|
||||
},
|
||||
keyboardOptions = keyboardOptions,
|
||||
label = { Text(label) },
|
||||
placeholder = {
|
||||
|
|
@ -128,7 +175,6 @@ private fun OutlinedProgressTextField(
|
|||
visible = isLoading,
|
||||
) { LinearProgressIndicator() }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -168,7 +214,7 @@ fun OutlinedTextFieldWithErrorTest() {
|
|||
) {
|
||||
OutlinedTextFieldWidget(
|
||||
modifier = modifier,
|
||||
textFieldData = Field.Data(""),
|
||||
fieldData = Field.Data("", false),
|
||||
label = "First label",
|
||||
placeholder = "1 placeholder",
|
||||
error = null,
|
||||
|
|
@ -176,7 +222,7 @@ fun OutlinedTextFieldWithErrorTest() {
|
|||
) {}
|
||||
OutlinedTextFieldWidget(
|
||||
modifier = modifier,
|
||||
textFieldData = Field.Data("First"),
|
||||
fieldData = Field.Data("First", false),
|
||||
label = "First label",
|
||||
placeholder = "1 placeholder",
|
||||
error = null,
|
||||
|
|
@ -184,7 +230,7 @@ fun OutlinedTextFieldWithErrorTest() {
|
|||
) {}
|
||||
OutlinedTextFieldWidget(
|
||||
modifier = modifier,
|
||||
textFieldData = Field.Data("First"),
|
||||
fieldData = Field.Data("First", false),
|
||||
label = "First label",
|
||||
placeholder = "1 placeholder",
|
||||
isLoading = true,
|
||||
|
|
@ -193,7 +239,7 @@ fun OutlinedTextFieldWithErrorTest() {
|
|||
) {}
|
||||
OutlinedTextFieldWidget(
|
||||
modifier = modifier,
|
||||
textFieldData = Field.Data("First"),
|
||||
fieldData = Field.Data("First", false),
|
||||
label = "First label",
|
||||
placeholder = "1 placeholder",
|
||||
error = SimpleError(),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.network.api.tangemTech.Coins
|
||||
import com.tangem.network.api.tangemTech.CurrenciesResponse
|
||||
import com.tangem.tap.common.redux.global.FiatCurrencyName
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
|
@ -52,7 +52,7 @@ fun BigDecimal.toFormattedFiatValue(fiatCurrencyName: FiatCurrencyName): String
|
|||
return "≈ ${fiatCurrencyName} $this"
|
||||
}
|
||||
|
||||
fun Coins.CurrenciesResponse.Currency.toFormattedString(): String = "${this.name} (${this.code}) - ${this.unit}"
|
||||
fun CurrenciesResponse.Currency.toFormattedString(): String = "${this.name} (${this.code}) - ${this.unit}"
|
||||
|
||||
fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString()
|
||||
|
||||
|
|
|
|||
|
|
@ -87,22 +87,21 @@ class TapWalletManager {
|
|||
return newResult
|
||||
}
|
||||
|
||||
suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, wallet: Wallet) {
|
||||
val currencies = wallet.getTokens()
|
||||
suspend fun loadFiatRate(currencyId: FiatCurrencyName, wallet: Wallet) {
|
||||
val coinsList = wallet.getTokens()
|
||||
.map { Currency.Token(it, wallet.blockchain, wallet.publicKey.derivationPath?.rawPath) }
|
||||
.plus(Currency.Blockchain(wallet.blockchain, wallet.publicKey.derivationPath?.rawPath))
|
||||
loadFiatRate(fiatCurrency, currencies)
|
||||
loadFiatRate(currencyId, coinsList)
|
||||
}
|
||||
|
||||
suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, currencies: List<Currency>) {
|
||||
suspend fun loadFiatRate(currencyId: FiatCurrencyName, coinsList: List<Currency>) {
|
||||
suspend fun handleFiatRatesResult(rates: Map<Currency, Result<BigDecimal>?>) {
|
||||
rates.forEach { (currency, priceResult) ->
|
||||
when (priceResult) {
|
||||
is Result.Success -> {
|
||||
dispatchOnMain(
|
||||
WalletAction.LoadFiatRate.Success(
|
||||
currency to priceResult.data
|
||||
))
|
||||
dispatchOnMain(WalletAction.LoadFiatRate.Success(
|
||||
currency to priceResult.data
|
||||
))
|
||||
}
|
||||
is Result.Failure -> dispatchOnMain(WalletAction.LoadFiatRate.Failure)
|
||||
null -> {}
|
||||
|
|
@ -111,26 +110,26 @@ class TapWalletManager {
|
|||
}
|
||||
|
||||
// get and submit previous result of equivalents.
|
||||
val throttledResult = currencies.filter { fiatRatesThrottler.isStillThrottled(it) }.map {
|
||||
val throttledResult = coinsList.filter { fiatRatesThrottler.isStillThrottled(it) }.map {
|
||||
Pair(it, fiatRatesThrottler.geValue(it))
|
||||
}
|
||||
if (throttledResult.isNotEmpty()) {
|
||||
handleFiatRatesResult(throttledResult.toMap())
|
||||
}
|
||||
|
||||
val toUpdateCurrencies = currencies.filter { !fiatRatesThrottler.isStillThrottled(it) }
|
||||
val toUpdateIds = toUpdateCurrencies.mapNotNull { it.coinId }.distinct()
|
||||
if (toUpdateIds.isEmpty()) return
|
||||
val currenciesToUpdate = coinsList.filter { !fiatRatesThrottler.isStillThrottled(it) }
|
||||
val coinIds = currenciesToUpdate.mapNotNull { it.coinId }.distinct()
|
||||
if (coinIds.isEmpty()) return
|
||||
|
||||
//TODO: refactoring: move fiatRatesThrottler to the TangemTechRepository
|
||||
when (val result = tangemTechService.coins.prices(fiatCurrency, toUpdateIds)) {
|
||||
when (val result = tangemTechService.rates(currencyId, coinIds)) {
|
||||
is Result.Success -> {
|
||||
val priceResultList: Map<String, Result<BigDecimal>> = result.data.prices.mapValues {
|
||||
val ratesResultList: Map<String, Result<BigDecimal>> = result.data.rates.mapValues {
|
||||
Result.Success(it.value.toBigDecimal())
|
||||
}
|
||||
val updatedCurrencies = mutableMapOf<Currency, Result<BigDecimal>?>()
|
||||
toUpdateCurrencies.forEach { currency ->
|
||||
priceResultList[currency.coinId]?.let {
|
||||
currenciesToUpdate.forEach { currency ->
|
||||
ratesResultList[currency.coinId]?.let {
|
||||
updatedCurrencies[currency] = it
|
||||
fiatRatesThrottler.updateThrottlingTo(currency)
|
||||
fiatRatesThrottler.setValue(currency, it)
|
||||
|
|
@ -250,10 +249,10 @@ class TapWalletManager {
|
|||
WalletAction.MultiWallet.AddBlockchains(blockchainNetworks, walletManagers),
|
||||
)
|
||||
}
|
||||
dispatchOnMain(
|
||||
WalletAction.MultiWallet.FindBlockchainsInUse,
|
||||
WalletAction.MultiWallet.FindTokensInUse,
|
||||
)
|
||||
// dispatchOnMain(
|
||||
// WalletAction.MultiWallet.FindBlockchainsInUse,
|
||||
// WalletAction.MultiWallet.FindTokensInUse,
|
||||
// )
|
||||
} else {
|
||||
val walletManagers = if (
|
||||
primaryTokens.isNotEmpty() &&
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.tap.common.extensions.containsAny
|
||||
import com.tangem.tap.common.extensions.removeBy
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import com.tangem.wallet.R
|
||||
import java.util.*
|
||||
|
||||
|
|
@ -17,6 +18,11 @@ class WarningMessagesManager(
|
|||
private val warningsList: MutableList<WarningMessage> = mutableListOf()
|
||||
|
||||
fun load(onComplete: VoidCallback? = null) {
|
||||
// exclude annoying remote debug warnings
|
||||
if (BuildConfig.DEBUG) {
|
||||
onComplete?.invoke()
|
||||
return
|
||||
}
|
||||
warningLoader.load { remoteList ->
|
||||
warningsList.clear()
|
||||
warningsList.addAll(remoteList)
|
||||
|
|
@ -189,17 +195,24 @@ class WarningMessagesManager(
|
|||
priority = WarningMessage.Priority.Warning,
|
||||
listOf(WarningMessage.Location.MainScreen),
|
||||
blockchains = null,
|
||||
titleResId = R.string.alert_funds_restoration_message,
|
||||
titleResId = R.string.alert_title,
|
||||
messageResId = R.string.alert_funds_restoration_message,
|
||||
origin = WarningMessage.Origin.Local,
|
||||
buttonTextId = R.string.warning_button_learn_more
|
||||
)
|
||||
|
||||
const val REMAINING_SIGNATURES_WARNING = 10
|
||||
private const val RESTORE_FUNDS_GUIDE_URL_RU =
|
||||
"https://tangem.com/ru/kak-vosstanovit-tokeny-otpravlennye-ne-na-tot-adres-v-tangem-wallet"
|
||||
private const val RESTORE_FUNDS_GUIDE_URL_EN =
|
||||
"https://tangem.com/en/how-to-recover-crypto-sent-to-the-wrong-address-in-tangem-wallet"
|
||||
|
||||
fun getRestoreFundsGuideUrl(locale: String): String {
|
||||
val code = if (locale == Locale("ru").language) "ru" else "en"
|
||||
return "https://tangem.com/$code/notion"
|
||||
return if (locale == Locale("ru").language) {
|
||||
RESTORE_FUNDS_GUIDE_URL_RU
|
||||
} else {
|
||||
RESTORE_FUNDS_GUIDE_URL_EN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -179,7 +179,7 @@ class CurrenciesRepository(val context: Application) {
|
|||
fun getSupportedTokens(isTestNet: Boolean = false): List<Currency> {
|
||||
val fileName = if (isTestNet) "testnet_tokens" else "tokens"
|
||||
val json = context.assets.readJsonFileToString(fileName)
|
||||
return currenciesAdapter.fromJson(json)!!.tokens
|
||||
return currenciesAdapter.fromJson(json)!!.coins
|
||||
.map { Currency.fromJsonObject(it) }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,20 +9,20 @@ data class CurrencyFromJson(
|
|||
val id: String,
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
val contracts: List<ContractFromJson>? = null
|
||||
val networks: List<ContractFromJson>? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ContractFromJson(
|
||||
val networkId: String,
|
||||
val address: String,
|
||||
val decimalCount: Int
|
||||
val contractAddress: String?,
|
||||
val decimalCount: Int?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CurrenciesFromJson(
|
||||
val imageHost: String?,
|
||||
val tokens: List<CurrencyFromJson>
|
||||
val coins: List<CurrencyFromJson>
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -45,26 +45,17 @@ data class Currency(
|
|||
name = currency.name,
|
||||
symbol = currency.symbol,
|
||||
iconUrl = getIconUrl(currency.id),
|
||||
contracts = prepareListOfContracts(currency.contracts, currency.id)
|
||||
contracts = currency.networks?.toContracts() ?: emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
private fun prepareListOfContracts(
|
||||
contractsFromJson: List<ContractFromJson>?,
|
||||
currencyId: String,
|
||||
): List<Contract> {
|
||||
val mainNetwork = Contract.fromCurrencyId(currencyId)
|
||||
val contracts = contractsFromJson?.toContracts() ?: emptyList()
|
||||
return (listOfNotNull(mainNetwork) + contracts).distinct()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Contract(
|
||||
val networkId: String,
|
||||
val blockchain: Blockchain,
|
||||
val address: String,
|
||||
val decimalCount: Int,
|
||||
val address: String?,
|
||||
val decimalCount: Int?,
|
||||
val iconUrl: String,
|
||||
) {
|
||||
|
||||
|
|
@ -74,23 +65,11 @@ data class Contract(
|
|||
return Contract(
|
||||
networkId = contract.networkId,
|
||||
blockchain = blockchain,
|
||||
address = contract.address,
|
||||
address = contract.contractAddress,
|
||||
decimalCount = contract.decimalCount,
|
||||
iconUrl = getIconUrl(contract.networkId)
|
||||
)
|
||||
}
|
||||
|
||||
fun fromCurrencyId(currencyId: String): Contract? {
|
||||
val blockchain = Blockchain.fromNetworkId(currencyId) ?: return null
|
||||
return Contract(
|
||||
networkId = currencyId,
|
||||
blockchain = blockchain,
|
||||
address = blockchain.currency,
|
||||
decimalCount = blockchain.decimals(),
|
||||
iconUrl = getIconUrl(currencyId)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.tangem.blockchain.common.Wallet
|
|||
import com.tangem.common.card.Card
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TwinCardNumber
|
||||
import com.tangem.network.api.tangemTech.Coins
|
||||
import com.tangem.network.api.tangemTech.CurrenciesResponse
|
||||
import com.tangem.network.api.tangemTech.TangemTechService
|
||||
import com.tangem.operations.pins.CheckUserCodesResponse
|
||||
import com.tangem.tap.common.redux.NotificationAction
|
||||
|
|
@ -48,7 +48,7 @@ sealed class DetailsAction : Action {
|
|||
object CreateBackup : DetailsAction()
|
||||
|
||||
sealed class AppCurrencyAction : DetailsAction() {
|
||||
data class SetCurrencies(val currencies: List<Coins.CurrenciesResponse.Currency>) : AppCurrencyAction()
|
||||
data class SetCurrencies(val currencies: List<CurrenciesResponse.Currency>) : AppCurrencyAction()
|
||||
object ChooseAppCurrency : AppCurrencyAction()
|
||||
object Cancel : AppCurrencyAction()
|
||||
data class SelectAppCurrency(val fiatCurrencyName: FiatCurrencyName) : AppCurrencyAction()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import com.tangem.common.CompletionResult
|
|||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.network.api.tangemTech.Coins
|
||||
import com.tangem.operations.pins.CheckUserCodesResponse
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.analytics.Analytics
|
||||
|
|
@ -77,14 +76,12 @@ class DetailsMiddleware {
|
|||
|
||||
scope.launch {
|
||||
val tangemTechService = action.tangemTechService
|
||||
when (val result = tangemTechService.coins.currencies()) {
|
||||
when (val result = tangemTechService.currencies()) {
|
||||
is Result.Success -> {
|
||||
val fiatCurrencies = result.data.currencies.filter {
|
||||
it.type == Coins.CurrenciesResponse.CurrencyType.Fiat.type
|
||||
}
|
||||
if (fiatCurrencies.isNotEmpty() && fiatCurrencies.toSet() != storedFiatCurrencies.toSet()) {
|
||||
fiatCurrenciesPrefStorage.save(fiatCurrencies)
|
||||
dispatchOnMain(DetailsAction.AppCurrencyAction.SetCurrencies(fiatCurrencies))
|
||||
val currenciesList = result.data.currencies
|
||||
if (currenciesList.isNotEmpty() && currenciesList.toSet() != storedFiatCurrencies.toSet()) {
|
||||
fiatCurrenciesPrefStorage.save(currenciesList)
|
||||
dispatchOnMain(DetailsAction.AppCurrencyAction.SetCurrencies(currenciesList))
|
||||
}
|
||||
}
|
||||
is Result.Failure -> {}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.tap.features.details.redux
|
|||
import android.net.Uri
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.network.api.tangemTech.Coins
|
||||
import com.tangem.network.api.tangemTech.CurrenciesResponse
|
||||
import com.tangem.tap.common.entities.Button
|
||||
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
|
||||
import com.tangem.tap.common.redux.global.FiatCurrencyName
|
||||
|
|
@ -55,5 +55,5 @@ enum class SecurityOption { LongTap, PassCode, AccessCode }
|
|||
data class AppCurrencyState(
|
||||
val fiatCurrencyName: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
|
||||
val showAppCurrencyDialog: Boolean = false,
|
||||
val fiatCurrencies: List<Coins.CurrenciesResponse.Currency>? = null,
|
||||
val fiatCurrencies: List<CurrenciesResponse.Currency>? = null,
|
||||
)
|
||||
|
|
@ -2,7 +2,7 @@ package com.tangem.tap.features.details.ui
|
|||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.tangem.network.api.tangemTech.Coins
|
||||
import com.tangem.network.api.tangemTech.CurrenciesResponse
|
||||
import com.tangem.tap.common.extensions.toFormattedString
|
||||
import com.tangem.tap.common.redux.global.FiatCurrencyName
|
||||
import com.tangem.tap.features.details.redux.DetailsAction
|
||||
|
|
@ -13,11 +13,11 @@ class CurrencySelectionDialog {
|
|||
|
||||
var dialog: AlertDialog? = null
|
||||
|
||||
fun show(currencies: List<Coins.CurrenciesResponse.Currency>, currentAppCurrency: FiatCurrencyName, context: Context) {
|
||||
fun show(currenciesList: List<CurrenciesResponse.Currency>, currentAppCurrency: FiatCurrencyName, context: Context) {
|
||||
|
||||
if (dialog == null) {
|
||||
val currenciesToShow = currencies.map { it.toFormattedString() }.toTypedArray()
|
||||
var currentSelection = currencies.indexOfFirst { it.code == currentAppCurrency }
|
||||
val currenciesToShow = currenciesList.map { it.toFormattedString() }.toTypedArray()
|
||||
var currentSelection = currenciesList.indexOfFirst { it.code == currentAppCurrency }
|
||||
|
||||
dialog = AlertDialog.Builder(context)
|
||||
.setTitle(context.getString(R.string.details_row_title_currency))
|
||||
|
|
@ -25,7 +25,7 @@ class CurrencySelectionDialog {
|
|||
store.dispatch(DetailsAction.AppCurrencyAction.Cancel)
|
||||
}
|
||||
.setPositiveButton(context.getString(R.string.common_done)) { _, _ ->
|
||||
val selectedCurrency = currencies[currentSelection]
|
||||
val selectedCurrency = currenciesList[currentSelection]
|
||||
store.dispatch(DetailsAction.AppCurrencyAction.SelectAppCurrency(selectedCurrency.code))
|
||||
}
|
||||
.setOnDismissListener {
|
||||
|
|
|
|||
|
|
@ -98,11 +98,11 @@ fun FirstStoriesContent(
|
|||
if (screenState.value != StartingScreenState.MEET_TANGEM) {
|
||||
TextAutoSize(
|
||||
modifier = Modifier
|
||||
.padding(start = 40.dp, end = 40.dp, bottom = 100.dp)
|
||||
.padding(start = 20.dp, end = 20.dp, bottom = 100.dp)
|
||||
.alpha(if (screenState.value == StartingScreenState.SHOW_CARD) 0f else 1f),
|
||||
text = text?.let { stringResource(text) } ?: "",
|
||||
textStyle = style,
|
||||
fontSizeRange = FontSizeRange(40.sp, 60.sp)
|
||||
fontSizeRange = FontSizeRange(20.sp, 60.sp)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ fun TokenContractAddressView(screenFieldData: ScreenFieldData) {
|
|||
val tokenField = screenFieldData.field as TokenField
|
||||
|
||||
OutlinedTextFieldWidget(
|
||||
textFieldData = tokenField.data,
|
||||
fieldData = tokenField.data,
|
||||
labelId = R.string.custom_token_contract_address_input_title,
|
||||
placeholder = "0x0000000000000000000000000000000000000000",
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
|
|
@ -35,7 +35,7 @@ fun TokenContractAddressView(screenFieldData: ScreenFieldData) {
|
|||
error = screenFieldData.error,
|
||||
errorConverter = screenFieldData.errorConverter,
|
||||
) {
|
||||
domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(it)))
|
||||
domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(it, true)))
|
||||
}
|
||||
SpacerH8()
|
||||
}
|
||||
|
|
@ -47,14 +47,14 @@ fun TokenNameView(screenFieldData: ScreenFieldData) {
|
|||
val tokenField = screenFieldData.field as TokenField
|
||||
|
||||
OutlinedTextFieldWidget(
|
||||
textFieldData = tokenField.data,
|
||||
fieldData = tokenField.data,
|
||||
labelId = R.string.custom_token_name_input_title,
|
||||
placeholderId = R.string.custom_token_name_input_placeholder,
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
error = screenFieldData.error,
|
||||
errorConverter = screenFieldData.errorConverter,
|
||||
) {
|
||||
domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data(it)))
|
||||
domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data(it, true)))
|
||||
}
|
||||
SpacerH8()
|
||||
}
|
||||
|
|
@ -72,7 +72,7 @@ fun TokenNetworkView(screenFieldData: ScreenFieldData, state: AddCustomTokenStat
|
|||
selectedItem = networkField.data,
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
textFieldConverter = { state.blockchainToName(it) ?: notSelected },
|
||||
) { domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(it))) }
|
||||
) { domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(it, true))) }
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
|
|
@ -83,13 +83,13 @@ fun TokenSymbolView(screenFieldData: ScreenFieldData) {
|
|||
val tokenField = screenFieldData.field as TokenField
|
||||
|
||||
OutlinedTextFieldWidget(
|
||||
textFieldData = tokenField.data,
|
||||
fieldData = tokenField.data,
|
||||
labelId = R.string.custom_token_token_symbol_input_title,
|
||||
placeholderId = R.string.custom_token_token_symbol_input_placeholder,
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
error = screenFieldData.error,
|
||||
errorConverter = screenFieldData.errorConverter,
|
||||
) { domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data(it))) }
|
||||
) { domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data(it, true))) }
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
|
|
@ -100,14 +100,14 @@ fun TokenDecimalsView(screenFieldData: ScreenFieldData) {
|
|||
val tokenField = screenFieldData.field as TokenField
|
||||
|
||||
OutlinedTextFieldWidget(
|
||||
textFieldData = tokenField.data,
|
||||
fieldData = tokenField.data,
|
||||
labelId = R.string.custom_token_decimals_input_title,
|
||||
placeholder = "8",
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
error = screenFieldData.error,
|
||||
errorConverter = screenFieldData.errorConverter,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
) { domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data(it))) }
|
||||
) { domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data(it, true))) }
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
|
|
@ -129,6 +129,6 @@ fun TokenDerivationPathView(screenFieldData: ScreenFieldData, state: AddCustomTo
|
|||
val blockchainName = state.blockchainToName(blockchain) ?: notSelected
|
||||
TitleSubtitle(derivationPathName, blockchainName)
|
||||
}
|
||||
) { domainStore.dispatch(AddCustomTokenAction.OnTokenDerivationPathChanged(Field.Data(it))) }
|
||||
) { domainStore.dispatch(AddCustomTokenAction.OnTokenDerivationPathChanged(Field.Data(it, true))) }
|
||||
SpacerH8()
|
||||
}
|
||||
|
|
@ -17,5 +17,5 @@ fun SelectTokenNetworkDialog(dialog: DomainDialog.SelectTokenDialog, onDismissRe
|
|||
items = dialog.items,
|
||||
onSelect = dialog.onSelect,
|
||||
onDismissRequest = onDismissRequest
|
||||
) { contract -> TitleSubtitle(dialog.networkIdConverter(contract.networkId), contract.address) }
|
||||
) { network -> TitleSubtitle(dialog.networkIdConverter(network.networkId), network.contractAddress ?: "") }
|
||||
}
|
||||
|
|
@ -12,14 +12,10 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.domain.features.addCustomToken.AddCustomTokenService
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.network.api.tangemTech.TangemTechService
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -40,7 +36,7 @@ fun TestAddCustomTokenActions() {
|
|||
ActionRow("All in one") { AllInOne() }
|
||||
|
||||
// Any action
|
||||
// ActionRow("CustomActions - find tokens active=false, decimals != null") { CustomActions() }
|
||||
// ActionRow("CustomActions - find coins active=false, decimals != null") { CustomActions() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,41 +143,6 @@ private fun UnknownContracts() {
|
|||
|
||||
@Composable
|
||||
private fun CustomActions() {
|
||||
|
||||
CustomActionButton(
|
||||
name = "Find tokens in several networks",
|
||||
action = {
|
||||
val manager = AddCustomTokenService(TangemTechService())
|
||||
val currencies = manager.tokens()
|
||||
val asdfsd = mutableMapOf<String, MutableList<Any>>()
|
||||
val contractAddresses = currencies.mapNotNull { currency ->
|
||||
currency.contracts?.map { it.address }
|
||||
}.flatten()
|
||||
contractAddresses.take(500).forEachIndexed() { index, address ->
|
||||
when (val result = manager.checkAddress(address)) {
|
||||
is Result.Success -> {
|
||||
val contractList = mutableListOf<Any>()
|
||||
result.data.forEach { token ->
|
||||
token.contracts.forEach { contract ->
|
||||
if (!contract.active && contract.decimalCount != null) {
|
||||
contractList.add(contract)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (contractList.isNotEmpty()) {
|
||||
val list = asdfsd[address] ?: mutableListOf()
|
||||
list.addAll(contractList)
|
||||
asdfsd[address] = list
|
||||
}
|
||||
Timber.e("Success. handle $index item from size ${contractAddresses.size}. Result = ${asdfsd.size}")
|
||||
}
|
||||
is Result.Failure -> {}
|
||||
}
|
||||
}
|
||||
val result = asdfsd.filter { it.value.size > 1 }
|
||||
if (result.isEmpty()) return@CustomActionButton
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -69,9 +69,9 @@ fun CollapsedCurrencyItem(
|
|||
Row {
|
||||
if (!currency.contracts.isNullOrEmpty()) {
|
||||
currency.contracts.map { contract ->
|
||||
if (contract.address == currency.symbol) {
|
||||
if (contract.address == null) {
|
||||
BlockchainNetworkItem(
|
||||
blockchain = Blockchain.fromNetworkId(contract.networkId),
|
||||
blockchain = contract.blockchain,
|
||||
isMainNetwork = true,
|
||||
addedBlockchains = addedBlockchains
|
||||
)
|
||||
|
|
|
|||
|
|
@ -132,12 +132,13 @@ fun ExpandedCurrencyItem(
|
|||
) {
|
||||
blockchains.map { blockchain ->
|
||||
val contract = currency.contracts.firstOrNull { it.blockchain == blockchain }
|
||||
val added = if (contract != null && contract.address != currency.symbol) {
|
||||
?: return@map
|
||||
val added = if (contract.address != null) {
|
||||
addedTokens.map { it.token.contractAddress }.contains(contract.address)
|
||||
} else {
|
||||
addedBlockchains.contains(blockchain)
|
||||
}
|
||||
val canBeRemoved = if (contract != null && contract.address != currency.symbol) {
|
||||
val canBeRemoved = if (contract.address != null) {
|
||||
!nonRemovableTokens.contains(contract.address)
|
||||
} else {
|
||||
!nonRemovableBlockchains.contains(blockchain)
|
||||
|
|
|
|||
|
|
@ -32,31 +32,30 @@ import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
|
|||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun NetworkItem(
|
||||
currency: Currency, contract: Contract?,
|
||||
currency: Currency, contract: Contract,
|
||||
blockchain: Blockchain, allowToAdd: Boolean,
|
||||
added: Boolean, canBeRemoved: Boolean,
|
||||
onAddCurrencyToggled: (Currency, TokenWithBlockchain?) -> Unit,
|
||||
onNetworkItemClicked: (ContractAddress) -> Unit
|
||||
) {
|
||||
|
||||
val isBlockchain = contract == null || contract.address == currency.symbol
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
enabled = allowToAdd,
|
||||
onLongClick = {
|
||||
if (!isBlockchain) onNetworkItemClicked(contract!!.address)
|
||||
contract.address?.let { onNetworkItemClicked(it) }
|
||||
},
|
||||
onClick = {},
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
) {
|
||||
Box(modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(start = 8.dp, top = 16.dp, bottom = 16.dp, end = 6.dp)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(start = 8.dp, top = 16.dp, bottom = 16.dp, end = 6.dp)
|
||||
) {
|
||||
SubcomposeAsyncImage(
|
||||
model = if (added) blockchain.getRoundIconRes() else blockchain.getGreyedOutIconRes(),
|
||||
|
|
@ -66,7 +65,7 @@ fun NetworkItem(
|
|||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
)
|
||||
if (isBlockchain) {
|
||||
if (contract.address == null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
|
|
@ -98,29 +97,29 @@ fun NetworkItem(
|
|||
)
|
||||
Spacer(modifier = Modifier.size(3.dp))
|
||||
Text(
|
||||
text = if (isBlockchain) "MAIN" else blockchain.getNetworkName().uppercase(),
|
||||
text = if (contract.address == null) "MAIN" else blockchain.getNetworkName().uppercase(),
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = if (!isBlockchain) Color(0xFF8E8E93) else Color(0xFF1ACE80),
|
||||
color = if (contract.address != null) Color(0xFF8E8E93) else Color(0xFF1ACE80),
|
||||
)
|
||||
}
|
||||
|
||||
if (allowToAdd) {
|
||||
val token = if (!isBlockchain) {
|
||||
val token = if (contract.address != null) {
|
||||
Token(
|
||||
id = currency.id,
|
||||
name = currency.name,
|
||||
symbol = currency.symbol,
|
||||
contractAddress = contract!!.address,
|
||||
decimals = contract.decimalCount,
|
||||
contractAddress = contract.address,
|
||||
decimals = contract.decimalCount!!,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val tokenWithBlockchain =
|
||||
token?.let { TokenWithBlockchain(token, contract!!.blockchain) }
|
||||
token?.let { TokenWithBlockchain(token, contract.blockchain) }
|
||||
|
||||
val currencyToSave = if (isBlockchain && contract != null) {
|
||||
val currencyToSave = if (contract.address == null) {
|
||||
currency.copy(id = contract.networkId)
|
||||
} else {
|
||||
currency
|
||||
|
|
|
|||
|
|
@ -58,8 +58,8 @@ sealed class WalletAction : Action {
|
|||
|
||||
data class AddToken(val token: Token, val blockchain: BlockchainNetwork) : MultiWallet()
|
||||
data class SaveCurrencies(val blockchainNetworks: List<BlockchainNetwork>) : MultiWallet()
|
||||
object FindTokensInUse : MultiWallet()
|
||||
object FindBlockchainsInUse : MultiWallet()
|
||||
// object FindTokensInUse : MultiWallet()
|
||||
// object FindBlockchainsInUse : MultiWallet()
|
||||
|
||||
data class TokenLoaded(
|
||||
val amount: Amount,
|
||||
|
|
@ -96,7 +96,7 @@ sealed class WalletAction : Action {
|
|||
}
|
||||
|
||||
data class LoadFiatRate(
|
||||
val wallet: Wallet? = null, val currencyList: List<Currency>? = null,
|
||||
val wallet: Wallet? = null, val coinsList: List<Currency>? = null,
|
||||
) : WalletAction() {
|
||||
data class Success(val fiatRate: Pair<Currency, BigDecimal?>) : WalletAction()
|
||||
object Failure : WalletAction()
|
||||
|
|
|
|||
|
|
@ -397,7 +397,10 @@ sealed interface Currency {
|
|||
}
|
||||
|
||||
fun isCustomCurrency(derivationStyle: DerivationStyle?): Boolean {
|
||||
if (this is Token && this.token.id == null) return true
|
||||
|
||||
if (derivationPath == null || derivationStyle == null) return false
|
||||
|
||||
return derivationPath != blockchain.derivationPath(derivationStyle)?.rawPath
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
package com.tangem.tap.features.wallet.redux.middlewares
|
||||
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.tap.common.extensions.safeUpdate
|
||||
import com.tangem.tap.common.redux.global.GlobalState
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.currenciesRepository
|
||||
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
|
||||
import com.tangem.tap.domain.extensions.makeWalletManagersForApp
|
||||
import com.tangem.tap.domain.tokens.BlockchainNetwork
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.demo.isDemoCard
|
||||
|
|
@ -19,7 +18,6 @@ import com.tangem.tap.features.wallet.redux.WalletState
|
|||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -29,7 +27,7 @@ class MultiWalletMiddleware {
|
|||
action: WalletAction.MultiWallet, walletState: WalletState?, globalState: GlobalState?,
|
||||
) {
|
||||
val globalState = globalState ?: return
|
||||
val tapWalletManager = globalState.tapWalletManager
|
||||
// val tapWalletManager = globalState.tapWalletManager
|
||||
|
||||
when (action) {
|
||||
is WalletAction.MultiWallet.AddBlockchains -> {
|
||||
|
|
@ -57,19 +55,17 @@ class MultiWalletMiddleware {
|
|||
blockchainNetwork = action.blockchain
|
||||
)
|
||||
}
|
||||
store.dispatch(
|
||||
WalletAction.LoadFiatRate(
|
||||
currencyList = listOf(
|
||||
Currency.Blockchain(
|
||||
action.blockchain.blockchain,
|
||||
action.blockchain.derivationPath
|
||||
)
|
||||
store.dispatch(WalletAction.LoadFiatRate(
|
||||
coinsList = listOf(
|
||||
Currency.Blockchain(
|
||||
action.blockchain.blockchain,
|
||||
action.blockchain.derivationPath
|
||||
)
|
||||
)
|
||||
)
|
||||
store.dispatch(
|
||||
WalletAction.LoadWallet(action.blockchain, action.walletManager)
|
||||
)
|
||||
))
|
||||
store.dispatch(WalletAction.LoadWallet(
|
||||
action.blockchain, action.walletManager
|
||||
))
|
||||
}
|
||||
is WalletAction.MultiWallet.SaveCurrencies -> {
|
||||
globalState.scanResponse?.card?.cardId?.let {
|
||||
|
|
@ -105,94 +101,94 @@ class MultiWalletMiddleware {
|
|||
}
|
||||
}
|
||||
}
|
||||
is WalletAction.MultiWallet.FindBlockchainsInUse -> {
|
||||
val scanResponse = globalState.scanResponse ?: return
|
||||
if (scanResponse.supportsHdWallet()) return
|
||||
|
||||
val cardFirmware = scanResponse.card.firmwareVersion
|
||||
val blockchains = currenciesRepository.getBlockchains(cardFirmware)
|
||||
.filterNot { walletState?.blockchains?.contains(it) == true }
|
||||
.map { BlockchainNetwork(it, null, emptyList()) }
|
||||
val walletManagers =
|
||||
tapWalletManager.walletManagerFactory.makeWalletManagersForApp(
|
||||
scanResponse,
|
||||
blockchains
|
||||
)
|
||||
|
||||
scope.launch {
|
||||
walletManagers.map { walletManager ->
|
||||
async(Dispatchers.IO) {
|
||||
walletManager.safeUpdate()
|
||||
val wallet = walletManager.wallet
|
||||
val coinAmount = wallet.amounts[AmountType.Coin]?.value
|
||||
if (coinAmount != null && !coinAmount.isZero()) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
|
||||
if (walletState?.getWalletData(blockchainNetwork) == null) {
|
||||
store.dispatch(WalletAction.MultiWallet.AddBlockchain(
|
||||
blockchainNetwork, walletManager
|
||||
))
|
||||
store.dispatch(WalletAction.LoadWallet.Success(
|
||||
wallet = wallet,
|
||||
blockchain = blockchainNetwork
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is WalletAction.MultiWallet.FindTokensInUse -> {
|
||||
val scanResponse = globalState.scanResponse ?: return
|
||||
if (scanResponse.supportsHdWallet()) return
|
||||
|
||||
val walletFactory = tapWalletManager.walletManagerFactory
|
||||
val card = scanResponse.card
|
||||
|
||||
val walletManager = walletState?.getWalletManager(
|
||||
Currency.Blockchain(Blockchain.Ethereum, null)
|
||||
)
|
||||
?: walletFactory.makeWalletManagerForApp(
|
||||
scanResponse,
|
||||
Currency.Blockchain(Blockchain.Ethereum, null)
|
||||
)
|
||||
|
||||
val tokenFinder = walletManager as? TokenFinder ?: return
|
||||
scope.launch {
|
||||
val result = tokenFinder.findTokens()
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
if (result.data.isNotEmpty()) {
|
||||
val blockchainNetwork = BlockchainNetwork(
|
||||
walletManager.wallet.blockchain,
|
||||
walletManager.wallet.publicKey.derivationPath?.rawPath,
|
||||
walletManager.cardTokens.toList()
|
||||
)
|
||||
currenciesRepository.saveUpdatedCurrency(
|
||||
card.cardId,
|
||||
blockchainNetwork
|
||||
)
|
||||
store.dispatch(
|
||||
WalletAction.MultiWallet.AddBlockchain(
|
||||
blockchainNetwork,
|
||||
walletManager
|
||||
)
|
||||
)
|
||||
store.dispatch(
|
||||
WalletAction.MultiWallet.AddTokens(
|
||||
walletManager.cardTokens.toList(),
|
||||
blockchainNetwork
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// is WalletAction.MultiWallet.FindBlockchainsInUse -> {
|
||||
// val scanResponse = globalState.scanResponse ?: return
|
||||
// if (scanResponse.supportsHdWallet()) return
|
||||
//
|
||||
// val cardFirmware = scanResponse.card.firmwareVersion
|
||||
// val blockchains = currenciesRepository.getBlockchains(cardFirmware)
|
||||
// .filterNot { walletState?.blockchains?.contains(it) == true }
|
||||
// .map { BlockchainNetwork(it, null, emptyList()) }
|
||||
// val walletManagers =
|
||||
// tapWalletManager.walletManagerFactory.makeWalletManagersForApp(
|
||||
// scanResponse,
|
||||
// blockchains
|
||||
// )
|
||||
//
|
||||
// scope.launch {
|
||||
// walletManagers.map { walletManager ->
|
||||
// async(Dispatchers.IO) {
|
||||
// walletManager.safeUpdate()
|
||||
// val wallet = walletManager.wallet
|
||||
// val coinAmount = wallet.amounts[AmountType.Coin]?.value
|
||||
// if (coinAmount != null && !coinAmount.isZero()) {
|
||||
// scope.launch(Dispatchers.Main) {
|
||||
// val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
|
||||
// if (walletState?.getWalletData(blockchainNetwork) == null) {
|
||||
// store.dispatch(WalletAction.MultiWallet.AddBlockchain(
|
||||
// blockchainNetwork, walletManager
|
||||
// ))
|
||||
// store.dispatch(WalletAction.LoadWallet.Success(
|
||||
// wallet = wallet,
|
||||
// blockchain = blockchainNetwork
|
||||
// ))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// is WalletAction.MultiWallet.FindTokensInUse -> {
|
||||
// val scanResponse = globalState.scanResponse ?: return
|
||||
// if (scanResponse.supportsHdWallet()) return
|
||||
//
|
||||
// val walletFactory = tapWalletManager.walletManagerFactory
|
||||
// val card = scanResponse.card
|
||||
//
|
||||
// val walletManager = walletState?.getWalletManager(
|
||||
// Currency.Blockchain(Blockchain.Ethereum, null)
|
||||
// )
|
||||
// ?: walletFactory.makeWalletManagerForApp(
|
||||
// scanResponse,
|
||||
// Currency.Blockchain(Blockchain.Ethereum, null)
|
||||
// )
|
||||
//
|
||||
// val tokenFinder = walletManager as? TokenFinder ?: return
|
||||
// scope.launch {
|
||||
// val result = tokenFinder.findTokens()
|
||||
//
|
||||
// withContext(Dispatchers.Main) {
|
||||
// when (result) {
|
||||
// is Result.Success -> {
|
||||
// if (result.data.isNotEmpty()) {
|
||||
// val blockchainNetwork = BlockchainNetwork(
|
||||
// walletManager.wallet.blockchain,
|
||||
// walletManager.wallet.publicKey.derivationPath?.rawPath,
|
||||
// walletManager.cardTokens.toList()
|
||||
// )
|
||||
// currenciesRepository.saveUpdatedCurrency(
|
||||
// card.cardId,
|
||||
// blockchainNetwork
|
||||
// )
|
||||
// store.dispatch(
|
||||
// WalletAction.MultiWallet.AddBlockchain(
|
||||
// blockchainNetwork,
|
||||
// walletManager
|
||||
// )
|
||||
// )
|
||||
// store.dispatch(
|
||||
// WalletAction.MultiWallet.AddTokens(
|
||||
// walletManager.cardTokens.toList(),
|
||||
// blockchainNetwork
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -227,7 +223,7 @@ class MultiWalletMiddleware {
|
|||
store.dispatch(WalletAction.MultiWallet.AddBlockchain(blockchainNetwork, it))
|
||||
} ?: return
|
||||
|
||||
store.dispatch(WalletAction.LoadFiatRate(currencyList = tokens.map { token ->
|
||||
store.dispatch(WalletAction.LoadFiatRate(coinsList = tokens.map { token ->
|
||||
Currency.Token(
|
||||
token, blockchainNetwork.blockchain, blockchainNetwork.derivationPath
|
||||
)
|
||||
|
|
|
|||
|
|
@ -101,27 +101,26 @@ class WalletMiddleware {
|
|||
warningsMiddleware.tryToShowAppRatingWarning(action.wallet)
|
||||
}
|
||||
is WalletAction.LoadFiatRate -> {
|
||||
val tapWalletManager = globalState.tapWalletManager
|
||||
val fiatAppCurrency = globalState.appCurrency
|
||||
val appCurrencyId = globalState.appCurrency
|
||||
scope.launch {
|
||||
when {
|
||||
action.wallet != null -> {
|
||||
globalState.tapWalletManager.loadFiatRate(
|
||||
fiatCurrency = fiatAppCurrency,
|
||||
currencyId = appCurrencyId,
|
||||
wallet = action.wallet,
|
||||
)
|
||||
}
|
||||
action.currencyList != null -> {
|
||||
action.coinsList != null -> {
|
||||
globalState.tapWalletManager.loadFiatRate(
|
||||
fiatCurrency = fiatAppCurrency,
|
||||
currencies = action.currencyList,
|
||||
currencyId = appCurrencyId,
|
||||
coinsList = action.coinsList,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
val currencyList = walletState.walletsData.map { it.currency }
|
||||
val coinsList = walletState.walletsData.map { it.currency }
|
||||
globalState.tapWalletManager.loadFiatRate(
|
||||
fiatCurrency = fiatAppCurrency,
|
||||
currencies = currencyList,
|
||||
currencyId = appCurrencyId,
|
||||
coinsList = coinsList,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,8 +151,8 @@ class MultiWalletReducer {
|
|||
|
||||
is WalletAction.MultiWallet.SetPrimaryToken ->
|
||||
state.copy(primaryToken = action.token)
|
||||
is WalletAction.MultiWallet.FindTokensInUse -> state
|
||||
is WalletAction.MultiWallet.FindBlockchainsInUse -> state
|
||||
// is WalletAction.MultiWallet.FindTokensInUse -> state
|
||||
// is WalletAction.MultiWallet.FindBlockchainsInUse -> state
|
||||
is WalletAction.MultiWallet.SaveCurrencies -> state
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,13 +86,13 @@ class WarningMessageVH(val binding: LayoutWarningBinding) : RecyclerView.ViewHol
|
|||
binding.btnClose.hide()
|
||||
|
||||
val buttonAction =
|
||||
when (warning.titleResId) {
|
||||
R.string.warning_important_security_info -> {
|
||||
when {
|
||||
warning.titleResId == R.string.warning_important_security_info -> {
|
||||
View.OnClickListener {
|
||||
store.dispatch(WalletAction.ShowDialog.SignedHashesMultiWalletDialog)
|
||||
}
|
||||
}
|
||||
R.string.alert_funds_restoration_message -> {
|
||||
warning.messageResId == R.string.alert_funds_restoration_message -> {
|
||||
binding.btnClose.show()
|
||||
binding.btnClose.setOnClickListener {
|
||||
store.dispatch(GlobalAction.HideWarningMessage(warning))
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.tap.persistence
|
|||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.network.api.tangemTech.Coins
|
||||
import com.tangem.network.api.tangemTech.CurrenciesResponse
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -21,14 +21,14 @@ class FiatCurrenciesPrefStorage(
|
|||
}
|
||||
}
|
||||
|
||||
fun save(currencies: List<Coins.CurrenciesResponse.Currency>) {
|
||||
fun save(currencies: List<CurrenciesResponse.Currency>) {
|
||||
val json: String = converter.toJson(currencies)
|
||||
return preferences.edit().putString(FIAT_CURRENCIES_KEY, json).apply()
|
||||
}
|
||||
|
||||
fun restore(): List<Coins.CurrenciesResponse.Currency> {
|
||||
fun restore(): List<CurrenciesResponse.Currency> {
|
||||
val json = preferences.getString(FIAT_CURRENCIES_KEY, "")
|
||||
val type = converter.typedList(Coins.CurrenciesResponse.Currency::class.java)
|
||||
val type = converter.typedList(CurrenciesResponse.Currency::class.java)
|
||||
if (json.isNullOrBlank()) return emptyList()
|
||||
|
||||
return converter.fromJson(json, type) ?: emptyList()
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
<string name="currency_subtitle_expanded">Available networks</string>
|
||||
<string name="alert_manage_tokens_addresses_message">Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.</string>
|
||||
<string name="contract_address_copied_message">Contract address copied!</string>
|
||||
<string name="alert_funds_restoration_message">Funds restoration instuction</string>
|
||||
<string name="alert_funds_restoration_message">If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds</string>
|
||||
|
||||
<string name="common_custom">Custom</string>
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
<string name="currency_subtitle_expanded">Available networks</string>
|
||||
<string name="alert_manage_tokens_addresses_message">Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.</string>
|
||||
<string name="contract_address_copied_message">Contract address copied!</string>
|
||||
<string name="alert_funds_restoration_message">Funds restoration instuction</string>
|
||||
<string name="alert_funds_restoration_message">If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds</string>
|
||||
|
||||
<string name="common_custom">Custom</string>
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
<string name="currency_subtitle_expanded">Available networks</string>
|
||||
<string name="alert_manage_tokens_addresses_message">Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.</string>
|
||||
<string name="contract_address_copied_message">Contract address copied!</string>
|
||||
<string name="alert_funds_restoration_message">Funds restoration instuction</string>
|
||||
<string name="alert_funds_restoration_message">If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds</string>
|
||||
|
||||
<string name="common_custom">Custom</string>
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
<string name="currency_subtitle_expanded">Доступные сети</string>
|
||||
<string name="alert_manage_tokens_addresses_message">Внимание! Валюты на разных сетях имеют разные адреса. Убедитесь, что адрес соответствует сети, в которой вы отправляете средства.</string>
|
||||
<string name="contract_address_copied_message">Адрес контракта скопирован!</string>
|
||||
<string name="alert_funds_restoration_message">Инструкция по восстановлению средств</string>
|
||||
<string name="alert_funds_restoration_message">Если вы совершили ошибку с выбором сети при переводе средств с биржи, эта инструкция поможет вам восстановить средства</string>
|
||||
|
||||
<string name="common_custom">Пользовательский</string>
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
<string name="currency_subtitle_expanded">Available networks</string>
|
||||
<string name="alert_manage_tokens_addresses_message">Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.</string>
|
||||
<string name="contract_address_copied_message">Contract address copied!</string>
|
||||
<string name="alert_funds_restoration_message">Funds restoration instuction</string>
|
||||
<string name="alert_funds_restoration_message">If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds</string>
|
||||
|
||||
<string name="common_custom">Custom</string>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.domain
|
||||
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.network.api.tangemTech.Coins
|
||||
import com.tangem.network.api.tangemTech.CoinsResponse
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -11,9 +11,9 @@ sealed interface DomainDialog {
|
|||
data class DialogError(val error: DomainError) : DomainDialog
|
||||
|
||||
data class SelectTokenDialog(
|
||||
val items: List<Coins.CheckAddressResponse.Token.Contract>,
|
||||
val items: List<CoinsResponse.Coin.Network>,
|
||||
val networkIdConverter: (String) -> String,
|
||||
val onSelect: (Coins.CheckAddressResponse.Token.Contract) -> Unit,
|
||||
val onSelect: (CoinsResponse.Coin.Network) -> Unit,
|
||||
val onClose: VoidCallback = {}
|
||||
) : DomainDialog
|
||||
}
|
||||
|
|
@ -12,8 +12,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
|
|||
"binance-smart-chain/test" -> Blockchain.BSCTestnet
|
||||
"ethereum" -> Blockchain.Ethereum
|
||||
"ethereum/test" -> Blockchain.EthereumTestnet
|
||||
"polygon-pos" -> Blockchain.Polygon
|
||||
"polygon-pos/test" -> Blockchain.PolygonTestnet
|
||||
"polygon-pos", "matic-network" -> Blockchain.Polygon
|
||||
"polygon-pos/test", "matic-network/test" -> Blockchain.PolygonTestnet
|
||||
"solana" -> Blockchain.Solana
|
||||
"solana/test" -> Blockchain.SolanaTestnet
|
||||
"fantom" -> Blockchain.Fantom
|
||||
|
|
@ -26,11 +26,11 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
|
|||
"dogecoin" -> Blockchain.Dogecoin
|
||||
"ducatus" -> Blockchain.Ducatus
|
||||
"litecoin" -> Blockchain.Litecoin
|
||||
"rsk" -> Blockchain.RSK
|
||||
"rootstock" -> Blockchain.RSK
|
||||
"stellar" -> Blockchain.Stellar
|
||||
"stellar/test" -> Blockchain.StellarTestnet
|
||||
"tezos" -> Blockchain.Tezos
|
||||
"ripple" -> Blockchain.XRP
|
||||
"xrp", "ripple" -> Blockchain.XRP
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
@ -57,21 +57,36 @@ fun Blockchain.toNetworkId(): String {
|
|||
Blockchain.Fantom -> "fantom"
|
||||
Blockchain.FantomTestnet -> "fantom/test"
|
||||
Blockchain.Litecoin -> "litecoin"
|
||||
Blockchain.Polygon -> "matic-network"
|
||||
Blockchain.PolygonTestnet -> "matic-networks/test"
|
||||
Blockchain.Polygon -> "polygon-pos"
|
||||
Blockchain.PolygonTestnet -> "polygon-pos/test"
|
||||
Blockchain.RSK -> "rootstock"
|
||||
Blockchain.Stellar -> "stellar"
|
||||
Blockchain.StellarTestnet -> "stellar/test"
|
||||
Blockchain.Solana -> "solana"
|
||||
Blockchain.SolanaTestnet -> "solana/test"
|
||||
Blockchain.Tezos -> "tezos"
|
||||
Blockchain.XRP -> "ripple"
|
||||
Blockchain.XRP -> "xrp"
|
||||
}
|
||||
}
|
||||
|
||||
fun Blockchain.toCoinId(): String {
|
||||
return when (this) {
|
||||
Blockchain.BSC, Blockchain.Binance -> "binancecoin"
|
||||
else -> this.toNetworkId()
|
||||
Blockchain.Binance, Blockchain.BinanceTestnet, Blockchain.BSC, Blockchain.BSCTestnet -> "binancecoin"
|
||||
Blockchain.Bitcoin, Blockchain.BitcoinTestnet -> "bitcoin"
|
||||
Blockchain.BitcoinCash, Blockchain.BitcoinCashTestnet -> "bitcoin-cash"
|
||||
Blockchain.Ethereum, Blockchain.EthereumTestnet -> "ethereum"
|
||||
Blockchain.Stellar, Blockchain.StellarTestnet -> "stellar"
|
||||
Blockchain.Cardano, Blockchain.CardanoShelley -> "cardano"
|
||||
Blockchain.Polygon, Blockchain.PolygonTestnet -> "matic-network"
|
||||
Blockchain.Avalanche, Blockchain.AvalancheTestnet -> "avalanche-2"
|
||||
Blockchain.Solana, Blockchain.SolanaTestnet -> "solana"
|
||||
Blockchain.Fantom, Blockchain.FantomTestnet -> "fantom"
|
||||
Blockchain.Ducatus -> "ducatus"
|
||||
Blockchain.Litecoin -> "litecoin"
|
||||
Blockchain.RSK -> "rootstock"
|
||||
Blockchain.Tezos -> "tezos"
|
||||
Blockchain.XRP -> "ripple"
|
||||
Blockchain.Dogecoin -> "dogecoin"
|
||||
else -> "unknown"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
package com.tangem.domain.common.form
|
||||
|
||||
import com.tangem.blockchain.blockchains.binance.BinanceAddressService
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumAddressService
|
||||
import com.tangem.blockchain.blockchains.solana.SolanaAddressService
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.address.AddressService
|
||||
import com.tangem.common.Validator
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -46,12 +48,15 @@ class TokenContractAddressValidator : CustomTokenValidator<String>() {
|
|||
|
||||
private fun getAddressService(): AddressService {
|
||||
return when (blockchain) {
|
||||
Blockchain.Solana, Blockchain.SolanaTestnet -> SolanaAddressService()
|
||||
Blockchain.Unknown -> EthereumAddressService()
|
||||
Blockchain.Solana, Blockchain.SolanaTestnet -> SolanaAddressService()
|
||||
Blockchain.Binance -> BinanceAddressService()
|
||||
Blockchain.BinanceTestnet -> BinanceAddressService(true)
|
||||
else -> {
|
||||
if (blockchain.isEvm()) {
|
||||
EthereumAddressService()
|
||||
} else {
|
||||
Timber.e("Throw for blockchain: ${blockchain.fullName}")
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ interface Field<T> {
|
|||
|
||||
data class Data<Data>(
|
||||
val value: Data,
|
||||
val isUserInput: Boolean = true
|
||||
val isUserInput: Boolean
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,31 +10,37 @@ import kotlinx.coroutines.launch
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class ValueDebouncer<T>(
|
||||
private val debounce: Long = 400,
|
||||
private val onValueChanged: (T?) -> Unit
|
||||
private val initialValue: T,
|
||||
private val debounceDuration: Long = 400,
|
||||
private val onValueChanged: (T) -> Unit,
|
||||
private val onEmitValueReceived: (T) -> Unit = {},
|
||||
) {
|
||||
|
||||
private var value: T? = null
|
||||
private val debounceScope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
private val flow = MutableStateFlow(value)
|
||||
var emittedValue: T = initialValue
|
||||
private set
|
||||
var debounced: T = initialValue
|
||||
private set
|
||||
|
||||
private val debounceScope: CoroutineScope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
private val flow = MutableStateFlow(debounced)
|
||||
|
||||
init {
|
||||
initFlow()
|
||||
}
|
||||
|
||||
private fun initFlow() {
|
||||
debounceScope.launch {
|
||||
flow.filter { if (value == null) true else value != it }
|
||||
.debounce(debounce)
|
||||
flow.filter { if (debounced == null) true else debounced != it }
|
||||
.debounce(debounceDuration)
|
||||
.onEach {
|
||||
value = it
|
||||
debounced = it
|
||||
onValueChanged(it)
|
||||
}
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fun isDebounced(value: T): Boolean = this.debounced == value
|
||||
|
||||
fun emmit(emmitValue: T) {
|
||||
emittedValue = emmitValue
|
||||
onEmitValueReceived.invoke(emmitValue)
|
||||
debounceScope.launch { flow.emit(emmitValue) }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.domain.features.addCustomToken
|
||||
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.network.api.tangemTech.Coins
|
||||
import com.tangem.network.api.tangemTech.CoinsResponse
|
||||
import com.tangem.network.api.tangemTech.TangemTechService
|
||||
|
||||
/**
|
||||
|
|
@ -11,41 +11,31 @@ class AddCustomTokenService(
|
|||
private val tangemTechService: TangemTechService
|
||||
) {
|
||||
|
||||
suspend fun checkAddress(
|
||||
suspend fun findToken(
|
||||
contractAddress: String,
|
||||
networkId: String? = null
|
||||
): Result<List<Coins.CheckAddressResponse.Token>> {
|
||||
val result = tangemTechService.coins.checkAddress(contractAddress, networkId)
|
||||
networkId: String? = null,
|
||||
active: Boolean? = null,
|
||||
): Result<List<CoinsResponse.Coin>> {
|
||||
val result = tangemTechService.coins(contractAddress, networkId, active)
|
||||
return when (result) {
|
||||
is Result.Success -> {
|
||||
val resultTokens = result.data.tokens
|
||||
var tokensList = mutableListOf<Coins.CheckAddressResponse.Token>()
|
||||
resultTokens.forEach { token ->
|
||||
val contractsWithTheSameAddress = token.contracts
|
||||
.filter { it.address == contractAddress }
|
||||
.filter { it.decimalCount != null }
|
||||
if (contractsWithTheSameAddress.isNotEmpty()) {
|
||||
val newToken = token.copy(contracts = contractsWithTheSameAddress)
|
||||
tokensList.add(newToken)
|
||||
var coinsList = mutableListOf<CoinsResponse.Coin>()
|
||||
result.data.coins.forEach { coin ->
|
||||
val networksWithTheSameAddress = coin.networks
|
||||
.filter { it.contractAddress != null || it.decimalCount != null }
|
||||
.filter { it.contractAddress == contractAddress }
|
||||
if (networksWithTheSameAddress.isNotEmpty()) {
|
||||
val newToken = coin.copy(networks = networksWithTheSameAddress)
|
||||
coinsList.add(newToken)
|
||||
}
|
||||
}
|
||||
if (tokensList.size > 1) {
|
||||
if (coinsList.size > 1) {
|
||||
// https://tangem.slack.com/archives/GMXC6PP71/p1649672562078679
|
||||
tokensList = mutableListOf(tokensList[0])
|
||||
coinsList = mutableListOf(coinsList[0])
|
||||
}
|
||||
Result.Success(tokensList)
|
||||
Result.Success(coinsList)
|
||||
}
|
||||
is Result.Failure -> result
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun tokens(): List<Coins.TokensResponse.Token> {
|
||||
return when (val result = tangemTechService.coins.tokens()) {
|
||||
is Result.Success -> {
|
||||
val tokens = result.data.tokens
|
||||
tokens.filter { it.contracts.isNullOrEmpty() }
|
||||
}
|
||||
is Result.Failure -> emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,14 +19,14 @@ enum class CustomTokenFieldId : FieldId {
|
|||
|
||||
data class TokenField(
|
||||
override val id: FieldId,
|
||||
) : BaseDataField<String>(id, Field.Data(""))
|
||||
) : BaseDataField<String>(id, Field.Data("", false))
|
||||
|
||||
data class TokenBlockchainField(
|
||||
override val id: FieldId,
|
||||
val itemList: List<Blockchain>,
|
||||
) : BaseDataField<Blockchain>(id, Field.Data(Blockchain.Unknown))
|
||||
) : 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))
|
||||
) : BaseDataField<Blockchain>(id, Field.Data(Blockchain.Unknown, false))
|
||||
|
|
@ -23,7 +23,7 @@ import com.tangem.domain.redux.DomainState
|
|||
import com.tangem.domain.redux.dispatchOnMain
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.domain.redux.global.DomainGlobalAction
|
||||
import com.tangem.network.api.tangemTech.Coins
|
||||
import com.tangem.network.api.tangemTech.CoinsResponse
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -63,26 +63,28 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
|
||||
when (val error = ContractAddress.validateValue(address)) {
|
||||
null -> {
|
||||
// valid contract address
|
||||
ContractAddress.removeError()
|
||||
unlockTokenFields()
|
||||
updateTokenDetailFields(false)
|
||||
changeBlockchainNetworkList()
|
||||
checkAndUpdateAddButton()
|
||||
manageFoundTokenChanges(requestInfoAboutToken(address))
|
||||
}
|
||||
AddCustomTokenError.FieldIsEmpty -> {
|
||||
// empty contract address
|
||||
ContractAddress.removeError()
|
||||
clearTokenDetailsFields()
|
||||
updateTokenDetailFields(false)
|
||||
changeBlockchainNetworkList()
|
||||
return
|
||||
checkAndUpdateAddButton()
|
||||
}
|
||||
AddCustomTokenError.InvalidContractAddress -> {
|
||||
ContractAddress.addError(error)
|
||||
unlockTokenFields()
|
||||
updateTokenDetailFields(hubState.tokensAnyFieldsIsFilled())
|
||||
changeBlockchainNetworkList()
|
||||
return
|
||||
checkAndUpdateAddButton()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
if (!action.contractAddress.isUserInput) return
|
||||
|
||||
manageFoundTokenChanges(requestInfoAboutToken(address))
|
||||
}
|
||||
is OnTokenNetworkChanged -> {
|
||||
if (!action.blockchainNetwork.isUserInput) return
|
||||
|
|
@ -98,7 +100,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
selectedNetwork = action.blockchainNetwork.value
|
||||
)
|
||||
updateWarningAlreadyAdded(isAlreadyAdded)
|
||||
updateAddButton()
|
||||
checkAndUpdateAddButton()
|
||||
}
|
||||
}
|
||||
is OnTokenDerivationPathChanged -> {
|
||||
|
|
@ -114,19 +116,22 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
)
|
||||
}
|
||||
updateWarningAlreadyAdded(isAlreadyAdded)
|
||||
updateAddButton()
|
||||
checkAndUpdateAddButton()
|
||||
}
|
||||
is OnTokenNameChanged -> {
|
||||
changeBlockchainNetworkList()
|
||||
updateAddButton()
|
||||
// changeBlockchainNetworkList()
|
||||
// updateTokenDetailFields(hubState.tokensAnyFieldsIsFilled())
|
||||
checkAndUpdateAddButton()
|
||||
}
|
||||
is OnTokenSymbolChanged -> {
|
||||
changeBlockchainNetworkList()
|
||||
updateAddButton()
|
||||
// changeBlockchainNetworkList()
|
||||
// updateTokenDetailFields(hubState.tokensAnyFieldsIsFilled())
|
||||
checkAndUpdateAddButton()
|
||||
}
|
||||
is OnTokenDecimalsChanged -> {
|
||||
changeBlockchainNetworkList()
|
||||
updateAddButton()
|
||||
// changeBlockchainNetworkList()
|
||||
// updateTokenDetailFields(hubState.tokensAnyFieldsIsFilled())
|
||||
checkAndUpdateAddButton()
|
||||
}
|
||||
is OnAddCustomTokenClicked -> {
|
||||
val state = hubState
|
||||
|
|
@ -153,7 +158,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
}
|
||||
|
||||
/**
|
||||
* This feature is only needed until Solana tokens are added.
|
||||
* This feature is only needed until Solana coins are added.
|
||||
* While they are not there - this function excludes the Solana blockchain if the user has
|
||||
* filled in at least one field of the token.
|
||||
*/
|
||||
|
|
@ -167,6 +172,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
|
||||
val networkFieldIsFilled = Network.isFilled()
|
||||
val newNetworkField = Network.getField<TokenBlockchainField>().copy(itemList = newNetworkBlockchainList)
|
||||
newNetworkField.data = Field.Data(Network.getFieldValue(), newNetworkField.data.isUserInput)
|
||||
if (networkFieldIsFilled) {
|
||||
val listSameSize = networkBlockchainList.size == newNetworkBlockchainList.size
|
||||
val newListLessThanOld = networkBlockchainList.size > newNetworkBlockchainList.size
|
||||
|
|
@ -197,7 +203,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
|
||||
private suspend fun requestInfoAboutToken(
|
||||
contractAddress: String,
|
||||
): List<Coins.CheckAddressResponse.Token> {
|
||||
): List<CoinsResponse.Coin> {
|
||||
val tangemTechServiceManager = requireNotNull(hubState.tangemTechServiceManager)
|
||||
dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = true))))
|
||||
|
||||
|
|
@ -210,7 +216,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
// got the result faster than 500ms and the delay would only be the difference between them.
|
||||
delay(500)
|
||||
|
||||
val foundTokensResult = tangemTechServiceManager.checkAddress(contractAddress, selectedNetworkId)
|
||||
val foundTokensResult = tangemTechServiceManager.findToken(contractAddress, selectedNetworkId)
|
||||
val result = when (foundTokensResult) {
|
||||
is Result.Success -> foundTokensResult.data
|
||||
is Result.Failure -> {
|
||||
|
|
@ -223,16 +229,15 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
return result
|
||||
}
|
||||
|
||||
private suspend fun manageFoundTokenChanges(foundTokens: List<Coins.CheckAddressResponse.Token>) {
|
||||
private suspend fun manageFoundTokenChanges(foundTokens: List<CoinsResponse.Coin>) {
|
||||
if (foundTokens.isEmpty()) {
|
||||
// token not found - it's completely custom
|
||||
TokenAlreadyAdded.remove()
|
||||
PotentialScamToken.add()
|
||||
|
||||
dispatchOnMain(SetFoundTokenId(null))
|
||||
clearTokenFields()
|
||||
unlockTokenFields()
|
||||
updateAddButton()
|
||||
updateTokenDetailFields(true)
|
||||
checkAndUpdateAddButton()
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -240,24 +245,24 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
val foundToken = foundTokens[0]
|
||||
dispatchOnMain(SetFoundTokenId(foundToken.id))
|
||||
when {
|
||||
foundToken.contracts.isEmpty() -> {
|
||||
foundToken.networks.isEmpty() -> {
|
||||
Timber.e("Unexpected state -> throw to FB")
|
||||
}
|
||||
foundToken.contracts.size == 1 -> {
|
||||
foundToken.networks.size == 1 -> {
|
||||
// token with single contract address
|
||||
val singleTokenContract = foundToken.contracts[0]
|
||||
val singleTokenContract = foundToken.networks[0]
|
||||
fillTokenFields(foundToken, singleTokenContract)
|
||||
|
||||
val isInAppSavedTokens = isTokenPersistIntoAppSavedTokensList()
|
||||
if (isInAppSavedTokens) {
|
||||
lockTokenFields()
|
||||
lockAddButton()
|
||||
updateTokenDetailFields(false)
|
||||
updateAddButton(false)
|
||||
PotentialScamToken.replace(TokenAlreadyAdded)
|
||||
} else {
|
||||
// not in the saved tokens list
|
||||
if (singleTokenContract.active) {
|
||||
lockTokenFields()
|
||||
unlockAddButton()
|
||||
// not in the saved coins list
|
||||
if (foundToken.active) {
|
||||
updateTokenDetailFields(false)
|
||||
updateAddButton(true)
|
||||
if (hubState.derivationPathIsSelected()) {
|
||||
PotentialScamToken.add()
|
||||
} else {
|
||||
|
|
@ -265,7 +270,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
PotentialScamToken.remove()
|
||||
}
|
||||
} else {
|
||||
unlockAddButton()
|
||||
updateAddButton(true)
|
||||
PotentialScamToken.add()
|
||||
}
|
||||
}
|
||||
|
|
@ -274,7 +279,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
PotentialScamToken.replace(TokenAlreadyAdded)
|
||||
|
||||
val dialog = DomainDialog.SelectTokenDialog(
|
||||
items = foundToken.contracts,
|
||||
items = foundToken.networks,
|
||||
networkIdConverter = { networkId ->
|
||||
val blockchain = Blockchain.fromNetworkId(networkId)
|
||||
if (blockchain == null || blockchain == Blockchain.Unknown) {
|
||||
|
|
@ -286,8 +291,8 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
hubScope.launch {
|
||||
// find how to connect to the upper coroutineContext and dispatch through them
|
||||
fillTokenFields(foundToken, selectedContract)
|
||||
lockTokenFields()
|
||||
unlockAddButton()
|
||||
updateTokenDetailFields(false)
|
||||
updateAddButton(true)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -296,54 +301,38 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun replaceWarnings(
|
||||
warningsAdd: MutableSet<AddCustomTokenError.Warning> = mutableSetOf(),
|
||||
warningsRemove: MutableSet<AddCustomTokenError.Warning> = mutableSetOf(),
|
||||
) {
|
||||
if (warningsAdd.isNotEmpty() || warningsRemove.isNotEmpty()) {
|
||||
dispatchOnMain(Warning.Replace(warningsRemove.toSet(), warningsAdd.toSet()))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateAddButton() {
|
||||
private suspend fun checkAndUpdateAddButton() {
|
||||
val state = hubState
|
||||
if (state.warnings.contains(TokenAlreadyAdded)) {
|
||||
lockAddButton()
|
||||
updateAddButton(false)
|
||||
return
|
||||
}
|
||||
when {
|
||||
// token
|
||||
state.tokensOneFieldsIsFilled() -> {
|
||||
lockAddButton()
|
||||
state.tokensFieldsIsFilled() && state.networkIsSelected() -> {
|
||||
val error = ContractAddress.validateValue(ContractAddress.getFieldValue<String>())
|
||||
updateAddButton(error == null)
|
||||
}
|
||||
// token
|
||||
state.tokensFieldsIsFilled() && state.networkIsSelected() -> {
|
||||
unlockAddButton()
|
||||
state.tokensAnyFieldsIsFilled() -> {
|
||||
updateAddButton(false)
|
||||
}
|
||||
// blockchain
|
||||
else -> {
|
||||
if (state.networkIsSelected()) {
|
||||
val alreadyAdded = isBlockchainPersistIntoAppSavedTokensList()
|
||||
if (alreadyAdded) {
|
||||
lockAddButton()
|
||||
updateAddButton(false)
|
||||
} else {
|
||||
unlockAddButton()
|
||||
updateAddButton(true)
|
||||
}
|
||||
} else {
|
||||
lockAddButton()
|
||||
updateAddButton(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun lockAddButton() {
|
||||
dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(false)))
|
||||
}
|
||||
|
||||
private suspend fun unlockAddButton() {
|
||||
dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(true)))
|
||||
}
|
||||
|
||||
/**
|
||||
* These are helper functions.
|
||||
*/
|
||||
|
|
@ -490,46 +479,45 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
}
|
||||
|
||||
private suspend fun fillTokenFields(
|
||||
token: Coins.CheckAddressResponse.Token,
|
||||
contract: Coins.CheckAddressResponse.Token.Contract,
|
||||
token: CoinsResponse.Coin,
|
||||
coinNetwork: CoinsResponse.Coin.Network,
|
||||
) {
|
||||
val blockchain = Blockchain.fromNetworkId(contract.networkId) ?: Blockchain.Unknown
|
||||
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(contract.decimalCount.toString(), false))
|
||||
Decimals.setFieldValue(Field.Data(coinNetwork.decimalCount.toString(), false))
|
||||
dispatchOnMain(UpdateForm(hubState))
|
||||
}
|
||||
|
||||
private suspend fun clearTokenFields() {
|
||||
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 lockTokenFields() {
|
||||
private suspend fun updateTokenDetailFields(isEnabled: Boolean = true) {
|
||||
val state = hubState
|
||||
val action = Screen.UpdateTokenFields(listOf(
|
||||
Network to state.screenState.network.copy(isEnabled = false),
|
||||
Name to state.screenState.name.copy(isEnabled = false),
|
||||
Symbol to state.screenState.symbol.copy(isEnabled = false),
|
||||
Decimals to state.screenState.decimals.copy(isEnabled = false),
|
||||
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 unlockTokenFields() {
|
||||
val state = hubState
|
||||
private suspend fun updateBlockchainNetworkField(isEnabled: Boolean) {
|
||||
val action = Screen.UpdateTokenFields(listOf(
|
||||
Network to state.screenState.network.copy(isEnabled = true),
|
||||
Name to state.screenState.name.copy(isEnabled = true),
|
||||
Symbol to state.screenState.symbol.copy(isEnabled = true),
|
||||
Decimals to state.screenState.decimals.copy(isEnabled = true),
|
||||
Name to hubState.screenState.name.copy(isEnabled = isEnabled),
|
||||
))
|
||||
dispatchOnMain(action)
|
||||
}
|
||||
|
||||
private suspend fun updateAddButton(isEnabled: Boolean) {
|
||||
dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(isEnabled)))
|
||||
}
|
||||
|
||||
private suspend fun AddCustomTokenError.Warning.add() {
|
||||
dispatchOnMain(Warning.Add(setOf(this)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,14 +64,14 @@ data class AddCustomTokenState(
|
|||
}
|
||||
|
||||
// except network
|
||||
fun tokensOneFieldsIsFilled(): Boolean {
|
||||
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.size == 1
|
||||
return errorsList.isNotEmpty()
|
||||
}
|
||||
|
||||
fun networkIsSelected(): Boolean {
|
||||
|
|
@ -85,7 +85,7 @@ data class AddCustomTokenState(
|
|||
}
|
||||
|
||||
fun getCustomTokenType(): CustomTokenType = when {
|
||||
tokensOneFieldsIsFilled() || tokensFieldsIsFilled() -> CustomTokenType.Token
|
||||
tokensAnyFieldsIsFilled() || tokensFieldsIsFilled() -> CustomTokenType.Token
|
||||
else -> CustomTokenType.Blockchain
|
||||
}
|
||||
|
||||
|
|
@ -172,11 +172,11 @@ data class AddCustomTokenState(
|
|||
Blockchain.Unknown,
|
||||
Blockchain.Ethereum,
|
||||
Blockchain.BSC,
|
||||
Blockchain.Binance,
|
||||
Blockchain.Binance, // not evm
|
||||
Blockchain.Polygon,
|
||||
Blockchain.Avalanche,
|
||||
Blockchain.Fantom,
|
||||
Blockchain.Solana, // should be unsupported for tokens until they are added to the Blockchain SDK
|
||||
Blockchain.Solana, // not evm. Should be unsupported for coins until they are added to the Blockchain SDK
|
||||
)
|
||||
if (type == CustomTokenType.Token) networks.remove(Blockchain.Solana)
|
||||
|
||||
|
|
|
|||
|
|
@ -102,13 +102,24 @@ internal abstract class BaseStoreHub<State>(
|
|||
}
|
||||
|
||||
protected abstract suspend fun handleAction(action: Action, storeState: DomainState, cancel: ValueCallback<Action>)
|
||||
protected abstract fun reduceAction(action: Action, state: State): State
|
||||
|
||||
@Deprecated(
|
||||
replaceWith = ReplaceWith("ReStoreReducer<T>"),
|
||||
message = "must return a Reducer instance. The reducer must be a separate component - this closes " +
|
||||
"access to the states that can be obtained from ReStoreHub"
|
||||
)
|
||||
abstract fun reduceAction(action: Action, state: State): State
|
||||
// protected abstract fun getReducer(): ReStoreReducer<State>
|
||||
|
||||
protected abstract fun getHubState(storeState: DomainState): State
|
||||
protected abstract fun updateStoreState(storeState: DomainState, newHubState: State): DomainState
|
||||
|
||||
}
|
||||
|
||||
internal interface ReStoreReducer<State> {
|
||||
fun reduceAction(action: Action, state: State): State
|
||||
}
|
||||
|
||||
internal suspend inline fun ReStoreHub<*, *>.dispatchOnMain(vararg actions: Action) {
|
||||
withMainContext { actions.forEach { domainStore.dispatch(it) } }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,53 +8,39 @@ import java.math.BigDecimal
|
|||
interface HttpResponse
|
||||
sealed interface TangemTechResponse : HttpResponse
|
||||
|
||||
sealed class Coins : TangemTechResponse {
|
||||
data class PricesResponse(val prices: Map<String, Double>) : Coins()
|
||||
data class CoinsResponse(
|
||||
val imageHost: String,
|
||||
val coins: List<Coin>,
|
||||
val total: Int
|
||||
) : TangemTechResponse {
|
||||
|
||||
data class CheckAddressResponse(val imageHost: String?, val tokens: List<Token>, val total: Int) : Coins() {
|
||||
data class Token(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
val active: Boolean,
|
||||
val contracts: List<Contract>
|
||||
) {
|
||||
data class Contract(
|
||||
val networkId: String,
|
||||
val address: String,
|
||||
val decimalCount: BigDecimal?,
|
||||
val active: Boolean
|
||||
)
|
||||
}
|
||||
data class Coin(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
val active: Boolean,
|
||||
val networks: List<Network> = listOf()
|
||||
) : TangemTechResponse {
|
||||
|
||||
data class Network(
|
||||
val networkId: String,
|
||||
val contractAddress: String? = null,
|
||||
val decimalCount: BigDecimal? = null,
|
||||
) : TangemTechResponse
|
||||
}
|
||||
}
|
||||
|
||||
data class TokensResponse(val imageHost: String, val tokens: List<Token>, val total: Int) : Coins() {
|
||||
data class Token(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
val contracts: List<Contract>?
|
||||
) {
|
||||
data class Contract(
|
||||
val networkId: String,
|
||||
val address: String,
|
||||
val decimalCount: BigDecimal?,
|
||||
)
|
||||
}
|
||||
}
|
||||
//rates.keys = networkId's
|
||||
data class RatesResponse(val rates: Map<String, Double>) : TangemTechResponse
|
||||
|
||||
data class CurrenciesResponse(val currencies: List<Currency>) {
|
||||
data class Currency(
|
||||
val id: String,
|
||||
val code: String, // this is an uppercase id
|
||||
val name: String,
|
||||
val rateBTC: String,
|
||||
val unit: String, // $, €, ₽
|
||||
val type: String,
|
||||
)
|
||||
data class CurrenciesResponse(val currencies: List<Currency>) {
|
||||
|
||||
enum class CurrencyType(val type: String) {
|
||||
Fiat("fiat"), Crypto("crypto")
|
||||
}
|
||||
}
|
||||
data class Currency(
|
||||
val id: String,
|
||||
val code: String, // this is an uppercase id
|
||||
val name: String,
|
||||
val rateBTC: String,
|
||||
val unit: String, // $, €, ₽
|
||||
val type: String,
|
||||
) : TangemTechResponse
|
||||
}
|
||||
|
|
@ -8,22 +8,20 @@ import retrofit2.http.Query
|
|||
*/
|
||||
interface TangemTechApi {
|
||||
|
||||
@GET("coins/prices")
|
||||
suspend fun coinsPrices(
|
||||
@Query("currency") currency: String,
|
||||
@Query("ids") ids: String,
|
||||
): Coins.PricesResponse
|
||||
|
||||
@GET("coins/check-address")
|
||||
suspend fun coinsCheckAddress(
|
||||
@Query("contractAddress") contractAddress: String,
|
||||
@GET("coins")
|
||||
suspend fun coins(
|
||||
@Query("contractAddress") contractAddress: String? = null,
|
||||
@Query("networkId") networkId: String? = null,
|
||||
): Coins.CheckAddressResponse
|
||||
@Query("active") active: Boolean? = null,
|
||||
): CoinsResponse
|
||||
|
||||
@GET("coins/currencies")
|
||||
suspend fun coinsCurrencies(): Coins.CurrenciesResponse
|
||||
@GET("rates")
|
||||
suspend fun rates(
|
||||
@Query("currencyId") currencyId: String,
|
||||
@Query("coinIds") coinIds: String,
|
||||
): RatesResponse
|
||||
|
||||
@GET("coins/tokens")
|
||||
suspend fun coinsTokens(): Coins.TokensResponse
|
||||
@GET("currencies")
|
||||
suspend fun currencies(): CurrenciesResponse
|
||||
|
||||
}
|
||||
|
|
@ -12,19 +12,33 @@ import kotlinx.coroutines.withContext
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class TangemTechService {
|
||||
|
||||
val coins: CoinsRoute = CoinsRoute()
|
||||
|
||||
private val techRoutes: List<TangemTechRoute> = listOf(
|
||||
coins
|
||||
)
|
||||
|
||||
private val headerInterceptors = mutableListOf<AddHeaderInterceptor>(
|
||||
CacheControlHttpInterceptor(cacheMaxAge)
|
||||
)
|
||||
|
||||
|
||||
private var api: TangemTechApi = createApi()
|
||||
|
||||
suspend fun coins(
|
||||
contractAddress: String? = null,
|
||||
networkId: String? = null,
|
||||
active: Boolean? = null,
|
||||
): Result<CoinsResponse> = withContext(Dispatchers.IO) {
|
||||
performRequest { api.coins(contractAddress, networkId, active) }
|
||||
}
|
||||
|
||||
suspend fun rates(
|
||||
currency: String,
|
||||
ids: List<String>
|
||||
): Result<RatesResponse> = withContext(Dispatchers.IO) {
|
||||
performRequest {
|
||||
api.rates(currency.lowercase(), ids.joinToString(","))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun currencies(): Result<CurrenciesResponse> = withContext(Dispatchers.IO) {
|
||||
performRequest { api.currencies() }
|
||||
}
|
||||
|
||||
fun addHeaderInterceptors(interceptors: List<AddHeaderInterceptor>) {
|
||||
headerInterceptors.removeAll(interceptors)
|
||||
headerInterceptors.addAll(interceptors)
|
||||
|
|
@ -37,49 +51,11 @@ class TangemTechService {
|
|||
interceptors = headerInterceptors.toList(),
|
||||
// logEnabled = true,
|
||||
)
|
||||
return retrofit.create(TangemTechApi::class.java).apply {
|
||||
techRoutes.forEach { it.setApi(this) }
|
||||
}
|
||||
return retrofit.create(TangemTechApi::class.java)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val baseUrl = "https://api.tangem-tech.com/"
|
||||
const val baseUrl = "https://api.tangem-tech.com/v1/"
|
||||
const val cacheMaxAge = 600
|
||||
}
|
||||
}
|
||||
|
||||
private interface TangemTechRoute {
|
||||
fun setApi(api: TangemTechApi)
|
||||
}
|
||||
|
||||
class CoinsRoute : TangemTechRoute {
|
||||
private lateinit var api: TangemTechApi
|
||||
|
||||
override fun setApi(api: TangemTechApi) {
|
||||
this.api = api
|
||||
}
|
||||
|
||||
suspend fun prices(
|
||||
currency: String,
|
||||
ids: List<String>
|
||||
): Result<Coins.PricesResponse> = withContext(Dispatchers.IO) {
|
||||
performRequest {
|
||||
api.coinsPrices(currency.lowercase(), ids.joinToString(","))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkAddress(
|
||||
contractAddress: String,
|
||||
networkId: String? = null
|
||||
): Result<Coins.CheckAddressResponse> = withContext(Dispatchers.IO) {
|
||||
performRequest { api.coinsCheckAddress(contractAddress, networkId) }
|
||||
}
|
||||
|
||||
suspend fun currencies(): Result<Coins.CurrenciesResponse> = withContext(Dispatchers.IO) {
|
||||
performRequest { api.coinsCurrencies() }
|
||||
}
|
||||
|
||||
suspend fun tokens(): Result<Coins.TokensResponse> = withContext(Dispatchers.IO) {
|
||||
performRequest { api.coinsTokens() }
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue