Updated on 2026-08-14

This commit is contained in:
Tangem 2023-05-26 17:49:43 +08:00
parent b39427d355
commit d225269f60
9 changed files with 148 additions and 114 deletions

View file

@ -13,8 +13,10 @@
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="LINE_BREAK_AFTER_MULTILINE_WHEN_ENTRY" value="false" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="5" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="3" />
<option name="ALLOW_TRAILING_COMMA" value="true" />
<option name="BLANK_LINES_BEFORE_DECLARATION_WITH_COMMENT_OR_ANNOTATION_ON_SEPARATE_LINE" value="0" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.customtoken.impl.di
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.lib.crypto.DerivationManager
import com.tangem.tap.features.customtoken.impl.data.DefaultCustomTokenRepository
import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor
import com.tangem.tap.features.customtoken.impl.domain.DefaultCustomTokenInteractor
@ -26,7 +25,6 @@ internal object CustomTokenInteractorModule {
tangemTechApi: TangemTechApi,
appCoroutineDispatcherProvider: AppCoroutineDispatcherProvider,
reduxStateHolder: AppStateHolder,
derivationManager: DerivationManager,
): CustomTokenInteractor {
return DefaultCustomTokenInteractor(
featureRepository = DefaultCustomTokenRepository(
@ -34,7 +32,6 @@ internal object CustomTokenInteractorModule {
dispatchers = appCoroutineDispatcherProvider,
reduxStateHolder = reduxStateHolder,
),
derivationManager = derivationManager,
reduxStateHolder = reduxStateHolder,
)
}

View file

@ -1,8 +1,8 @@
package com.tangem.tap.features.customtoken.impl.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
import com.tangem.tap.features.wallet.models.Currency
/**
* Custom token interactor
@ -14,6 +14,6 @@ interface CustomTokenInteractor {
/** Find token by [address] and [blockchain] */
suspend fun findToken(address: String, blockchain: Blockchain): FoundToken
/** Save token [currency] with contact address [address] */
suspend fun saveToken(currency: Currency, address: String)
/** Save token [customCurrency] */
suspend fun saveToken(customCurrency: CustomCurrency)
}

View file

@ -1,34 +1,40 @@
package com.tangem.tap.features.customtoken.impl.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toMapKey
import com.tangem.common.flatMap
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.models.Currency.NativeToken
import com.tangem.lib.crypto.models.Currency.NonNativeToken
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.*
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.scope
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletCurrenciesManager
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay
import timber.log.Timber
/**
* Default implementation of custom token interactor
*
* @property featureRepository feature repository
* @property derivationManager derivation manager
* @property reduxStateHolder redux state holder
*
[REDACTED_AUTHOR]
*/
class DefaultCustomTokenInteractor(
private val featureRepository: CustomTokenRepository,
private val derivationManager: DerivationManager,
private val reduxStateHolder: AppStateHolder,
) : CustomTokenInteractor {
@ -39,58 +45,111 @@ class DefaultCustomTokenInteractor(
)
}
override suspend fun saveToken(currency: Currency, address: String) {
val hasDerivation = derivationManager.hasDerivation(
networkId = currency.blockchain.toNetworkId(),
derivationPath = requireNotNull(currency.derivationPath),
)
override suspend fun saveToken(customCurrency: CustomCurrency) {
val scanResponse = reduxStateHolder.scanResponse ?: return
if (!hasDerivation) {
derivationManager.deriveMissingBlockchains(
when (currency) {
is Currency.Blockchain -> NativeToken(
id = requireNotNull(currency.coinId),
name = currency.currencyName,
symbol = currency.currencySymbol,
networkId = currency.blockchain.toNetworkId(),
)
is Currency.Token -> NonNativeToken(
id = requireNotNull(currency.coinId),
name = currency.currencyName,
symbol = currency.currencySymbol,
networkId = currency.blockchain.toNetworkId(),
contractAddress = address,
decimalCount = currency.decimals,
)
},
)
val currency = Currency.fromCustomCurrency(customCurrency)
val isNeedToDerive = isNeedToDerive(scanResponse, currency)
if (isNeedToDerive) {
deriveMissingBlockchains(scanResponse = scanResponse, currencyList = listOf(currency)) {
submitAdd(scanResponse = it, currency = currency)
}
} else {
submitAdd(scanResponse, currency)
}
submitAdd(
scanResponse = requireNotNull(reduxStateHolder.scanResponse),
currency = currency,
)
}
private fun submitAdd(scanResponse: ScanResponse, currency: Currency) {
private fun isNeedToDerive(scanResponse: ScanResponse, currency: Currency): Boolean {
return currency.derivationPath?.let { !scanResponse.hasDerivation(currency.blockchain, it) } ?: false
}
private suspend fun deriveMissingBlockchains(
scanResponse: ScanResponse,
currencyList: List<Currency>,
onSuccess: suspend (ScanResponse) -> Unit,
) {
val derivationDataList = listOfNotNull(
getDerivations(EllipticCurve.Secp256k1, scanResponse, currencyList),
getDerivations(EllipticCurve.Ed25519, scanResponse, currencyList),
)
val derivations = derivationDataList.associate(TokensMiddleware.DerivationData::derivations)
if (derivations.isEmpty()) {
onSuccess(scanResponse)
return
}
when (val result = tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations)) {
is CompletionResult.Success -> {
val newDerivedKeys = result.data.entries
val oldDerivedKeys = scanResponse.derivedKeys
val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet()
val updatedDerivedKeys = walletKeys.associateWith { walletKey ->
val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap())
val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
ExtendedPublicKeysMap(oldDerivations + newDerivations)
}
val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys)
store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse))
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(updatedScanResponse)
}
is CompletionResult.Failure -> {
store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens"))
}
}
}
private fun getDerivations(
curve: EllipticCurve,
scanResponse: ScanResponse,
currencyList: List<Currency>,
): TokensMiddleware.DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val manageTokensCandidates = currencyList.map { it.blockchain }.distinct().filter {
it.getSupportedCurves().contains(curve)
}.mapNotNull {
it.derivationPath(scanResponse.card.derivationStyle)
}
val customTokensCandidates = currencyList.filter {
it.blockchain.getSupportedCurves().contains(curve)
}.mapNotNull { it.derivationPath }.map { DerivationPath(it) }
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct()
if (bothCandidates.isEmpty()) return null
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
val alreadyDerivedKeys: ExtendedPublicKeysMap =
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
if (toDerive.isEmpty()) return null
return TokensMiddleware.DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
}
private suspend fun submitAdd(scanResponse: ScanResponse, currency: Currency) {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to add currencies, no user wallet selected")
return
}
scope.launch {
userWalletsListManager.update(
userWalletId = selectedUserWallet.walletId,
update = { userWallet ->
userWallet.copy(scanResponse = scanResponse)
},
)
.flatMap { updatedUserWallet ->
walletCurrenciesManager.addCurrencies(
userWallet = updatedUserWallet,
currenciesToAdd = listOf(currency),
)
}
}
userWalletsListManager.update(
userWalletId = selectedUserWallet.walletId,
update = { userWallet -> userWallet.copy(scanResponse = scanResponse) },
)
.flatMap { updatedUserWallet ->
walletCurrenciesManager.addCurrencies(
userWallet = updatedUserWallet,
currenciesToAdd = listOf(currency),
)
}
}
}

View file

@ -1,8 +1,8 @@
package com.tangem.tap.features.customtoken.impl.presentation.viewmodels
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.common.analytics.events.ManageTokens
import com.tangem.tap.features.wallet.models.Currency
/** Analytics sender for tokens list screen */
class AddCustomTokenAnalyticsSender(private val analyticsEventHandler: AnalyticsEventHandler) {
@ -11,25 +11,7 @@ class AddCustomTokenAnalyticsSender(private val analyticsEventHandler: Analytics
analyticsEventHandler.send(ManageTokens.CustomToken.ScreenOpened)
}
fun sendWhenAddTokenButtonClicked(currency: Currency, address: String) {
analyticsEventHandler.send(
when (currency) {
is Currency.Blockchain -> {
ManageTokens.CustomToken.TokenWasAdded.Blockchain(
derivationPath = currency.derivationPath,
blockchain = currency.blockchain,
)
}
is Currency.Token -> {
ManageTokens.CustomToken.TokenWasAdded.Token(
symbol = currency.currencySymbol,
derivationPath = currency.derivationPath,
blockchain = currency.blockchain,
contractAddress = address,
)
}
},
)
fun sendWhenAddTokenButtonClicked(customCurrency: CustomCurrency) {
analyticsEventHandler.send(ManageTokens.CustomToken.TokenWasAdded(customCurrency))
}
}

View file

@ -18,6 +18,7 @@ import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.extensions.*
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
@ -198,6 +199,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
(blockchain.isEvm() || blockchain.canHandleTokens()) &&
reduxStateHolder.scanResponse?.card?.supportedBlockchains()?.contains(blockchain) == true
}
.sortedBy(Blockchain::fullName)
.map(::createNetworkSelectorItem)
}
@ -321,7 +323,9 @@ internal class AddCustomTokenViewModel @Inject constructor(
}
private fun isDerivationPathSelected(): Boolean {
return uiState.form.derivationPathSelectorField?.selectedItem?.blockchain != Blockchain.Unknown
return with(uiState.form.derivationPathSelectorField?.selectedItem?.blockchain) {
this != null && this != Blockchain.Unknown
}
}
private fun updateWarnings() {
@ -516,20 +520,16 @@ internal class AddCustomTokenViewModel @Inject constructor(
}
private fun getDerivationPath(): DerivationPath? {
val isNotDerivationPathSelected = !isDerivationPathSelected()
val network = if (isNotDerivationPathSelected) {
uiState.form.networkSelectorField.selectedItem.blockchain
val derivationStyle = if (!isDerivationPathSelected()) {
reduxStateHolder.scanResponse?.card?.derivationStyle
} else {
uiState.form.derivationPathSelectorField?.selectedItem?.blockchain
DerivationStyle.LEGACY
}
return network?.derivationPath(
style = if (isNotDerivationPathSelected) {
reduxStateHolder.scanResponse?.card?.derivationStyle
} else {
DerivationStyle.LEGACY
},
)
return when (val derivationNetwork = uiState.form.derivationPathSelectorField?.selectedItem?.blockchain) {
Blockchain.Unknown -> uiState.form.networkSelectorField.selectedItem.blockchain
else -> derivationNetwork
}?.derivationPath(derivationStyle)
}
private inner class ActionsHandler(private val featureRouter: CustomTokenRouter) {
@ -626,34 +626,32 @@ internal class AddCustomTokenViewModel @Inject constructor(
fun onAddCustomTokenClick() {
if (!isNetworkSelected()) return
val address = uiState.form.contractAddressInputField.value
val currency = when (getCustomTokenType()) {
CustomTokenType.TOKEN -> {
Currency.Token(
CustomCurrency.CustomToken(
token = Token(
name = uiState.form.tokenNameInputField.value,
symbol = uiState.form.tokenSymbolInputField.value,
contractAddress = address,
contractAddress = uiState.form.contractAddressInputField.value,
decimals = requireNotNull(uiState.form.decimalsInputField.value.toIntOrNull()),
id = foundToken?.id,
),
blockchain = uiState.form.networkSelectorField.selectedItem.blockchain,
derivationPath = getDerivationPath()?.rawPath,
network = uiState.form.networkSelectorField.selectedItem.blockchain,
derivationPath = getDerivationPath(),
)
}
CustomTokenType.BLOCKCHAIN -> {
Currency.Blockchain(
blockchain = uiState.form.networkSelectorField.selectedItem.blockchain,
derivationPath = getDerivationPath()?.rawPath,
CustomCurrency.CustomBlockchain(
network = uiState.form.networkSelectorField.selectedItem.blockchain,
derivationPath = getDerivationPath(),
)
}
}
analyticsSender.sendWhenAddTokenButtonClicked(currency, address)
analyticsSender.sendWhenAddTokenButtonClicked(currency)
viewModelScope.launch(dispatchers.main) {
runCatching(dispatchers.io) { featureInteractor.saveToken(currency, address) }
viewModelScope.launch(dispatchers.io) {
runCatching { featureInteractor.saveToken(currency) }
.onSuccess { featureRouter.openWalletScreen() }
.onFailure(Timber::e)
}

View file

@ -1,11 +1,7 @@
package com.tangem.tap.features.walletSelector.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
@ -26,7 +22,7 @@ internal fun RenameWalletDialogContent(dialog: DialogModel.RenameWalletDialog) {
TextInputDialog(
fieldValue = value,
confirmButton = DialogButton(
title = stringResource(id = R.string.common_save),
title = stringResource(id = R.string.common_ok),
enabled = value.text.isNotEmpty() && value.text != dialog.currentName,
onClick = { dialog.onConfirm(value.text) },
),

View file

@ -310,8 +310,8 @@ data class CardDTO(
internal fun fromSdkStatus(sdkStatus: Card.BackupStatus?): BackupStatus? {
return when (sdkStatus) {
is Card.BackupStatus.NoBackup -> NoBackup
is Card.BackupStatus.CardLinked -> CardLinked(sdkStatus.cardCount)
is Card.BackupStatus.Active -> Active(sdkStatus.cardCount)
is Card.BackupStatus.CardLinked -> CardLinked(sdkStatus.cardsCount)
is Card.BackupStatus.Active -> Active(sdkStatus.cardsCount)
null -> null
}
}

View file

@ -75,7 +75,7 @@ reactiveNetwork = "3.0.8"
# region Tangem
tangemBlockchainSdk = "develop-222"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-242"
tangemCardSdk = "develop-248"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds
# endregion Tangem