Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-13 17:42:41 +08:00
parent e5d135edb4
commit a0834799ea
15 changed files with 141 additions and 64 deletions

View file

@ -130,7 +130,7 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data
android:host="sell-request.tangem.com" android:host="redirect_sell"
android:scheme="tangem" /> android:scheme="tangem" />
</intent-filter> </intent-filter>

View file

@ -318,6 +318,7 @@ object TradeCryptoMiddleware {
SendRouter.TRANSACTION_ID_KEY to txInfo?.transactionId, SendRouter.TRANSACTION_ID_KEY to txInfo?.transactionId,
SendRouter.DESTINATION_ADDRESS_KEY to txInfo?.destinationAddress, SendRouter.DESTINATION_ADDRESS_KEY to txInfo?.destinationAddress,
SendRouter.AMOUNT_KEY to txInfo?.amount, SendRouter.AMOUNT_KEY to txInfo?.amount,
SendRouter.TAG_KEY to txInfo?.tag,
) )
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle)) store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle))
} }

View file

@ -0,0 +1,37 @@
package com.tangem.tap.network.exchangeServices.moonpay
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Blockchain.*
import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPaySupportedCurrency
internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency?
get() = when (this) {
Algorand -> MoonPaySupportedCurrency(networkCode = "algorand", currencyCode = "algo")
Aptos -> MoonPaySupportedCurrency(networkCode = "aptos", currencyCode = "apt")
Arbitrum -> MoonPaySupportedCurrency(networkCode = "arbitrum", currencyCode = "eth_arbitrum")
Avalanche -> MoonPaySupportedCurrency(networkCode = "avalanche_c_chain", currencyCode = "avax_cchain")
Binance -> MoonPaySupportedCurrency(networkCode = "bnb_chain", currencyCode = "bnb")
Bitcoin -> MoonPaySupportedCurrency(networkCode = "bitcoin", currencyCode = "btc")
BitcoinCash -> MoonPaySupportedCurrency(networkCode = "bitcoin_cash", currencyCode = "bch")
BSC -> MoonPaySupportedCurrency(networkCode = "binance_smart_chain", currencyCode = "bnb_bsc")
Cardano -> MoonPaySupportedCurrency(networkCode = "cardano", currencyCode = "ada")
Cosmos -> MoonPaySupportedCurrency(networkCode = "cosmos", currencyCode = "atom")
Dogecoin -> MoonPaySupportedCurrency(networkCode = "dogecoin", currencyCode = "doge")
Ethereum -> MoonPaySupportedCurrency(networkCode = "ethereum", currencyCode = "eth")
EthereumClassic -> MoonPaySupportedCurrency(networkCode = "ethereum_classic", currencyCode = "etc")
Hedera -> MoonPaySupportedCurrency(networkCode = "hedera", currencyCode = "hbar")
Litecoin -> MoonPaySupportedCurrency(networkCode = "litecoin", currencyCode = "ltc")
Near -> MoonPaySupportedCurrency(networkCode = "near", currencyCode = "near")
Optimism -> MoonPaySupportedCurrency(networkCode = "optimism", currencyCode = "eth_optimism")
Polkadot -> MoonPaySupportedCurrency(networkCode = "polkadot", currencyCode = "dot")
Polygon -> MoonPaySupportedCurrency(networkCode = "polygon", currencyCode = "matic_polygon")
Ravencoin -> MoonPaySupportedCurrency(networkCode = "ravencoin", currencyCode = "rvn")
Solana -> MoonPaySupportedCurrency(networkCode = "solana", currencyCode = "sol")
Stellar -> MoonPaySupportedCurrency(networkCode = "stellar", currencyCode = "xlm")
Tezos -> MoonPaySupportedCurrency(networkCode = "tezos", currencyCode = "xtz")
TON -> MoonPaySupportedCurrency(networkCode = "ton", currencyCode = "ton")
Tron -> MoonPaySupportedCurrency(networkCode = "tron", currencyCode = "trx")
VeChain -> MoonPaySupportedCurrency(networkCode = "vechain", currencyCode = "vet")
XRP -> MoonPaySupportedCurrency(networkCode = "ripple", currencyCode = "xrp")
else -> null
}

View file

@ -12,6 +12,7 @@ import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SCHEME import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SCHEME
import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency
import javax.crypto.Mac import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
@ -35,42 +36,46 @@ class MoonPayService(
override suspend fun update() { override suspend fun update() {
withIOContext { withIOContext {
performRequest { performRequest {
val userStatusResult = performRequest { api.getUserStatus(apiKey) } val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) {
if (userStatusResult is Result.Failure) return@performRequest is Result.Failure -> return@performRequest
is Result.Success -> result.data
val currenciesResult = performRequest { api.getCurrencies(apiKey) }
if (currenciesResult is Result.Failure) return@performRequest
val userStatus = (userStatusResult as Result.Success).data
val currencies = (currenciesResult as Result.Success).data
// val currenciesToBuy = mutableListOf<String>()
val currenciesToSell = mutableListOf<String>()
currencies.forEach { currencyStatus ->
if (currencyStatus.type != "crypto" || currencyStatus.isSuspended ||
!currencyStatus.supportsLiveMode
) {
return@forEach
}
if (userStatus.countryCode == "USA") {
if (!currencyStatus.isSupportedInUS) return@forEach
if (currencyStatus.notAllowedUSStates.contains(userStatus.stateCode)) return@forEach
}
val currencyCode = currencyStatus.code.uppercase()
// currenciesToBuy.add(currencyCode)
if (currencyStatus.isSellSupported) currenciesToSell.add(currencyCode)
} }
// currenciesToBuy.sort()
currenciesToSell.sort() val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) {
is Result.Failure -> return@performRequest
is Result.Success -> result.data
}
val currenciesToSell = currencies
.filter { currency ->
checkGeneralRequirements(currency) && checkUSARequirements(userStatus, currency)
}
.mapNotNull { currency ->
MoonPayAvailableCurrency(
currencyCode = currency.code,
networkCode = currency.metadata?.networkCode ?: return@mapNotNull null,
contractAddress = currency.metadata.contractAddress,
)
}
status = MoonPayStatus(currenciesToSell, userStatus, currencies) status = MoonPayStatus(currenciesToSell, userStatus, currencies)
} }
} }
} }
private fun checkGeneralRequirements(currency: MoonPayCurrencies): Boolean {
return currency.type == "crypto" && !currency.isSuspended && currency.supportsLiveMode &&
currency.isSellSupported
}
private fun checkUSARequirements(userStatus: MoonPayUserStatus, currency: MoonPayCurrencies): Boolean {
return if (userStatus.countryCode == "USA") {
currency.isSupportedInUS && !currency.notAllowedUSStates.contains(userStatus.stateCode)
} else {
true
}
}
override fun isBuyAllowed(): Boolean = false override fun isBuyAllowed(): Boolean = false
override fun isSellAllowed(): Boolean { override fun isSellAllowed(): Boolean {
@ -80,21 +85,21 @@ class MoonPayService(
override fun availableForBuy(currency: Currency): Boolean = false override fun availableForBuy(currency: Currency): Boolean = false
override fun availableForSell(currency: Currency): Boolean { override fun availableForSell(currency: Currency): Boolean {
val availableForSell = status?.availableForSell ?: return false
val metadata = status?.responseCurrencies?.filter { it.isSellSupported }?.map { it.metadata }
if (!isSellAllowed()) return false if (!isSellAllowed()) return false
return when (currency) { val availableForSell = status?.availableForSell ?: return false
is Currency.Blockchain -> {
val blockchain = currency.blockchain val supportedCurrency = currency.blockchain.moonPaySupportedCurrency ?: return false
when { return availableForSell.any {
blockchain.isTestnet() -> false when (currency) {
blockchain == Blockchain.Unknown || currency.blockchain == Blockchain.BSC -> false is Currency.Blockchain -> {
else -> availableForSell.contains(currency.currencySymbol) it.networkCode.equals(other = supportedCurrency.networkCode, ignoreCase = true) &&
it.currencyCode.equals(other = supportedCurrency.currencyCode, ignoreCase = true)
}
is Currency.Token -> {
it.networkCode.equals(other = supportedCurrency.networkCode, ignoreCase = true) &&
it.contractAddress.equals(other = currency.token.contractAddress, ignoreCase = true)
} }
}
is Currency.Token -> {
metadata?.any { it?.contractAddress.equals(currency.token.contractAddress, ignoreCase = true) } ?: false
} }
} }
} }
@ -103,7 +108,7 @@ class MoonPayService(
action: CurrencyExchangeManager.Action, action: CurrencyExchangeManager.Action,
blockchain: Blockchain, blockchain: Blockchain,
cryptoCurrencyName: CryptoCurrencyName, cryptoCurrencyName: CryptoCurrencyName,
fatCurrency: String, fiatCurrencyName: String,
walletAddress: String, walletAddress: String,
isDarkTheme: Boolean, isDarkTheme: Boolean,
): String { ): String {
@ -115,7 +120,8 @@ class MoonPayService(
.appendQueryParameter("apiKey", apiKey) .appendQueryParameter("apiKey", apiKey)
.appendQueryParameter("baseCurrencyCode", cryptoCurrencyName) .appendQueryParameter("baseCurrencyCode", cryptoCurrencyName)
.appendQueryParameter("refundWalletAddress", walletAddress) .appendQueryParameter("refundWalletAddress", walletAddress)
.appendQueryParameter("redirectURL", "tangem://sell-request.tangem.com") .appendQueryParameter("redirectURL", "tangem://redirect_sell")
if (isDarkTheme) uri.appendQueryParameter("theme", "dark") if (isDarkTheme) uri.appendQueryParameter("theme", "dark")
val originalQuery = uri.build().encodedQuery ?: uri.build().toString() val originalQuery = uri.build().encodedQuery ?: uri.build().toString()
@ -147,7 +153,7 @@ class MoonPayService(
} }
private data class MoonPayStatus( private data class MoonPayStatus(
val availableForSell: List<String>, val availableForSell: List<MoonPayAvailableCurrency>,
val responseUserStatus: MoonPayUserStatus, val responseUserStatus: MoonPayUserStatus,
val responseCurrencies: List<MoonPayCurrencies>, val responseCurrencies: List<MoonPayCurrencies>,
) )

View file

@ -0,0 +1,7 @@
package com.tangem.tap.network.exchangeServices.moonpay.models
internal data class MoonPayAvailableCurrency(
val currencyCode: String,
val networkCode: String,
val contractAddress: String?,
)

View file

@ -0,0 +1,3 @@
package com.tangem.tap.network.exchangeServices.moonpay.models
internal data class MoonPaySupportedCurrency(val networkCode: String, val currencyCode: String?)

View file

@ -4,13 +4,14 @@ import com.tangem.core.deeplink.DeepLink
class SellCurrencyDeepLink(val onReceive: (data: Data) -> Unit) : DeepLink { class SellCurrencyDeepLink(val onReceive: (data: Data) -> Unit) : DeepLink {
override val uri: String = "tangem://sell-request.tangem.com" override val uri: String = "tangem://redirect_sell"
override fun onReceive(params: Map<String, String>) { override fun onReceive(params: Map<String, String>) {
val data = Data( val data = Data(
transactionId = params["transactionId"] ?: return, transactionId = params["transactionId"] ?: return,
baseCurrencyAmount = params["baseCurrencyAmount"] ?: return, baseCurrencyAmount = params["baseCurrencyAmount"] ?: return,
depositWalletAddress = params["depositWalletAddress"] ?: return, depositWalletAddress = params["depositWalletAddress"] ?: return,
depositWalletAddressTag = params["depositWalletAddressTag"],
) )
onReceive(data) onReceive(data)
@ -20,5 +21,6 @@ class SellCurrencyDeepLink(val onReceive: (data: Data) -> Unit) : DeepLink {
val transactionId: String, val transactionId: String,
val baseCurrencyAmount: String, val baseCurrencyAmount: String,
val depositWalletAddress: String, val depositWalletAddress: String,
val depositWalletAddressTag: String?,
) )
} }

View file

@ -41,8 +41,9 @@ sealed class TradeCryptoAction : Action {
data class Swap(val cryptoCurrency: CryptoCurrency) : TradeCryptoAction() data class Swap(val cryptoCurrency: CryptoCurrency) : TradeCryptoAction()
data class TransactionInfo( data class TransactionInfo(
val amount: String,
val destinationAddress: String,
val transactionId: String, val transactionId: String,
val destinationAddress: String,
val amount: String,
val tag: String? = null,
) )
} }

View file

@ -11,6 +11,7 @@ interface SendRouter {
const val USER_WALLET_ID_KEY = "send_user_wallet_id" const val USER_WALLET_ID_KEY = "send_user_wallet_id"
const val TRANSACTION_ID_KEY = "send_transaction_id" const val TRANSACTION_ID_KEY = "send_transaction_id"
const val AMOUNT_KEY = "send_amount" const val AMOUNT_KEY = "send_amount"
const val TAG_KEY = "send_tag"
const val DESTINATION_ADDRESS_KEY = "send_destination_address" const val DESTINATION_ADDRESS_KEY = "send_destination_address"
} }
} }

View file

@ -87,16 +87,18 @@ internal class SendStateFactory(
val state = currentStateProvider() val state = currentStateProvider()
return state.copy( return state.copy(
amountState = state.amountState ?: amountStateConverter.convert(""), amountState = state.amountState ?: amountStateConverter.convert(""),
recipientState = state.recipientState ?: recipientStateConverter.convert(""), recipientState = state.recipientState
?: recipientStateConverter.convert(SendRecipientStateConverter.Data("", null)),
feeState = state.feeState ?: feeStateConverter.convert(Unit), feeState = state.feeState ?: feeStateConverter.convert(Unit),
) )
} }
fun getReadyState(amount: String, destinationAddress: String): SendUiState { fun getReadyState(amount: String, destinationAddress: String, memo: String?): SendUiState {
val state = currentStateProvider() val state = currentStateProvider()
return state.copy( return state.copy(
amountState = state.amountState ?: amountStateConverter.convert(amount), amountState = state.amountState ?: amountStateConverter.convert(amount),
recipientState = state.recipientState ?: recipientStateConverter.convert(destinationAddress), recipientState = state.recipientState
?: recipientStateConverter.convert(SendRecipientStateConverter.Data(destinationAddress, memo)),
feeState = state.feeState ?: feeStateConverter.convert(Unit), feeState = state.feeState ?: feeStateConverter.convert(Unit),
isEditingDisabled = true, isEditingDisabled = true,
) )

View file

@ -1,5 +1,6 @@
package com.tangem.features.send.impl.presentation.state.recipient package com.tangem.features.send.impl.presentation.state.recipient
import androidx.annotation.StringRes
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
@ -15,13 +16,18 @@ import com.tangem.utils.converter.Converter
internal class SendRecipientMemoFieldConverter( internal class SendRecipientMemoFieldConverter(
private val clickIntents: SendClickIntents, private val clickIntents: SendClickIntents,
private val cryptoCurrencyStatus: Provider<CryptoCurrencyStatus>, private val cryptoCurrencyStatus: Provider<CryptoCurrencyStatus>,
) : Converter<Int, SendTextField.RecipientMemo> { ) : Converter<SendRecipientMemoFieldConverter.Data, SendTextField.RecipientMemo> {
fun convertOrNull(): SendTextField.RecipientMemo? { fun convertOrNull(memoValue: String?): SendTextField.RecipientMemo? {
val cryptoCurrency = cryptoCurrencyStatus().currency val cryptoCurrency = cryptoCurrencyStatus().currency
val memo = memoValue ?: ""
return when (cryptoCurrency.network.id.value) { return when (cryptoCurrency.network.id.value) {
Blockchain.XRP.id -> convert(R.string.send_destination_tag_field) Blockchain.XRP.id -> {
convert(
value = Data(memo = memo, label = R.string.send_destination_tag_field),
)
}
Blockchain.Binance.id, Blockchain.Binance.id,
Blockchain.TON.id, Blockchain.TON.id,
Blockchain.Cosmos.id, Blockchain.Cosmos.id,
@ -30,24 +36,30 @@ internal class SendRecipientMemoFieldConverter(
Blockchain.Stellar.id, Blockchain.Stellar.id,
Blockchain.Hedera.id, Blockchain.Hedera.id,
Blockchain.Algorand.id, Blockchain.Algorand.id,
-> convert(R.string.send_extras_hint_memo) -> {
convert(
value = Data(memo = memo, label = R.string.send_extras_hint_memo),
)
}
else -> null else -> null
} }
} }
override fun convert(value: Int): SendTextField.RecipientMemo { override fun convert(value: Data): SendTextField.RecipientMemo {
return SendTextField.RecipientMemo( return SendTextField.RecipientMemo(
value = "", value = value.memo,
onValueChange = clickIntents::onRecipientMemoValueChange, onValueChange = clickIntents::onRecipientMemoValueChange,
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done, imeAction = ImeAction.Done,
keyboardType = KeyboardType.Text, keyboardType = KeyboardType.Text,
), ),
placeholder = resourceReference(R.string.send_optional_field), placeholder = resourceReference(R.string.send_optional_field),
label = resourceReference(value), label = resourceReference(value.label),
error = resourceReference(R.string.send_memo_destination_tag_error), error = resourceReference(R.string.send_memo_destination_tag_error),
disabledText = resourceReference(R.string.send_additional_field_already_included), disabledText = resourceReference(R.string.send_additional_field_already_included),
isEnabled = true, isEnabled = true,
) )
} }
data class Data(val memo: String, @StringRes val label: Int)
} }

View file

@ -10,7 +10,7 @@ import kotlinx.collections.immutable.persistentListOf
internal class SendRecipientStateConverter( internal class SendRecipientStateConverter(
private val clickIntents: SendClickIntents, private val clickIntents: SendClickIntents,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>, private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<String, SendStates.RecipientState> { ) : Converter<SendRecipientStateConverter.Data, SendStates.RecipientState> {
private val addressFieldConverter by lazy { SendRecipientAddressFieldConverter(clickIntents) } private val addressFieldConverter by lazy { SendRecipientAddressFieldConverter(clickIntents) }
private val memoFieldConverter by lazy { private val memoFieldConverter by lazy {
@ -20,14 +20,16 @@ internal class SendRecipientStateConverter(
) )
} }
override fun convert(value: String): SendStates.RecipientState { override fun convert(value: Data): SendStates.RecipientState {
return SendStates.RecipientState( return SendStates.RecipientState(
addressTextField = addressFieldConverter.convert(value), addressTextField = addressFieldConverter.convert(value.address),
memoTextField = memoFieldConverter.convertOrNull(), memoTextField = memoFieldConverter.convertOrNull(value.memo),
network = cryptoCurrencyStatusProvider().currency.network.name, network = cryptoCurrencyStatusProvider().currency.network.name,
isPrimaryButtonEnabled = false, isPrimaryButtonEnabled = false,
wallets = persistentListOf(), wallets = persistentListOf(),
recent = persistentListOf(), recent = persistentListOf(),
) )
} }
data class Data(val address: String, val memo: String? = null)
} }

View file

@ -99,6 +99,7 @@ internal class SendViewModel @Inject constructor(
private val transactionId: String? = savedStateHandle[SendRouter.TRANSACTION_ID_KEY] private val transactionId: String? = savedStateHandle[SendRouter.TRANSACTION_ID_KEY]
private val amount: String? = savedStateHandle[SendRouter.AMOUNT_KEY] private val amount: String? = savedStateHandle[SendRouter.AMOUNT_KEY]
private val destinationAddress: String? = savedStateHandle[SendRouter.DESTINATION_ADDRESS_KEY] private val destinationAddress: String? = savedStateHandle[SendRouter.DESTINATION_ADDRESS_KEY]
private val memo: String? = savedStateHandle[SendRouter.TAG_KEY]
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow() private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
@ -342,7 +343,7 @@ internal class SendViewModel @Inject constructor(
stateRouter.showSend() stateRouter.showSend()
} }
transactionId != null && amount != null && destinationAddress != null -> { transactionId != null && amount != null && destinationAddress != null -> {
uiState = stateFactory.getReadyState(amount, destinationAddress) uiState = stateFactory.getReadyState(amount, destinationAddress, memo)
stateRouter.showFee() stateRouter.showFee()
updateNotifications() updateNotifications()
} }

View file

@ -175,9 +175,10 @@ internal class TokenDetailsViewModel @Inject constructor(
status = cryptoCurrencyStatus ?: return, status = cryptoCurrencyStatus ?: return,
transactionInfo = data.let { transactionInfo = data.let {
TransactionInfo( TransactionInfo(
amount = it.baseCurrencyAmount,
transactionId = it.transactionId, transactionId = it.transactionId,
destinationAddress = it.depositWalletAddress, destinationAddress = it.depositWalletAddress,
amount = it.baseCurrencyAmount,
tag = it.depositWalletAddressTag,
) )
}, },
) )

View file

@ -84,6 +84,7 @@ internal class WalletDeepLinksHandler @Inject constructor(
amount = it.baseCurrencyAmount, amount = it.baseCurrencyAmount,
destinationAddress = it.depositWalletAddress, destinationAddress = it.depositWalletAddress,
transactionId = it.transactionId, transactionId = it.transactionId,
tag = it.depositWalletAddressTag,
) )
} }