Updated on 2026-08-14
This commit is contained in:
commit
bf0dd570e7
19 changed files with 1608 additions and 75 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
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/?¤cy=BTC
|
||||
val builder = Uri.Builder()
|
||||
.scheme(environment.baseUri.scheme)
|
||||
.authority(environment.baseUri.authority)
|
||||
.appendPath("direct")
|
||||
.appendPath(environment.sidValue)
|
||||
.appendPath(walletAddress)
|
||||
.appendQueryParameter("currency", cryptoCurrencyName)
|
||||
// if we set the paymentCurrency, then the Utorg widget didn't work
|
||||
// .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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue