Updated on 2026-08-14

This commit is contained in:
Tangem 2022-07-08 12:57:20 +03:00
parent cef51116e6
commit 17066df741
26 changed files with 407 additions and 506 deletions

View file

@ -11,6 +11,7 @@ import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TangemSigner
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.store
import com.tangem.tap.tangemSdk
import java.math.BigDecimal
@ -18,57 +19,20 @@ import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
interface ExchangeService {
suspend fun isBuyAllowed(): Boolean
suspend fun availableToBuy(): List<String>
suspend fun isSellAllowed(): Boolean
suspend fun availableToSell(): List<String>
}
interface ExchangeUrlBuilder {
fun getUrl(
action: CurrencyExchangeManager.Action,
blockchain: Blockchain,
cryptoCurrencyName: CryptoCurrencyName,
fiatCurrencyName: String,
walletAddress: String,
): String?
fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String?
companion object {
const val SCHEME = "https"
const val URL_SELL = "sell.moonpay.com"
const val SUCCESS_URL = "tangem://success.tangem.com"
}
}
class CurrencyExchangeManager(
private val onramperService: ExchangeService,
private val moonPayService: ExchangeService,
private val buyService: ExchangeService,
private val sellService: ExchangeService,
) : ExchangeService, ExchangeUrlBuilder {
var status: CurrencyExchangeStatus? = null
private set
suspend fun getStatus(): CurrencyExchangeStatus {
val isBuyAllowed = isBuyAllowed()
val isSellAllowed = isSellAllowed()
val availableToBuy = availableToBuy()
val availableToSell = availableToSell()
status = CurrencyExchangeStatus(
isBuyAllowed,
isSellAllowed,
availableToBuy,
availableToSell,
)
return status!!
override suspend fun update() {
buyService.update()
sellService.update()
}
override suspend fun isBuyAllowed(): Boolean = onramperService.isBuyAllowed()
override suspend fun availableToBuy(): List<String> = onramperService.availableToBuy()
override suspend fun isSellAllowed(): Boolean = moonPayService.isSellAllowed()
override suspend fun availableToSell(): List<String> = moonPayService.availableToSell()
override fun isBuyAllowed(): Boolean = buyService.isBuyAllowed()
override fun isSellAllowed(): Boolean = sellService.isSellAllowed()
override fun availableForBuy(currency: Currency): Boolean = buyService.availableForBuy(currency)
override fun availableForSell(currency: Currency): Boolean = sellService.availableForSell(currency)
override fun getUrl(
action: Action,
@ -96,22 +60,15 @@ class CurrencyExchangeManager(
private fun getExchangeUrlBuilder(action: Action): ExchangeUrlBuilder {
return when (action) {
Action.Buy -> onramperService
Action.Sell -> moonPayService
Action.Buy -> buyService
Action.Sell -> sellService
} as ExchangeUrlBuilder
}
enum class Action { Buy, Sell }
}
data class CurrencyExchangeStatus(
val isBuyAllowed: Boolean,
val isSellAllowed: Boolean,
val availableToBuy: List<String>,
val availableToSell: List<String>,
)
suspend fun CurrencyExchangeManager.buyErc20Tokens(walletManager: EthereumWalletManager, token: Token) {
suspend fun CurrencyExchangeManager.buyErc20TestnetTokens(walletManager: EthereumWalletManager, token: Token) {
walletManager.safeUpdate()
val amountToSend = Amount(walletManager.wallet.blockchain)

View file

@ -0,0 +1,29 @@
package com.tangem.tap.network.exchangeServices
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.features.wallet.redux.Currency
interface ExchangeService {
suspend fun update()
fun isBuyAllowed(): Boolean
fun isSellAllowed(): Boolean
fun availableForBuy(currency: Currency):Boolean
fun availableForSell(currency: Currency):Boolean
}
interface ExchangeUrlBuilder {
fun getUrl(
action: CurrencyExchangeManager.Action,
blockchain: Blockchain,
cryptoCurrencyName: String,
fiatCurrencyName: String,
walletAddress: String,
): String?
fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String?
companion object {
const val SCHEME = "https"
const val SUCCESS_URL = "tangem://success.tangem.com"
}
}

View file

@ -0,0 +1,57 @@
package com.tangem.tap.network.exchangeServices.mercuryo
import com.squareup.moshi.Json
import retrofit2.http.GET
import retrofit2.http.Path
/**
[REDACTED_AUTHOR]
*/
private val CurrenciesUrl = "https://api.mercuryo.io/v1.6/lib/currencies"
interface MercuryoApi {
@GET("{apiVersion}/lib/currencies")
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(
val status: Int,
val data: Data,
) {
data class Data(
val fiat: List<String>,
val crypto: List<String>,
val config: Config
)
data class Config(
val base: Map<String, String>,
@Json(name = "has_withdrawal_fee")
val hasWithdrawalFee: Map<String, Boolean>,
@Json(name = "display_options")
val displayOptions: Map<String, DisplayOption>,
val icons: Map<String, Any>,
)
data class DisplayOption(
@Json(name = "fullname")
val fullName: String,
@Json(name = "total_digits")
val totalDigits: Int,
@Json(name = "display_digits")
val displayDigits: Int,
)
}

View file

@ -0,0 +1,127 @@
package com.tangem.tap.network.exchangeServices.mercuryo
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.network.common.createRetrofitInstance
import com.tangem.tap.common.extensions.urlEncode
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder
/**
[REDACTED_AUTHOR]
*/
class MercuryoService(
private val apiVersion: String,
private val mercuryoWidgetId: String,
) : ExchangeService, ExchangeUrlBuilder {
private val api: MercuryoApi = createRetrofitInstance(MercuryoApi.BASE_URL)
.create(MercuryoApi::class.java)
private val blockchainsAvailableToBuy = mutableListOf<Blockchain>()
private val tokensAvailableToBy = mutableMapOf<String, MutableList<Blockchain>>()
override suspend fun update() {
when (val result = performRequest { api.currencies(apiVersion) }) {
is Result.Success -> {
val response = result.data
if (response.status == 200) {
// all currencies which can be bought
val currenciesAvailableToBy = response.data.crypto
// tokens which can be bought only from specific blockchain network
val supportedTokensWithNetwork = response.data.config.base
currenciesAvailableToBy.forEach { currencyName ->
val blockchain = blockchainFromCurrencyName(currencyName)
if (blockchain == null) {
// suppose its a token
supportedTokensWithNetwork[currencyName]?.let {
blockchainFromCurrencyName(it)
}?.let { blockchainNetwork ->
val supportedInBlockchainsNetwork = tokensAvailableToBy[currencyName]
?: mutableListOf()
supportedInBlockchainsNetwork.add(blockchainNetwork)
tokensAvailableToBy[currencyName] = supportedInBlockchainsNetwork
}
} else {
blockchainsAvailableToBuy.add(blockchain)
}
}
}
}
is Result.Failure -> {
blockchainsAvailableToBuy.clear()
tokensAvailableToBy.clear()
}
}
}
override fun isBuyAllowed(): Boolean = true
override fun isSellAllowed(): Boolean = false
override fun availableForBuy(currency: Currency): Boolean {
if (!isBuyAllowed()) return false
// blockchains which cant be defined by mercuryo service
val unsupportedBlockchains = listOf(Blockchain.Unknown, Blockchain.Binance, Blockchain.Arbitrum)
val blockchain = currency.blockchain
return when (currency) {
is Currency.Blockchain -> {
when {
blockchain.isTestnet() -> blockchain.getTestnetTopUpUrl() != null
unsupportedBlockchains.contains(blockchain) -> false
else -> {
blockchainsAvailableToBuy.contains(currency.blockchain)
}
}
}
is Currency.Token -> {
val supportedInBlockchains = tokensAvailableToBy[currency.currencySymbol] ?: return false
supportedInBlockchains.contains(currency.blockchain)
}
}
}
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()
val builder = Uri.Builder()
.scheme(ExchangeUrlBuilder.SCHEME)
.authority("exchange.mercuryo.io")
.appendQueryParameter("widget_id", mercuryoWidgetId)
.appendQueryParameter("type", action.name.lowercase())
.appendQueryParameter("currency", cryptoCurrencyName)
.appendQueryParameter("address", walletAddress.urlEncode())
.appendQueryParameter("fix_currency", "true")
.appendQueryParameter("return_url", ExchangeUrlBuilder.SUCCESS_URL)
val url = builder.build().toString()
return url
}
override fun getSellCryptoReceiptUrl(
action: CurrencyExchangeManager.Action,
transactionId: String
): String? = null
private fun blockchainFromCurrencyName(currencyName: String): Blockchain? = when (currencyName) {
"BNB" -> Blockchain.BSC
"ETH" -> Blockchain.Ethereum
else -> Blockchain.values().find { it.currency.lowercase() == currencyName.lowercase() }
}
}

View file

@ -5,15 +5,15 @@ 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.network.common.createRetrofitInstance
import com.tangem.tap.common.extensions.urlEncode
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.features.wallet.redux.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 com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.URL_SELL
import kotlinx.coroutines.coroutineScope
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
@ -29,14 +29,14 @@ class MoonPayService(
private var status: MoonPayStatus? = null
private suspend fun updateStatus() {
try {
coroutineScope {
override suspend fun update() {
withIOContext {
performRequest {
val userStatusResult = performRequest { api.getUserStatus(apiKey) }
if (userStatusResult is Result.Failure) return@coroutineScope userStatusResult
if (userStatusResult is Result.Failure) return@performRequest
val currenciesResult = performRequest { api.getCurrencies(apiKey) }
if (currenciesResult is Result.Failure) return@coroutineScope currenciesResult
if (currenciesResult is Result.Failure) return@performRequest
val userStatus = (userStatusResult as Result.Success).data
val currencies = (currenciesResult as Result.Success).data
@ -65,29 +65,31 @@ class MoonPayService(
status = MoonPayStatus(currenciesToSell, userStatus, currencies)
}
} catch (error: Error) {
status = null
Result.Failure(error)
}
}
override suspend fun isBuyAllowed(): Boolean = false
override fun isBuyAllowed(): Boolean = false
override suspend fun availableToBuy(): List<String> = listOf()
override suspend fun isSellAllowed(): Boolean {
refreshStatus()
override fun isSellAllowed(): Boolean {
return status?.responseUserStatus?.isSellAllowed ?: false
}
override suspend fun availableToSell(): List<String> {
refreshStatus()
return status?.availableToSell ?: emptyList()
}
override fun availableForBuy(currency: Currency): Boolean = false
private suspend fun refreshStatus() {
if (status == null) {
updateStatus()
override fun availableForSell(currency: Currency): Boolean {
val availableForSell = status?.availableForSell ?: return false
if (!isSellAllowed()) return false
return when (currency) {
is Currency.Blockchain -> {
val blockchain = currency.blockchain
when {
blockchain.isTestnet() -> false
blockchain == Blockchain.Unknown || currency.blockchain == Blockchain.BSC -> false
else -> availableForSell.contains(currency.currencySymbol)
}
}
is Currency.Token -> false
}
}
@ -133,10 +135,14 @@ class MoonPayService(
val sha256encoded = sha256Hmac.doFinal(data.toByteArray())
return Base64.encodeToString(sha256encoded, Base64.NO_WRAP)
}
companion object {
const val URL_SELL = "sell.moonpay.com"
}
}
private data class MoonPayStatus(
val availableToSell: List<String>,
val availableForSell: List<String>,
val responseUserStatus: MoonPayUserStatus,
val responseCurrencies: List<MoonPayCurrencies>
)

View file

@ -1,83 +0,0 @@
package com.tangem.tap.network.exchangeServices.onramper
import com.squareup.moshi.JsonClass
import retrofit2.http.GET
import retrofit2.http.Path
interface OnramperApi {
@GET("gateways")
suspend fun gateways(): GatewaysResponse
@GET("rate/{fromCurrency}/{toCurrency}/{paymentMethod}/{amount}")
suspend fun rate(
@Path("fromCurrency") fromCurrency: String,
@Path("toCurrency") toCurrency: String,
@Path("paymentMethod") paymentMethod: String,
@Path("amount") amount: Int,
): RateResponse
companion object {
val BASE_URL = "https://onramper.tech/"
}
}
@JsonClass(generateAdapter = true)
data class GatewaysResponse(
val gateways: List<OnramperGateway>
)
@JsonClass(generateAdapter = true)
data class OnramperGateway(
val identifier: String,
val paymentMethods: List<String>,
val fiatCurrencies: List<OnramperCurrency>,
val cryptoCurrencies: List<OnramperCurrency>
)
@JsonClass(generateAdapter = true)
data class OnramperCurrency(
val id: String,
val code: String,
val precision: Int
)
@JsonClass(generateAdapter = true)
data class RateResponse(
val identifier: String,
val duration: OnramperDuration,
val available: Boolean,
val error: OnramperError? = null,
val rate: Double? = null,
val fees: Double? = null,
val requiredKYC: List<String>? = null,
val receivedCrypto: Double? = null,
val nextStep: OnramperNextStep? = null,
)
@JsonClass(generateAdapter = true)
data class OnramperNextStep(
val type: String,
val url: String,
val message: String,
val extraData: List<OnramperExtraData>
)
@JsonClass(generateAdapter = true)
data class OnramperExtraData(
val type: String,
val name: String,
val humanName: String,
)
@JsonClass(generateAdapter = true)
data class OnramperDuration(
val seconds: Long,
val message: String
)
@JsonClass(generateAdapter = true)
data class OnramperError(
val type: String,
val message: String,
val limit: Double
)

View file

@ -1,116 +0,0 @@
package com.tangem.tap.network.exchangeServices.onramper
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.network.common.AddHeaderInterceptor
import com.tangem.network.common.createRetrofitInstance
import com.tangem.tap.common.extensions.urlEncode
import com.tangem.tap.common.redux.global.CryptoCurrencyName
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 com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SUCCESS_URL
import kotlinx.coroutines.coroutineScope
import java.util.*
/**
[REDACTED_AUTHOR]
*/
class OnramperService(
val apiKey: String
) : ExchangeService, ExchangeUrlBuilder {
private val api: OnramperApi by lazy {
createRetrofitInstance(
baseUrl = OnramperApi.BASE_URL,
interceptors = listOf(
AddHeaderInterceptor(mapOf("Authorization" to "Basic $apiKey")),
)
).create(OnramperApi::class.java)
}
private var status: OnramperStatus? = null
private suspend fun updateStatus() {
try {
coroutineScope {
val result = performRequest { api.gateways() }
if (result is Result.Failure) return@coroutineScope result
val response = (result as Result.Success).data
val currenciesToBuy = extractCurrenciesToBuy(response).sorted()
val status = OnramperStatus(currenciesToBuy, response)
this@OnramperService.status = status
}
} catch (error: Error) {
status = null
Result.Failure(error)
}
}
private fun extractCurrenciesToBuy(response: GatewaysResponse): List<String> {
return response.gateways.map { gateway ->
gateway.cryptoCurrencies.map { currency -> currency.code }
}.flatten().toMutableSet().toList()
}
override suspend fun isBuyAllowed(): Boolean {
refreshStatus()
return status != null
}
override suspend fun availableToBuy(): List<String> {
refreshStatus()
return status?.availableToBuy ?: emptyList()
}
private suspend fun refreshStatus() {
if (status == null) {
updateStatus()
}
}
override suspend fun isSellAllowed(): Boolean = false
override suspend fun availableToSell(): List<String> = listOf()
override fun getUrl(
action: CurrencyExchangeManager.Action,
blockchain: Blockchain,
cryptoCurrencyName: CryptoCurrencyName,
fiatCurrency: String,
walletAddress: String,
): String? {
var languageCode = Locale.getDefault().language
if (languageCode.isEmpty()) languageCode = "en"
val builder = Uri.Builder()
.scheme(SCHEME)
.authority("widget.onramper.com")
.appendQueryParameter("apiKey", this.apiKey.urlEncode())
.appendQueryParameter("defaultCrypto", cryptoCurrencyName)
.appendQueryParameter("wallets", "${blockchain.currency}:$walletAddress".urlEncode())
.appendQueryParameter("redirectURL", SUCCESS_URL)
.appendQueryParameter("defaultFiat", fiatCurrency)
.appendQueryParameter("language", languageCode)
status?.apply {
val gateways = responseGateways.gateways.joinToString(",") { it.identifier }.urlEncode()
builder.appendQueryParameter("onlyGateways", gateways)
}
val url = builder.build().toString()
return url
}
override fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? = null
}
private data class OnramperStatus(
val availableToBuy: List<String>,
val responseGateways: GatewaysResponse
)