Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-30 14:35:20 +07:00
parent 514eca0b00
commit 203b1c52cc
17 changed files with 152 additions and 29 deletions

View file

@ -478,8 +478,6 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
}
private val MEMO_RESTRICTED_NETWORKS = setOf(
Blockchain.XRP.toNetworkId(),
Blockchain.Stellar.toNetworkId(),
Blockchain.InternetComputer.toNetworkId(),
Blockchain.Casper.toNetworkId(),
Blockchain.Algorand.toNetworkId(),

View file

@ -38,6 +38,7 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.walletManager)
implementation(projects.domain.transaction)
implementation(projects.domain.wallets.models)
// endregion

View file

@ -13,6 +13,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
import com.tangem.domain.tokens.repository.YieldSupplyWarningsViewedRepository
import com.tangem.domain.transaction.MemoValidatorFacade
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -47,10 +48,12 @@ internal object TokensDataModule {
@Singleton
fun provideCurrencyChecksRepository(
walletManagersFacade: WalletManagersFacade,
memoValidatorFacade: MemoValidatorFacade,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
): CurrencyChecksRepository {
return DefaultCurrencyChecksRepository(
walletManagersFacade = walletManagersFacade,
memoValidatorFacade = memoValidatorFacade,
coroutineDispatchers = coroutineDispatcherProvider,
)
}

View file

@ -14,6 +14,7 @@ import com.tangem.domain.tokens.model.CurrencyAmount
import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.transaction.MemoValidatorFacade
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.isZero
@ -23,6 +24,7 @@ import java.math.BigDecimal
internal class DefaultCurrencyChecksRepository(
private val walletManagersFacade: WalletManagersFacade,
private val memoValidatorFacade: MemoValidatorFacade,
private val coroutineDispatchers: CoroutineDispatcherProvider,
) : CurrencyChecksRepository {
@ -112,6 +114,10 @@ internal class DefaultCurrencyChecksRepository(
return if (manager is ReserveAmountProvider) manager.isAccountFunded(address) else true
}
override suspend fun checkIfMemoRequired(network: Network, address: String): Boolean {
return memoValidatorFacade.isMemoRequired(network, address)
}
override suspend fun checkUtxoAmountLimit(
userWalletId: UserWalletId,
network: Network,

View file

@ -0,0 +1,42 @@
package com.tangem.data.transaction
import com.tangem.blockchain.common.memo.MemoState
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.domain.models.network.Network
import com.tangem.domain.transaction.MemoValidatorFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
internal class DefaultMemoValidatorFacade(
private val blockchainSDKFactory: BlockchainSDKFactory,
private val dispatchers: CoroutineDispatcherProvider,
) : MemoValidatorFacade {
override suspend fun isMemoRequired(network: Network, destinationAddress: String): Boolean =
withContext(dispatchers.io) {
val blockchain = network.toBlockchain()
val factory = blockchainSDKFactory.getMemoValidatorFactorySync() ?: return@withContext false
val validator = factory.create(blockchain)
when (val result = validator.isMemoRequired(destinationAddress)) {
is Result.Success -> result.data
is Result.Failure -> false
}
}
override suspend fun validateMemo(network: Network, memo: String): Boolean = withContext(dispatchers.io) {
val blockchain = network.toBlockchain()
val factory = blockchainSDKFactory.getMemoValidatorFactorySync() ?: return@withContext true
val validator = factory.create(blockchain)
when (val result = validator.validateMemo(memo)) {
is Result.Success -> when (result.data) {
MemoState.Valid,
MemoState.NotSupported,
-> true
MemoState.Invalid -> false
}
is Result.Failure -> true
}
}
}

View file

@ -6,12 +6,10 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.NameResolver
import com.tangem.blockchain.common.ResolveAddressResult
import com.tangem.blockchain.common.ReverseResolveAddressResult
import com.tangem.blockchain.common.TransactionValidator
import com.tangem.blockchain.common.memo.MemoState
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.transaction.MemoValidatorFacade
import com.tangem.domain.transaction.WalletAddressServiceRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.ParsedQrCode
@ -21,6 +19,7 @@ import kotlinx.coroutines.withContext
class DefaultWalletAddressServiceRepository(
private val walletManagersFacade: WalletManagersFacade,
private val memoValidatorFacade: MemoValidatorFacade,
private val dispatchers: CoroutineDispatcherProvider,
) : WalletAddressServiceRepository {
@ -96,28 +95,11 @@ class DefaultWalletAddressServiceRepository(
}
}
override suspend fun validateMemo(userWalletId: UserWalletId, network: Network, memo: String): Boolean =
withContext(dispatchers.io) {
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
network = network,
) ?: return@withContext true
override suspend fun validateMemo(network: Network, memo: String): Boolean =
memoValidatorFacade.validateMemo(network, memo)
val memoStateResult = (walletManager as? TransactionValidator)?.validateMemo(memo)
if (memoStateResult != null) {
when (memoStateResult) {
is Result.Success -> when (memoStateResult.data) {
MemoState.NotSupported,
MemoState.Valid,
-> true
MemoState.Invalid -> false
}
is Result.Failure -> true
}
} else {
true
}
}
override suspend fun isMemoRequired(network: Network, destinationAddress: String): Boolean =
memoValidatorFacade.isMemoRequired(network, destinationAddress)
override suspend fun parseSharedAddress(input: String, network: Network): ParsedQrCode {
val blockchain = network.toBlockchain()

View file

@ -3,15 +3,18 @@ package com.tangem.data.transaction.di
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
import com.tangem.data.transaction.DefaultFeeRepository
import com.tangem.data.transaction.DefaultGaslessTransactionRepository
import com.tangem.data.transaction.DefaultMemoValidatorFacade
import com.tangem.data.transaction.DefaultTransactionRepository
import com.tangem.data.transaction.DefaultWalletAddressServiceRepository
import com.tangem.data.transaction.error.DefaultFeeErrorResolver
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.datasource.api.gasless.GaslessTxServiceApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.GaslessTransactionRepository
import com.tangem.domain.transaction.MemoValidatorFacade
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.WalletAddressServiceRepository
import com.tangem.domain.transaction.error.FeeErrorResolver
@ -52,14 +55,28 @@ internal object TransactionDataModule {
)
}
@Provides
@Singleton
fun providesMemoValidatorFacade(
blockchainSDKFactory: BlockchainSDKFactory,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
): MemoValidatorFacade {
return DefaultMemoValidatorFacade(
blockchainSDKFactory = blockchainSDKFactory,
dispatchers = coroutineDispatcherProvider,
)
}
@Provides
@Singleton
fun providesWalletAddressServiceRepository(
walletManagersFacade: WalletManagersFacade,
memoValidatorFacade: MemoValidatorFacade,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
): WalletAddressServiceRepository {
return DefaultWalletAddressServiceRepository(
walletManagersFacade = walletManagersFacade,
memoValidatorFacade = memoValidatorFacade,
dispatchers = coroutineDispatcherProvider,
)
}

View file

@ -11,4 +11,5 @@ data class CryptoCurrencyCheck(
val utxoAmountLimit: UtxoAmountLimit?,
val isAccountFunded: Boolean,
val rentWarning: CryptoCurrencyWarning.Rent?,
val isMemoRequired: Boolean = false,
)

View file

@ -45,6 +45,14 @@ class GetCurrencyCheckUseCase(
} else {
false
}
val isMemoRequired = if (recipientAddress != null) {
currencyChecksRepository.checkIfMemoRequired(
network = network,
address = recipientAddress,
)
} else {
false
}
val utxoAmountLimit = if (currency is CryptoCurrency.Coin && amount != null && fee != null) {
currencyChecksRepository.checkUtxoAmountLimit(
userWalletId = userWalletId,
@ -65,6 +73,7 @@ class GetCurrencyCheckUseCase(
utxoAmountLimit = utxoAmountLimit,
isAccountFunded = isAccountFunded,
rentWarning = rentWarning,
isMemoRequired = isMemoRequired,
)
}
}

View file

@ -35,6 +35,9 @@ interface CurrencyChecksRepository {
/** Returns true if account with [address] was reserved with minimum amount */
suspend fun checkIfAccountFunded(userWalletId: UserWalletId, network: Network, address: String): Boolean
/** Returns true if a memo/destination tag is required for the given [address] on [network] */
suspend fun checkIfMemoRequired(network: Network, address: String): Boolean
/** Checks if transaction amount is within the UTXO limit */
suspend fun checkUtxoAmountLimit(
userWalletId: UserWalletId,

View file

@ -0,0 +1,21 @@
package com.tangem.domain.transaction
import com.tangem.domain.models.network.Network
/**
* Facade for memo validation operations.
*/
interface MemoValidatorFacade {
/**
* Returns true if a memo/destination tag is required for the given [destinationAddress] on [network].
* Returns false on network errors or for unsupported blockchains.
*/
suspend fun isMemoRequired(network: Network, destinationAddress: String): Boolean
/**
* Returns true if [memo] is valid for the given [network], or if memo is not supported.
* Returns true on errors (lenient fallback).
*/
suspend fun validateMemo(network: Network, memo: String): Boolean
}

View file

@ -23,7 +23,9 @@ interface WalletAddressServiceRepository {
suspend fun validateAddress(userWalletId: UserWalletId, network: Network, address: String): Boolean
suspend fun validateMemo(userWalletId: UserWalletId, network: Network, memo: String): Boolean
suspend fun validateMemo(network: Network, memo: String): Boolean
suspend fun isMemoRequired(network: Network, destinationAddress: String): Boolean
suspend fun parseSharedAddress(input: String, network: Network): ParsedQrCode
}

View file

@ -22,7 +22,6 @@ class ValidateWalletMemoUseCase(
): Either<ValidateMemoError, Unit> {
return try {
val isValidMemo = walletAddressServiceRepository.validateMemo(
userWalletId = userWalletId,
network = cryptoCurrency.network,
memo = memo,
)

View file

@ -34,6 +34,7 @@ import com.tangem.domain.tokens.GetCurrencyCheckUseCase
import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.features.send.v2.api.SendNotificationsComponent
import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData
@ -68,6 +69,7 @@ internal class NotificationsModel @Inject constructor(
private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase,
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
private val validateTransactionUseCase: ValidateTransactionUseCase,
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
private val getTronFeeNotificationShowCountUseCase: GetTronFeeNotificationShowCountUseCase,
private val incrementNotificationsShowCountUseCase: IncrementNotificationsShowCountUseCase,
private val notificationsUpdateTrigger: SendNotificationsUpdateTrigger,
@ -350,6 +352,10 @@ internal class NotificationsModel @Inject constructor(
params.callback.onAmountReduceTo(reduceTo)
},
)
addDestinationTagRequiredNotification(
isMemoRequired = currencyCheck.isMemoRequired,
memo = memo,
)
addHighFeeWarningNotification(
enteredAmountValue = enteredAmount,
cryptoCurrencyStatus = cryptoCurrencyStatus,
@ -370,6 +376,21 @@ internal class NotificationsModel @Inject constructor(
addTronNetworkFeesNotification()
}
private suspend fun MutableList<NotificationUM>.addDestinationTagRequiredNotification(
isMemoRequired: Boolean,
memo: String?,
) {
if (!isMemoRequired || contains(NotificationUM.Error.DestinationTagRequired)) return
val isMemoInvalid = memo.isNullOrEmpty() || validateWalletMemoUseCase(
userWalletId = userWalletId,
cryptoCurrency = currency,
memo = memo,
).isLeft()
if (isMemoInvalid) {
add(NotificationUM.Error.DestinationTagRequired)
}
}
private suspend fun MutableList<NotificationUM>.addTronNetworkFeesNotification() {
val cryptoCurrency = cryptoCurrencyStatus.currency
val isTronToken = cryptoCurrency is CryptoCurrency.Token &&

View file

@ -5,7 +5,7 @@
# https://github.com/tangem/tangem-sdk-android/
# https://github.com/tangem/vico
tangemBlockchainSdk = "develop-1455"
tangemBlockchainSdk = "develop-1461"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-598"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^

View file

@ -1,6 +1,7 @@
package com.tangem.blockchainsdk
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.common.memo.MemoValidatorFactory
/**
* Blockchain SDK components factory
@ -14,4 +15,7 @@ interface BlockchainSDKFactory {
/** Get [WalletManagerFactory] synchronously */
suspend fun getWalletManagerFactorySync(): WalletManagerFactory?
/** Get [MemoValidatorFactory] synchronously */
suspend fun getMemoValidatorFactorySync(): MemoValidatorFactory?
}

View file

@ -2,6 +2,7 @@ package com.tangem.blockchainsdk
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.common.memo.MemoValidatorFactory
import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager
import com.tangem.datasource.local.config.providers.models.ProviderModel
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -32,6 +33,7 @@ internal class DefaultBlockchainSDKFactory(
private val mainScope = CoroutineScope(dispatchers.main)
private val walletManagerFactory: Flow<WalletManagerFactory?> = createWalletManagerFactory()
private val memoValidatorFactory: Flow<MemoValidatorFactory?> = createMemoValidatorFactory()
override suspend fun init() {
coroutineScope {
@ -41,6 +43,8 @@ internal class DefaultBlockchainSDKFactory(
override suspend fun getWalletManagerFactorySync(): WalletManagerFactory? = walletManagerFactory.firstOrNull()
override suspend fun getMemoValidatorFactorySync(): MemoValidatorFactory? = memoValidatorFactory.firstOrNull()
private fun createWalletManagerFactory(): Flow<WalletManagerFactory?> {
return combine(
flow = flowOf(blockchainSdkConfig),
@ -51,4 +55,14 @@ internal class DefaultBlockchainSDKFactory(
// don't use Lazily because some features (WC) require initialized factory on app started
.stateIn(scope = mainScope, started = SharingStarted.Eagerly, initialValue = null)
}
private fun createMemoValidatorFactory(): Flow<MemoValidatorFactory?> {
return combine(
flow = flowOf(blockchainSdkConfig),
flow2 = blockchainProvidersTypesManager.get(),
) { config, providerTypes ->
MemoValidatorFactory(config = config, blockchainProviderTypes = providerTypes)
}
.stateIn(scope = mainScope, started = SharingStarted.Eagerly, initialValue = null)
}
}