Updated on 2026-08-14

This commit is contained in:
Tangem 2023-02-12 18:25:51 +03:00
parent 5a20f6d533
commit ee1ad7a2c3
19 changed files with 1607 additions and 75 deletions

View file

@ -3,28 +3,37 @@ package com.tangem.tap.common.redux.global
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.ifNotNull
import com.tangem.common.services.Result
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.LogConfig
import com.tangem.domain.common.ProductType
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.configurable.config.Config
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.exchangeServices.BuyExchangeService
import com.tangem.tap.network.exchangeServices.CardExchangeRules
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoApi
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoEnvironment
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService
import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService
import com.tangem.tap.network.exchangeServices.utorg.UtorgAuthProvider
import com.tangem.tap.network.exchangeServices.utorg.UtorgEnvironment
import com.tangem.tap.network.exchangeServices.utorg.UtorgExchangeService
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userTokensRepository
import com.tangem.wallet.BuildConfig
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
@ -121,38 +130,23 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
}
is GlobalAction.ExchangeManager.Init -> {
val appStateSafe = appState() ?: return
val config = appStateSafe.globalState.configManager?.config
ifNotNull(
config?.mercuryoWidgetId,
config?.mercuryoSecret,
config?.moonPayApiKey,
config?.moonPayApiSecretKey,
) { mercuryoWidgetId, mercuryoSecret, moonPayKey, moonPaySecretKey ->
scope.launch {
val buyService = MercuryoService(
apiVersion = MercuryoApi.API_VERSION,
mercuryoWidgetId = mercuryoWidgetId,
secret = mercuryoSecret,
logEnabled = LogConfig.network.mercuryoService,
)
val sellService = MoonPayService(
apiKey = moonPayKey,
secretKey = moonPaySecretKey,
logEnabled = LogConfig.network.moonPayService,
)
val cardProvider = {
store.state.globalState.scanResponse?.card
?: store.state.globalState.onboardingState.onboardingManager?.scanResponse?.card
}
val config = appStateSafe.globalState.configManager?.config ?: return
val exchangeManager = CurrencyExchangeManager(
buyService = buyService,
sellService = sellService,
primaryRules = CardExchangeRules(cardProvider),
)
store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager))
store.dispatchOnMain(GlobalAction.ExchangeManager.Update)
scope.launch {
val scanResponseProvider: () -> ScanResponse? = {
store.state.globalState.scanResponse
?: store.state.globalState.onboardingState.onboardingManager?.scanResponse
}
val productTypeProvider: () -> ProductType? = { scanResponseProvider.invoke()?.productType }
val cardProvider: () -> CardDTO? = { scanResponseProvider.invoke()?.card }
val exchangeManager = CurrencyExchangeManager(
buyService = makeBuyExchangeService(config, productTypeProvider),
sellService = makeSellExchangeService(config),
primaryRules = CardExchangeRules(cardProvider),
)
store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager))
store.dispatchOnMain(GlobalAction.ExchangeManager.Update)
}
}
is GlobalAction.ExchangeManager.Init.Success -> {}
@ -207,4 +201,38 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
}
}
}
}
private fun makeSellExchangeService(config: Config): ExchangeService {
return MoonPayService(
apiKey = config.moonPayApiKey,
secretKey = config.moonPayApiSecretKey,
logEnabled = LogConfig.network.moonPayService,
)
}
private fun makeBuyExchangeService(config: Config, productTypeProvider: () -> ProductType?): ExchangeService {
return BuyExchangeService(
productTypeProvider = productTypeProvider,
mercuryoService = makeMercuryoExchangeService(config),
utorgService = makeUtorgExchangeService(config),
)
}
private fun makeMercuryoExchangeService(config: Config): MercuryoService {
val mercuryoEnvironment = MercuryoEnvironment.prod(config.mercuryoWidgetId, config.mercuryoSecret)
return MercuryoService(mercuryoEnvironment)
}
private fun makeUtorgExchangeService(config: Config): UtorgExchangeService {
val saltPayConfig = requireNotNull(config.saltPayConfig)
val utorgAuthProvider = UtorgAuthProvider(saltPayConfig.kycProvider.sidValue)
val utorgEnvironment = if (BuildConfig.DEBUG) {
UtorgEnvironment.stage(utorgAuthProvider, LogConfig.network.utorgService)
// UtorgEnvironment.mock()
} else {
UtorgEnvironment.prod(utorgAuthProvider)
}
return UtorgExchangeService(utorgEnvironment)
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.AmountType
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Token
@ -17,6 +18,7 @@ import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens
import com.tangem.tap.scope
@ -40,10 +42,9 @@ class TradeCryptoMiddleware {
action: WalletAction.TradeCryptoAction.Buy,
) {
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
val currency = chooseAppropriateCurrency(store.state.walletState) ?: return
val currency = selectedWalletData.currency
Analytics.send(Token.ButtonBuy(AnalyticsParam.CurrencyType.Currency(currency)))
if (action.checkUserLocation && state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE) {
store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog())
return
@ -87,12 +88,12 @@ class TradeCryptoMiddleware {
private fun proceedSellAction() {
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
val currency = chooseAppropriateCurrency(store.state.walletState) ?: return
val appCurrency = store.state.globalState.appCurrency
val addresses = selectedWalletData.walletAddresses?.list.orEmpty()
if (addresses.isEmpty()) return
val currency = selectedWalletData.currency
Analytics.send(Token.ButtonSell(AnalyticsParam.CurrencyType.Currency(currency)))
store.state.globalState.exchangeManager.getUrl(
@ -107,6 +108,17 @@ class TradeCryptoMiddleware {
}
}
private fun chooseAppropriateCurrency(walletState: WalletState): Currency? {
return if (walletState.primaryTokenData == null) {
walletState.selectedWalletData?.currency
} else {
walletState.primaryTokenData?.currency as? Currency.Token
}.guard {
store.dispatchDebugErrorNotification("Can't select an appropriate currency for a Trade action")
return null
}
}
private fun preconfigureAndOpenSendScreen(action: WalletAction.TradeCryptoAction.SendCrypto) {
val selectedWalletData = store.state.walletState.selectedWalletData ?: return

View file

@ -119,7 +119,8 @@ class BalanceWidget(
tvErrorDescriptions.text =
fragment.getString(
R.string.no_account_generic,
data.amountToCreateAccount, data.currencySymbol,
data.amountToCreateAccount,
data.currencySymbol,
)
}
BalanceStatus.UnknownBlockchain -> with(binding.lBalanceError) {

View file

@ -435,7 +435,8 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
lBalanceError.tvErrorDescriptions.text =
getString(
R.string.no_account_generic,
data.amountToCreateAccount, data.currencySymbol,
data.amountToCreateAccount,
data.currencySymbol,
)
}
}

View file

@ -0,0 +1,62 @@
package com.tangem.tap.network.exchangeServices
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.ProductType
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService
import com.tangem.tap.network.exchangeServices.utorg.UtorgExchangeService
import com.tangem.tap.scope
import kotlinx.coroutines.launch
/**
[REDACTED_AUTHOR]
* Temporary wrapper for the buy services. Service switches based on selected product type.
* Just now - UtorgService used only for the SaltPay cards
*/
class BuyExchangeService(
private val productTypeProvider: () -> ProductType?,
private val mercuryoService: MercuryoService,
private val utorgService: UtorgExchangeService,
) : ExchangeService, ExchangeUrlBuilder {
init {
scope.launch {
mercuryoService.update()
utorgService.update()
}
}
private val currentService: ExchangeService
get() = when (productTypeProvider.invoke()) {
ProductType.SaltPay -> utorgService
else -> mercuryoService
}
override suspend fun update() {
currentService.update()
}
override fun featureIsSwitchedOn(): Boolean = currentService.featureIsSwitchedOn()
override fun isBuyAllowed(): Boolean = currentService.isBuyAllowed()
override fun isSellAllowed(): Boolean = currentService.isSellAllowed()
override fun availableForBuy(currency: Currency): Boolean = currentService.availableForBuy(currency)
override fun availableForSell(currency: Currency): Boolean = currentService.availableForSell(currency)
override fun getUrl(
action: CurrencyExchangeManager.Action,
blockchain: Blockchain,
cryptoCurrencyName: String,
fiatCurrencyName: String,
walletAddress: String,
): String? {
return currentService.getUrl(action, blockchain, cryptoCurrencyName, fiatCurrencyName, walletAddress)
}
override fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? {
return currentService.getSellCryptoReceiptUrl(action, transactionId)
}
}

View file

@ -24,7 +24,7 @@ class CurrencyExchangeManager(
private val buyService: ExchangeService,
private val sellService: ExchangeService,
private val primaryRules: ExchangeRules,
) : ExchangeService, ExchangeUrlBuilder {
) : ExchangeService {
override fun featureIsSwitchedOn(): Boolean = primaryRules.featureIsSwitchedOn()
@ -59,7 +59,7 @@ class CurrencyExchangeManager(
blockchain,
cryptoCurrencyName,
fiatCurrencyName,
walletAddress
walletAddress,
)
}
@ -97,8 +97,11 @@ suspend fun CurrencyExchangeManager.buyErc20TestnetTokens(
val amountToSend = Amount(walletManager.wallet.blockchain)
val destinationAddress = token.contractAddress
val feeResult = walletManager.getFee(amountToSend,
destinationAddress) as? Result.Success ?: return
val feeResult =
walletManager.getFee(
amountToSend,
destinationAddress,
) as? Result.Success ?: return
val fee = feeResult.data[0]
if ((walletManager.wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO) < fee.value) {
@ -116,8 +119,8 @@ suspend fun CurrencyExchangeManager.buyErc20TestnetTokens(
GlobalAction.UpdateWalletSignedHashes(
walletSignedHashes = signResponse.totalSignedHashes,
walletPublicKey = walletManager.wallet.publicKey.seedKey,
remainingSignatures = signResponse.remainingSignatures
)
remainingSignatures = signResponse.remainingSignatures,
),
)
}
walletManager.send(transaction, signer)

View file

@ -7,11 +7,11 @@ import com.tangem.tap.features.wallet.models.Currency
interface Exchanger {
fun isBuyAllowed(): Boolean
fun isSellAllowed(): Boolean
fun availableForBuy(currency: Currency):Boolean
fun availableForSell(currency: Currency):Boolean
fun availableForBuy(currency: Currency): Boolean
fun availableForSell(currency: Currency): Boolean
}
interface ExchangeService: Feature, Exchanger {
interface ExchangeService : Feature, Exchanger, ExchangeUrlBuilder {
suspend fun update()
companion object {
@ -22,11 +22,23 @@ interface ExchangeService: Feature, Exchanger {
override fun isSellAllowed(): Boolean = false
override fun availableForBuy(currency: Currency): Boolean = false
override fun availableForSell(currency: Currency): Boolean = false
override fun getUrl(
action: CurrencyExchangeManager.Action,
blockchain: Blockchain,
cryptoCurrencyName: String,
fiatCurrencyName: String,
walletAddress: String,
): String? = null
override fun getSellCryptoReceiptUrl(
action: CurrencyExchangeManager.Action,
transactionId: String,
): String? = null
}
}
}
interface ExchangeRules: Feature, Exchanger {
interface ExchangeRules : Feature, Exchanger {
companion object {
fun dummy(): ExchangeRules = object : ExchangeRules {

View file

@ -11,11 +11,6 @@ interface MercuryoApi {
suspend fun currencies(
@Path("apiVersion") apiVersion: String,
): MercuryoCurrenciesResponse
companion object {
const val BASE_URL = "https://api.mercuryo.io/"
const val API_VERSION = "v1.6"
}
}
data class MercuryoCurrenciesResponse(
@ -25,7 +20,7 @@ data class MercuryoCurrenciesResponse(
data class Data(
val fiat: List<String>,
val crypto: List<String>,
val config: Config
val config: Config,
)
data class Config(

View file

@ -0,0 +1,36 @@
package com.tangem.tap.network.exchangeServices.mercuryo
import com.tangem.datasource.api.common.createRetrofitInstance
/**
[REDACTED_AUTHOR]
*/
data class MercuryoEnvironment(
val baseUrl: String,
val apiVersion: String,
val widgetId: String,
val secret: String,
val mercuryoApi: MercuryoApi,
) {
companion object {
private const val BASE_URL = "https://api.mercuryo.io/"
private const val API_VERSION = "v1.6"
fun prod(
widgetId: String,
secret: String,
apiVersion: String = API_VERSION,
): MercuryoEnvironment {
return MercuryoEnvironment(
baseUrl = BASE_URL,
apiVersion = apiVersion,
widgetId = widgetId,
secret = secret,
mercuryoApi = createRetrofitInstance(
baseUrl = BASE_URL,
logEnabled = false,
).create(MercuryoApi::class.java),
)
}
}
}

View file

@ -6,7 +6,6 @@ import com.tangem.common.extensions.calculateSha512
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.datasource.api.common.createRetrofitInstance
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
@ -17,16 +16,10 @@ import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder
[REDACTED_AUTHOR]
*/
class MercuryoService(
private val apiVersion: String,
private val mercuryoWidgetId: String,
private val secret: String,
private val logEnabled: Boolean,
) : ExchangeService, ExchangeUrlBuilder {
private val environment: MercuryoEnvironment,
) : ExchangeService {
private val api: MercuryoApi = createRetrofitInstance(
baseUrl = MercuryoApi.BASE_URL,
logEnabled = logEnabled,
).create(MercuryoApi::class.java)
private val api: MercuryoApi = environment.mercuryoApi
private val blockchainsAvailableToBuy = mutableListOf<Blockchain>()
private val tokensAvailableToBy = mutableMapOf<String, MutableList<Blockchain>>()
@ -34,7 +27,7 @@ class MercuryoService(
override fun featureIsSwitchedOn(): Boolean = true
override suspend fun update() {
when (val result = performRequest { api.currencies(apiVersion) }) {
when (val result = performRequest { api.currencies(environment.apiVersion) }) {
is Result.Success -> {
val response = result.data
if (response.status == 200) {
@ -108,14 +101,14 @@ class MercuryoService(
blockchain: Blockchain,
cryptoCurrencyName: CryptoCurrencyName,
fiatCurrencyName: String,
walletAddress: String
walletAddress: String,
): String {
if (action == CurrencyExchangeManager.Action.Sell) throw UnsupportedOperationException()
val builder = Uri.Builder()
.scheme(ExchangeUrlBuilder.SCHEME)
.authority("exchange.mercuryo.io")
.appendQueryParameter("widget_id", mercuryoWidgetId)
.appendQueryParameter("widget_id", environment.widgetId)
.appendQueryParameter("type", action.name.lowercase())
.appendQueryParameter("currency", cryptoCurrencyName)
.appendQueryParameter("address", walletAddress)
@ -128,13 +121,12 @@ class MercuryoService(
}
private fun signature(address: String): String {
return (address + secret).calculateSha512().toHexString().lowercase()
return (address + environment.secret).calculateSha512().toHexString().lowercase()
}
override fun getSellCryptoReceiptUrl(
action: CurrencyExchangeManager.Action,
transactionId: String
transactionId: String,
): String? = null
private fun blockchainFromCurrencyName(currencyName: String): Blockchain? = when (currencyName) {

View file

@ -5,14 +5,13 @@ import android.util.Base64
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.domain.common.extensions.withIOContext
import com.tangem.datasource.api.common.createRetrofitInstance
import com.tangem.domain.common.extensions.withIOContext
import com.tangem.tap.common.extensions.urlEncode
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SCHEME
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
@ -21,7 +20,7 @@ class MoonPayService(
private val apiKey: String,
private val secretKey: String,
private val logEnabled: Boolean,
) : ExchangeService, ExchangeUrlBuilder {
) : ExchangeService {
private val api: MoonPayApi by lazy {
createRetrofitInstance(
@ -103,7 +102,7 @@ class MoonPayService(
blockchain: Blockchain,
cryptoCurrencyName: CryptoCurrencyName,
fatCurrency: String,
walletAddress: String
walletAddress: String,
): String? {
if (action == CurrencyExchangeManager.Action.Buy) throw UnsupportedOperationException()
@ -130,7 +129,6 @@ class MoonPayService(
.appendPath("transaction_receipt")
.appendQueryParameter("transactionId", transactionId).build().toString()
return url
}
private fun createSignature(data: String): String {
@ -149,5 +147,5 @@ class MoonPayService(
private data class MoonPayStatus(
val availableForSell: List<String>,
val responseUserStatus: MoonPayUserStatus,
val responseCurrencies: List<MoonPayCurrencies>
val responseCurrencies: List<MoonPayCurrencies>,
)

View file

@ -0,0 +1,88 @@
package com.tangem.tap.network.exchangeServices.utorg
import android.net.Uri
import com.tangem.datasource.api.common.createRetrofitInstance
import com.tangem.tap.network.exchangeServices.utorg.api.UtorgApi
import com.tangem.tap.network.exchangeServices.utorg.mock.MockUtorgApi
import okhttp3.Interceptor
import okhttp3.Response
import java.util.*
/**
[REDACTED_AUTHOR]
*/
data class UtorgEnvironment(
val baseUri: Uri,
val sidValue: String,
val apiVersion: String,
val utorgApi: UtorgApi,
val successUrl: String = "https://success.tangem.com",
) {
companion object {
private val PROD_BASE_URL = Uri.parse("https://app.utorg.pro")
private const val PROD_VERSION = "v1"
private val STAGE_BASE_URL = Uri.parse("https://app-stage.utorg.pro")
private const val STAGE_VERSION = "v1"
fun prod(
authProvider: UtorgAuthProvider,
apiVersion: String = PROD_VERSION,
): UtorgEnvironment = UtorgEnvironment(
baseUri = PROD_BASE_URL,
sidValue = authProvider.sidValue,
apiVersion = apiVersion,
utorgApi = createApi(PROD_BASE_URL, authProvider, false),
)
fun stage(
authProvider: UtorgAuthProvider,
logEnabled: Boolean,
apiVersion: String = STAGE_VERSION,
): UtorgEnvironment = UtorgEnvironment(
baseUri = STAGE_BASE_URL,
sidValue = authProvider.sidValue,
apiVersion = apiVersion,
utorgApi = createApi(STAGE_BASE_URL, authProvider, logEnabled),
)
fun mock(): UtorgEnvironment = UtorgEnvironment(
baseUri = Uri.EMPTY,
sidValue = "",
apiVersion = "",
utorgApi = MockUtorgApi(),
)
private fun createApi(baseUri: Uri, authProvider: UtorgAuthProvider, logEnabled: Boolean): UtorgApi {
return createRetrofitInstance(
baseUrl = "$baseUri/",
interceptors = listOf(UtorgAuthHeaderInterceptor(authProvider)),
logEnabled = logEnabled,
).create(UtorgApi::class.java)
}
}
}
data class UtorgAuthProvider(
val sidValue: String,
val headerSidKey: String = "X-AUTH-SID",
val headerNonceKey: String = "X-AUTH-NONCE",
) {
val clientNonce: String
get() = UUID.randomUUID().toString()
}
private class UtorgAuthHeaderInterceptor(
private val authProvider: UtorgAuthProvider,
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
.newBuilder()
.addHeader(authProvider.headerSidKey, authProvider.sidValue)
.addHeader(authProvider.headerNonceKey, authProvider.clientNonce)
.build()
return chain.proceed(request)
}
}

View file

@ -0,0 +1,116 @@
package com.tangem.tap.network.exchangeServices.utorg
import android.net.Uri
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.utorg.api.RequestSuccessUrl
import com.tangem.tap.network.exchangeServices.utorg.api.model.UtorgCurrencyData
import com.tangem.tap.network.exchangeServices.utorg.api.model.UtorgCurrencyType
import com.tangem.utils.converter.Converter
/**
[REDACTED_AUTHOR]
*/
class UtorgExchangeService(
private val environment: UtorgEnvironment,
private val currencyConverter: Converter<UtorgCurrencyData, Blockchain?> = ChainToBlockchainConverter(),
) : ExchangeService {
private val api = environment.utorgApi
private val utorgCurrencies = mutableListOf<UtorgCurrencyData>()
override fun featureIsSwitchedOn(): Boolean = true
override suspend fun update() {
when (val result = performRequest { api.getCurrency(environment.apiVersion) }) {
is Result.Success -> {
if (!result.data.isSuccess()) return
val utorgCryptos = result.data.toSuccess().data.filter {
it.enabled && it.type == UtorgCurrencyType.CRYPTO
}
utorgCurrencies.clear()
utorgCurrencies.addAll(utorgCryptos)
performRequest {
api.setSuccessUrl(environment.apiVersion, RequestSuccessUrl(environment.successUrl))
}
}
is Result.Failure -> {
utorgCurrencies.clear()
}
}
}
override fun isBuyAllowed(): Boolean = true
override fun isSellAllowed(): Boolean = false
override fun availableForBuy(currency: Currency): Boolean {
return true
if (!isBuyAllowed()) return false
val foundUtorgCurrency = utorgCurrencies.firstOrNull { utorgCurrency ->
val utorgBlockchain = currencyConverter.convert(utorgCurrency) ?: return@firstOrNull false
val isSameBlockchain = utorgBlockchain == currency.blockchain
val isSameSymbol = utorgCurrency.symbol.lowercase() == currency.currencySymbol.lowercase()
isSameBlockchain && isSameSymbol
}
return foundUtorgCurrency != null
}
override fun availableForSell(currency: Currency): Boolean = false
override fun getUrl(
action: CurrencyExchangeManager.Action,
blockchain: Blockchain,
cryptoCurrencyName: CryptoCurrencyName,
fiatCurrencyName: String,
walletAddress: String,
): String {
if (action == CurrencyExchangeManager.Action.Sell) throw UnsupportedOperationException()
// https://app-stage.utorg.pro/direct/testSID/mvmeaWyWiuVYdZdySgofAt6CmKhJbRhaWA/?&currency=BTC
val builder = Uri.Builder()
.scheme(environment.baseUri.scheme)
.authority(environment.baseUri.authority)
.appendPath("direct")
.appendPath(environment.sidValue)
.appendPath(walletAddress)
.appendQueryParameter("currency", cryptoCurrencyName)
.appendQueryParameter("paymentCurrency", "USD")
val url = builder.build().toString()
return url
}
override fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? = null
}
private class ChainToBlockchainConverter : Converter<UtorgCurrencyData, Blockchain?> {
override fun convert(value: UtorgCurrencyData): Blockchain? {
return when (value.chain?.uppercase()) {
"ARBITRUM" -> Blockchain.Arbitrum
"AVALANCHE" -> Blockchain.Avalanche
"BINANCE_SMART_CHAIN" -> Blockchain.BSC
"BITCOIN" -> Blockchain.Bitcoin
"BNB" -> Blockchain.Binance
"ETHEREUM" -> Blockchain.Ethereum
"GNOSIS" -> Blockchain.Gnosis
"POLYGON" -> Blockchain.Polygon
"RIPPLE" -> Blockchain.XRP
"RSK" -> Blockchain.RSK
"SOLANA" -> Blockchain.Solana
else -> null
// "APTOS", "ATOM", "NEAR", "ZKSYNC", "VECHAIN", "VELAS", "VELAS_EVM" -> null
}
}
}

View file

@ -0,0 +1,67 @@
package com.tangem.tap.network.exchangeServices.utorg.api
import com.squareup.moshi.Json
import com.tangem.tap.network.exchangeServices.utorg.api.model.UtorgCurrencyResponse
import retrofit2.http.Body
import retrofit2.http.POST
import retrofit2.http.Path
/**
[REDACTED_AUTHOR]
*/
interface UtorgApi {
@POST("api/merchant/{apiVersion}/settings/currency")
suspend fun getCurrency(
@Path("apiVersion") apiVersion: String,
): UtorgCurrencyResponse
@POST("api/merchant/{apiVersion}/settings/successUrl")
suspend fun setSuccessUrl(
@Path("apiVersion") apiVersion: String,
@Body request: RequestSuccessUrl,
)
}
data class RequestSuccessUrl(
val url: String,
)
interface UtorgResponse<Data> {
val success: Boolean
val timestamp: Long
val data: Data?
val error: UtorgErrorResponse?
fun isSuccess(): Boolean = success && data != null && error == null
@Throws(NullPointerException::class)
fun toSuccess(): UtorgSuccessResponse<Data> {
val internalTimestamp = this.timestamp
val internalData = this.data
return object : UtorgSuccessResponse<Data> {
override val timestamp: Long = internalTimestamp
override val data: Data = internalData!!
}
}
@Throws(NullPointerException::class)
fun toError(): UtorgErrorResponse = error!!
}
interface UtorgSuccessResponse<T> {
val timestamp: Long
val data: T
}
data class UtorgErrorResponse(
@Json(name = "message") val message: String?,
@Json(name = "type") val type: UtorgErrorType,
)
enum class UtorgErrorType {
UNAUTHORIZED,
UNKNOWN_ERROR,
BAD_REQUEST,
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.network.exchangeServices.utorg.api.model
import com.squareup.moshi.Json
import com.tangem.tap.network.exchangeServices.utorg.api.UtorgErrorResponse
import com.tangem.tap.network.exchangeServices.utorg.api.UtorgResponse
class UtorgCurrencyResponse(
@Json(name = "success") override val success: Boolean,
@Json(name = "timestamp") override val timestamp: Long,
@Json(name = "data") override val data: List<UtorgCurrencyData>?,
@Json(name = "error") override val error: UtorgErrorResponse?,
) : UtorgResponse<List<UtorgCurrencyData>>
data class UtorgCurrencyData(
@Json(name = "currency") val currency: String,
@Json(name = "symbol") val symbol: String,
@Json(name = "enabled") val enabled: Boolean,
@Json(name = "type") val type: UtorgCurrencyType,
@Json(name = "caption") val caption: String?,
@Json(name = "chain") val chain: String?,
)
enum class UtorgCurrencyType {
FIAT, CRYPTO
}

View file

@ -0,0 +1,58 @@
package com.tangem.tap.network.exchangeServices.utorg.mock
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.tap.network.exchangeServices.utorg.api.RequestSuccessUrl
import com.tangem.tap.network.exchangeServices.utorg.api.UtorgApi
import com.tangem.tap.network.exchangeServices.utorg.api.UtorgErrorResponse
import com.tangem.tap.network.exchangeServices.utorg.api.UtorgErrorType
import com.tangem.tap.network.exchangeServices.utorg.api.model.UtorgCurrencyResponse
import kotlinx.coroutines.delay
/**
[REDACTED_AUTHOR]
*/
class MockUtorgApi : UtorgApi {
var nextDelay = 500L
var nextResponseType = ResponseType.Success
var nextErrorType = UtorgErrorType.UNKNOWN_ERROR
override suspend fun getCurrency(apiVersion: String): UtorgCurrencyResponse {
delay(nextDelay)
return UtorgCurrencyResponse(
success = createIsSuccess(),
timestamp = createTime(),
data = createData(MockUtorgSuccessDataResponse.GetCurrency),
error = createError(),
)
}
override suspend fun setSuccessUrl(apiVersion: String, request: RequestSuccessUrl) {
delay(nextDelay)
}
private fun createIsSuccess(): Boolean = when (nextResponseType) {
ResponseType.Success -> true
ResponseType.Error -> false
}
private fun createTime(): Long = System.currentTimeMillis()
private inline fun <reified T> createData(response: MockUtorgSuccessDataResponse): T? = when (nextResponseType) {
ResponseType.Success -> MoshiJsonConverter.INSTANCE.fromJson(response.json)!!
ResponseType.Error -> null
}
private fun createError(): UtorgErrorResponse? = when (nextResponseType) {
ResponseType.Success -> null
ResponseType.Error -> UtorgErrorResponse(
message = "Optional message",
type = nextErrorType,
)
}
enum class ResponseType {
Success, Error
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.tap.network.exchangeServices.utorg.mock
import com.tangem.tap.network.exchangeServices.utorg.mock.json.getCurrencyFull
/**
[REDACTED_AUTHOR]
*/
sealed class MockUtorgSuccessDataResponse(
val json: String,
) {
object GetCurrency : MockUtorgSuccessDataResponse(getCurrencyFull)
}