Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-10 15:12:05 +04:00
parent d5f9410905
commit 498e73d6df
16 changed files with 63 additions and 7 deletions

View file

@ -67,6 +67,7 @@ interface TangemExpressApi {
@Query("refundAddress") refundAddress: String?, // for cex only
@Query("refundExtraId") refundExtraId: String?, // for cex only
@Query("partnerOperationType") partnerOperationType: String?, // swap/ swap-and-send
@Query("toExtraId") toExtraId: String?, // swap-and-send memo
): ApiResponse<ExchangeDataResponse>
@GET("exchange-status")

View file

@ -35,6 +35,9 @@ data class ExchangeProvider(
@Json(name = "exchangeOnlyWithinSingleAddress")
val isExchangeOnlyWithinSingleAddress: Boolean = false,
@Json(name = "isExtraIdSupported")
val isExtraIdSupported: Boolean = false,
)
@JsonClass(generateAdapter = false)

View file

@ -20,6 +20,7 @@ internal class ExpressProviderConverter : Converter<ExchangeProvider, ExpressPro
isRecommended = value.isRecommended,
slippage = value.slippage,
isExchangeOnlyWithinSingleAddress = value.isExchangeOnlyWithinSingleAddress,
isExtraIdSupported = value.isExtraIdSupported,
)
}

View file

@ -208,6 +208,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
toCryptoCurrency: CryptoCurrency,
fromAmount: String,
toAddress: String,
toExtraId: String?,
expressProvider: ExpressProvider,
rateType: ExpressRateType,
expressOperationType: ExpressOperationType,
@ -247,6 +248,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
toExtraId = toExtraId,
).getOrThrow()
if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) {

View file

@ -17,6 +17,7 @@ import java.math.BigDecimal
* @property isRecommended flag that indicates if this provider is recommended
* @property slippage provider slippage
* @property isExchangeOnlyWithinSingleAddress flag that indicates if exchange is only allowed within a single address
* @property isExtraIdSupported flag that indicates if provider supports transaction extras (memo, destination tag)
*
* Uses to store transaction data in datastore, when extends - should always add default value
* to support backward compatibility
@ -43,4 +44,6 @@ data class ExpressProvider(
val slippage: BigDecimal?,
@Json(name = "exchangeOnlyWithinSingleAddress")
val isExchangeOnlyWithinSingleAddress: Boolean = false,
@Json(name = "isExtraIdSupported")
val isExtraIdSupported: Boolean = false,
)

View file

@ -83,6 +83,7 @@ interface SwapRepositoryV2 {
toCryptoCurrency: CryptoCurrency,
fromAmount: String,
toAddress: String,
toExtraId: String?,
expressProvider: ExpressProvider,
rateType: ExpressRateType,
expressOperationType: ExpressOperationType,

View file

@ -24,6 +24,7 @@ class GetSwapDataUseCase(
fromAmount: String,
toCryptoCurrency: CryptoCurrency,
toAddress: String,
toExtraId: String?,
expressProvider: ExpressProvider,
rateType: ExpressRateType,
expressOperationType: ExpressOperationType,
@ -34,6 +35,7 @@ class GetSwapDataUseCase(
fromAmount = fromAmount,
toCryptoCurrency = toCryptoCurrency,
toAddress = toAddress,
toExtraId = toExtraId,
expressProvider = expressProvider,
rateType = rateType,
expressOperationType = expressOperationType,

View file

@ -69,12 +69,20 @@ class GetSwapSupportedPairsUseCase(
.filter { pair ->
// Search available to swap currency
cryptoCurrencyList.any { currencyStatus ->
// Allowed only on networks without tx extras (e.i. memo and destination tag)
val isExtrasSupported = currencyStatus.network.transactionExtrasType.isTxExtrasSupported()
currencyStatus.id == pair.to.currency.id && !isExtrasSupported
currencyStatus.id == pair.to.currency.id
}
}
.map { pair -> SwapCryptoCurrency(groupingCurrency(pair), pair.providers) }
.map { pair ->
val toCurrency = groupingCurrency(pair)
val isTxExtrasSupported = toCurrency.currency.network.transactionExtrasType.isTxExtrasSupported()
val filteredProviders = if (isTxExtrasSupported) {
pair.providers.filter { it.isExtraIdSupported }
} else {
pair.providers
}
SwapCryptoCurrency(toCurrency, filteredProviders)
}
.filter { it.providers.isNotEmpty() }
.toList()
val unavailableCryptoCurrencies =

View file

@ -13,6 +13,7 @@ internal data class ConfirmData(
val reduceAmountBy: BigDecimal,
val isIgnoreReduce: Boolean,
val enteredDestination: String?,
val enteredMemo: String?,
val fee: Fee?,
val feeError: GetFeeError?,
val fromCryptoCurrencyStatus: CryptoCurrencyStatus?,

View file

@ -142,6 +142,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
reduceAmountBy = amountState?.reduceAmountBy.takeIf { isQuoteContent }.orZero(),
isIgnoreReduce = amountState?.isIgnoreReduce == true,
enteredDestination = destinationUM?.addressTextField?.actualAddress,
enteredMemo = destinationUM?.memoTextField?.value,
fee = feeSelectorUM?.selectedFeeItem?.fee.takeIf { isQuoteContent },
feeError = (uiState.value.feeSelectorUM as? FeeSelectorUM.Error)?.error.takeIf { isQuoteContent },
fromCryptoCurrencyStatus = amountUM?.swapDirection?.withSwapDirection(

View file

@ -104,6 +104,7 @@ internal class SwapTransactionSender @AssistedInject constructor(
fromAmount = fromAmount.toStringWithRightOffset(fromStatus.currency.decimals),
toCryptoCurrency = toStatus.currency,
toAddress = destination,
toExtraId = confirmData.enteredMemo,
expressProvider = provider,
rateType = rateType,
expressOperationType = expressOperationType,

View file

@ -125,7 +125,10 @@ private fun SuccessContent(sendWithSwapUM: SendWithSwapUM, modifier: Modifier =
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
)
DestinationBlock(destinationUM.addressTextField)
DestinationBlock(
address = destinationUM.addressTextField,
memo = destinationUM.memoTextField,
)
FeeBlock(feeSelectorUM = feeSelectorUM)
Spacer(Modifier.height(16.dp))
}
@ -247,7 +250,11 @@ private fun FeeBlock(feeSelectorUM: FeeSelectorUM.Content) {
}
@Composable
private fun DestinationBlock(address: DestinationTextFieldUM.RecipientAddress, modifier: Modifier = Modifier) {
private fun DestinationBlock(
address: DestinationTextFieldUM.RecipientAddress,
memo: DestinationTextFieldUM.RecipientMemo?,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
@ -279,6 +286,14 @@ private fun DestinationBlock(address: DestinationTextFieldUM.RecipientAddress, m
.background(TangemTheme.colors.background.tertiary),
)
}
if (memo != null && memo.value.isNotBlank()) {
Text(
text = stringResourceSafe(R.string.send_memo, memo.value),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
)
}
}
}
@ -304,7 +319,17 @@ private fun SendWithSwapSuccessContent_Preview() {
isValuePasted = false,
blockchainAddress = "0x391316d97a07027a0702c8A002c8A0C25d8470",
),
memoTextField = null,
memoTextField = DestinationTextFieldUM.RecipientMemo(
value = "123123123",
keyboardOptions = KeyboardOptions(),
placeholder = TextReference.EMPTY,
label = resourceReference(R.string.send_recipient),
isError = false,
error = null,
isValuePasted = false,
isEnabled = true,
disabledText = TextReference.EMPTY,
),
recent = persistentListOf(),
wallets = persistentListOf(),
networkName = "Polygon",

View file

@ -286,6 +286,7 @@ internal class DefaultSwapRepository(
expressOperationType: ExpressOperationType,
refundAddress: String?, // for cex only
refundExtraId: String?, // for cex only
toExtraId: String?, // for networks with memo only
): Either<ExpressDataError, SwapDataModel> {
return withContext(coroutineDispatcher.io) {
try {
@ -311,6 +312,7 @@ internal class DefaultSwapRepository(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
toExtraId = toExtraId,
).getOrThrow()
if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) {
val txDetails = parseTxDetails(response.txDetailsJson)

View file

@ -48,6 +48,7 @@ class SwapPairInfoConverter : Converter<SwapPairsWithProviders, PairsWithProvide
privacyPolicy = exchangeProvider.privacyPolicy,
isRecommended = exchangeProvider.isRecommended,
slippage = exchangeProvider.slippage,
isExtraIdSupported = exchangeProvider.isExtraIdSupported,
)
}
@ -67,6 +68,7 @@ class SwapPairInfoConverter : Converter<SwapPairsWithProviders, PairsWithProvide
privacyPolicy = additionalProvider.privacyPolicy,
isRecommended = additionalProvider.isRecommended,
slippage = additionalProvider.slippage,
isExtraIdSupported = additionalProvider.isExtraIdSupported,
)
}

View file

@ -68,6 +68,7 @@ interface SwapRepository {
expressOperationType: ExpressOperationType,
refundAddress: String? = null, // for cex only
refundExtraId: String? = null, // for cex only
toExtraId: String? = null, // for networks with memo only
): Either<ExpressDataError, SwapDataModel>
// TODO: Add target error handling, remove either ([REDACTED_JIRA])

View file

@ -53,6 +53,8 @@ data class SwapProvider(
val isRecommended: Boolean = false,
@Json(name = "slippage")
val slippage: BigDecimal?,
@Json(name = "isExtraIdSupported")
val isExtraIdSupported: Boolean = false,
)
@JsonClass(generateAdapter = false)