From d0ef5c70cd7cae67012243daf6c6d3fe6a150310 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 11 Apr 2026 09:38:37 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 9 ++ core/res/src/main/res/values/strings.xml | 1 + .../ic_dynamic_addresses_badge_16.xml | 9 ++ .../DefaultWalletManagersFacade.kt | 6 +- .../domain/models/TokenReceiveConfig.kt | 6 +- domain/transaction/build.gradle.kts | 2 + .../usecase/ReceiveAddressesFactory.kt | 90 ++++++++++++++----- .../tangempay/model/TangemPayAddFundsModel.kt | 4 +- .../tokenreceive/entity/ReceiveAddress.kt | 2 + .../entity/TokenReceiveStateFactory.kt | 68 ++++++++------ .../ui/TokenReceiveAssetsContent.kt | 36 ++++++++ gradle/tangem_dependencies.toml | 2 +- 12 files changed, 175 insertions(+), 60 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_dynamic_addresses_badge_16.xml diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 9b3b5503e7..d88224fe30 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -5,6 +5,9 @@ import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.models.DemoConfig +import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles +import com.tangem.domain.dynamicaddresses.GetDynamicReceiveAddressUseCase +import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.notifications.repository.PushNotificationsRepository @@ -257,10 +260,16 @@ internal object TransactionDomainModule { fun provideReceiveAddressesFactory( getEnsNameUseCase: GetEnsNameUseCase, getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, + getDynamicReceiveAddressUseCase: GetDynamicReceiveAddressUseCase, + dynamicAddressesRepository: DynamicAddressesRepository, + dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles, ): ReceiveAddressesFactory { return ReceiveAddressesFactory( getEnsNameUseCase = getEnsNameUseCase, getViewedTokenReceiveWarningUseCase = getViewedTokenReceiveWarningUseCase, + getDynamicReceiveAddressUseCase = getDynamicReceiveAddressUseCase, + dynamicAddressesRepository = dynamicAddressesRepository, + dynamicAddressesFeatureToggles = dynamicAddressesFeatureToggles, ) } diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 1ec9514666..586600272c 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -487,6 +487,7 @@ Disable dynamic addresses Dynamic addresses disabled Dynamic addresses enabled + Dynamic address Use a new address for each transaction to reduce traceability and improve on-chain privacy. Enhanced Privacy Easily receive funds in UTXO-based networks with automatic address generation — no manual address management required. diff --git a/core/ui/src/main/res/drawable/ic_dynamic_addresses_badge_16.xml b/core/ui/src/main/res/drawable/ic_dynamic_addresses_badge_16.xml new file mode 100644 index 0000000000..5ab98c661f --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_dynamic_addresses_badge_16.xml @@ -0,0 +1,9 @@ + + + diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt index 1055b56685..d86119e490 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt @@ -476,12 +476,12 @@ internal class DefaultWalletManagersFacade @Inject constructor( val dynamicAddressesManager = walletManager as? DynamicAddressesManager ?: return null return dynamicAddressesManager.usedAddresses .filter { usedAddress -> - val nodes = runCatching { DerivationPath(usedAddress.path).nodes }.getOrNull() + val nodes = runCatching { DerivationPath(usedAddress.derivationPath).nodes }.getOrNull() ?: return@filter false nodes.size >= XPUB_PATH_MIN_NODES && nodes[nodes.size - 2].index == RECEIVE_CHAIN_INDEX } .maxByOrNull { usedAddress -> - runCatching { DerivationPath(usedAddress.path).nodes.last().index }.getOrDefault(0L) + runCatching { DerivationPath(usedAddress.derivationPath).nodes.last().index }.getOrDefault(0L) } ?.address } @@ -490,7 +490,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = network) val dynamicAddressesManager = walletManager as? DynamicAddressesManager ?: return false return dynamicAddressesManager.usedAddresses.any { usedAddress -> - val nodes = runCatching { DerivationPath(usedAddress.path).nodes }.getOrNull() + val nodes = runCatching { DerivationPath(usedAddress.derivationPath).nodes }.getOrNull() ?: return@any false val isBaseAddress = nodes.size >= XPUB_PATH_MIN_NODES && nodes[nodes.size - 2].index == RECEIVE_CHAIN_INDEX && diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/TokenReceiveConfig.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/TokenReceiveConfig.kt index 467675af70..f0504c15ca 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/TokenReceiveConfig.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/TokenReceiveConfig.kt @@ -18,11 +18,11 @@ data class TokenReceiveConfig( @Serializable data class ReceiveAddressModel( - val nameService: NameService, + val displayType: DisplayType, val value: String, ) { - enum class NameService { - Default, Legacy, Ens + enum class DisplayType { + Default, Legacy, Ens, Dynamic, } } diff --git a/domain/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts index aae9c5368c..cc13ce06eb 100644 --- a/domain/transaction/build.gradle.kts +++ b/domain/transaction/build.gradle.kts @@ -27,6 +27,8 @@ dependencies { implementation(projects.libs.crypto) implementation(projects.domain.account.status) + implementation(projects.domain.dynamicAddresses) + implementation(projects.domain.dynamicAddresses.models) implementation(projects.domain.models) implementation(projects.domain.legacy) implementation(projects.domain.walletManager) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ReceiveAddressesFactory.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ReceiveAddressesFactory.kt index e0b596f81e..93a25ab440 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ReceiveAddressesFactory.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ReceiveAddressesFactory.kt @@ -1,5 +1,9 @@ package com.tangem.domain.transaction.usecase +import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles +import com.tangem.domain.dynamicaddresses.GetDynamicReceiveAddressUseCase +import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus +import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository import com.tangem.domain.models.Asset import com.tangem.domain.models.ReceiveAddressModel import com.tangem.domain.models.TokenReceiveConfig @@ -12,10 +16,15 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase import com.tangem.domain.transaction.R import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.flow.firstOrNull class ReceiveAddressesFactory( private val getEnsNameUseCase: GetEnsNameUseCase, private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, + private val getDynamicReceiveAddressUseCase: GetDynamicReceiveAddressUseCase, + private val dynamicAddressesRepository: DynamicAddressesRepository, + private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles, ) { suspend fun create( @@ -32,27 +41,14 @@ class ReceiveAddressesFactory( address = addresses.defaultAddress.value, ) - val receiveAddresses = buildList { - ensName?.let { ens -> - add( - ReceiveAddressModel( - nameService = ReceiveAddressModel.NameService.Ens, - value = ens, - ), - ) - } - addresses.availableAddresses.map { address -> - add( - ReceiveAddressModel( - nameService = when (address.type) { - NetworkAddress.Address.Type.Primary -> ReceiveAddressModel.NameService.Default - NetworkAddress.Address.Type.Secondary -> ReceiveAddressModel.NameService.Legacy - }, - value = address.value, - ), - ) - } + val dynamicAddress = getDynamicAddressIfEnabled(userWalletId, cryptoCurrency) + + val receiveAddresses = if (dynamicAddress != null) { + buildDynamicAddressList(ensName, dynamicAddress) + } else { + buildStandardAddressList(ensName, addresses) } + return TokenReceiveConfig( shouldShowWarning = cryptoCurrency.name !in getViewedTokenReceiveWarningUseCase(), cryptoCurrency = cryptoCurrency, @@ -64,6 +60,52 @@ class ReceiveAddressesFactory( ) } + private suspend fun getDynamicAddressIfEnabled( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): String? { + if (!dynamicAddressesFeatureToggles.isDynamicAddressesEnabled) return null + if (cryptoCurrency !is CryptoCurrency.Coin) return null + + val status = dynamicAddressesRepository.getStatus(userWalletId, cryptoCurrency.network).firstOrNull() + if (status != DynamicAddressesStatus.ENABLED) return null + + return getDynamicReceiveAddressUseCase(userWalletId, cryptoCurrency.network) + .onLeft { TangemLogger.e("Failed to get dynamic receive address: ${it.message}") } + .getOrNull() + } + + private fun buildDynamicAddressList(ensName: String?, dynamicAddress: String): List = + buildList { + ensName?.let { ens -> + add(ReceiveAddressModel(displayType = ReceiveAddressModel.DisplayType.Ens, value = ens)) + } + add( + ReceiveAddressModel( + displayType = ReceiveAddressModel.DisplayType.Dynamic, + value = dynamicAddress, + ), + ) + } + + private fun buildStandardAddressList(ensName: String?, addresses: NetworkAddress): List = + buildList { + ensName?.let { ens -> + add(ReceiveAddressModel(displayType = ReceiveAddressModel.DisplayType.Ens, value = ens)) + } + addresses.availableAddresses.map { address -> + add( + ReceiveAddressModel( + displayType = when (address.type) { + NetworkAddress.Address.Type.Primary -> ReceiveAddressModel.DisplayType.Default + NetworkAddress.Address.Type.Secondary -> ReceiveAddressModel.DisplayType.Legacy + }, + value = address.value, + ), + ) + } + } + suspend fun createForNft( userWalletId: UserWalletId, addresses: NetworkAddress, @@ -82,7 +124,7 @@ class ReceiveAddressesFactory( ensName?.let { ens -> add( ReceiveAddressModel( - nameService = ReceiveAddressModel.NameService.Ens, + displayType = ReceiveAddressModel.DisplayType.Ens, value = ens, ), ) @@ -90,9 +132,9 @@ class ReceiveAddressesFactory( addresses.availableAddresses.map { address -> add( ReceiveAddressModel( - nameService = when (address.type) { - NetworkAddress.Address.Type.Primary -> ReceiveAddressModel.NameService.Default - NetworkAddress.Address.Type.Secondary -> ReceiveAddressModel.NameService.Legacy + displayType = when (address.type) { + NetworkAddress.Address.Type.Primary -> ReceiveAddressModel.DisplayType.Default + NetworkAddress.Address.Type.Secondary -> ReceiveAddressModel.DisplayType.Legacy }, value = address.value, ), diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayAddFundsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayAddFundsModel.kt index b1a7a7133a..40b49906f5 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayAddFundsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayAddFundsModel.kt @@ -5,7 +5,7 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.domain.models.ReceiveAddressModel -import com.tangem.domain.models.ReceiveAddressModel.NameService +import com.tangem.domain.models.ReceiveAddressModel.DisplayType import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.pay.model.TangemPayTopUpData import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -42,7 +42,7 @@ internal class TangemPayAddFundsModel @Inject constructor( depositAddress = params.depositAddress, receiveAddress = listOf( ReceiveAddressModel( - nameService = NameService.Default, + displayType = DisplayType.Default, value = params.depositAddress, ), ), diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/ReceiveAddress.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/ReceiveAddress.kt index b5264d8402..1a65fcfde1 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/ReceiveAddress.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/ReceiveAddress.kt @@ -19,6 +19,8 @@ internal data class ReceiveAddress( data class Default(override val displayName: TextReference) : Primary data class Legacy(override val displayName: TextReference) : Primary + + data class Dynamic(override val displayName: TextReference) : Primary } } } \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/TokenReceiveStateFactory.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/TokenReceiveStateFactory.kt index bcd955a4af..8ca7790141 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/TokenReceiveStateFactory.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/TokenReceiveStateFactory.kt @@ -20,6 +20,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList +@Suppress("MagicNumber") internal class TokenReceiveStateFactory( private val currentStateProvider: Provider, private val cryptoCurrency: CryptoCurrency, @@ -66,8 +67,9 @@ internal class TokenReceiveStateFactory( compareBy { address -> when (address.type) { is Ens -> 0 - is Primary.Default -> 1 - is Primary.Legacy -> 2 + is Primary.Dynamic -> 1 + is Primary.Default -> 2 + is Primary.Legacy -> 3 } }, ) @@ -82,32 +84,18 @@ internal class TokenReceiveStateFactory( addresses: List, cryptoCurrency: CryptoCurrency, ): ImmutableList { - val needUseToLegacyAndDefaultName = addresses.any { it.nameService == ReceiveAddressModel.NameService.Legacy } + val shouldUseToLegacyAndDefaultName = addresses.any { it.displayType == ReceiveAddressModel.DisplayType.Legacy } val receiveAddresses = addresses.map { model -> - val type = when (model.nameService) { - ReceiveAddressModel.NameService.Default -> { - val displayName = when (cryptoCurrency) { - is CryptoCurrency.Coin -> cryptoCurrency.name - is CryptoCurrency.Token -> cryptoCurrency.symbol - } - - Primary.Default( - displayName = if (needUseToLegacyAndDefaultName) { - TextReference.Res(R.string.domain_receive_assets_default_address) - } else { - TextReference.Combined( - wrappedList( - TextReference.Str(displayName), - TextReference.Str(" "), - TextReference.Res(R.string.common_address), - ), - ) - }, - ) + val type = when (model.displayType) { + ReceiveAddressModel.DisplayType.Default -> { + Primary.Default(displayName = defaultDisplayName(cryptoCurrency, shouldUseToLegacyAndDefaultName)) } - ReceiveAddressModel.NameService.Ens -> Ens - ReceiveAddressModel.NameService.Legacy -> Primary.Legacy( + ReceiveAddressModel.DisplayType.Dynamic -> { + Primary.Dynamic(displayName = coinAddressDisplayName(cryptoCurrency)) + } + ReceiveAddressModel.DisplayType.Ens -> Ens + ReceiveAddressModel.DisplayType.Legacy -> Primary.Legacy( displayName = resourceReference( R.string.domain_receive_assets_legacy_address, WrappedList(listOf(cryptoCurrency.name)), @@ -124,8 +112,9 @@ internal class TokenReceiveStateFactory( compareBy { address -> when (address.type) { is Ens -> 0 - is Primary.Default -> 1 - is Primary.Legacy -> 2 + is Primary.Dynamic -> 1 + is Primary.Default -> 2 + is Primary.Legacy -> 3 } }, ) @@ -170,4 +159,29 @@ internal class TokenReceiveStateFactory( isGrayscale = false, shouldShowCustomBadge = false, ) + + private fun defaultDisplayName( + cryptoCurrency: CryptoCurrency, + needUseToLegacyAndDefaultName: Boolean, + ): TextReference { + return if (needUseToLegacyAndDefaultName) { + TextReference.Res(R.string.domain_receive_assets_default_address) + } else { + coinAddressDisplayName(cryptoCurrency) + } + } + + private fun coinAddressDisplayName(cryptoCurrency: CryptoCurrency): TextReference { + val displayName = when (cryptoCurrency) { + is CryptoCurrency.Coin -> cryptoCurrency.name + is CryptoCurrency.Token -> cryptoCurrency.symbol + } + return TextReference.Combined( + wrappedList( + TextReference.Str(displayName), + TextReference.Str(" "), + TextReference.Res(R.string.common_address), + ), + ) + } } \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveAssetsContent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveAssetsContent.kt index 63202e22b0..61602b31b4 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveAssetsContent.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveAssetsContent.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -300,6 +301,7 @@ private fun PrimaryAddressesItems( onShareClick = { onShareClick(selectedAddress.value) }, primaryType = selectedAddress.type as ReceiveAddress.Type.Primary, address = selectedAddress.value, + isDynamicAddress = selectedAddress.type is ReceiveAddress.Type.Primary.Dynamic, snackbarHostState = snackbarHostState, ) } @@ -345,6 +347,7 @@ private fun AddressItem( onShareClick: () -> Unit, primaryType: ReceiveAddress.Type.Primary, address: String, + isDynamicAddress: Boolean, snackbarHostState: SnackbarHostState, modifier: Modifier = Modifier, ) { @@ -369,6 +372,11 @@ private fun AddressItem( SpacerH(12.dp) + if (isDynamicAddress) { + DynamicAddressBadge() + SpacerH(8.dp) + } + Text( text = primaryType.displayName.resolveReference(), color = TangemTheme.colors.text.primary1, @@ -564,6 +572,34 @@ private fun ActionButtonWithResizableText(config: ActionButtonConfig, modifier: ) } +@Composable +private fun DynamicAddressBadge(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .background( + color = TangemTheme.colors.text.accent.copy(alpha = 0.1f), + shape = RoundedCornerShape(percent = 50), + ) + .padding(horizontal = 12.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + painter = painterResource( + id = R.drawable.ic_dynamic_addresses_badge_16, + ), + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = TangemTheme.colors.icon.accent, + ) + Text( + text = stringResourceSafe(R.string.dynamic_addresses_receive_badge), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.accent, + ) + } +} + @Composable @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index d9b87d6b95..7dce45b0c8 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1486" +tangemBlockchainSdk = "develop-1487" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-602" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^