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 ^