Updated on 2026-08-14

This commit is contained in:
Tangem 2022-03-03 10:38:44 +00:00
commit f1bc442a65
35 changed files with 743 additions and 431 deletions

View file

@ -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")
}
}

View file

@ -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"
}

View file

@ -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)

View file

@ -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,
)
}

View file

@ -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()
}
}

View file

@ -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))
}
}
}

View file

@ -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)

View file

@ -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

View file

@ -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())
}
}

View file

@ -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,

View file

@ -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?,

View file

@ -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

View file

@ -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)
}
}

View file

@ -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 = "&currencyCode="
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 }
}

View file

@ -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
)
}
}

View file

@ -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

View file

@ -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 -> {

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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)

View file

@ -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()
}

View file

@ -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))

View file

@ -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 -> {

View file

@ -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))
}
}
}

View file

@ -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)

View file

@ -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(

View file

@ -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 -> {

View file

@ -0,0 +1,136 @@
package com.tangem.tap.network.exchangeServices
import com.tangem.Message
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.extensions.Result
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TangemSigner
import com.tangem.tap.store
import com.tangem.tap.tangemSdk
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
interface ExchangeService {
suspend fun isBuyAllowed(): Boolean
suspend fun availableToBuy(): List<String>
suspend fun isSellAllowed(): Boolean
suspend fun availableToSell(): List<String>
}
interface ExchangeUrlBuilder {
fun getUrl(
action: CurrencyExchangeManager.Action,
blockchain: Blockchain,
cryptoCurrencyName: CryptoCurrencyName,
fiatCurrency: String,
walletAddress: String,
): String?
fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String?
companion object {
const val SCHEME = "https"
const val URL_SELL = "sell.moonpay.com"
const val SUCCESS_URL = "tangem://success.tangem.com"
}
}
class CurrencyExchangeManager(
private val onramperService: ExchangeService,
private val moonPayService: ExchangeService,
) : ExchangeService, ExchangeUrlBuilder {
var status: CurrencyExchangeStatus? = null
private set
suspend fun getStatus(): CurrencyExchangeStatus {
val isBuyAllowed = isBuyAllowed()
val isSellAllowed = isSellAllowed()
val availableToBuy = availableToBuy()
val availableToSell = availableToSell()
status = CurrencyExchangeStatus(
isBuyAllowed,
isSellAllowed,
availableToBuy,
availableToSell,
)
return status!!
}
override suspend fun isBuyAllowed(): Boolean = onramperService.isBuyAllowed()
override suspend fun availableToBuy(): List<String> = onramperService.availableToBuy()
override suspend fun isSellAllowed(): Boolean = moonPayService.isSellAllowed()
override suspend fun availableToSell(): List<String> = moonPayService.availableToSell()
override fun getUrl(
action: Action,
blockchain: Blockchain,
cryptoCurrencyName: CryptoCurrencyName,
fiatCurrency: String,
walletAddress: String,
): String? {
if (blockchain.isTestnet()) return blockchain.getTestnetTopUpUrl()
val urlBuilder = getExchangeUrlBuilder(action)
return urlBuilder.getUrl(action, blockchain, cryptoCurrencyName, fiatCurrency, walletAddress)
}
override fun getSellCryptoReceiptUrl(action: Action, transactionId: String): String? {
val urlBuilder = getExchangeUrlBuilder(action)
return urlBuilder.getSellCryptoReceiptUrl(action, transactionId)
}
private fun getExchangeUrlBuilder(action: Action): ExchangeUrlBuilder {
return when (action) {
Action.Buy -> onramperService
Action.Sell -> moonPayService
} as ExchangeUrlBuilder
}
enum class Action { Buy, Sell }
}
data class CurrencyExchangeStatus(
val isBuyAllowed: Boolean,
val isSellAllowed: Boolean,
val availableToBuy: List<String>,
val availableToSell: List<String>,
)
suspend fun CurrencyExchangeManager.buyErc20Tokens(walletManager: EthereumWalletManager, token: Token) {
walletManager.safeUpdate()
val amountToSend = Amount(walletManager.wallet.blockchain)
val destinationAddress = token.contractAddress
val feeResult = walletManager.getFee(amountToSend,
destinationAddress) as? Result.Success ?: return
val fee = feeResult.data[0]
if ((walletManager.wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO) < fee.value) {
return
}
val transaction = walletManager.createTransaction(amountToSend, fee, destinationAddress)
val signer = TangemSigner(
tangemSdk = tangemSdk, Message()
) { signResponse ->
store.dispatch(
GlobalAction.UpdateWalletSignedHashes(
walletSignedHashes = signResponse.totalSignedHashes,
walletPublicKey = walletManager.wallet.publicKey.seedKey,
remainingSignatures = signResponse.remainingSignatures
)
)
}
walletManager.send(transaction, signer)
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,111 @@
package com.tangem.tap.network.exchangeServices.onramper
import android.net.Uri
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.tap.common.extensions.urlEncode
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.network.createRetrofitInstance
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SCHEME
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SUCCESS_URL
import kotlinx.coroutines.coroutineScope
import java.util.*
/**
[REDACTED_AUTHOR]
*/
class OnramperService(
val apiKey: String
) : ExchangeService, ExchangeUrlBuilder {
private val api: OnramperApi by lazy {
createRetrofitInstance(OnramperApi.BASE_URL, listOf(AddKeyToHeaderInterceptor(apiKey)))
.create(OnramperApi::class.java)
}
private var status: OnramperStatus? = null
private suspend fun updateStatus() {
try {
coroutineScope {
val result = performRequest { api.gateways() }
if (result is Result.Failure) return@coroutineScope result
val response = (result as Result.Success).data
val currenciesToBuy = extractCurrenciesToBuy(response).sorted()
val status = OnramperStatus(currenciesToBuy, response)
this@OnramperService.status = status
}
} catch (error: Error) {
status = null
Result.Failure(error)
}
}
private fun extractCurrenciesToBuy(response: GatewaysResponse): List<String> {
return response.gateways.map { gateway ->
gateway.cryptoCurrencies.map { currency -> currency.code }
}.flatten().toMutableSet().toList()
}
override suspend fun isBuyAllowed(): Boolean {
refreshStatus()
return status != null
}
override suspend fun availableToBuy(): List<String> {
refreshStatus()
return status?.availableToBuy ?: emptyList()
}
private suspend fun refreshStatus() {
if (status == null) {
updateStatus()
}
}
override suspend fun isSellAllowed(): Boolean = false
override suspend fun availableToSell(): List<String> = listOf()
override fun getUrl(
action: CurrencyExchangeManager.Action,
blockchain: Blockchain,
cryptoCurrencyName: CryptoCurrencyName,
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
)

View file

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

View file

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

View file

@ -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">