Updated on 2026-08-14
This commit is contained in:
parent
b39427d355
commit
d225269f60
9 changed files with 148 additions and 114 deletions
2
.idea/codeStyles/Project.xml
generated
2
.idea/codeStyles/Project.xml
generated
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue