Updated on 2026-08-14

This commit is contained in:
Tangem 2022-03-03 09:38:35 +03:00
parent 0041c0b7e5
commit b4ab6e480e
29 changed files with 669 additions and 387 deletions

View file

@ -0,0 +1,136 @@
package com.tangem.tap.network.exchangeServices
import com.tangem.Message
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.extensions.Result
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.store
import com.tangem.tap.tangemSdk
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,
fiatCurrency: 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,
) : 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 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 getUrl(
action: Action,
blockchain: Blockchain,
cryptoCurrencyName: CryptoCurrencyName,
fiatCurrency: String,
walletAddress: String,
): String? {
if (blockchain.isTestnet()) return blockchain.getTestnetTopUpUrl()
val urlBuilder = getExchangeUrlBuilder(action)
return urlBuilder.getUrl(action, blockchain, cryptoCurrencyName, fiatCurrency, walletAddress)
}
override fun getSellCryptoReceiptUrl(action: Action, transactionId: String): String? {
val urlBuilder = getExchangeUrlBuilder(action)
return urlBuilder.getSellCryptoReceiptUrl(action, transactionId)
}
private fun getExchangeUrlBuilder(action: Action): ExchangeUrlBuilder {
return when (action) {
Action.Buy -> onramperService
Action.Sell -> moonPayService
} 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) {
walletManager.safeUpdate()
val amountToSend = Amount(walletManager.wallet.blockchain)
val destinationAddress = token.contractAddress
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) {
return
}
val transaction = walletManager.createTransaction(amountToSend, fee, destinationAddress)
val signer = TangemSigner(
tangemSdk = tangemSdk, Message()
) { signResponse ->
store.dispatch(
GlobalAction.UpdateWalletSignedHashes(
walletSignedHashes = signResponse.totalSignedHashes,
walletPublicKey = walletManager.wallet.publicKey.seedKey,
remainingSignatures = signResponse.remainingSignatures
)
)
}
walletManager.send(transaction, signer)
}

View file

@ -0,0 +1,50 @@
package com.tangem.tap.network.exchangeServices.moonpay
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import retrofit2.http.GET
import retrofit2.http.Query
interface MoonPayApi {
@GET(MOOONPAY_IP_ADDRESS_REQUEST_URL)
suspend fun getUserStatus(
@Query("apiKey") moonPayApiKey: String,
): MoonPayUserStatus
@GET(MOOONPAY_CURRENCIES_REQUEST_URL)
suspend fun getCurrencies(
@Query("apiKey") moonPayApiKey: String,
): List<MoonPayCurrencies>
companion object {
const val MOOONPAY_BASE_URL = "https://api.moonpay.com/"
const val MOOONPAY_IP_ADDRESS_REQUEST_URL = "v4/ip_address/"
const val MOOONPAY_CURRENCIES_REQUEST_URL = "v3/currencies/"
}
}
@JsonClass(generateAdapter = true)
data class MoonPayUserStatus(
val isBuyAllowed: Boolean,
val isSellAllowed: Boolean,
@Json(name = "isAllowed")
val isMoonpayAllowed: Boolean,
@Json(name = "alpha3")
val countryCode: String,
@Json(name = "state")
val stateCode: String,
)
@JsonClass(generateAdapter = true)
data class MoonPayCurrencies(
val type: String,
val code: String,
val supportsLiveMode: Boolean = false,
val isSuspended: Boolean = true,
val isSupportedInUS: Boolean = false,
val isSellSupported: Boolean = false,
val notAllowedUSStates: List<String> = emptyList(),
)

View file

@ -0,0 +1,142 @@
package com.tangem.tap.network.exchangeServices.moonpay
import android.net.Uri
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.tap.common.extensions.urlEncode
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.network.createRetrofitInstance
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
class MoonPayService(
private val apiKey: String,
private val secretKey: String,
) : ExchangeService, ExchangeUrlBuilder {
private val api: MoonPayApi by lazy {
createRetrofitInstance(MoonPayApi.MOOONPAY_BASE_URL)
.create(MoonPayApi::class.java)
}
private var status: MoonPayStatus? = null
private suspend fun updateStatus() {
try {
coroutineScope {
val userStatusResult = performRequest { api.getUserStatus(apiKey) }
if (userStatusResult is Result.Failure) return@coroutineScope userStatusResult
val currenciesResult = performRequest { api.getCurrencies(apiKey) }
if (currenciesResult is Result.Failure) return@coroutineScope currenciesResult
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()
status = MoonPayStatus(currenciesToSell, userStatus, currencies)
}
} catch (error: Error) {
status = null
Result.Failure(error)
}
}
override suspend fun isBuyAllowed(): Boolean = false
override suspend fun availableToBuy(): List<String> = listOf()
override suspend fun isSellAllowed(): Boolean {
refreshStatus()
return status?.responseUserStatus?.isSellAllowed ?: false
}
override suspend fun availableToSell(): List<String> {
refreshStatus()
return status?.availableToSell ?: emptyList()
}
private suspend fun refreshStatus() {
if (status == null) {
updateStatus()
}
}
override fun getUrl(
action: CurrencyExchangeManager.Action,
blockchain: Blockchain,
cryptoCurrencyName: CryptoCurrencyName,
fatCurrency: String,
walletAddress: String
): String? {
if (action == CurrencyExchangeManager.Action.Buy) throw UnsupportedOperationException()
val uri = Uri.Builder()
.scheme(SCHEME)
.authority(URL_SELL)
.appendQueryParameter("apiKey", apiKey.urlEncode())
.appendQueryParameter("baseCurrencyCode", cryptoCurrencyName.urlEncode())
.appendQueryParameter("refundWalletAddress", walletAddress.urlEncode())
.appendQueryParameter("redirectURL", "tangem://sell-request.tangem.com".urlEncode())
val originalQuery = uri.build().toString()
val signature = createSignature(originalQuery)
uri.appendQueryParameter("signature", signature.urlEncode())
val url = uri.build().toString()
return url
}
override fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? {
val url = Uri.Builder()
.scheme(SCHEME)
.authority(URL_SELL)
.appendPath("transaction_receipt")
.appendQueryParameter("transactionId", transactionId).build().toString()
return url
}
private fun createSignature(data: String): String {
val sha256Hmac = Mac.getInstance("HmacSHA256")
val secretKey = SecretKeySpec(secretKey.toByteArray(), "HmacSHA256")
sha256Hmac.init(secretKey)
val sha256encoded = sha256Hmac.doFinal(data.toByteArray())
return Base64.encodeToString(sha256encoded, Base64.NO_WRAP)
}
}
private data class MoonPayStatus(
val availableToSell: List<String>,
val responseUserStatus: MoonPayUserStatus,
val responseCurrencies: List<MoonPayCurrencies>
)

View file

@ -0,0 +1,94 @@
package com.tangem.tap.network.exchangeServices.onramper
import com.squareup.moshi.JsonClass
import okhttp3.Interceptor
import okhttp3.Response
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/"
}
}
class AddKeyToHeaderInterceptor(
private val key: String
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder().addHeader("Authorization", "Basic $key").build()
return chain.proceed(request)
}
}
@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

@ -0,0 +1,111 @@
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.tap.common.extensions.urlEncode
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.network.createRetrofitInstance
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(OnramperApi.BASE_URL, listOf(AddKeyToHeaderInterceptor(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,
fatCurrency: 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", fatCurrency)
.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
)

View file

@ -1,23 +0,0 @@
package com.tangem.tap.network.moonpay
import retrofit2.http.GET
import retrofit2.http.Query
interface MoonpayApi {
@GET(MOOONPAY_IP_ADDRESS_REQUEST_URL)
suspend fun getUserStatus(
@Query("apiKey") moonpayApiKey: String,
): MoonPayUserStatus
@GET(MOOONPAY_CURRENCIES_REQUEST_URL)
suspend fun getCurrencies(
@Query("apiKey") moonpayApiKey: String,
): List<MoonpayCurrencies>
companion object {
const val MOOONPAY_BASE_URL = "https://api.moonpay.com/"
const val MOOONPAY_IP_ADDRESS_REQUEST_URL = "v4/ip_address/"
const val MOOONPAY_CURRENCIES_REQUEST_URL = "v3/currencies/"
}
}

View file

@ -1,90 +0,0 @@
package com.tangem.tap.network.moonpay
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.tap.network.createRetrofitInstance
import kotlinx.coroutines.coroutineScope
class MoonpayService {
private val moonpayApi: MoonpayApi by lazy {
createRetrofitInstance(MoonpayApi.MOOONPAY_BASE_URL)
.create(MoonpayApi::class.java)
}
suspend fun getMoonpayStatus(moonpayApiKey: String): Result<MoonpayStatus> {
return try {
coroutineScope {
val userStatusResult = performRequest { moonpayApi.getUserStatus(moonpayApiKey) }
if (userStatusResult is Result.Failure) return@coroutineScope userStatusResult
val currenciesResult = performRequest { moonpayApi.getCurrencies(moonpayApiKey) }
if (currenciesResult is Result.Failure) return@coroutineScope currenciesResult
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)
}
Result.Success(MoonpayStatus(
isBuyAllowed = userStatus.isBuyAllowed,
isSellAllowed = userStatus.isSellAllowed,
availableToBuy = if (userStatus.isBuyAllowed) currenciesToBuy else emptyList(),
availableToSell = if (userStatus.isSellAllowed) currenciesToSell else emptyList()
))
}
} catch (error: Error) {
Result.Failure(error)
}
}
}
data class MoonpayStatus(
val isBuyAllowed: Boolean,
val isSellAllowed: Boolean,
val availableToBuy: List<String>,
val availableToSell: List<String>,
)
@JsonClass(generateAdapter = true)
data class MoonPayUserStatus(
val isBuyAllowed: Boolean,
val isSellAllowed: Boolean,
@Json(name = "isAllowed")
val isMoonpayAllowed: Boolean,
@Json(name = "alpha3")
val countryCode: String,
@Json(name = "state")
val stateCode: String,
)
@JsonClass(generateAdapter = true)
data class MoonpayCurrencies(
val type: String,
val code: String,
val supportsLiveMode: Boolean = false,
val isSuspended: Boolean = true,
val isSupportedInUS: Boolean = false,
val isSellSupported: Boolean = false,
val notAllowedUSStates: List<String> = emptyList(),
)