Updated on 2026-08-14
This commit is contained in:
commit
f1bc442a65
35 changed files with 743 additions and 431 deletions
|
|
@ -5,7 +5,6 @@ import android.nfc.NfcAdapter
|
|||
import android.nfc.Tag
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.domain.topup.TradeCryptoHelper
|
||||
import com.tangem.tap.domain.walletconnect.WalletConnectManager
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||
import com.tangem.tap.features.home.redux.HomeAction
|
||||
|
|
@ -15,6 +14,11 @@ import timber.log.Timber
|
|||
|
||||
class IntentHandler {
|
||||
|
||||
private val TRANSACTION_ID_PARAM = "transactionId"
|
||||
private val CURRENCY_CODE_PARAM = "baseCurrencyCode"
|
||||
private val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount"
|
||||
private val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress"
|
||||
|
||||
fun handleIntent(intent: Intent?) {
|
||||
handleBackgroundScan(intent)
|
||||
handleWalletConnectLink(intent)
|
||||
|
|
@ -23,8 +27,8 @@ class IntentHandler {
|
|||
|
||||
private fun handleBackgroundScan(intent: Intent?) {
|
||||
if (intent != null && (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action ||
|
||||
NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action
|
||||
)
|
||||
NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action
|
||||
)
|
||||
) {
|
||||
val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
|
||||
if (tag != null) {
|
||||
|
|
@ -45,13 +49,13 @@ class IntentHandler {
|
|||
private fun handleSellCurrencyCallback(intent: Intent?) {
|
||||
try {
|
||||
val transactionID =
|
||||
intent?.data?.getQueryParameter(TradeCryptoHelper.TRANSACTION_ID_PARAM) ?: return
|
||||
intent?.data?.getQueryParameter(TRANSACTION_ID_PARAM) ?: return
|
||||
val currency =
|
||||
intent.data?.getQueryParameter(TradeCryptoHelper.CURRENCY_CODE_PARAM) ?: return
|
||||
intent.data?.getQueryParameter(CURRENCY_CODE_PARAM) ?: return
|
||||
val amount =
|
||||
intent.data?.getQueryParameter(TradeCryptoHelper.CURRENCY_AMOUNT_PARAM) ?: return
|
||||
intent.data?.getQueryParameter(CURRENCY_AMOUNT_PARAM) ?: return
|
||||
val destinationAddress =
|
||||
intent.data?.getQueryParameter(TradeCryptoHelper.DEPOSIT_WALLET_ADDRESS_PARAM)
|
||||
intent.data?.getQueryParameter(DEPOSIT_WALLET_ADDRESS_PARAM)
|
||||
?: return
|
||||
|
||||
Timber.d("MoonPay Sell: $amount $currency to $destinationAddress")
|
||||
|
|
@ -63,7 +67,7 @@ class IntentHandler {
|
|||
transactionId = transactionID
|
||||
))
|
||||
} catch (exception: Exception) {
|
||||
Timber.d("Not Moonpay URL")
|
||||
Timber.d("Not MoonPay URL")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.*
|
||||
import android.net.Uri
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.utils.widget.ImageFilterView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.squareup.picasso.Callback
|
||||
import com.squareup.picasso.Picasso
|
||||
import com.squareup.picasso.Transformation
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.IconsUtil
|
||||
import com.tangem.blockchain.common.Token
|
||||
|
|
@ -43,6 +42,7 @@ fun Picasso.loadCurrenciesIcon(
|
|||
setTokenImage(imageView, textView, token)
|
||||
}
|
||||
this.load(url)
|
||||
.transform(RoundedCornersTransform())
|
||||
.noPlaceholder()
|
||||
?.into(imageView,
|
||||
object : Callback {
|
||||
|
|
@ -74,19 +74,10 @@ private fun setOfflineCurrencyImage(
|
|||
token: Token?,
|
||||
blockchain: Blockchain,
|
||||
) {
|
||||
if (token != null) {
|
||||
setTokenImage(imageView, textView, token)
|
||||
} else {
|
||||
setBlockchainImage(imageView, textView, blockchain)
|
||||
when (token) {
|
||||
null -> setBlockchainImage(imageView, textView, blockchain)
|
||||
else -> setTokenImage(imageView, textView, token)
|
||||
}
|
||||
if (blockchain.isTestnet()) imageView.tint(R.color.tint)
|
||||
}
|
||||
|
||||
fun ImageView.tint(colorRes: Int) {
|
||||
// val color = ContextCompat.getColor(context, colorRes);
|
||||
// ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(color));
|
||||
this.setColorFilter(ContextCompat.getColor(context, colorRes), PorterDuff.Mode.DARKEN);
|
||||
|
||||
}
|
||||
|
||||
private fun setBlockchainImage(
|
||||
|
|
@ -96,6 +87,7 @@ private fun setBlockchainImage(
|
|||
) {
|
||||
imageView.setImageResource(blockchain.getIconRes())
|
||||
imageView.colorFilter = null
|
||||
if (blockchain.isTestnet()) imageView.saturation = 0f
|
||||
textView.text = null
|
||||
}
|
||||
|
||||
|
|
@ -112,3 +104,29 @@ private fun setTokenImage(
|
|||
}
|
||||
textView.text = token.symbol.take(1)
|
||||
}
|
||||
|
||||
private class RoundedCornersTransform : Transformation {
|
||||
|
||||
override fun transform(source: Bitmap): Bitmap {
|
||||
val size = source.width.coerceAtMost(source.height)
|
||||
val x = (source.width - size) / 2
|
||||
val y = (source.height - size) / 2
|
||||
val squaredBitmap = Bitmap.createBitmap(source, x, y, size, size)
|
||||
if (squaredBitmap != source) source.recycle()
|
||||
|
||||
val paint = Paint().apply {
|
||||
shader = BitmapShader(squaredBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
val rectF = RectF(0f, 0f, source.width.toFloat(), source.height.toFloat())
|
||||
val radius = size / 8f
|
||||
val bitmap = Bitmap.createBitmap(size, size, source.config)
|
||||
Canvas(bitmap).drawRoundRect(rectF, radius, radius, paint)
|
||||
squaredBitmap.recycle()
|
||||
|
||||
return bitmap
|
||||
}
|
||||
|
||||
override fun key(): String = "rounded_corners"
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.tap.common.extensions
|
|||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.text.Spannable
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import androidx.core.content.ContextCompat
|
||||
|
|
@ -21,24 +22,24 @@ fun String?.ellipsizeBeforeSpace(allowedSize: Int): String {
|
|||
val startIndex = endIndex - sizeDifference
|
||||
val newString = this.removeRange(startIndex, endIndex)
|
||||
return newString.substring(0 until startIndex) + "..." +
|
||||
newString.substring(startIndex until newString.length)
|
||||
newString.substring(startIndex until newString.length)
|
||||
}
|
||||
|
||||
fun String.colorSegment(
|
||||
context: Context,
|
||||
color: Int,
|
||||
startIndex: Int = 0,
|
||||
endIndex: Int = this.length
|
||||
context: Context,
|
||||
color: Int,
|
||||
startIndex: Int = 0,
|
||||
endIndex: Int = this.length
|
||||
): Spannable {
|
||||
return this.toSpannable()
|
||||
.also { spannable ->
|
||||
spannable.setSpan(
|
||||
ForegroundColorSpan(ContextCompat.getColor(context, color)),
|
||||
startIndex,
|
||||
endIndex,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
.also { spannable ->
|
||||
spannable.setSpan(
|
||||
ForegroundColorSpan(ContextCompat.getColor(context, color)),
|
||||
startIndex,
|
||||
endIndex,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun String.toQrCode(): Bitmap {
|
||||
|
|
@ -59,4 +60,6 @@ fun String.toQrCode(): Bitmap {
|
|||
}
|
||||
}
|
||||
return bmp
|
||||
}
|
||||
}
|
||||
|
||||
fun String.urlEncode(): String = Uri.encode(this)
|
||||
|
|
@ -7,11 +7,11 @@ import com.tangem.common.services.Result
|
|||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.extensions.amountToCreateAccount
|
||||
import com.tangem.tap.domain.getFirstToken
|
||||
import com.tangem.tap.domain.topup.TradeCryptoHelper
|
||||
import com.tangem.tap.features.demo.isDemoWallet
|
||||
import com.tangem.tap.features.wallet.redux.AddressData
|
||||
import com.tangem.tap.features.wallet.redux.reducers.createAddressesData
|
||||
import com.tangem.tap.network.NetworkConnectivity
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.store
|
||||
import timber.log.Timber
|
||||
|
||||
|
|
@ -44,17 +44,17 @@ suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
|
|||
}
|
||||
|
||||
fun WalletManager?.getToUpUrl(): String? {
|
||||
val globalState = store.state.globalState
|
||||
val currencyExchangeManager = globalState.currencyExchangeManager ?: return null
|
||||
val wallet = this?.wallet ?: return null
|
||||
val config = store.state.globalState.configManager?.config ?: return null
|
||||
val defaultAddress = wallet.address
|
||||
|
||||
return TradeCryptoHelper.getUrl(
|
||||
TradeCryptoHelper.Action.Buy,
|
||||
wallet.blockchain,
|
||||
wallet.blockchain.currency,
|
||||
defaultAddress,
|
||||
config.moonPayApiKey,
|
||||
config.moonPayApiSecretKey
|
||||
val defaultAddress = wallet.address
|
||||
return currencyExchangeManager.getUrl(
|
||||
action = CurrencyExchangeManager.Action.Buy,
|
||||
blockchain = wallet.blockchain,
|
||||
cryptoCurrencyName = wallet.blockchain.currency,
|
||||
fiatCurrency = globalState.appCurrency,
|
||||
walletAddress = defaultAddress,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import com.tangem.tap.domain.tasks.product.ScanResponse
|
|||
import com.tangem.tap.features.details.redux.SecurityOption
|
||||
import com.tangem.tap.features.feedback.EmailData
|
||||
import com.tangem.tap.features.feedback.FeedbackManager
|
||||
import com.tangem.tap.network.moonpay.MoonpayStatus
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import org.rekotlin.Action
|
||||
|
||||
sealed class GlobalAction : Action {
|
||||
|
|
@ -76,7 +76,7 @@ sealed class GlobalAction : Action {
|
|||
data class SendFeedback(val emailData: EmailData) : GlobalAction()
|
||||
data class UpdateFeedbackInfo(val walletManagers: List<WalletManager>) : GlobalAction()
|
||||
|
||||
object GetMoonPayStatus : GlobalAction() {
|
||||
data class Success(val moonPayStatus: MoonpayStatus) : GlobalAction()
|
||||
object InitCurrencyExchangeManager : GlobalAction() {
|
||||
data class Success(val exchangeManager: CurrencyExchangeManager) : GlobalAction()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ package com.tangem.tap.common.redux.global
|
|||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.common.extensions.ifNotNull
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
|
|
@ -12,10 +12,11 @@ import com.tangem.tap.common.redux.AppState
|
|||
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.moonpay.MoonpayService
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService
|
||||
import com.tangem.tap.network.exchangeServices.onramper.OnramperService
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
|
||||
class GlobalMiddleware {
|
||||
companion object {
|
||||
|
|
@ -69,19 +70,21 @@ private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState
|
|||
}
|
||||
is GlobalAction.UpdateFeedbackInfo -> {
|
||||
store.state.globalState.feedbackManager?.infoHolder
|
||||
?.setWalletsInfo(action.walletManagers)
|
||||
?.setWalletsInfo(action.walletManagers)
|
||||
}
|
||||
is GlobalAction.GetMoonPayStatus -> {
|
||||
val apiKey = appState()?.globalState?.configManager?.config?.moonPayApiKey
|
||||
if (apiKey != null) {
|
||||
is GlobalAction.InitCurrencyExchangeManager -> {
|
||||
val config = appState()?.globalState?.configManager?.config
|
||||
ifNotNull(
|
||||
config?.onramperApiKey,
|
||||
config?.moonPayApiKey,
|
||||
config?.moonPayApiSecretKey,
|
||||
) { onramperKey, moonPayKey, moonPaySecretKey ->
|
||||
scope.launch {
|
||||
val result = MoonpayService().getMoonpayStatus(apiKey)
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
store.dispatchOnMain(GlobalAction.GetMoonPayStatus.Success(result.data))
|
||||
}
|
||||
is Result.Failure -> Timber.e(result.error)
|
||||
}
|
||||
val onramper = OnramperService(onramperKey)
|
||||
val moonPay = MoonPayService(moonPayKey, moonPaySecretKey)
|
||||
val exchangeManager = CurrencyExchangeManager(onramper, moonPay)
|
||||
exchangeManager.getStatus()
|
||||
store.dispatchOnMain(GlobalAction.InitCurrencyExchangeManager.Success(exchangeManager))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.tangem.tap.common.redux.global
|
|||
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.onboarding.OnboardingManager
|
||||
import com.tangem.tap.network.moonpay.MoonpayStatus
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import org.rekotlin.Action
|
||||
|
||||
|
|
@ -19,7 +18,7 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
|
|||
is GlobalAction.Onboarding.Start -> {
|
||||
val onboardingManager = if (action.scanResponse != null) {
|
||||
val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage
|
||||
OnboardingManager(action.scanResponse, usedCardsPrefStorage)
|
||||
OnboardingManager(action.scanResponse, usedCardsPrefStorage)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
@ -67,19 +66,8 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
|
|||
is GlobalAction.HideDialog -> {
|
||||
globalState.copy(dialog = null)
|
||||
}
|
||||
is GlobalAction.GetMoonPayStatus.Success -> {
|
||||
val fiatExchangeIsEnabled = globalState.configManager?.config?.isTopUpEnabled ?: false
|
||||
val moonpayStatus = if (fiatExchangeIsEnabled) {
|
||||
action.moonPayStatus
|
||||
} else {
|
||||
MoonpayStatus(
|
||||
isBuyAllowed = false,
|
||||
isSellAllowed = false,
|
||||
availableToBuy = emptyList(),
|
||||
availableToSell = emptyList()
|
||||
)
|
||||
}
|
||||
globalState.copy(moonpayStatus = moonpayStatus)
|
||||
is GlobalAction.InitCurrencyExchangeManager.Success -> {
|
||||
globalState.copy(currencyExchangeManager = action.exchangeManager)
|
||||
}
|
||||
is GlobalAction.SetIfCardVerifiedOnline ->
|
||||
globalState.copy(cardVerifiedOnline = action.verified)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import com.tangem.tap.domain.tasks.product.ScanResponse
|
|||
import com.tangem.tap.features.feedback.FeedbackManager
|
||||
import com.tangem.tap.features.onboarding.OnboardingManager
|
||||
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
|
||||
import com.tangem.tap.network.moonpay.MoonpayStatus
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import org.rekotlin.StateType
|
||||
|
||||
data class GlobalState(
|
||||
|
|
@ -27,7 +27,7 @@ data class GlobalState(
|
|||
val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
|
||||
val scanCardFailsCounter: Int = 0,
|
||||
val dialog: StateDialog? = null,
|
||||
val moonpayStatus: MoonpayStatus? = null,
|
||||
val currencyExchangeManager: CurrencyExchangeManager? = null,
|
||||
val resources: AndroidResources = AndroidResources(),
|
||||
val analyticsHandlers: AnalyticsHandler? = null,
|
||||
) : StateType
|
||||
|
|
|
|||
|
|
@ -138,8 +138,7 @@ class TapWalletManager {
|
|||
loadMultiWalletData(data, blockchain, null)
|
||||
}
|
||||
}
|
||||
val moonPayStatus = store.state.globalState.moonpayStatus
|
||||
store.dispatch(WalletAction.LoadWallet(moonPayStatus))
|
||||
store.dispatch(WalletAction.LoadWallet())
|
||||
store.dispatch(WalletAction.LoadFiatRate())
|
||||
}
|
||||
}
|
||||
|
|
@ -201,8 +200,7 @@ class TapWalletManager {
|
|||
return@withContext
|
||||
}
|
||||
|
||||
val moonPayStatus = store.state.globalState.moonpayStatus
|
||||
store.dispatch(WalletAction.LoadWallet(moonPayStatus))
|
||||
store.dispatch(WalletAction.LoadWallet())
|
||||
store.dispatch(WalletAction.LoadFiatRate())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.tangem.tap.domain.configurable.Loader
|
|||
data class Config(
|
||||
val coinMarketCapKey: String = "f6622117-c043-47a0-8975-9d673ce484de",
|
||||
val moonPayApiKey: String = "pk_test_kc90oYTANy7UQdBavDKGfL4K9l6VEPE",
|
||||
val onramperApiKey: String = "pk_test_Ix2aCF3ej_5tcDKkBR7MChIvf5Nb0oPORPQ3Oal5G8I0",
|
||||
val moonPayApiSecretKey: String = "sk_test_V8w4M19LbDjjYOt170s0tGuvXAgyEb1C",
|
||||
val appsFlyerDevKey: String = "",
|
||||
val blockchainSdkConfig: BlockchainSdkConfig = BlockchainSdkConfig(),
|
||||
|
|
@ -81,6 +82,7 @@ class ConfigManager(
|
|||
config = config.copy(
|
||||
coinMarketCapKey = values.coinMarketCapKey,
|
||||
moonPayApiKey = values.moonPayApiKey,
|
||||
onramperApiKey = values.onramperApiKey,
|
||||
moonPayApiSecretKey = values.moonPayApiSecretKey,
|
||||
blockchainSdkConfig = BlockchainSdkConfig(
|
||||
blockchairApiKey = values.blockchairApiKey,
|
||||
|
|
@ -94,6 +96,7 @@ class ConfigManager(
|
|||
defaultConfig = defaultConfig.copy(
|
||||
coinMarketCapKey = values.coinMarketCapKey,
|
||||
moonPayApiKey = values.moonPayApiKey,
|
||||
onramperApiKey = values.onramperApiKey,
|
||||
moonPayApiSecretKey = values.moonPayApiSecretKey,
|
||||
blockchainSdkConfig = BlockchainSdkConfig(
|
||||
blockchairApiKey = values.blockchairApiKey,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ class FeatureModel(
|
|||
class ConfigValueModel(
|
||||
val coinMarketCapKey: String,
|
||||
val moonPayApiKey: String,
|
||||
val onramperApiKey: String,
|
||||
val moonPayApiSecretKey: String,
|
||||
val blockchairApiKey: String?,
|
||||
val blockchairAuthorizationToken: String?,
|
||||
|
|
|
|||
|
|
@ -2,13 +2,22 @@ package com.tangem.tap.domain.extensions
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.tap.features.wallet.redux.Currency
|
||||
import com.tangem.tap.network.moonpay.MoonpayStatus
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeStatus
|
||||
import com.tangem.tap.store
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
fun MoonpayStatus.buyIsAllowed(currency: Currency): Boolean {
|
||||
fun CurrencyExchangeManager.buyIsAllowed(currency: Currency): Boolean {
|
||||
return this.status?.buyIsAllowed(currency) ?: false
|
||||
}
|
||||
|
||||
fun CurrencyExchangeManager.sellIsAllowed(currency: Currency): Boolean {
|
||||
return this.status?.sellIsAllowed(currency) ?: false
|
||||
}
|
||||
|
||||
fun CurrencyExchangeStatus.buyIsAllowed(currency: Currency): Boolean {
|
||||
if (store.state.globalState.configManager?.config?.isTopUpEnabled == false) return false
|
||||
if (!isBuyAllowed) return false
|
||||
|
||||
|
|
@ -25,16 +34,17 @@ fun MoonpayStatus.buyIsAllowed(currency: Currency): Boolean {
|
|||
}
|
||||
}
|
||||
|
||||
fun MoonpayStatus.sellIsAllowed(currency: Currency): Boolean {
|
||||
fun CurrencyExchangeStatus.sellIsAllowed(currency: Currency): Boolean {
|
||||
if (store.state.globalState.configManager?.config?.isTopUpEnabled == false) return false
|
||||
if (!isSellAllowed) return false
|
||||
|
||||
return when (currency) {
|
||||
is Currency.Blockchain -> {
|
||||
if (currency.blockchain == Blockchain.Unknown || currency.blockchain == Blockchain.BSC) {
|
||||
false
|
||||
} else {
|
||||
availableToSell.contains(currency.currencySymbol)
|
||||
val blockchain = currency.blockchain
|
||||
when {
|
||||
blockchain.isTestnet() -> false
|
||||
blockchain == Blockchain.Unknown || currency.blockchain == Blockchain.BSC -> false
|
||||
else -> availableToSell.contains(currency.currencySymbol)
|
||||
}
|
||||
}
|
||||
is Currency.Token -> false
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
package com.tangem.tap.domain.topup
|
||||
|
||||
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.Token
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.tap.common.extensions.safeUpdate
|
||||
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
|
||||
|
||||
class TopUpManager {
|
||||
suspend fun topUpTestErc20Tokens(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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
package com.tangem.tap.domain.topup
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.tap.common.redux.global.CryptoCurrencyName
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class TradeCryptoHelper {
|
||||
|
||||
companion object {
|
||||
|
||||
const val TRANSACTION_ID_PARAM = "transactionId"
|
||||
const val CURRENCY_CODE_PARAM = "baseCurrencyCode"
|
||||
const val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount"
|
||||
const val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress"
|
||||
|
||||
private const val REDIRECT_URL_BUY = "tangem://success.tangem.com"
|
||||
private const val REDIRECT_URL_SELL_REQUEST = "tangem://sell-request.tangem.com"
|
||||
|
||||
private const val BASE_URL_BUY =
|
||||
"https://buy.moonpay.io"
|
||||
// "https://buy-staging.moonpay.io" // TESTNET
|
||||
private const val BASE_URL_SELL =
|
||||
"https://sell.moonpay.com"
|
||||
// "https://sell-staging.moonpay.com" // TESTNET
|
||||
|
||||
private const val API_KEY_PATH = "?apiKey="
|
||||
private const val CURRENCY_PATH = "¤cyCode="
|
||||
private const val WALLET_ADDRESS_PATH = "&walletAddress="
|
||||
|
||||
private const val REFUND_WALLET_ADDRESS_PATH = "&refundWalletAddress="
|
||||
private const val BASE_CURRENCY_PATH = "&baseCurrencyCode="
|
||||
|
||||
private const val REDIRECT_URL_PATH = "&redirectURL="
|
||||
private const val SIGNATURE_PATH = "&signature="
|
||||
|
||||
private const val TRANSACTION_RECEIPT_PATH = "transaction_receipt?transactionId="
|
||||
|
||||
private const val BASE_CURRENCY_USD = "USD"
|
||||
|
||||
fun getUrl(
|
||||
action: Action,
|
||||
blockchain: Blockchain?,
|
||||
cryptoCurrencyName: CryptoCurrencyName,
|
||||
walletAddress: String,
|
||||
apiKey: String,
|
||||
secretKey: String,
|
||||
): String {
|
||||
val originalQuery: String
|
||||
val baseUrl: String
|
||||
when (action) {
|
||||
Action.Buy -> {
|
||||
if (blockchain?.isTestnet() == true) {
|
||||
return blockchain.getTestnetTopUpUrl() ?: ""
|
||||
}
|
||||
baseUrl = BASE_URL_BUY
|
||||
originalQuery = API_KEY_PATH + apiKey.urlEncode() +
|
||||
CURRENCY_PATH + cryptoCurrencyName.urlEncode() +
|
||||
BASE_CURRENCY_PATH + BASE_CURRENCY_USD.urlEncode() +
|
||||
WALLET_ADDRESS_PATH + walletAddress.urlEncode() +
|
||||
REDIRECT_URL_PATH + REDIRECT_URL_BUY.urlEncode()
|
||||
}
|
||||
Action.Sell -> {
|
||||
baseUrl = BASE_URL_SELL
|
||||
originalQuery = API_KEY_PATH + apiKey.urlEncode() +
|
||||
BASE_CURRENCY_PATH + cryptoCurrencyName.urlEncode() +
|
||||
REFUND_WALLET_ADDRESS_PATH + walletAddress.urlEncode() +
|
||||
REDIRECT_URL_PATH + REDIRECT_URL_SELL_REQUEST.urlEncode()
|
||||
}
|
||||
}
|
||||
val signature = createSignature(originalQuery, secretKey)
|
||||
return baseUrl + originalQuery + SIGNATURE_PATH + signature.urlEncode()
|
||||
|
||||
}
|
||||
|
||||
fun getSellCryptoReceiptUrl(transactionId: String): String {
|
||||
return BASE_URL_SELL + TRANSACTION_RECEIPT_PATH + transactionId
|
||||
}
|
||||
|
||||
private fun String.urlEncode(): String {
|
||||
return Uri.encode(this)
|
||||
}
|
||||
|
||||
private fun createSignature(data: String, key: String): String {
|
||||
val sha256Hmac = Mac.getInstance("HmacSHA256")
|
||||
val secretKey = SecretKeySpec(key.toByteArray(), "HmacSHA256")
|
||||
sha256Hmac.init(secretKey)
|
||||
val sha256encoded = sha256Hmac.doFinal(data.toByteArray())
|
||||
return Base64.encodeToString(sha256encoded, Base64.NO_WRAP)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Action { Buy, Sell }
|
||||
}
|
||||
|
|
@ -25,13 +25,12 @@ fun HomeButtons(
|
|||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
modifier = modifier.fillMaxWidth()
|
||||
modifier = modifier
|
||||
) {
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(42.dp)
|
||||
,
|
||||
.height(48.dp),
|
||||
onClick = onScanButtonClick,
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
backgroundColor = if (isDarkBackground) darkColorBackground else Color.White,
|
||||
|
|
@ -42,14 +41,15 @@ fun HomeButtons(
|
|||
text = stringResource(id = R.string.home_button_scan),
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 16.sp,
|
||||
textAlign = TextAlign.Center
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(42.dp),
|
||||
.height(48.dp),
|
||||
onClick = onShopButtonClick,
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
backgroundColor = if (isDarkBackground) Color.White else darkColorBackground,
|
||||
|
|
@ -60,7 +60,8 @@ fun HomeButtons(
|
|||
text = stringResource(id = R.string.home_button_order),
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 16.sp,
|
||||
textAlign = TextAlign.Center
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ fun StoriesScreen(
|
|||
isDarkBackground = isDarkBackground,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(37.dp)
|
||||
.padding(16.dp, 0.dp, 16.dp, 37.dp)
|
||||
.fillMaxWidth(),
|
||||
onScanButtonClick = onScanButtonClick,
|
||||
onShopButtonClick = onShopButtonClick
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ private val homeMiddleware: Middleware<AppState> = { dispatch, state ->
|
|||
when (action) {
|
||||
is HomeAction.Init -> {
|
||||
store.dispatch(GlobalAction.RestoreAppCurrency)
|
||||
store.dispatch(GlobalAction.GetMoonPayStatus)
|
||||
store.dispatch(GlobalAction.InitCurrencyExchangeManager)
|
||||
store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted()))
|
||||
}
|
||||
is HomeAction.ShouldScanCardOnResume -> {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ data class OnboardingNoteState(
|
|||
get() = steps.indexOf(currentStep)
|
||||
|
||||
val isBuyAllowed: Boolean by ReadOnlyProperty<Any, Boolean> { thisRef, property ->
|
||||
store.state.globalState.moonpayStatus?.buyIsAllowed(walletBalance.currency) ?: false
|
||||
store.state.globalState.currencyExchangeManager?.buyIsAllowed(walletBalance.currency) ?: false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ data class TwinCardsState(
|
|||
get() = currentStep == TwinCardsStep.CreateSecondWallet || currentStep == TwinCardsStep.CreateThirdWallet
|
||||
|
||||
val isBuyAllowed: Boolean by ReadOnlyProperty<Any, Boolean> { thisRef, property ->
|
||||
store.state.globalState.moonpayStatus?.buyIsAllowed(walletBalance.currency) ?: false
|
||||
store.state.globalState.currencyExchangeManager?.buyIsAllowed(walletBalance.currency) ?: false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import android.view.inputmethod.EditorInfo
|
|||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import com.tangem.tap.common.GlobalLayoutStateHandler
|
||||
import com.tangem.tap.common.KeyboardObserver
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
|
|
@ -71,17 +72,25 @@ class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<
|
|||
}
|
||||
}
|
||||
|
||||
private var cardTranslationY = 70f
|
||||
private fun setupCardsImages() {
|
||||
binding.imvSecond.animate()
|
||||
.translationY(70f)
|
||||
.scaleX(0.9f)
|
||||
.scaleY(0.9f)
|
||||
.start()
|
||||
binding.imvThird.animate()
|
||||
.translationY(140f)
|
||||
.scaleX(0.8f)
|
||||
.scaleY(0.8f)
|
||||
.start()
|
||||
GlobalLayoutStateHandler(binding.imvSecond).apply {
|
||||
onStateChanged = {
|
||||
cardTranslationY = it.height * 0.15f
|
||||
binding.imvSecond.animate()
|
||||
.translationY(cardTranslationY)
|
||||
.scaleX(0.9f)
|
||||
.scaleY(0.9f)
|
||||
.start()
|
||||
binding.imvThird.animate()
|
||||
.translationY(cardTranslationY * 2)
|
||||
.scaleX(0.8f)
|
||||
.scaleY(0.8f)
|
||||
.start()
|
||||
detach()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun setupProductSelection() = with(binding) {
|
||||
|
|
@ -130,7 +139,7 @@ class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<
|
|||
}
|
||||
|
||||
private fun showOrHideThirdCardWithAnimation(show: Boolean) = with(binding) {
|
||||
val translationY = if (show) 140f else 0f
|
||||
val translationY = if (show) cardTranslationY * 2 else cardTranslationY
|
||||
if (show) imvThird.show()
|
||||
imvThird.animate()
|
||||
.translationY(translationY)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import com.tangem.tap.common.redux.NotificationAction
|
|||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||
import com.tangem.tap.domain.tokens.CardCurrencies
|
||||
import com.tangem.tap.network.moonpay.MoonpayStatus
|
||||
import com.tangem.wallet.R
|
||||
import org.rekotlin.Action
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -25,11 +24,7 @@ sealed class WalletAction : Action {
|
|||
}
|
||||
|
||||
|
||||
data class LoadWallet(
|
||||
val moonpayStatus: MoonpayStatus? = null,
|
||||
val blockchain: Blockchain? = null,
|
||||
) :
|
||||
WalletAction() {
|
||||
data class LoadWallet(val blockchain: Blockchain? = null) : WalletAction() {
|
||||
data class Success(val wallet: Wallet) : WalletAction()
|
||||
data class NoAccount(val wallet: Wallet, val amountToCreateAccount: String) : WalletAction()
|
||||
data class Failure(val wallet: Wallet, val errorMessage: String? = null) : WalletAction()
|
||||
|
|
@ -146,5 +141,5 @@ sealed class WalletAction : Action {
|
|||
|
||||
data class ChangeSelectedAddress(val type: AddressType) : WalletAction()
|
||||
|
||||
data class SetWalletRent(val blockchain: Blockchain, val minRent: String, val rentExempt: String): WalletAction()
|
||||
data class SetWalletRent(val blockchain: Blockchain, val minRent: String, val rentExempt: String) : WalletAction()
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ import com.tangem.tap.features.wallet.models.toPendingTransactions
|
|||
import com.tangem.tap.features.wallet.models.toPendingTransactionsForToken
|
||||
import com.tangem.tap.features.wallet.ui.BalanceStatus
|
||||
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
|
||||
import com.tangem.tap.network.moonpay.MoonpayStatus
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.store
|
||||
import org.rekotlin.StateType
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -160,12 +160,18 @@ data class WalletState(
|
|||
return updatedWallets + remainingWallets
|
||||
}
|
||||
|
||||
fun updateTradeCryptoState(moonpayStatus: MoonpayStatus?, walletData: WalletData): WalletData {
|
||||
return walletData.copy(tradeCryptoState = TradeCryptoState.from(moonpayStatus, walletData))
|
||||
fun updateTradeCryptoState(
|
||||
exchangeManager: CurrencyExchangeManager?,
|
||||
walletData: WalletData
|
||||
): WalletData {
|
||||
return walletData.copy(tradeCryptoState = TradeCryptoState.from(exchangeManager, walletData))
|
||||
}
|
||||
|
||||
fun updateTradeCryptoState(moonpayStatus: MoonpayStatus?, walletDataList: List<WalletData>): List<WalletData> {
|
||||
return walletDataList.map { it.copy(tradeCryptoState = TradeCryptoState.from(moonpayStatus, it)) }
|
||||
fun updateTradeCryptoState(
|
||||
exchangeManager: CurrencyExchangeManager?,
|
||||
walletDataList: List<WalletData>
|
||||
): List<WalletData> {
|
||||
return walletDataList.map { it.copy(tradeCryptoState = TradeCryptoState.from(exchangeManager, it)) }
|
||||
}
|
||||
|
||||
fun addWalletManagers(newWalletManagers: List<WalletManager>): WalletState {
|
||||
|
|
@ -226,8 +232,8 @@ data class TradeCryptoState(
|
|||
val buyingAllowed: Boolean = false,
|
||||
) {
|
||||
companion object {
|
||||
fun from(moonpayStatus: MoonpayStatus?, walletData: WalletData): TradeCryptoState {
|
||||
val status = moonpayStatus ?: return walletData.tradeCryptoState
|
||||
fun from(exchangeManager: CurrencyExchangeManager?, walletData: WalletData): TradeCryptoState {
|
||||
val status = exchangeManager ?: return walletData.tradeCryptoState
|
||||
val currency = walletData.currency
|
||||
|
||||
return TradeCryptoState(status.sellIsAllowed(currency), status.buyIsAllowed(currency))
|
||||
|
|
|
|||
|
|
@ -63,10 +63,7 @@ class MultiWalletMiddleware {
|
|||
}
|
||||
}
|
||||
store.dispatch(WalletAction.LoadFiatRate(currency = Currency.Blockchain(action.blockchain)))
|
||||
store.dispatch(WalletAction.LoadWallet(
|
||||
moonpayStatus = globalState.moonpayStatus,
|
||||
blockchain = action.blockchain
|
||||
)
|
||||
store.dispatch(WalletAction.LoadWallet(action.blockchain)
|
||||
)
|
||||
}
|
||||
is WalletAction.MultiWallet.SaveCurrencies -> {
|
||||
|
|
|
|||
|
|
@ -6,17 +6,16 @@ import com.tangem.tap.common.extensions.dispatchOnMain
|
|||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.domain.topup.TopUpManager
|
||||
import com.tangem.tap.domain.topup.TradeCryptoHelper
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.send.redux.PrepareSendScreen
|
||||
import com.tangem.tap.features.send.redux.SendAction
|
||||
import com.tangem.tap.features.wallet.redux.Currency
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.network.exchangeServices.buyErc20Tokens
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
class TradeCryptoMiddleware {
|
||||
|
|
@ -33,44 +32,38 @@ class TradeCryptoMiddleware {
|
|||
|
||||
private fun startExchange(action: WalletAction.TradeCryptoAction) {
|
||||
val selectedWalletData = store.state.walletState.getSelectedWalletData()
|
||||
val config = store.state.globalState.configManager?.config ?: return
|
||||
val exchangeManager = store.state.globalState.currencyExchangeManager ?: return
|
||||
val addresses = selectedWalletData?.walletAddresses ?: return
|
||||
if (addresses.list.isEmpty()) return
|
||||
val appCurrency = store.state.globalState.appCurrency
|
||||
|
||||
val defaultAddress = addresses.list[0].address
|
||||
val currency = selectedWalletData.currency
|
||||
val currencySymbol = selectedWalletData.currency?.currencySymbol ?: return
|
||||
val currencySymbol = selectedWalletData.currency.currencySymbol
|
||||
|
||||
val exchangeAction = if (action is WalletAction.TradeCryptoAction.Buy) {
|
||||
TradeCryptoHelper.Action.Buy
|
||||
CurrencyExchangeManager.Action.Buy
|
||||
} else {
|
||||
TradeCryptoHelper.Action.Sell
|
||||
CurrencyExchangeManager.Action.Sell
|
||||
}
|
||||
|
||||
if (exchangeAction == TradeCryptoHelper.Action.Buy &&
|
||||
if (exchangeAction == CurrencyExchangeManager.Action.Buy &&
|
||||
currency is Currency.Token && currency.blockchain.isTestnet()
|
||||
) {
|
||||
val walletManager = store.state.walletState.getWalletManager(currency.token)
|
||||
if (walletManager !is EthereumWalletManager) return
|
||||
|
||||
scope.launch {
|
||||
TopUpManager().topUpTestErc20Tokens(
|
||||
walletManager = walletManager, token = currency.token
|
||||
)
|
||||
}
|
||||
scope.launch { exchangeManager.buyErc20Tokens(walletManager, currency.token) }
|
||||
return
|
||||
}
|
||||
|
||||
val url = TradeCryptoHelper.getUrl(
|
||||
exchangeManager.getUrl(
|
||||
action = exchangeAction,
|
||||
blockchain = currency?.blockchain,
|
||||
blockchain = currency.blockchain,
|
||||
cryptoCurrencyName = currencySymbol,
|
||||
walletAddress = defaultAddress,
|
||||
apiKey = config.moonPayApiKey,
|
||||
secretKey = config.moonPayApiSecretKey
|
||||
)
|
||||
Timber.d("Moonpay $exchangeAction URL: $url")
|
||||
store.dispatchOnMain(NavigationAction.OpenUrl(url))
|
||||
fiatCurrency = appCurrency,
|
||||
walletAddress = defaultAddress)?.let { store.dispatchOnMain(NavigationAction.OpenUrl(it)) }
|
||||
|
||||
}
|
||||
|
||||
private fun preconfigureAndOpenSendScreen(action: WalletAction.TradeCryptoAction.SendCrypto) {
|
||||
|
|
@ -91,8 +84,11 @@ class TradeCryptoMiddleware {
|
|||
}
|
||||
|
||||
private fun openReceiptUrl(transactionId: String) {
|
||||
val exchangeManager = store.state.globalState.currencyExchangeManager ?: return
|
||||
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
val url = TradeCryptoHelper.getSellCryptoReceiptUrl(transactionId)
|
||||
store.dispatchOnMain(NavigationAction.OpenUrl(url))
|
||||
exchangeManager.getSellCryptoReceiptUrl(CurrencyExchangeManager.Action.Sell, transactionId)?.let {
|
||||
store.dispatchOnMain(NavigationAction.OpenUrl(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -51,12 +51,12 @@ class WarningsMiddleware {
|
|||
}
|
||||
is WalletAction.Warnings.CheckRemainingSignatures -> {
|
||||
if (action.remainingSignatures != null &&
|
||||
action.remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING
|
||||
action.remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING
|
||||
) {
|
||||
store.state.globalState.warningManager
|
||||
?.removeWarnings(
|
||||
messageRes = R.string.warning_low_signatures_format
|
||||
)
|
||||
?.removeWarnings(
|
||||
messageRes = R.string.warning_low_signatures_format
|
||||
)
|
||||
addWarningMessage(
|
||||
warning =
|
||||
WarningMessagesManager.remainingSignaturesNotEnough(action.remainingSignatures),
|
||||
|
|
@ -100,7 +100,7 @@ class WarningsMiddleware {
|
|||
addWarningMessage(WarningMessagesManager.onlineVerificationFailed())
|
||||
}
|
||||
}
|
||||
if (scanResponse.isDemoCard()){
|
||||
if (scanResponse.isDemoCard()) {
|
||||
addWarningMessage(WarningMessagesManager.demoCardWarning())
|
||||
}
|
||||
setWarningMessages()
|
||||
|
|
@ -110,7 +110,7 @@ class WarningsMiddleware {
|
|||
private fun showWarningLowRemainingSignaturesIfNeeded(card: Card) {
|
||||
val remainingSignatures = card.remainingSignatures
|
||||
if (remainingSignatures != null &&
|
||||
remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING
|
||||
remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING
|
||||
) {
|
||||
addWarningMessage(
|
||||
WarningMessagesManager.remainingSignaturesNotEnough(
|
||||
|
|
@ -123,7 +123,7 @@ class WarningsMiddleware {
|
|||
private fun checkIfWarningNeeded(
|
||||
scanResponse: ScanResponse,
|
||||
): WarningMessage? {
|
||||
if (scanResponse.isTangemTwins()) return null
|
||||
if (scanResponse.isTangemTwins() || scanResponse.isDemoCard()) return null
|
||||
|
||||
if (scanResponse.card.isMultiwalletAllowed) {
|
||||
return if (scanResponse.card.hasSignedHashes()) {
|
||||
|
|
@ -135,7 +135,7 @@ class WarningsMiddleware {
|
|||
}
|
||||
|
||||
val validator = store.state.walletState.walletManagers.firstOrNull()
|
||||
as? SignatureCountValidator
|
||||
as? SignatureCountValidator
|
||||
return if (validator == null) {
|
||||
if (scanResponse.card.hasSignedHashes()) {
|
||||
WarningMessagesManager.alreadySignedHashesWarning()
|
||||
|
|
@ -160,7 +160,7 @@ class WarningsMiddleware {
|
|||
if (scanResponse.isTangemTwins() || card.isMultiwalletAllowed) return
|
||||
|
||||
val validator = store.state.walletState.walletManagers.firstOrNull()
|
||||
as? SignatureCountValidator
|
||||
as? SignatureCountValidator
|
||||
scope.launch {
|
||||
val signedHashes = card.getSingleWallet()?.totalSignedHashes ?: 0
|
||||
val result = validator?.validateSignatureCount(signedHashes)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class OnWalletLoadedReducer {
|
|||
|
||||
private fun onMultiWalletLoaded(wallet: Wallet, walletState: WalletState): WalletState {
|
||||
val fiatCurrencySymbol = store.state.globalState.appCurrency
|
||||
val moonpayStatus = store.state.globalState.moonpayStatus
|
||||
val exchangeManager = store.state.globalState.currencyExchangeManager
|
||||
|
||||
val coinAmountValue = wallet.amounts[AmountType.Coin]?.value
|
||||
if (walletState.getWalletData(wallet.blockchain) == null) {
|
||||
|
|
@ -62,7 +62,7 @@ class OnWalletLoadedReducer {
|
|||
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
|
||||
mainButton = WalletMainButton.SendButton(coinSendButton),
|
||||
currency = Currency.Blockchain(wallet.blockchain),
|
||||
tradeCryptoState = TradeCryptoState.from(moonpayStatus, walletData)
|
||||
tradeCryptoState = TradeCryptoState.from(exchangeManager, walletData)
|
||||
)
|
||||
|
||||
val tokens = wallet.getTokens().mapNotNull { token ->
|
||||
|
|
@ -86,7 +86,7 @@ class OnWalletLoadedReducer {
|
|||
),
|
||||
pendingTransactions = tokenPendingTransactions.removeUnknownTransactions(),
|
||||
mainButton = WalletMainButton.SendButton(tokenSendButton),
|
||||
tradeCryptoState = TradeCryptoState.from(moonpayStatus, tokenWalletData)
|
||||
tradeCryptoState = TradeCryptoState.from(exchangeManager, tokenWalletData)
|
||||
)
|
||||
}
|
||||
val newWallets = (tokens + newWalletData).mapNotNull { it }
|
||||
|
|
@ -106,7 +106,7 @@ class OnWalletLoadedReducer {
|
|||
if (wallet.blockchain != walletState.primaryBlockchain) return walletState
|
||||
|
||||
val fiatCurrencySymbol = store.state.globalState.appCurrency
|
||||
val moonpayStatus = store.state.globalState.moonpayStatus
|
||||
val exchangeManager = store.state.globalState.currencyExchangeManager
|
||||
|
||||
val token = wallet.getFirstToken()
|
||||
val tokenData = if (token != null) {
|
||||
|
|
@ -154,7 +154,7 @@ class OnWalletLoadedReducer {
|
|||
),
|
||||
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
|
||||
mainButton = WalletMainButton.SendButton(sendButtonEnabled),
|
||||
tradeCryptoState = TradeCryptoState.from(moonpayStatus, walletState.primaryWallet)
|
||||
tradeCryptoState = TradeCryptoState.from(exchangeManager, walletState.primaryWallet)
|
||||
)
|
||||
val wallets = walletData?.let { listOf(walletData) } ?: emptyList()
|
||||
return walletState.copy(
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
|
|||
|
||||
if (action !is WalletAction) return state.walletState
|
||||
|
||||
val moonpayStatus = store.state.globalState.moonpayStatus
|
||||
val exchangeManager = store.state.globalState.currencyExchangeManager
|
||||
var newState = state.walletState
|
||||
|
||||
when (action) {
|
||||
|
|
@ -103,7 +103,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
|
|||
currencySymbol = wallet.currencyData.currencySymbol,
|
||||
),
|
||||
mainButton = WalletMainButton.SendButton(false),
|
||||
tradeCryptoState = TradeCryptoState.from(action.moonpayStatus, wallet)
|
||||
tradeCryptoState = TradeCryptoState.from(exchangeManager, wallet)
|
||||
)
|
||||
}
|
||||
newState = newState.copy(
|
||||
|
|
@ -124,11 +124,11 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
|
|||
currencySymbol = wallet.currencyData.currencySymbol,
|
||||
),
|
||||
mainButton = WalletMainButton.SendButton(false),
|
||||
tradeCryptoState = TradeCryptoState.from(action.moonpayStatus, wallet)
|
||||
tradeCryptoState = TradeCryptoState.from(exchangeManager, wallet)
|
||||
)
|
||||
}
|
||||
val wallets = newState.replaceSomeWallets(newWallets)
|
||||
newState = newState.copy(wallets = newState.updateTradeCryptoState(moonpayStatus, wallets))
|
||||
newState = newState.copy(wallets = newState.updateTradeCryptoState(exchangeManager, wallets))
|
||||
}
|
||||
}
|
||||
is WalletAction.LoadWallet.Success -> newState =
|
||||
|
|
@ -153,7 +153,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
|
|||
}
|
||||
newState = newState.copy(
|
||||
state = progressState,
|
||||
wallets = newState.updateTradeCryptoState(moonpayStatus, wallets)
|
||||
wallets = newState.updateTradeCryptoState(exchangeManager, wallets)
|
||||
)
|
||||
}
|
||||
is WalletAction.LoadWallet.Failure -> {
|
||||
|
|
@ -188,7 +188,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
|
|||
}
|
||||
newState = newState.copy(
|
||||
state = progressState,
|
||||
wallets = newState.updateTradeCryptoState(moonpayStatus, wallets)
|
||||
wallets = newState.updateTradeCryptoState(exchangeManager, wallets)
|
||||
)
|
||||
}
|
||||
is WalletAction.SetArtworkId -> {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
|
|
@ -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>
|
||||
)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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,
|
||||
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
|
||||
)
|
||||
|
|
@ -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/"
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
|
|
@ -53,10 +53,12 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="36dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="44dp"
|
||||
android:layout_marginEnd="36dp"
|
||||
android:layout_weight="1"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="44dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/tv_header"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue