Updated on 2026-08-14

This commit is contained in:
Tangem 2024-01-10 16:46:53 +03:00
commit cf782b8e13
174 changed files with 2953 additions and 1544 deletions

View file

@ -86,6 +86,7 @@ dependencies {
implementation(projects.features.swap.api)
implementation(projects.features.swap.presentation)
implementation(projects.features.swap.domain)
implementation(projects.features.swap.domain.api)
implementation(projects.features.swap.data)
implementation(projects.features.tester.api)
implementation(projects.features.tester.impl)

@ -1 +1 @@
Subproject commit 6ef8ce45d183905b5752e2d33c1d8bf2f4bcace6
Subproject commit 4a1be08512fe2b45504f2bdaa0e35f5c11e11319

View file

@ -1,4 +1,4 @@
package com.tangem.feature.onboarding.data.di
package com.tangem.tap.di.domain
import android.content.Context
import com.tangem.crypto.bip39.Wordlist
@ -14,11 +14,11 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class OnboardingDataModule {
class MnemonicModule {
@Provides
@Singleton
fun provideSeedPhraseSdkRepository(@ApplicationContext context: Context): MnemonicRepository {
fun provideMnemonicRepository(@ApplicationContext context: Context): MnemonicRepository {
return DefaultMnemonicRepository(Wordlist.getWordlist(context))
}
}

View file

@ -8,6 +8,7 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.settings.*
import com.tangem.domain.settings.repositories.AppRatingRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.repositories.SwapPromoRepository
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository
import dagger.Module
@ -103,4 +104,20 @@ internal object SettingsDomainModule {
fun provideIsWalletsScrollPreviewEnabled(settingsRepository: SettingsRepository): IsWalletsScrollPreviewEnabled {
return IsWalletsScrollPreviewEnabled(settingsRepository = settingsRepository)
}
@Provides
@ViewModelScoped
fun provideShouldShowSwapPromoWalletUseCase(
swapPromoRepository: SwapPromoRepository,
): ShouldShowSwapPromoWalletUseCase {
return ShouldShowSwapPromoWalletUseCase(swapPromoRepository)
}
@Provides
@ViewModelScoped
fun provideShouldShowSwapPromoTokenUseCase(
swapPromoRepository: SwapPromoRepository,
): ShouldShowSwapPromoTokenUseCase {
return ShouldShowSwapPromoTokenUseCase(swapPromoRepository)
}
}

View file

@ -1,9 +1,11 @@
package com.tangem.tap.di.domain
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -95,6 +97,9 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
swapRepository: SwapRepository,
showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
dispatchers: CoroutineDispatcherProvider,
): GetCurrencyWarningsUseCase {
return GetCurrencyWarningsUseCase(
@ -102,6 +107,9 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
marketCryptoCurrencyRepository = marketCryptoCurrencyRepository,
swapRepository = swapRepository,
showSwapPromoTokenUseCase = showSwapPromoTokenUseCase,
dispatchers = dispatchers,
)
}

View file

@ -8,6 +8,7 @@ import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.walletmanager.DefaultWalletManagersFacade
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.feature.onboarding.data.MnemonicRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -24,6 +25,7 @@ internal object WalletManagersFacadeModule {
walletManagersStore: WalletManagersStore,
userWalletsStore: UserWalletsStore,
configManager: ConfigManager,
mnemonicRepository: MnemonicRepository,
assetReader: AssetReader,
@SdkMoshi moshi: Moshi,
): WalletManagersFacade {
@ -33,6 +35,7 @@ internal object WalletManagersFacadeModule {
configManager = configManager,
assetReader = assetReader,
moshi = moshi,
mnemonic = mnemonicRepository.generateDefaultMnemonic(),
)
}
}

View file

@ -4,7 +4,7 @@ import com.tangem.Message
import com.tangem.blockchain.blockchains.ethereum.EthereumGasLoader
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.Companion.toKeccak
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.Result

View file

@ -21,6 +21,10 @@ class DemoTransactionSender(private val walletManager: WalletManager) : Transact
)
}
override suspend fun estimateFee(amount: Amount, destination: String): Result<TransactionFee> {
return getFee(amount, walletManager.wallet.address)
}
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
val signerResponse = signer.sign(
hash = getDataToSign(),

View file

@ -15,7 +15,7 @@ internal class DefaultExpressAuthProvider(
private var uuid = AtomicReference(UUID.randomUUID())
override fun getApiKey(): String {
return configManager.config.tangemExpressApiKey
return configManager.config.express?.apiKey ?: ""
}
override fun getUserId(): String {

View file

@ -1,13 +0,0 @@
package com.tangem.tap.network.auth
import com.tangem.datasource.config.ConfigManager
import com.tangem.lib.auth.AuthBearerProvider
internal class DefaultOneInchProvider(
private val configManager: ConfigManager,
) : AuthBearerProvider {
override fun getApiKey(): String {
return configManager.config.oneInchApiKey
}
}

View file

@ -2,12 +2,10 @@ package com.tangem.tap.network.auth.di
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.lib.auth.AuthBearerProvider
import com.tangem.lib.auth.AuthProvider
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.tap.network.auth.DefaultAuthProvider
import com.tangem.tap.network.auth.DefaultExpressAuthProvider
import com.tangem.tap.network.auth.DefaultOneInchProvider
import com.tangem.tap.proxy.AppStateHolder
import dagger.Module
import dagger.Provides
@ -36,12 +34,4 @@ class AuthModule {
configManager = configManager,
)
}
@Provides
@Singleton
fun provideOneInchAuthProvider(configManager: ConfigManager): AuthBearerProvider {
return DefaultOneInchProvider(
configManager = configManager,
)
}
}

View file

@ -11,6 +11,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TangemSdkManager
@ -63,6 +64,10 @@ class AppStateHolder @Inject constructor() : WalletsStateHolder, ReduxNavControl
mainStore?.dispatch(action)
}
override suspend fun dispatchWithMain(action: Action) {
mainStore?.dispatchWithMain(action)
}
override suspend fun onUserWalletSelected(userWallet: UserWallet) {
mainStore?.onUserWalletSelected(userWallet)
}

View file

@ -1,10 +1,17 @@
package com.tangem.tap.proxy
import androidx.core.text.isDigitsOnly
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.Message
import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras
import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.blockchains.optimism.OptimismWalletManager
import com.tangem.blockchain.blockchains.stellar.StellarMemo
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
import com.tangem.blockchain.blockchains.ton.TonTransactionExtras
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
@ -140,6 +147,26 @@ class TransactionManagerImpl(
return blockchain.getExploreTxUrl(txAddress)
}
override fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? {
val blockchain = Blockchain.fromNetworkId(networkId)
if (memo == null) return null
return when (blockchain) {
Blockchain.Stellar -> {
val xlmMemo = if (memo.isNotEmpty() && memo.isDigitsOnly()) {
StellarMemo.Id(memo.toBigInteger())
} else {
StellarMemo.Text(memo)
}
StellarTransactionExtras(xlmMemo)
}
Blockchain.Binance -> BinanceTransactionExtras(memo)
Blockchain.XRP -> memo.toLongOrNull()?.let { XrpTransactionBuilder.XrpTransactionExtras(it) }
Blockchain.Cosmos -> CosmosTransactionExtras(memo)
Blockchain.TON -> TonTransactionExtras(memo)
else -> null
}
}
override fun getNativeTokenDecimals(networkId: String): Int {
return Blockchain.fromNetworkId(networkId)?.decimals() ?: error("blockchain not found")
}

View file

@ -49,6 +49,7 @@ interface TangemExpressApi {
@Query("providerId") providerId: String,
@Query("rateType") rateType: String,
@Query("toAddress") toAddress: String,
@Query("requestId") requestId: String,
): ApiResponse<ExchangeDataResponse>
@GET("exchange-status")

View file

@ -3,6 +3,10 @@ package com.tangem.datasource.api.express.models.response
import com.squareup.moshi.Json
import java.math.BigDecimal
data class ExchangeDataResponseWithTxDetails(
val dataResponse: ExchangeDataResponse,
val txDetails: TxDetails,
)
data class ExchangeDataResponse(
@Json(name = "fromAmount")
val fromAmount: String,
@ -16,12 +20,26 @@ data class ExchangeDataResponse(
@Json(name = "toDecimals")
val toDecimals: Int,
@Json(name = "txType")
val txType: TxType,
@Json(name = "txId")
val txId: String, // inner tangem-express transaction id
@Json(name = "txDetailsJson")
val txDetailsJson: String,
@Json(name = "signature")
val signature: String,
)
data class TxDetails(
@Json(name = "payoutAddress")
val payoutAddress: String,
@Json(name = "requestId")
val requestId: String,
@Json(name = "txType")
val txType: TxType,
@Json(name = "txFrom")
val txFrom: String?, // account for debiting tokens (same as toAddress) if DEX, null if CEX
@ -39,6 +57,12 @@ data class ExchangeDataResponse(
@Json(name = "externalTxUrl")
val externalTxUrl: String?, // null if DEX, url of provider exchange status page if CEX
@Json(name = "txExtraIdName")
val txExtraIdName: String?,
@Json(name = "txExtraId")
val txExtraId: String?,
)
enum class TxType {

View file

@ -17,6 +17,12 @@ data class ExchangeProvider(
@Json(name = "imageSmall")
val imageSmallUrl: String,
@Json(name = "termsOfUse")
val termsOfUse: String?,
@Json(name = "privacyPolicy")
val privacyPolicy: String?,
)
enum class ExchangeProviderType {

View file

@ -48,6 +48,9 @@ enum class ExchangeStatus {
@Json(name = "verifying")
VERIFYING,
@Json(name = "expired")
CANCELLED,
}
data class ExchangeStatusError(

View file

@ -1,205 +0,0 @@
package com.tangem.datasource.api.oneinch
import com.tangem.datasource.api.oneinch.models.AllowanceResponse
import com.tangem.datasource.api.oneinch.models.ApproveCalldataResponse
import com.tangem.datasource.api.oneinch.models.ApproveSpenderResponse
import com.tangem.datasource.api.oneinch.models.ProtocolsResponse
import com.tangem.datasource.api.oneinch.models.QuoteResponse
import com.tangem.datasource.api.oneinch.models.StatusResponse
import com.tangem.datasource.api.oneinch.models.SwapResponse
import com.tangem.datasource.api.oneinch.models.TokensResponse
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Query
interface OneInchApi {
/**
* Healthcheck return 200 if service is available
*
* @return [StatusResponse]
*/
@GET("healthcheck")
suspend fun healthcheck(): StatusResponse
//region Approve
/**
* Address of the 1inch router that must be trusted to spend funds for the exchange
*
* @return [ApproveSpenderResponse]
*/
@GET("approve/spender")
suspend fun approveSpender(): ApproveSpenderResponse
/**
* Generate data for calling the contract in order to allow the 1inch router to spend funds
*
* @param tokenAddress Token address you want to exchange
* @param amount The number of tokens that the 1inch router is allowed to spend.
* If not specified, it will be allowed to spend an infinite amount of tokens.
*
* @return [ApproveCalldataResponse] Transaction body to allow the exchange with the 1inch router
*/
@GET("approve/transaction")
suspend fun approveTransaction(
@Query("tokenAddress") tokenAddress: String,
@Query("amount") amount: String? = null,
): ApproveCalldataResponse
/**
* Get the number of tokens that the 1inch router is allowed to spend
*
* @param tokenAddress Token address you want to exchange
* @param walletAddress Wallet address for which you want to check
*
* @return [AllowanceResponse]
*/
@GET("approve/allowance")
suspend fun approveAllowance(
@Query("tokenAddress") tokenAddress: String,
@Query("walletAddress") walletAddress: String,
): Response<AllowanceResponse>
//endregion Approve
//region Info
/**
* List of tokens that are available for swap in the 1inch Aggregation protocol
*
* @return [TokensResponse]
*/
@GET("tokens")
suspend fun tokensAvailable(): TokensResponse
/**
* List of liquidity sources that are available for routing in the 1inch Aggregation protocol
*
* @return
*/
@GET("liquidity-sources")
suspend fun liquiditySources(): ProtocolsResponse
//endregion Info
//region Swap
/**
* Find the best quote to exchange via 1inch router
*
* @param fromTokenAddress Example : 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
* @param toTokenAddress Example : 0x111111111117dc0aa78b770fa6a738034120c302
* @param amount amount of a token to sell, set in minimal divisible units e.g.:
* 1.00 DAI set as 1000000000000000000
* 51.03 USDC set as 51030000
*
* @param protocols default: all
* @param fee this percentage of fromTokenAddress token amount will be sent to referrerAddress,
* the rest will be used as input for a swap
* Min: 0; max: 3; Max: 0; max: 3; default: 0; !should be the same for quote and swap!
*
* @param gasLimit maximum amount of gas for a swap;
* @param connectorTokens token-connectors can be specified via this parameter.
* The more is set the longer route estimation will take.
* If not set, default token-connectors will be usedmax: 5; !should be the same for quote and swap!
*
* @param complexityLevel maximum number of token-connectors to be used in a transaction.
* The more is used the longer route estimation will take
* min: 0; max: 3; default: 2; !should be the same for quote and swap!
*
* @param mainRouteParts default: 10; max: 50 !should be the same for quote and swap!
* @param parts limit maximum number of parts each main route parts can be split into;
* should be the same for a quote and swap
* default: 20; max: 100
*
* @param gasPrice 1inch takes in account gas expenses to determine exchange route.
* It is important to use the same gas price on the quote and swap methods.
* Gas price set in wei: 12.5 GWEI set as 12500000000
* default: fast from network
*
* @return [QuoteResponse]
*/
@GET("quote")
suspend fun quote(
@Query("src") fromTokenAddress: String,
@Query("dst") toTokenAddress: String,
@Query("amount") amount: String,
@Query("protocols") protocols: String? = null,
@Query("fee") fee: String? = null,
@Query("gasLimit") gasLimit: String? = null,
@Query("connectorTokens") connectorTokens: String? = null,
@Query("complexityLevel") complexityLevel: String? = null,
@Query("mainRouteParts") mainRouteParts: String? = null,
@Query("parts") parts: String? = null,
@Query("gasPrice") gasPrice: String? = null,
@Query("includeTokensInfo") includeTokensInfo: Boolean = true,
): Response<QuoteResponse>
/**
* Generate data for calling the 1inch router for exchange
*
* @param fromTokenAddress Example : 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
* @param toTokenAddress Example : 0x111111111117dc0aa78b770fa6a738034120c302
* @param amount amount of a token to sell, set in minimal divisible units e.g.:
* 1.00 DAI set as 1000000000000000000
* 51.03 USDC set as 51030000
*
* @param fromAddress The address that calls the 1inch contract
* @param slippage limit of price slippage you are willing to accept in percentage, may be set with decimals.
* &slippage=0.5 means 0.5% slippage is acceptable. Low values increase chances that transaction will fail,
* high values increase chances of front running. min: 0; max: 50;
*
* @param protocols default: all
* @param destinationAddress Receiver of destination currency. default: fromAddress
* @param fee this percentage of fromTokenAddress token amount will be sent to referrerAddress,
* the rest will be used as input for a swap
* Min: 0; max: 3; Max: 0; max: 3; default: 0; !should be the same for quote and swap!
*
* @param permit https://eips.ethereum.org/EIPS/eip-2612
* @param compatibilityMode Allows to build calldata without optimized routers
* @param burnChi If true, CHI will be burned from fromAddress to compensate gas.
* Check CHI balance and allowance before turning that on. CHI should be approved for the spender address
*
* @param connectorTokens token-connectors can be specified via this parameter.
* The more is set the longer route estimation will take.
* If not set, default token-connectors will be usedmax: 5; !should be the same for quote and swap!
*
* @param complexityLevel maximum number of token-connectors to be used in a transaction.
* The more is used the longer route estimation will take
* min: 0; max: 3; default: 2; !should be the same for quote and swap!
*
* @param mainRouteParts default: 10; max: 50 !should be the same for quote and swap!
* @param parts limit maximum number of parts each main route parts can be split into;
* should be the same for a quote and swap
* default: 20; max: 100
*
* @param gasLimit maximum amount of gas for a swap;
* @param gasPrice 1inch takes in account gas expenses to determine exchange route.
* It is important to use the same gas price on the quote and swap methods.
* Gas price set in wei: 12.5 GWEI set as 12500000000
* default: fast from network
*
* @return [SwapResponse]
*/
@GET("swap")
suspend fun swap(
@Query("src") fromTokenAddress: String,
@Query("dst") toTokenAddress: String,
@Query("amount") amount: String,
@Query("from") fromAddress: String,
@Query("slippage") slippage: Int,
@Query("protocols") protocols: String? = null,
@Query("receiver") destinationAddress: String? = null,
@Query("referrer") referrerAddress: String? = null,
@Query("fee") fee: String? = null,
@Query("disableEstimate") disableEstimate: Boolean? = null,
@Query("permit") permit: String? = null,
@Query("compatibility") compatibilityMode: Boolean? = null,
@Query("burnChi") burnChi: Boolean? = null,
@Query("allowPartialFill") allowPartialFill: Boolean? = null,
@Query("parts") parts: String? = null,
@Query("mainRouteParts") mainRouteParts: String? = null,
@Query("connectorTokens") connectorTokens: String? = null,
@Query("complexityLevel") complexityLevel: String? = null,
@Query("gasLimit") gasLimit: String? = null,
@Query("gasPrice") gasPrice: String? = null,
@Query("includeTokensInfo") includeTokensInfo: Boolean = true,
): Response<SwapResponse>
//endregion Swap
}

View file

@ -1,14 +0,0 @@
package com.tangem.datasource.api.oneinch
class OneInchApiFactory {
private val oneInchApiMap = mutableMapOf<String, OneInchApi>()
fun putApi(networkId: String, api: OneInchApi) {
oneInchApiMap[networkId] = api
}
fun getApi(networkId: String): OneInchApi {
return oneInchApiMap[networkId] ?: error("no api found for networkId $networkId")
}
}

View file

@ -1,5 +0,0 @@
package com.tangem.datasource.api.oneinch.errors
import com.tangem.datasource.api.oneinch.models.SwapErrorDto
class OneIncResponseException(val data: SwapErrorDto) : Exception()

View file

@ -1,7 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
data class AllowanceResponse(
@Json(name = "allowance") val allowance: String,
)

View file

@ -1,18 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Approve calldata response
*
* @property data The encoded data to call the approve method on the swapped token contract
* @property gasPrice Gas price for fast transaction processing
* @property toAddress Token address that will be allowed to exchange through 1inch router
* @property value Native token value in WEI (for approve is always 0)
*/
data class ApproveCalldataResponse(
@Json(name = "data") val data: String,
@Json(name = "gasPrice") val gasPrice: String,
@Json(name = "to") val toAddress: String,
@Json(name = "value") val value: String,
)

View file

@ -1,12 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Approve spender response
*
* @property address Address of the 1inch router that must be trusted to spend funds for the exchange
*/
data class ApproveSpenderResponse(
@Json(name = "address") val address: String,
)

View file

@ -1,27 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Protocols response
*
* @property protocols List of protocols that are available for routing in the 1inch Aggregation protocol
*/
data class ProtocolsResponse(
@Json(name = "protocols") val protocols: List<ProtocolImageDto>,
)
/**
* Protocol image
*
* @property id Protocol id
* @property title Protocol title
* @property image Protocol logo image
* @property imageColor Protocol logo image in color
*/
data class ProtocolImageDto(
@Json(name = "id") val id: String,
@Json(name = "title") val title: String,
@Json(name = "img") val image: String,
@Json(name = "img_color") val imageColor: String,
)

View file

@ -1,14 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Quote response
*
* @property toToken Destination token info
* @property toTokenAmount Expected amount of destination token
*/
data class QuoteResponse(
@Json(name = "toToken") val toToken: TokenOneInchDto,
@Json(name = "toAmount") val toTokenAmount: String,
)

View file

@ -1,8 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
data class StatusResponse(
@Json(name = "status") val status: String,
@Json(name = "provider") val provider: String,
)

View file

@ -1,42 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Swap error dto
*
* One of the following errors:
*
* -Insufficient liquidity
* -Cannot estimate
* -You may not have enough ETH balance for gas fee
* -FromTokenAddress cannot be equals to toTokenAddress
* -Cannot estimate. Don't forget about miner fee. Try to leave the buffer of ETH for gas
* -Not enough balance
* -Not enough allowance
*
* @property statusCode HTTP code
* @property error Error code description
* @property description Error description (one of the following)
* @property requestId Request id
* @property meta Meta information
* @constructor Create empty Swap error dto
*/
data class SwapErrorDto(
@Json(name = "statusCode") val statusCode: Int,
@Json(name = "error") val error: String,
@Json(name = "description") val description: String,
@Json(name = "requestId") val requestId: String,
@Json(name = "meta") val meta: List<NestErrorMeta>,
)
/**
* Nest error meta
*
* @property type Type of field
* @property value Value of field
*/
data class NestErrorMeta(
@Json(name = "type") val type: String,
@Json(name = "value") val value: String,
)

View file

@ -1,10 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
data class SwapResponse(
@Json(name = "fromToken") val fromToken: TokenOneInchDto,
@Json(name = "toToken") val toToken: TokenOneInchDto,
@Json(name = "toAmount") val toTokenAmount: String,
@Json(name = "tx") val transaction: TransactionDto,
)

View file

@ -1,20 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Token one inch
*
* @property symbol token symbol
* @property name token name
* @property address token address
* @property decimals token decimals
* @property logoURI token logo image url
*/
data class TokenOneInchDto(
@Json(name = "symbol") val symbol: String,
@Json(name = "name") val name: String,
@Json(name = "address") val address: String,
@Json(name = "decimals") val decimals: Int,
@Json(name = "logoURI") val logoURI: String,
)

View file

@ -1,12 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Tokens response
*
* @property tokens List of supported tokens
*/
data class TokensResponse(
@Json(name = "tokens") val tokens: Map<String, TokenOneInchDto>,
)

View file

@ -1,23 +0,0 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Transaction dto
*
* @property fromAddress transactions will be sent from this address
* @property toAddress transactions will be sent to our(1inch) contract address
* @property data The encoded data to call the approve method on the swapped token contract
* @property value Native token value in WEI (for approve is always 0)
* @property gasPrice maximum amount of gas for a swap default: 11500000; max: 11500000
* @property gas estimated amount of the gas limit, increase this value by 25%
* @constructor Create empty Transaction dto
*/
data class TransactionDto(
@Json(name = "from") val fromAddress: String,
@Json(name = "to") val toAddress: String,
@Json(name = "data") val data: String,
@Json(name = "value") val value: String,
@Json(name = "gasPrice") val gasPrice: String,
@Json(name = "gas") val gas: String,
)

View file

@ -103,11 +103,9 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
amplitudeApiKey = configValues.amplitudeApiKey,
shopify = configValues.shopifyShop,
sprinklr = configValues.sprinklr,
swapReferrerAccount = configValues.swapReferrerAccount,
walletConnectProjectId = configValues.walletConnectProjectId,
tangemComAuthorization = configValues.tangemComAuthorization,
tangemExpressApiKey = configValues.tangemExpressApiKey,
oneInchApiKey = configValues.oneInchApiKey,
express = configValues.express,
)
}

View file

@ -16,9 +16,7 @@ data class Config(
val isCreatingTwinCardsAllowed: Boolean = false,
val shopify: ShopifyShop? = null,
val sprinklr: SprinklrConfig? = null,
val swapReferrerAccount: SwapReferrerAccount? = null,
val walletConnectProjectId: String = "",
val tangemComAuthorization: String? = null,
val tangemExpressApiKey: String = "",
val oneInchApiKey: String = "",
val express: ExpressModel? = null,
)

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.config.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ExpressModel(
@Json(name = "apiKey")
val apiKey: String,
@Json(name = "signVerifierPublicKey")
val signVerifierPublicKey: String,
)

View file

@ -35,14 +35,12 @@ class ConfigValueModel(
val sprinklr: SprinklrConfig?,
val tronGridApiKey: String,
val amplitudeApiKey: String,
val swapReferrerAccount: SwapReferrerAccount?,
val kaspaSecondaryApiUrl: String,
val walletConnectProjectId: String,
val tangemComAuthorization: String?,
val chiaFireAcademyApiKey: String?,
val chiaTangemApiKey: String?,
val tangemExpressApiKey: String,
val oneInchApiKey: String,
val express: ExpressModel?,
)
@JsonClass(generateAdapter = true)
@ -84,11 +82,6 @@ data class AppsFlyer(
val appsFlyerAppID: String,
)
data class SwapReferrerAccount(
val address: String,
val fee: String,
)
class ConfigModel(
val features: FeatureModel?,
val configValues: ConfigValueModel?,

View file

@ -0,0 +1,6 @@
package com.tangem.datasource.crypto
interface DataSignatureVerifier {
fun verifySignature(signature: String, data: String): Boolean
}

View file

@ -0,0 +1,17 @@
package com.tangem.datasource.crypto
import com.tangem.common.extensions.hexToBytes
import com.tangem.crypto.CryptoUtils
import com.tangem.datasource.config.ConfigManager
internal class Sha256SignatureVerifier(private val configManager: ConfigManager) : DataSignatureVerifier {
override fun verifySignature(signature: String, data: String): Boolean {
val pubKey = configManager.config.express?.signVerifierPublicKey ?: return false
return CryptoUtils.verify(
publicKey = pubKey.hexToBytes().takeLast(n = 65).toByteArray(),
message = data.toByteArray(),
signature = signature.hexToBytes(),
)
}
}

View file

@ -1,96 +0,0 @@
package com.tangem.datasource.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.oneinch.OneInchApi
import com.tangem.datasource.api.oneinch.OneInchApiFactory
import com.tangem.datasource.utils.RequestHeader
import com.tangem.datasource.utils.addHeaders
import com.tangem.datasource.utils.addLoggers
import com.tangem.lib.auth.AuthBearerProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class OneInchApisModule {
@Provides
@Singleton
fun provideOneInchApiFactory(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
auth1Inch: AuthBearerProvider,
): OneInchApiFactory {
val networks = mapOf(
ETH_NETWORK to ONE_INCH_ETH_PATH,
BSC_NETWORK to ONE_INCH_BSC_PATH,
POLYGON_NETWORK to ONE_INCH_POLYGON_PATH,
OPTIMISM_NETWORK to ONE_INCH_OPTIMISM_PATH,
ARBITRUM_NETWORK to ONE_INCH_ARBITRUM_PATH,
GNOSIS_NETWORK to ONE_INCH_GNOSIS_PATH,
AVALANCHE_NETWORK to ONE_INCH_AVALANCHE_PATH,
FANTOM_NETWORK to ONE_INCH_FANTOM_PATH,
)
val apiFactory = OneInchApiFactory()
for ((network, path) in networks) {
apiFactory.putApi(
networkId = network,
api = createOneInchApiWithUrl("$ONE_INCH_BASE_URL$path", moshi, context, auth1Inch),
)
}
return apiFactory
}
private fun createOneInchApiWithUrl(
url: String,
moshi: Moshi,
context: Context,
auth1Inch: AuthBearerProvider,
): OneInchApi {
return Retrofit.Builder()
.addConverterFactory(
MoshiConverterFactory.create(moshi),
)
.baseUrl(url)
.client(
OkHttpClient.Builder()
.addHeaders(RequestHeader.AuthBearerHeader(auth1Inch))
.addLoggers(context)
.build(),
)
.build()
.create(OneInchApi::class.java)
}
companion object {
private const val ONE_INCH_BASE_URL = "https://api.1inch.dev/swap/v5.2/"
private const val ONE_INCH_ETH_PATH = "1/"
private const val ONE_INCH_BSC_PATH = "56/"
private const val ONE_INCH_POLYGON_PATH = "137/"
private const val ONE_INCH_OPTIMISM_PATH = "10/"
private const val ONE_INCH_ARBITRUM_PATH = "42161/"
private const val ONE_INCH_GNOSIS_PATH = "100/"
private const val ONE_INCH_AVALANCHE_PATH = "43114/"
private const val ONE_INCH_FANTOM_PATH = "250/"
private const val ETH_NETWORK = "ethereum"
private const val BSC_NETWORK = "binance-smart-chain"
private const val POLYGON_NETWORK = "polygon-pos"
private const val OPTIMISM_NETWORK = "optimistic-ethereum"
private const val ARBITRUM_NETWORK = "arbitrum-one"
private const val GNOSIS_NETWORK = "xdai"
private const val AVALANCHE_NETWORK = "avalanche"
private const val FANTOM_NETWORK = "fantom"
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.di
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.crypto.Sha256SignatureVerifier
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object SecurityModule {
@Provides
@Singleton
fun provideDataSignatureVerifier(configManager: ConfigManager): DataSignatureVerifier {
return Sha256SignatureVerifier(configManager)
}
}

View file

@ -0,0 +1,52 @@
package com.tangem.datasource.di
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import timber.log.Timber
class StatusCodeInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalResponse = chain.proceed(chain.request())
if (shouldInterceptResponse(originalResponse)) {
Timber.e("StatusCodeInterceptor INTERCEPTED%s", originalResponse.request.url.toString())
val body = getBody().toResponseBody("application/json".toMediaTypeOrNull())
val code = getCode()
return originalResponse.newBuilder()
.code(code)
.body(body)
.build()
}
return originalResponse
}
private fun shouldInterceptResponse(response: Response): Boolean {
return response.request.url.toString().contains("exchange-quote")
// && response.request.url.toString().contains("changenow")
}
private fun getCode(): Int {
return CODE_400
}
private fun getBody(): String {
return "{\n" +
" \"error\": {\n" +
" \"code\": 2250,\n" +
" \"description\": \"Core: exchange too small amount\",\n" +
" \"message\": \"Not valid\",\n" +
" \"minAmount\": 5\n" +
" }\n" +
"}"
}
companion object {
private const val CODE_400 = 400
}
}

View file

@ -32,6 +32,11 @@ class AppPreferencesStore(
return edit { transform(it) }
}
/** Get data [T] by [key]. If data is not found, it returns [default] */
inline fun <reified T> MutablePreferences.getOrDefault(key: Preferences.Key<T>, default: T): T {
return this[key] ?: default
}
/**
* Get nullable data [T] by string [key] from [MutablePreferences]
*

View file

@ -35,6 +35,8 @@ object PreferencesKeys {
val SWAP_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "swapTransactions") }
val SWAP_TRANSACTIONS_STATUSES_KEY by lazy { stringPreferencesKey(name = "swapTransactionsStatuses") }
val WALLETS_SCROLL_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "walletsScrollPreview") }
val SENT_ONE_TIME_EVENTS_KEY by lazy { stringPreferencesKey(name = "sentOneTimeEvents") }
@ -42,6 +44,10 @@ object PreferencesKeys {
val WALLETS_BALANCES_STATES_KEY by lazy { stringPreferencesKey(name = "walletsBalancesStates") }
val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") }
val IS_WALLET_SWAP_PROMO_SHOW_KEY by lazy { booleanPreferencesKey(name = "isWalletSwapPromoShown") }
val IS_TOKEN_SWAP_PROMO_SHOW_KEY by lazy { booleanPreferencesKey(name = "isTokenSwapPromoShown") }
}
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.local.preferences.utils
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.Types
import com.tangem.datasource.local.preferences.AppPreferencesStore
import kotlinx.coroutines.flow.Flow
@ -11,7 +12,15 @@ import kotlinx.coroutines.flow.map
/** Get flow of nullable data [T] by string [key] */
inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String>): Flow<T?> {
val adapter = moshi.adapter(T::class.java)
return data.map { it[key]?.let(adapter::fromJson) }
return data.map { preferences ->
preferences[key]?.let {
try {
adapter.fromJson(it)
} catch (e: JsonDataException) {
null
}
}
}
}
/**
@ -23,7 +32,13 @@ inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String
* */
inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String>, default: T): Flow<T> {
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
return data.map { it[key]?.let(adapter::fromJson) ?: default }
return data.map {
try {
it[key]?.let(adapter::fromJson) ?: default
} catch (e: JsonDataException) {
default
}
}
}
/**
@ -37,7 +52,13 @@ suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrNull(key: Pref
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
return data.firstOrNull()
?.get(key)
?.let(adapter::fromJson)
?.let {
try {
adapter.fromJson(it)
} catch (e: JsonDataException) {
null
}
}
}
/** Get data [T] by string [key]. If data is not found, it returns [default] */
@ -48,7 +69,13 @@ suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrDefault(
val adapter = moshi.adapter(T::class.java)
return data.firstOrNull()
?.get(key)
?.let(adapter::fromJson)
?.let {
try {
adapter.fromJson(it)
} catch (e: JsonDataException) {
default
}
}
?: default
}

View file

@ -15,4 +15,5 @@ enum class ExchangeAnalyticsStatus(val value: String) {
Fail("Fail"),
KYC("KYC"),
Refunded("Refunded"),
Cancelled("Canceled"),
}

View file

@ -190,25 +190,50 @@
<string name="disclaimer_title">Условия использования</string>
<string name="error_update_app">К сожалению, текущая версия приложения не готова к работе с этой картой, проверьте наличие обновлений</string>
<string name="error_wrong_wallet_tapped">Вы использовали карту от другого кошелька. Приложите карту, связанную с этим кошельком.</string>
<string name="exchange_receive_view_header">Вы получаете</string>
<string name="exchange_send_view_header">Вы отправляете</string>
<string name="exchange_tokens_available_tokens_header">Мои токены</string>
<string name="exchange_tokens_empty_tokens">У вас нет добавленных токенов. Добавьте токены для обмена</string>
<string name="express_token_list_empty_search">Токены не найдены. Пожалуйста, попробуйте другой запрос</string>
<string name="exchange_tokens_unavailable_tokens_header">Недоступен для обмена с %s</string>
<string name="express_cex_fee_explanation">Кроме того, в курс обмена включена комиссия сети за отправку обмененных средств на ваш адрес</string>
<string name="express_cex_status_button_title">Статус</string>
<string name="express_choose_providers_subtitle">Провайдеры проводят транзакции, обеспечивая плавный и эффективный обмен токенами</string>
<string name="express_choose_providers_title">Выберите провайдера</string>
<string name="express_exchange_notification_failed_text">Чтобы узнать причину, посетите сайт провайдера</string>
<string name="express_exchange_notification_failed_title">Чтобы вернуть ваши деньги, посетите сайт провайдера</string>
<string name="express_estimated_amount">Курс обмена</string>
<string name="express_more_providers_soon">Больше провайдеров на подходе.\nСледите за обновлениями!</string>
<string name="express_exchange_notification_failed_text">Чтобы вернуть ваши деньги, посетите сайт провайдера</string>
<string name="express_exchange_notification_failed_title">Операция не выполнена провайдером</string>
<string name="express_exchange_notification_verification_text">Посетите сайт провайдера для проверки</string>
<string name="express_exchange_notification_verification_title">Провайдер запрашивает прохождение верификации</string>
<string name="express_exchange_status_canceled">Отменен</string>
<string name="express_exchange_status_confirmed">Подтверждено</string>
<string name="express_exchange_status_confirming">Подтверждение</string>
<string name="express_exchange_status_confirming_active">Подтверждение...</string>
<string name="express_exchange_status_exchanged">Обменяно</string>
<string name="express_exchange_status_exchanging">Обмен</string>
<string name="express_exchange_status_exchanging_active">Обмен...</string>
<string name="express_exchange_status_failed">Неудачно</string>
<string name="express_exchange_status_received">Депозит получен</string>
<string name="express_exchange_status_receiving">Ожидание депозита</string>
<string name="express_exchange_status_receiving_active">Ожидаем пополнения...</string>
<string name="express_exchange_status_refunded">Возвращено</string>
<string name="express_exchange_status_sending">Отправляем </string>
<string name="express_exchange_status_sending_active">Отправка средств...</string>
<string name="express_exchange_status_sent">Отправлено</string>
<string name="express_exchange_status_verified">Верифицировано</string>
<string name="express_exchange_status_verifying">Требуется верификация</string>
<string name="express_exchange_token_list_subtitle">Список токенов в вашем кошельке</string>
<string name="express_fetch_best_rates">Получение наилучших курсов...</string>
<string name="express_floating_rate">Плавающая ставка</string>
<string name="express_go_to_provider">К провайдеру</string>
<string name="express_legal_one_placeholder">Пользуясь сервисом вы соглашаетесь с %s</string>
<string name="express_legal_two_placeholders">Пользуясь сервисом вы соглашаетесь с %1$s и %2$s</string>
<string name="express_privacy_policy">Политикой конфиденциальности</string>
<string name="express_provider">Провайдер</string>
<string name="express_provider_best_rate">Лучший курс</string>
<string name="express_provider_min_amount">Доступно с %s</string>
<string name="express_provider_not_available">Недоступно для этой пары</string>
<string name="express_provider_permission_needed">Требуется разрешение</string>
<string name="express_terms_of_use">Условиями использования</string>
<string name="feedback_data_collection_message">Информация ниже не является обязательной. Вы можете стереть её, если хотите.</string>
<string name="feedback_preface_rate_negative">Расскажите, каких функций вам не хватает, и мы постараемся вам помочь.</string>
<string name="feedback_preface_scan_failed">Скажите, пожалуйста, какая у вас карта?</string>
@ -245,6 +270,8 @@
<string name="main_promotion_credited">1INCH токены будут зачислены на адрес вашего кошелька в сети %s в течение 48 часов</string>
<string name="main_scan_card_warning_view_subtitle">Чтобы получить доступ ко всем сетям, вам необходимо отсканировать карту</string>
<string name="main_scan_card_warning_view_title">Отсканируйте карту</string>
<string name="main_swap_promotion_message">Обменивайте свои цифровые активы между различными сетями</string>
<string name="main_swap_promotion_title">Кроссчейн-своп теперь доступен</string>
<string name="main_tokens">Токены</string>
<string name="manage_tokens_add">Добавить</string>
<string name="manage_tokens_edit">Изменить</string>
@ -497,6 +524,7 @@
<string name="swapping_approve_information_text">Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен.</string>
<string name="swapping_approve_information_title">Подтвердить</string>
<string name="swapping_error_wrapper">Ошибка: %s</string>
<string name="swapping_from_title">Вы отправляете</string>
<string name="swapping_generic_error">Произошла ошибка. Пожалуйста, попробуйте еще раз.</string>
<string name="swapping_give_permission">Дать разрешение</string>
<string name="swapping_high_price_impact">Сильные колебания цены!</string>
@ -520,6 +548,7 @@
<string name="swapping_swap_action">Обменять</string>
<string name="swapping_swap_of_to">Обмен %s на</string>
<string name="swapping_tangem_fee_disclaimer">Котировки включают дополнительную комиссию Tangem в размере %s. Это помогает нам предоставлять первоклассный продукт.</string>
<string name="swapping_to_title">Вы получите</string>
<string name="swapping_token_list_other_tokens">Другие токены</string>
<string name="swapping_token_list_title">Выберите токен</string>
<string name="swapping_token_list_your_tokens">Ваши токены</string>
@ -541,6 +570,9 @@
<string name="token_details_unable_hide_alert_message">Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети.</string>
<string name="token_details_unable_hide_alert_title">Невозможно скрыть %s</string>
<string name="token_item_no_rate">Нет цены</string>
<string name="token_swap_promotion_button">Обменять</string>
<string name="token_swap_promotion_message">Обменяйте этот токен на другие активы в вашем портфеле</string>
<string name="token_swap_promotion_title">Представляем кроссчейн-своп</string>
<string name="transaction_history_contract_address">контракт: %s</string>
<string name="transaction_history_empty_transactions">У вас еще нет транзакций</string>
<string name="transaction_history_error_failed_to_load">Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию.</string>
@ -649,8 +681,12 @@
<string name="warning_developer_card_title">Не для пользователя!</string>
<string name="warning_existential_deposit_message">Cеть %1$s использует концепцию экзистенциального депозита. Если баланс вашего счета будет ниже %2$s, то он будет деактивирован, а средства на счете уничтожены.</string>
<string name="warning_existential_deposit_title">Для работы с сетью необходим депозит</string>
<string name="warning_express_active_transaction_message">Обмен будет доступен после завершения %s транзакции</string>
<string name="warning_express_active_transaction_title">У вас есть активная транзакция</string>
<string name="warning_express_no_exchangeable_coins_description">У вас в списке нет монет доступных для обмена с %s</string>
<string name="warning_express_no_exchangeable_coins_title">Нет доступных токенов для обмена</string>
<string name="warning_express_not_enough_fee_for_token_tx_description">Чтобы совершить транзакцию, вам необходимо внести немного %1$s %2$s</string>
<string name="warning_express_not_enough_fee_for_token_tx_title">Невозможно покрыть комиссию %s</string>
<string name="warning_express_refresh_required_title">Cервис временно недоступен</string>
<string name="warning_express_too_minimal_amount_description">Пожалуйста, измените сумму для обмена</string>
<string name="warning_express_too_minimal_amount_title">Сумма для обмена должна быть не менее %s</string>
@ -697,4 +733,7 @@
<string name="welcome_unlock_card">Сканировать карту</string>
<string name="welcome_unlock_description">Используйте %s или отсканируйте карту для входа в приложение</string>
<string name="welcome_unlock_title">C возвращением!</string>
<string name="express_exchange_by">Обмен через %s</string>
<string name="express_exchange_status_subtitle">Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий.</string>
<string name="express_exchange_status_title">Статус обмена</string>
</resources>

View file

@ -137,8 +137,6 @@
<string name="disclaimer_title">服務條款</string>
<string name="error_update_app">糟糕,當前版本的應用程序無法使用此卡,請檢查更新</string>
<string name="error_wrong_wallet_tapped">您使用了另一個錢包中的卡。點按與此錢包關聯的卡片</string>
<string name="exchange_receive_view_header">您收到</string>
<string name="exchange_send_view_header">您發送</string>
<string name="feedback_data_collection_message">以下信息是可選的。如果不想共享,可以將其刪除</string>
<string name="feedback_preface_rate_negative">告訴我們您缺少哪些功能,我們會盡力幫助您</string>
<string name="feedback_preface_scan_failed">請告訴我們你有什麼卡</string>

View file

@ -189,20 +189,22 @@
<string name="disclaimer_title">Terms of Service</string>
<string name="error_update_app">Oops, the current version of the application is not ready to work with this card, please check for updates.</string>
<string name="error_wrong_wallet_tapped">You have used a card from another wallet. Tap the card associated with this wallet</string>
<string name="exchange_receive_view_header">You receive</string>
<string name="exchange_send_view_header">You send</string>
<string name="exchange_tokens_available_tokens_header">My tokens</string>
<string name="exchange_tokens_empty_tokens">You haven\'t added any tokens yet. Add tokens via Market to swap</string>
<string name="express_token_list_empty_search">No tokens found. Please try another request</string>
<string name="exchange_tokens_unavailable_tokens_header">Cannot be swapped for %s</string>
<string name="express_cex_fee_explanation">Additionally, the network fee for sending the exchanged funds back to your address is included in the rate</string>
<string name="express_cex_status_button_title">Status</string>
<string name="express_choose_providers_subtitle">Providers facilitate transactions, ensuring smooth and efficient token swaps</string>
<string name="express_choose_providers_title">Choose provider</string>
<string name="express_more_providers_soon">More providers are coming soon.\nStay tuned!</string>
<string name="express_estimated_amount">Estimated amount</string>
<string name="express_exchange_by">Exchange by %s</string>
<string name="express_exchange_notification_failed_text">Visit providers website to refund your money</string>
<string name="express_exchange_notification_failed_title">Operation failed by provider</string>
<string name="express_exchange_notification_verification_text">Visit providers website for verification</string>
<string name="express_exchange_notification_verification_title">KYC verification required by provider</string>
<string name="express_exchange_status_canceled">Canceled</string>
<string name="express_exchange_status_confirmed">Confirmed</string>
<string name="express_exchange_status_confirming">Confirming</string>
<string name="express_exchange_status_confirming_active">Confirming…</string>
@ -217,18 +219,23 @@
<string name="express_exchange_status_sending">Sending to you</string>
<string name="express_exchange_status_sending_active">Sending to you…</string>
<string name="express_exchange_status_sent">Sent</string>
<string name="express_exchange_status_subtitle">Provider-sourced data. Estimated amount subject to change.</string>
<string name="express_exchange_status_subtitle">Provider-sourced data. Estimated amount subject to change due to market conditions.</string>
<string name="express_exchange_status_title">Exchange status</string>
<string name="express_exchange_status_verified">Verified</string>
<string name="express_exchange_status_verifying">Verification required</string>
<string name="express_exchange_token_list_subtitle">List of all tokens added to your wallet</string>
<string name="express_fetch_best_rates">Fetching best rates...</string>
<string name="express_floating_rate">Floating rate</string>
<string name="express_go_to_provider">Go to provider</string>
<string name="express_legal_one_placeholder">By using swap functionality, you agree with providers %s</string>
<string name="express_legal_two_placeholders">By using swap functionality, you agree with providers %1$s and %2$s</string>
<string name="express_privacy_policy">Privacy Policy</string>
<string name="express_provider">Provider</string>
<string name="express_provider_best_rate">Best rate</string>
<string name="express_provider_min_amount">Available from %s</string>
<string name="express_provider_not_available">Unavailable for this pair</string>
<string name="express_provider_permission_needed">Permission Needed</string>
<string name="express_provider_permission_needed">Permission Required</string>
<string name="express_terms_of_use">Terms of Use</string>
<string name="feedback_data_collection_message">The following information is optional. You can erase it if you don\'t want to share it.</string>
<string name="feedback_preface_rate_negative">Tell us what functions you are missing, and we will try to help you.</string>
<string name="feedback_preface_scan_failed">Please tell us what card do you have</string>
@ -263,6 +270,8 @@
<string name="main_promotion_credited">1INCH tokens will be credited to your %s wallet address within 48 hours</string>
<string name="main_scan_card_warning_view_subtitle">To access all the networks you need to scan the card</string>
<string name="main_scan_card_warning_view_title">Scan your card</string>
<string name="main_swap_promotion_message">Swap multiple currencies across several blockchains</string>
<string name="main_swap_promotion_title">Cross-chain swaps are now available</string>
<string name="main_tokens">Tokens</string>
<string name="manage_tokens_add">Add</string>
<string name="manage_tokens_edit">Edit</string>
@ -528,6 +537,7 @@
<string name="swapping_approve_information_text">All decentralized exchanges require approvals to prevent smart contracts from accessing your wallet without your permission. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the 1-inch smart contract to spend them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can swap your token after giving approval.</string>
<string name="swapping_approve_information_title">Approve</string>
<string name="swapping_error_wrapper">Error: %s</string>
<string name="swapping_from_title">You swap</string>
<string name="swapping_generic_error">There was an error. Please try again.</string>
<string name="swapping_give_permission">Give Permission</string>
<string name="swapping_high_price_impact">High price impact!</string>
@ -546,13 +556,12 @@
<string name="swapping_permission_rows_your_wallet">Your wallet</string>
<string name="swapping_permission_subheader">To continue, grant 1inch smart contracts permission to use your %s</string>
<string name="swapping_permission_unlimited">Unlimited</string>
<string name="swapping_success_from_title">You swap</string>
<string name="swapping_success_to_title">You receive</string>
<string name="swapping_success_view_explorer_button_title">View in Explorer</string>
<string name="swapping_success_view_title">In progress</string>
<string name="swapping_swap_action">Swap</string>
<string name="swapping_swap_of_to">Swap %s for</string>
<string name="swapping_tangem_fee_disclaimer">Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product.</string>
<string name="swapping_to_title">You receive</string>
<string name="swapping_token_list_other_tokens">Other tokens</string>
<string name="swapping_token_list_title">Choose token</string>
<string name="swapping_token_list_your_tokens">Your tokens</string>
@ -572,6 +581,9 @@
<string name="token_details_unable_hide_alert_message">The %1$s token is the main currency on the %2$s network and cannot be hidden as long as you have other tokens on this network in the list.</string>
<string name="token_details_unable_hide_alert_title">Unable to hide %s</string>
<string name="token_item_no_rate">No rate</string>
<string name="token_swap_promotion_message">Exchange this token for other assets in your portfolio</string>
<string name="token_swap_promotion_title">Introducing cross-chain swaps</string>
<string name="token_swap_promotion_button">Swap now</string>
<string name="transaction_history_contract_address">contract: %s</string>
<string name="transaction_history_empty_transactions">You don\'t have any transactions yet</string>
<string name="transaction_history_error_failed_to_load">Failed to load transaction history.\nClick on reload button to update the information.</string>
@ -680,11 +692,13 @@
<string name="warning_developer_card_title">Not for users!</string>
<string name="warning_existential_deposit_message">%1$s network requires an Existential Deposit. If your account drops below %2$s, it will be deactivated, and any remaining funds will be destroyed.</string>
<string name="warning_existential_deposit_title">Network requires Existential Deposit</string>
<string name="warning_express_active_transaction_message">Swap will be available after the %s transaction is complete</string>
<string name="warning_express_active_transaction_title">You have active transaction</string>
<string name="warning_express_no_exchangeable_coins_description">You do not have any %s exchangeable coins in your list</string>
<string name="warning_express_no_exchangeable_coins_title">No available tokens to swap</string>
<string name="warning_express_not_enough_fee_for_token_tx_description">To make a transaction you need to deposit some %1$s %2$s</string>
<string name="warning_express_not_enough_fee_for_token_tx_title">Unable to cover %s fee</string>
<string name="warning_express_refresh_required_title">Service temporary unavailable</string>
<string name="warning_express_refresh_required_title">Service temporarily unavailable</string>
<string name="warning_express_too_minimal_amount_description">Please change the amount to swap</string>
<string name="warning_express_too_minimal_amount_title">The amount to swap must be at least %s</string>
<string name="warning_failed_to_verify_card_message">This card might be a production sample or counterfeit</string>

View file

@ -1,13 +1,10 @@
package com.tangem.core.ui.components
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@ -25,23 +22,9 @@ sealed interface Keyboard {
/**
* Allows to subscribe to a soft keyboard to detect when it's open/closed
*/
@Deprecated("Use Modifier.imePadding() on pure Compose screens (without XML layouts)")
@Composable
fun keyboardAsState(): State<Keyboard> {
val density = LocalDensity.current
val imeInsets = WindowInsets.ime
.exclude(WindowInsets.navigationBars)
.getBottom(density)
return remember(imeInsets) {
derivedStateOf {
if (imeInsets > 0) {
Keyboard.Opened(
height = with(density) { imeInsets.toDp() },
)
} else {
Keyboard.Closed
}
}
}
val bottom = WindowInsets.ime.getBottom(LocalDensity.current)
val isImeVisible = bottom > 0
return rememberUpdatedState(if (isImeVisible) Keyboard.Opened(bottom.dp) else Keyboard.Closed)
}

View file

@ -95,7 +95,6 @@ private fun CollapsedSearchView(
modifier = Modifier
.background(TangemTheme.colors.background.secondary)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(

View file

@ -111,7 +111,7 @@ internal fun TangemButtonSize.toHorizontalContentPadding(icon: TangemButtonIconP
TangemButtonSize.Default,
TangemButtonSize.WideAction,
TangemButtonSize.TwoLines,
-> TangemTheme.dimens.spacing32 to TangemTheme.dimens.spacing32
-> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing16
TangemButtonSize.Text -> when (icon) {
is TangemButtonIconPosition.None -> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing16
is TangemButtonIconPosition.Start -> TangemTheme.dimens.spacing14 to TangemTheme.dimens.spacing16

View file

@ -6,12 +6,13 @@ import com.tangem.core.ui.extensions.TextReference
/**
* Notification component state
*
* @property title title
* @property subtitle subtitle
* @property iconResId icon resource id
* @property buttonsState buttons state
* @property onClick lambda be invoked when notification is clicked
* @property onCloseClick lambda be invoked when close button is clicked
* @property title title
* @property subtitle subtitle
* @property iconResId icon resource id
* @property backgroundResId background resource id
* @property buttonsState buttons state
* @property onClick lambda be invoked when notification is clicked
* @property onCloseClick lambda be invoked when close button is clicked
*
[REDACTED_AUTHOR]
*/
@ -19,6 +20,7 @@ data class NotificationConfig(
val title: TextReference,
val subtitle: TextReference,
@DrawableRes val iconResId: Int,
@DrawableRes val backgroundResId: Int? = null,
val buttonsState: ButtonsState? = null,
val onClick: (() -> Unit)? = null,
val onCloseClick: (() -> Unit)? = null,
@ -33,7 +35,11 @@ data class NotificationConfig(
val onClick: () -> Unit,
) : ButtonsState()
data class SecondaryButtonConfig(val text: TextReference, val onClick: () -> Unit) : ButtonsState()
data class SecondaryButtonConfig(
val text: TextReference,
@DrawableRes val iconResId: Int? = null,
val onClick: () -> Unit,
) : ButtonsState()
data class PairButtonsConfig(
val primaryText: TextReference,

View file

@ -0,0 +1,196 @@
package com.tangem.core.ui.components.notifications
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import androidx.constraintlayout.compose.Visibility
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonColors
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.LocalIsInDarkTheme
import com.tangem.core.ui.res.TangemColorPalette.Dark6
import com.tangem.core.ui.res.TangemColorPalette.Light4
import com.tangem.core.ui.res.TangemTheme
/**
* Custom notification with image background
* @see [Swap Promo](https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=8713-6307&mode=dev)
*/
@Suppress("LongMethod", "DestructuringDeclarationWithTooManyEntries")
@Composable
fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier = Modifier) {
val button = config.buttonsState as? NotificationConfig.ButtonsState.SecondaryButtonConfig
ConstraintLayout(
modifier = modifier
.defaultMinSize(minHeight = TangemTheme.dimens.size62)
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium),
) {
val (iconRef, titleRef, subtitleRef, closeIconRef, buttonRef, backgroundRef) = createRefs()
val spacing2 = TangemTheme.dimens.spacing2
val spacing12 = TangemTheme.dimens.spacing12
val spacing14 = TangemTheme.dimens.spacing14
Image(
painter = painterResource(config.backgroundResId ?: R.drawable.img_swap_promo_banner_background),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.constrainAs(backgroundRef) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
width = Dimension.fillToConstraints
height = Dimension.fillToConstraints
visibility = if (config.backgroundResId == null) Visibility.Gone else Visibility.Visible
},
)
Image(
painter = painterResource(config.iconResId),
contentDescription = null,
modifier = Modifier
.size(TangemTheme.dimens.size34)
.constrainAs(iconRef) {
start.linkTo(parent.start, spacing12)
linkTo(titleRef.top, subtitleRef.bottom, bias = 0.1f)
},
)
Text(
text = config.title.resolveReference(),
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.constantWhite,
modifier = Modifier.constrainAs(titleRef) {
top.linkTo(parent.top, spacing12)
start.linkTo(iconRef.end, spacing12)
end.linkTo(closeIconRef.start, spacing2)
width = Dimension.fillToConstraints
},
)
Text(
text = config.subtitle.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.constantWhite,
modifier = Modifier.constrainAs(subtitleRef) {
top.linkTo(titleRef.bottom, spacing2)
start.linkTo(iconRef.end, spacing12)
end.linkTo(parent.end, spacing12)
bottom.linkTo(buttonRef.top, spacing12, spacing14)
width = Dimension.fillToConstraints
},
)
Icon(
painter = painterResource(id = R.drawable.ic_close_24),
contentDescription = null,
tint = TangemTheme.colors.text.constantWhite,
modifier = Modifier
.size(TangemTheme.dimens.size16)
.constrainAs(closeIconRef) {
top.linkTo(parent.top, spacing12)
end.linkTo(parent.end, spacing12)
}
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
) {
config.onCloseClick?.invoke()
},
)
val isDarkMode = LocalIsInDarkTheme.current
TangemButton(
text = button?.text?.resolveReference().orEmpty(),
icon = TangemButtonIconPosition.Start(button?.iconResId ?: R.drawable.ic_exchange_vertical_24),
onClick = button?.onClick ?: {},
colors = TangemButtonColors(
backgroundColor = if (isDarkMode) Light4 else TangemTheme.colors.button.secondary,
contentColor = Dark6,
disabledBackgroundColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
),
enabled = true,
showProgress = false,
modifier = Modifier.constrainAs(buttonRef) {
start.linkTo(parent.start, spacing12)
end.linkTo(parent.end, spacing12)
bottom.linkTo(parent.bottom, spacing12)
width = Dimension.fillToConstraints
visibility = if (button == null) {
Visibility.Gone
} else {
Visibility.Visible
}
},
)
}
}
//region preview
@Preview
@Composable
private fun NotificationWithBackgroundPreview_Light(
@PreviewParameter(NotificationWithBackgroundPreviewProvider::class) config: NotificationConfig,
) {
TangemTheme {
NotificationWithBackground(config = config)
}
}
@Preview
@Composable
private fun NotificationWithBackgroundPreview_Dark(
@PreviewParameter(NotificationWithBackgroundPreviewProvider::class) config: NotificationConfig,
) {
TangemTheme(isDark = true) {
NotificationWithBackground(config = config)
}
}
private class NotificationWithBackgroundPreviewProvider : PreviewParameterProvider<NotificationConfig> {
override val values: Sequence<NotificationConfig>
get() = sequenceOf(
NotificationConfig(
title = resourceReference(id = R.string.main_swap_promotion_title),
subtitle = resourceReference(id = R.string.main_swap_promotion_message),
iconResId = R.drawable.img_swap_promo,
backgroundResId = R.drawable.img_swap_promo_banner_background,
),
NotificationConfig(
title = resourceReference(id = R.string.token_swap_promotion_title),
subtitle = stringReference(
"Swap multiple currencies between any chains you wish. Swap multiple " +
"currencies between any chains you wish. Swap multiple " +
"currencies between any chains you wish. Commission free period " +
"till Dec 31.",
),
iconResId = R.drawable.img_swap_promo,
backgroundResId = R.drawable.img_swap_promo_banner_background,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(id = R.string.token_swap_promotion_button),
onClick = {},
),
),
)
}
//endregion

View file

@ -44,7 +44,7 @@ fun TransactionDoneTitle(@StringRes titleRes: Int, date: Long, modifier: Modifie
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing32),
.padding(top = TangemTheme.dimens.spacing16),
)
Text(
text = stringResource(id = R.string.send_date_format, date.toDateFormat(), date.toTimeFormat()),

View file

@ -71,7 +71,6 @@ fun LazyListScope.txHistoryItems(
}
}
@OptIn(ExperimentalFoundationApi::class)
private fun LazyListScope.contentItems(
txHistoryItems: LazyPagingItems<TxHistoryState.TxHistoryItemState>,
isBalanceHidden: Boolean,
@ -93,7 +92,6 @@ private fun LazyListScope.contentItems(
state = item,
isBalanceHidden = isBalanceHidden,
modifier = modifier
.animateItemPlacement()
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = txHistoryItems.itemSnapshotList.lastIndex,

View file

@ -175,7 +175,7 @@ private fun darkThemeColors(): TangemColors {
key = TangemColorPalette.White,
),
stroke = TangemColors.Stroke(
primary = TangemColorPalette.Dark5,
primary = TangemColorPalette.Dark4,
secondary = TangemColorPalette.Dark1,
transparency = TangemColorPalette.Dark6,
),

View file

@ -22,7 +22,13 @@ object BigDecimalFormatter {
roundingMode = RoundingMode.DOWN
}
return formatter.format(cryptoAmount) + "\u2009$cryptoCurrency"
return formatter.format(cryptoAmount).let {
if (cryptoCurrency.isEmpty()) {
it
} else {
it + "\u2009$cryptoCurrency"
}
}
}
fun formatCryptoAmount(cryptoAmount: BigDecimal?, cryptoCurrency: CryptoCurrency): String {

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,62 @@
package com.tangem.data.settings
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_SHOW_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_SHOW_KEY
import com.tangem.datasource.local.preferences.utils.get
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.domain.settings.repositories.SwapPromoRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.util.Calendar
/**
* Repository for showing swap promo notification.
*/
class DefaultSwapPromoRepository(
private val appPreferencesStore: AppPreferencesStore,
) : SwapPromoRepository {
override fun isReadyToShowWallet(): Flow<Boolean> {
return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_SHOW_KEY, true)
.map { it && checkPromoPeriod() }
}
override fun isReadyToShowToken(): Flow<Boolean> {
return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_SHOW_KEY, true)
.map { it && checkPromoPeriod() }
}
override suspend fun setNeverToShowWallet() {
appPreferencesStore.store(
key = IS_WALLET_SWAP_PROMO_SHOW_KEY,
value = false,
)
}
override suspend fun setNeverToShowToken() {
appPreferencesStore.store(
key = IS_TOKEN_SWAP_PROMO_SHOW_KEY,
value = false,
)
}
private suspend fun checkPromoPeriod(): Boolean {
val calendar = Calendar.getInstance()
val currentTime = calendar.timeInMillis
calendar.set(END_YEAR_KEY, END_MONTH_KEY, END_DAY_KEY, 0, 0, 0)
val endTime = calendar.timeInMillis
val shouldShow = endTime - currentTime > 0
if (!shouldShow) {
setNeverToShowToken()
setNeverToShowWallet()
}
return shouldShow
}
companion object {
private const val END_DAY_KEY = 1
private const val END_MONTH_KEY = 1 // February
private const val END_YEAR_KEY = 2024
}
}

View file

@ -2,10 +2,12 @@ package com.tangem.data.settings.di
import com.tangem.data.settings.DefaultAppRatingRepository
import com.tangem.data.settings.DefaultSettingsRepository
import com.tangem.data.settings.DefaultSwapPromoRepository
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.settings.repositories.AppRatingRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.repositories.SwapPromoRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -36,4 +38,10 @@ internal object SettingsDataModule {
fun provideAppRatingRepository(appPreferencesStore: AppPreferencesStore): AppRatingRepository {
return DefaultAppRatingRepository(appPreferencesStore = appPreferencesStore)
}
@Provides
@Singleton
fun provideSwapPromoRepository(appPreferencesStore: AppPreferencesStore): SwapPromoRepository {
return DefaultSwapPromoRepository(appPreferencesStore = appPreferencesStore)
}
}

View file

@ -11,6 +11,10 @@ class DefaultMarketCryptoCurrencyRepository(
) : MarketCryptoCurrencyRepository {
override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
return getExchangeableFlag(userWalletId, cryptoCurrency) && !cryptoCurrency.isCustom
}
private suspend fun getExchangeableFlag(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE
return assetsStore.getSyncOrNull(userWalletId)?.find {

View file

@ -7,5 +7,7 @@ interface ReduxStateHolder {
fun dispatch(action: Action)
suspend fun dispatchWithMain(action: Action)
suspend fun onUserWalletSelected(userWallet: UserWallet)
}

View file

@ -10,11 +10,13 @@ import com.tangem.blockchain.blockchains.solana.RentProvider
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.address.Address
import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchain.common.address.EstimationFeeAddressFactory
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.config.ConfigManager
@ -45,6 +47,7 @@ class DefaultWalletManagersFacade(
private val walletManagersStore: WalletManagersStore,
private val userWalletsStore: UserWalletsStore,
configManager: ConfigManager,
mnemonic: Mnemonic,
assetReader: AssetReader,
moshi: Moshi,
) : WalletManagersFacade {
@ -55,6 +58,7 @@ class DefaultWalletManagersFacade(
private val sdkTokenConverter by lazy { SdkTokenConverter() }
private val txHistoryStateConverter by lazy { SdkTransactionHistoryStateConverter() }
private val txHistoryItemConverter by lazy { SdkTransactionHistoryItemConverter(assetReader, moshi) }
private val estimationFeeAddressFactory by lazy { EstimationFeeAddressFactory(mnemonic) }
override suspend fun update(
userWalletId: UserWalletId,
@ -247,7 +251,7 @@ class DefaultWalletManagersFacade(
derivationPath = derivationPath,
)
if (walletManager == null || blockchain == Blockchain.Unknown) {
Timber.w("Unable to get a wallet manager for blockchain: $blockchain")
Timber.w("Unable to create or find a wallet manager for blockchain: $blockchain")
return UpdateWalletManagerResult.UnreachableWithoutAddresses
}
@ -433,6 +437,26 @@ class DefaultWalletManagersFacade(
)
}
override suspend fun estimateFee(
amount: Amount,
userWalletId: UserWalletId,
network: Network,
): Result<TransactionFee>? {
val blockchain = Blockchain.fromId(network.id.value)
val walletManager = getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)
val destination = estimationFeeAddressFactory.makeAddress(blockchain)
return (walletManager as? TransactionSender)?.estimateFee(
amount = amount,
destination = destination,
)
}
override suspend fun validateTransaction(
amount: Amount,
fee: Amount?,

View file

@ -167,6 +167,15 @@ interface WalletManagersFacade {
network: Network,
): Result<TransactionFee>?
/**
* Returns estimated fee for transaction
*
* @param amount of transaction
* @param userWalletId selected wallet id
* @param network network of currency
*/
suspend fun estimateFee(amount: Amount, userWalletId: UserWalletId, network: Network): Result<TransactionFee>?
/**
* Validates transaction
*

View file

@ -10,6 +10,7 @@ import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.makeWalletManagerForApp
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse
import timber.log.Timber
internal class WalletManagerFactory(
private val configManager: ConfigManager,
@ -26,11 +27,16 @@ internal class WalletManagerFactory(
): WalletManager? {
val derivationParams = getDerivationParams(derivationPath, scanResponse.derivationStyleProvider)
return sdkWalletManagerFactory.makeWalletManagerForApp(
scanResponse = scanResponse,
blockchain = blockchain,
derivationParams = derivationParams,
)
return try {
sdkWalletManagerFactory.makeWalletManagerForApp(
scanResponse = scanResponse,
blockchain = blockchain,
derivationParams = derivationParams,
)
} catch (e: Throwable) {
Timber.w(e, "Failed to create wallet manager for $blockchain")
null
}
}
private fun getDerivationParams(

View file

@ -0,0 +1,11 @@
package com.tangem.domain.settings
import com.tangem.domain.settings.repositories.SwapPromoRepository
import kotlinx.coroutines.flow.Flow
class ShouldShowSwapPromoTokenUseCase(private val swapPromoRepository: SwapPromoRepository) {
operator fun invoke(): Flow<Boolean> = swapPromoRepository.isReadyToShowToken()
suspend fun neverToShow() = swapPromoRepository.setNeverToShowToken()
}

View file

@ -0,0 +1,11 @@
package com.tangem.domain.settings
import com.tangem.domain.settings.repositories.SwapPromoRepository
import kotlinx.coroutines.flow.Flow
class ShouldShowSwapPromoWalletUseCase(private val swapPromoRepository: SwapPromoRepository) {
operator fun invoke(): Flow<Boolean> = swapPromoRepository.isReadyToShowWallet()
suspend fun neverToShow() = swapPromoRepository.setNeverToShowWallet()
}

View file

@ -0,0 +1,13 @@
package com.tangem.domain.settings.repositories
import kotlinx.coroutines.flow.Flow
interface SwapPromoRepository {
fun isReadyToShowWallet(): Flow<Boolean>
fun isReadyToShowToken(): Flow<Boolean>
suspend fun setNeverToShowWallet()
suspend fun setNeverToShowToken()
}

View file

@ -18,6 +18,9 @@ dependencies {
implementation(projects.domain.txhistory.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.settings)
implementation(projects.features.swap.domain.api)
implementation(projects.features.swap.domain.models)
/** Project - Other */
implementation(projects.core.utils)

View file

@ -31,4 +31,6 @@ sealed class CryptoCurrencyWarning {
data class Rent(val rent: BigDecimal, val exemptionAmount: BigDecimal) : CryptoCurrencyWarning()
data class HasPendingTransactions(val blockchainSymbol: String) : CryptoCurrencyWarning()
object SwapPromo : CryptoCurrencyWarning()
}

View file

@ -0,0 +1,18 @@
package com.tangem.domain.tokens.models.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
sealed class TokenSwapPromoAnalyticsEvent(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Swap Promo", event, params, null) {
object Close : TokenSwapPromoAnalyticsEvent(event = "Button - Close")
class Exchange(
token: String,
) : TokenSwapPromoAnalyticsEvent(
event = "Button - Exchange Now",
params = mapOf("Token" to token),
)
}

View file

@ -1,6 +1,5 @@
package com.tangem.domain.tokens
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.tokens.model.CryptoCurrency
@ -12,7 +11,6 @@ import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -53,10 +51,9 @@ class GetCryptoCurrencyActionsUseCase(
val flow = networkFlow.mapLatest { maybeCoinStatus ->
createTokenActionsState(
userWalletId = userWallet.walletId,
userWallet = userWallet,
coinStatus = maybeCoinStatus.getOrNull(),
cryptoCurrencyStatus = cryptoCurrencyStatus,
cardTypesResolver = userWallet.scanResponse.cardTypesResolver,
)
}
@ -65,19 +62,17 @@ class GetCryptoCurrencyActionsUseCase(
}
private suspend fun createTokenActionsState(
userWalletId: UserWalletId,
userWallet: UserWallet,
coinStatus: CryptoCurrencyStatus?,
cryptoCurrencyStatus: CryptoCurrencyStatus,
cardTypesResolver: CardTypesResolver,
): TokenActionsState {
return TokenActionsState(
walletId = userWalletId,
walletId = userWallet.walletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
states = createListOfActions(
userWalletId,
userWallet,
coinStatus,
cryptoCurrencyStatus,
cardTypesResolver,
),
)
}
@ -87,10 +82,9 @@ class GetCryptoCurrencyActionsUseCase(
* Actions priority: [Buy Send Receive Sell Swap]
*/
private suspend fun createListOfActions(
userWalletId: UserWalletId,
userWallet: UserWallet,
coinStatus: CryptoCurrencyStatus?,
cryptoCurrencyStatus: CryptoCurrencyStatus,
cardTypesResolver: CardTypesResolver,
): List<TokenActionsState.ActionState> {
val cryptoCurrency = cryptoCurrencyStatus.currency
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.MissedDerivation) {
@ -116,14 +110,13 @@ class GetCryptoCurrencyActionsUseCase(
activeList.add(TokenActionsState.ActionState.Send(true))
}
val isMulticurrencyWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2()
// swap
if (isMulticurrencyWallet &&
marketCryptoCurrencyRepository.isExchangeable(userWalletId, cryptoCurrency)
) {
activeList.add(TokenActionsState.ActionState.Swap(true))
} else {
disabledList.add(TokenActionsState.ActionState.Swap(false))
if (userWallet.isMultiCurrency) {
if (marketCryptoCurrencyRepository.isExchangeable(userWallet.walletId, cryptoCurrency)) {
activeList.add(TokenActionsState.ActionState.Swap(true))
} else {
disabledList.add(TokenActionsState.ActionState.Swap(false))
}
}
// buy
@ -164,11 +157,13 @@ class GetCryptoCurrencyActionsUseCase(
return activeList + disabledList
}
private fun isSendDisabled(cryptoCurrencyStatus: CryptoCurrencyStatus, coinStatus: CryptoCurrencyStatus?): Boolean =
cryptoCurrencyStatus.value.amount.isNullOrZero() ||
coinStatus?.value?.amount.isNullOrZero() ||
currenciesRepository.hasPendingTransactions(
cryptoCurrencyStatus = cryptoCurrencyStatus,
coinStatus = coinStatus,
)
private fun isSendDisabled(
cryptoCurrencyStatus: CryptoCurrencyStatus,
coinStatus: CryptoCurrencyStatus?,
): Boolean = cryptoCurrencyStatus.value.amount.isNullOrZero() ||
coinStatus?.value?.amount.isNullOrZero() ||
currenciesRepository.hasPendingTransactions(
cryptoCurrencyStatus = cryptoCurrencyStatus,
coinStatus = coinStatus,
)
}

View file

@ -1,24 +1,34 @@
package com.tangem.domain.tokens
import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.flow.*
import java.math.BigDecimal
@Suppress("LongParameterList")
class GetCurrencyWarningsUseCase(
private val walletManagersFacade: WalletManagersFacade,
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val swapRepository: SwapRepository,
private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
private val showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
private val dispatchers: CoroutineDispatcherProvider,
) {
@ -29,9 +39,15 @@ class GetCurrencyWarningsUseCase(
isSingleWalletWithTokens: Boolean,
): Flow<Set<CryptoCurrencyWarning>> {
val currency = currencyStatus.currency
val operations = CurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
userWalletId = userWalletId,
)
return combine(
getCoinRelatedWarnings(
userWalletId = userWalletId,
operations = operations,
networkId = currency.network.id,
currencyId = currency.id,
derivationPath = derivationPath,
@ -39,10 +55,14 @@ class GetCurrencyWarningsUseCase(
),
flowOf(walletManagersFacade.getRentInfo(userWalletId, currency.network)),
flowOf(walletManagersFacade.getExistentialDeposit(userWalletId, currency.network)),
flowOf(getNetworkUnavailableWarning(currencyStatus)),
flowOf(getNetworkNoAccountWarning(currencyStatus)),
) { coinRelatedWarnings, maybeRentWarning, maybeEdWarning, maybeNetworkUnavailable, maybeNetworkNoAccount ->
getSwapPromoNotificationWarning(
operations = operations,
userWalletId = userWalletId,
currencyStatus = currencyStatus,
).conflate(),
) { coinRelatedWarnings, maybeRentWarning, maybeEdWarning, maybeSwapPromo ->
setOfNotNull(
maybeSwapPromo,
maybeRentWarning,
maybeEdWarning?.let {
CryptoCurrencyWarning.ExistentialDeposit(
@ -51,27 +71,77 @@ class GetCurrencyWarningsUseCase(
)
},
*coinRelatedWarnings.toTypedArray(),
maybeNetworkUnavailable,
maybeNetworkNoAccount,
getNetworkUnavailableWarning(currencyStatus),
getNetworkNoAccountWarning(currencyStatus),
)
}.flowOn(dispatchers.io)
}
private suspend fun getSwapPromoNotificationWarning(
operations: CurrenciesStatusesOperations,
userWalletId: UserWalletId,
currencyStatus: CryptoCurrencyStatus,
): Flow<CryptoCurrencyWarning?> {
val currency = currencyStatus.currency
val cryptoStatuses = operations.getCurrenciesStatusesSync()
return combine(
showSwapPromoTokenUseCase().conflate(),
flowOf(marketCryptoCurrencyRepository.isExchangeable(userWalletId, currency)).conflate(),
) { shouldShowSwapPromo, isExchangeable ->
if (shouldShowSwapPromo && isExchangeable && currencyStatus.value !is CryptoCurrencyStatus.Unreachable) {
cryptoStatuses.fold(
ifLeft = { null },
ifRight = { cryptoCurrencyStatuses ->
val pairs = runCatching(dispatchers.io) {
swapRepository.getPairsOnly(
LeastTokenInfo(
contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0",
network = currency.network.backendId,
),
cryptoCurrencyStatuses.map { it.currency },
)
}.getOrNull()?.pairs ?: emptyList()
val filteredCurrencies = cryptoCurrencyStatuses.filterNot {
it.currency.id == currency.id
}
val currencyPairs = pairs.filter {
it.from.network == currency.network.backendId ||
it.to.network == currency.network.backendId
}
val showPromo = currencyPairs.any { pair ->
val availablePair = if (currencyStatus.value.amount.isNullOrZero()) {
filteredCurrencies.filterNot { it.value.amount.isNullOrZero() }
} else {
filteredCurrencies
}
availablePair
.any {
it.currency.network.backendId == pair.to.network ||
it.currency.network.backendId == pair.from.network
}
}
if (showPromo) {
CryptoCurrencyWarning.SwapPromo
} else {
null
}
},
)
} else {
null
}
}
}
@Suppress("LongParameterList")
private suspend fun getCoinRelatedWarnings(
userWalletId: UserWalletId,
operations: CurrenciesStatusesOperations,
networkId: Network.ID,
currencyId: CryptoCurrency.ID,
derivationPath: Network.DerivationPath,
isSingleWalletWithTokens: Boolean,
): Flow<List<CryptoCurrencyWarning>> {
val operations = CurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
userWalletId = userWalletId,
)
val currencyFlow = if (isSingleWalletWithTokens) {
operations.getCurrencyStatusSingleWalletWithTokensFlow(currencyId)
} else {

View file

@ -2,6 +2,5 @@ package com.tangem.domain.transaction.error
sealed class GetFeeError {
data class DataError(val cause: Throwable?) : GetFeeError()
object UnknownError : GetFeeError()
}

View file

@ -0,0 +1,64 @@
package com.tangem.domain.transaction.usecase
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.Result
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import java.math.BigDecimal
/**
* Use case to estimate transaction fee
*/
class EstimateFeeUseCase(
private val walletManagersFacade: WalletManagersFacade,
private val dispatcher: CoroutineDispatcherProvider,
) {
suspend operator fun invoke(
amount: BigDecimal,
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Flow<Either<GetFeeError, TransactionFee>> {
return flow {
val result = walletManagersFacade.estimateFee(
amount = convertCryptoCurrencyToAmount(cryptoCurrency, amount),
userWalletId = userWalletId,
network = cryptoCurrency.network,
)
val maybeFee = when (result) {
is Result.Success -> result.data.right()
is Result.Failure -> GetFeeError.DataError(result.error).left()
null -> GetFeeError.UnknownError.left()
}
emit(maybeFee)
}.flowOn(dispatcher.io)
}
private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount(
currencySymbol = cryptoCurrency.symbol,
value = amount,
decimals = cryptoCurrency.decimals,
type = when (cryptoCurrency) {
is CryptoCurrency.Coin -> AmountType.Coin
is CryptoCurrency.Token -> AmountType.Token(
token = Token(
symbol = cryptoCurrency.symbol,
contractAddress = cryptoCurrency.contractAddress,
decimals = cryptoCurrency.decimals,
),
)
},
)
}

View file

@ -8,7 +8,7 @@ import com.tangem.crypto.bip39.Wordlist
/**
[REDACTED_AUTHOR]
*/
internal class DefaultMnemonicRepository(
class DefaultMnemonicRepository(
private val bip39Wordlist: Wordlist,
) : MnemonicRepository {

View file

@ -15,6 +15,8 @@ dependencies {
implementation(projects.core.datasource)
implementation(projects.core.utils)
implementation(projects.features.swap.domain)
implementation(projects.features.swap.domain.models)
implementation(projects.features.swap.domain.api)
/** Network */
implementation(deps.retrofit)
@ -35,6 +37,9 @@ dependencies {
/** Tangem SDKs */
implementation(deps.tangem.blockchain)
/** Others */
implementation(deps.timber)
/** DI */
implementation(deps.hilt.android)

View file

@ -1,53 +1,58 @@
package com.tangem.feature.swap
import arrow.core.Either
import arrow.core.left
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Approver
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import arrow.core.right
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.Result
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.request.PairsRequestBody
import com.tangem.datasource.api.express.models.response.ExchangeDataResponseWithTxDetails
import com.tangem.datasource.api.express.models.response.SwapPair
import com.tangem.datasource.api.express.models.response.SwapPairsWithProviders
import com.tangem.datasource.api.oneinch.OneInchApi
import com.tangem.datasource.api.oneinch.OneInchApiFactory
import com.tangem.datasource.api.express.models.response.TxDetails
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.converters.*
import com.tangem.feature.swap.domain.SwapRepository
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.ExpressException
import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.IOException
import java.math.BigDecimal
import java.util.UUID
import javax.inject.Inject
import com.tangem.datasource.api.express.models.request.LeastTokenInfo as NetworkLeastTokenInfo
@Suppress("LongParameterList")
internal class SwapRepositoryImpl @Inject constructor(
@Suppress("LongParameterList", "LargeClass")
internal class DefaultSwapRepository @Inject constructor(
private val tangemTechApi: TangemTechApi,
private val tangemExpressApi: TangemExpressApi,
private val oneInchApiFactory: OneInchApiFactory,
private val coroutineDispatcher: CoroutineDispatcherProvider,
private val configManager: ConfigManager,
private val walletManagersFacade: WalletManagersFacade,
private val walletsStateHolder: WalletsStateHolder,
private val errorsDataConverter: ErrorsDataConverter,
private val dataSignatureVerifier: DataSignatureVerifier,
moshi: Moshi,
) : SwapRepository {
private val tokensConverter = TokensConverter()
@ -56,55 +61,114 @@ internal class SwapRepositoryImpl @Inject constructor(
private val swapPairInfoConverter = SwapPairInfoConverter()
private val cryptoCurrencyFactory = CryptoCurrencyFactory()
private val exchangeStatusConverter = ExchangeStatusConverter()
private val txDetailsMoshiAdapter = moshi.adapter(TxDetails::class.java)
override suspend fun getPairs(
initialCurrency: LeastTokenInfo,
currencyList: List<CryptoCurrency>,
): List<SwapPairLeast> {
): PairsWithProviders {
return withContext(coroutineDispatcher.io) {
val initial = NetworkLeastTokenInfo(
contractAddress = initialCurrency.contractAddress,
network = initialCurrency.network,
)
val currenciesList = currencyList.map { leastTokenInfoConverter.convert(it) }
val pairs = async {
getPairsInternal(
from = arrayListOf(initial),
to = currenciesList,
try {
val initial = NetworkLeastTokenInfo(
contractAddress = initialCurrency.contractAddress,
network = initialCurrency.network,
)
}
val currenciesList = currencyList.map { leastTokenInfoConverter.convert(it) }
val reversedPairs = async {
getPairsInternal(
from = currenciesList,
to = arrayListOf(initial),
val pairsDeferred = async {
getPairsInternal(
from = arrayListOf(initial),
to = currenciesList,
)
}
val reversedPairsDeferred = async {
getPairsInternal(
from = currenciesList,
to = arrayListOf(initial),
)
}
val pairs = pairsDeferred.await().getOrThrow()
val reversedPairs = reversedPairsDeferred.await().getOrThrow()
val allPairs = pairs + reversedPairs
val providers = tangemExpressApi.getProviders().getOrThrow()
return@withContext swapPairInfoConverter.convert(
SwapPairsWithProviders(
swapPair = allPairs,
providers = providers,
),
)
} catch (exception: Exception) {
if (exception is ApiResponseError.HttpException) {
throw ExpressException(errorsDataConverter.convert(exception.errorBody ?: ""))
} else {
throw exception
}
}
}
}
val allPairs = pairs.await() + reversedPairs.await()
override suspend fun getPairsOnly(
initialCurrency: LeastTokenInfo,
currencyList: List<CryptoCurrency>,
): PairsWithProviders {
return withContext(coroutineDispatcher.io) {
try {
val initial = NetworkLeastTokenInfo(
contractAddress = initialCurrency.contractAddress,
network = initialCurrency.network,
)
val currenciesList = currencyList.map { leastTokenInfoConverter.convert(it) }
val providers = tangemExpressApi.getProviders().getOrThrow()
val pairsDeferred = async {
getPairsInternal(
from = arrayListOf(initial),
to = currenciesList,
)
}
return@withContext swapPairInfoConverter.convert(
SwapPairsWithProviders(
swapPair = allPairs,
providers = providers,
),
)
val reversedPairsDeferred = async {
getPairsInternal(
from = currenciesList,
to = arrayListOf(initial),
)
}
val pairs = pairsDeferred.await().getOrThrow()
val reversedPairs = reversedPairsDeferred.await().getOrThrow()
val allPairs = pairs + reversedPairs
return@withContext swapPairInfoConverter.convert(
SwapPairsWithProviders(
swapPair = allPairs,
providers = emptyList(),
),
)
} catch (exception: Exception) {
if (exception is ApiResponseError.HttpException) {
throw ExpressException(errorsDataConverter.convert(exception.errorBody ?: ""))
} else {
throw exception
}
}
}
}
private suspend fun getPairsInternal(
from: List<NetworkLeastTokenInfo>,
to: List<NetworkLeastTokenInfo>,
): List<SwapPair> {
): ApiResponse<List<SwapPair>> {
return tangemExpressApi.getPairs(
PairsRequestBody(
from = from,
to = to,
),
).getOrThrow()
)
}
override suspend fun getExchangeStatus(txId: String): Either<UnknownError, ExchangeStatusModel> {
@ -171,7 +235,7 @@ internal class SwapRepositoryImpl @Inject constructor(
toDecimals: Int,
providerId: String,
rateType: RateType,
): AggregatedSwapDataModel<QuoteModel> {
): Either<DataError, QuoteModel> {
return withContext(coroutineDispatcher.io) {
try {
val response = tangemExpressApi.getExchangeQuote(
@ -185,24 +249,16 @@ internal class SwapRepositoryImpl @Inject constructor(
providerId = providerId,
rateType = rateType.name.lowercase(),
).getOrThrow()
AggregatedSwapDataModel(
dataModel = QuoteModel(
toTokenAmount = createFromAmountWithOffset(response.toAmount, response.toDecimals),
allowanceContract = response.allowanceContract,
),
)
QuoteModel(
toTokenAmount = createFromAmountWithOffset(response.toAmount, response.toDecimals),
allowanceContract = response.allowanceContract,
).right()
} catch (ex: Exception) {
AggregatedSwapDataModel(null, getDataError(ex))
getDataError(ex).left()
}
}
}
override suspend fun addressForTrust(networkId: String): String {
return withContext(coroutineDispatcher.io) {
getOneInchApi(networkId).approveSpender().address
}
}
override suspend fun getExchangeData(
fromContractAddress: String,
fromNetwork: String,
@ -214,9 +270,10 @@ internal class SwapRepositoryImpl @Inject constructor(
providerId: String,
rateType: RateType,
toAddress: String,
): AggregatedSwapDataModel<SwapDataModel> {
): Either<DataError, SwapDataModel> {
return withContext(coroutineDispatcher.io) {
try {
val requestId = UUID.randomUUID().toString()
val response = tangemExpressApi.getExchangeData(
fromContractAddress = fromContractAddress,
fromNetwork = fromNetwork,
@ -228,18 +285,43 @@ internal class SwapRepositoryImpl @Inject constructor(
providerId = providerId,
rateType = rateType.name.lowercase(),
toAddress = toAddress,
requestId = requestId,
).getOrThrow()
AggregatedSwapDataModel(
dataModel = expressDataConverter.convert(response),
)
if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) {
val txDetails = parseTxDetails(response.txDetailsJson)
?: return@withContext DataError.UnknownError.left()
if (txDetails.requestId != requestId) {
return@withContext DataError.InvalidRequestIdError().left()
}
if (!toAddress.equals(txDetails.payoutAddress, ignoreCase = true)) {
return@withContext DataError.InvalidPayoutAddressError().left()
}
expressDataConverter.convert(
ExchangeDataResponseWithTxDetails(
dataResponse = response,
txDetails = txDetails,
),
).right()
} else {
DataError.InvalidSignatureError().left()
}
} catch (ex: Exception) {
AggregatedSwapDataModel(null, getDataError(ex))
getDataError(ex).left()
}
}
}
override fun getTangemFee(): Double {
return configManager.config.swapReferrerAccount?.fee?.toDoubleOrNull() ?: 0.0
override suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal? {
return walletManagersFacade.getExistentialDeposit(userWalletId, network)
}
private fun parseTxDetails(txDetailsJson: String): TxDetails? {
return try {
txDetailsMoshiAdapter.fromJson(txDetailsJson)
} catch (e: IOException) {
Timber.e(e, "error parsing txDetailsJson")
null
}
}
override suspend fun getAllowance(
@ -290,27 +372,27 @@ internal class SwapRepositoryImpl @Inject constructor(
return (walletManager as? Approver)?.getApproveData(
spenderAddress,
amount?.let { convertToAmount(it, currency, blockchain) },
amount?.let { convertToAmount(it, currency) },
) ?: error("Cannot cast to Approver")
}
private fun convertToAmount(amount: BigDecimal, currency: CryptoCurrency, blockchain: Blockchain): Amount {
return when (currency) {
is CryptoCurrency.Token -> {
Amount(value = amount, blockchain = blockchain)
}
is CryptoCurrency.Coin -> {
Amount(
currencySymbol = currency.symbol,
value = amount,
decimals = currency.decimals,
private fun convertToAmount(amount: BigDecimal, currency: CryptoCurrency): Amount {
return Amount(
currencySymbol = currency.symbol,
value = amount,
decimals = currency.decimals,
type = if (currency is CryptoCurrency.Token) {
AmountType.Token(
Token(
symbol = currency.symbol,
contractAddress = currency.contractAddress,
decimals = currency.decimals,
),
)
}
}
}
private fun getOneInchApi(networkId: String): OneInchApi {
return oneInchApiFactory.getApi(networkId)
} else {
AmountType.Coin
},
)
}
override fun getNativeTokenForNetwork(networkId: String): CryptoCurrency {

View file

@ -4,9 +4,11 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectList
import com.tangem.datasource.local.preferences.utils.getObjectListSync
import com.tangem.datasource.local.preferences.utils.getObjectMap
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.domain.SwapTransactionRepository
import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel
import com.tangem.feature.swap.domain.models.domain.SavedLastSwappedCryptoCurrency
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionModel
@ -24,11 +26,11 @@ class DefaultSwapTransactionRepository(
toCryptoCurrencyId: CryptoCurrency.ID,
transaction: SavedSwapTransactionModel,
) {
transaction.status?.let { storeTransactionState(transaction.txId, it) }
appPreferencesStore.editData { mutablePreferences ->
val savedTransactions: List<SavedSwapTransactionListModel>? = mutablePreferences.getObjectList(
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
)
val tokenTransactions = savedTransactions
?.firstOrNull {
it.checkId(
@ -62,14 +64,17 @@ class DefaultSwapTransactionRepository(
}
}
override fun getTransactions(
override suspend fun getTransactions(
userWalletId: UserWalletId,
cryptoCurrencyId: CryptoCurrency.ID,
): Flow<List<SavedSwapTransactionListModel>?> {
val txStatuses = appPreferencesStore.getObjectMap<ExchangeStatusModel>(
key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
)
return appPreferencesStore.getObjectList<SavedSwapTransactionListModel>(
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
).map { savedTransactions ->
savedTransactions
val currencyTxs = savedTransactions
?.filter {
it.userWalletId == userWalletId.stringValue &&
(
@ -77,6 +82,14 @@ class DefaultSwapTransactionRepository(
it.fromCryptoCurrencyId == cryptoCurrencyId.value
)
}
currencyTxs?.map { currencyTx ->
currencyTx.copy(
transactions = currencyTx.transactions.map { tx ->
tx.copy(status = txStatuses[tx.txId])
},
)
}
}
}
@ -86,6 +99,7 @@ class DefaultSwapTransactionRepository(
toCryptoCurrencyId: CryptoCurrency.ID,
txId: String,
) {
clearTransactionsStatuses(txId = txId)
appPreferencesStore.editData { mutablePreferences ->
val savedList: List<SavedSwapTransactionListModel>? = mutablePreferences.getObjectList(
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
@ -130,6 +144,22 @@ class DefaultSwapTransactionRepository(
}
}
override suspend fun storeTransactionState(txId: String, status: ExchangeStatusModel) {
appPreferencesStore.editData { mutablePreferences ->
val savedMap = mutablePreferences.getObjectMap<ExchangeStatusModel>(
key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
)
val updatesMap = savedMap?.toMutableMap() ?: mutableMapOf()
updatesMap[txId] = status
mutablePreferences.setObjectMap(
key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
value = updatesMap,
)
}
}
override suspend fun getLastSwappedCryptoCurrencyId(userWalletId: UserWalletId): String? {
val lastSwappedCurrencies = appPreferencesStore.getObjectListSync<SavedLastSwappedCryptoCurrency>(
key = PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY,
@ -194,4 +224,22 @@ class DefaultSwapTransactionRepository(
},
)
}
private suspend fun clearTransactionsStatuses(txId: String) {
appPreferencesStore.editData { mutablePreferences ->
val savedList = mutablePreferences.getObjectMap<ExchangeStatusModel>(
key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
)
val editedList = savedList?.filterNot { it.key == txId }
if (editedList.isNullOrEmpty()) {
mutablePreferences.remove(key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY)
} else {
mutablePreferences.setObjectMap(
key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
value = editedList,
)
}
}
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.swap.converters
import com.squareup.moshi.JsonAdapter
import com.tangem.datasource.api.express.models.response.ExpressError
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
@ -21,28 +22,47 @@ internal class ErrorsDataConverter(
2220 -> DataError.ExchangeProviderNotActiveError(code = error.code)
2230 -> DataError.ExchangeProviderNotAvailableError(code = error.code)
2240 -> DataError.ExchangeNotPossibleError(code = error.code)
2250 -> DataError.ExchangeTooSmallAmountError(
code = error.code,
amount = createFromAmountWithOffset(
requireNotNull(error.value?.minAmount),
requireNotNull(error.value?.decimals),
),
)
2260 -> DataError.ExchangeNotEnoughAllowanceError(
code = error.code,
currentAllowance = requireNotNull(error.value?.currentAllowance),
)
2250 -> tryParseExchangeTooSmallAmountError(error = error)
2260 -> tryParseExchangeNotEnoughAllowanceError(error = error)
2270 -> DataError.ExchangeNotEnoughBalanceError(code = error.code)
2280 -> DataError.ExchangeInvalidAddressError(code = error.code)
2290 -> DataError.ExchangeInvalidFromDecimalsError(
code = error.code,
receivedFromDecimals = requireNotNull(error.value?.receivedFromDecimals),
expressFromDecimals = requireNotNull(error.value?.expressFromDecimals),
)
else -> DataError.UnknownError
2290 -> tryParseExchangeInvalidFromDecimalsError(error = error)
else -> DataError.UnknownErrorWithCode(error.code)
}
} catch (e: Exception) {
return DataError.UnknownError
}
}
private fun tryParseExchangeTooSmallAmountError(error: ExpressError): DataError {
val minAmount = error.value?.minAmount ?: return DataError.UnknownErrorWithCode(error.code)
val decimals = error.value?.decimals ?: return DataError.UnknownErrorWithCode(error.code)
return DataError.ExchangeTooSmallAmountError(
code = error.code,
amount = createFromAmountWithOffset(minAmount, decimals),
)
}
private fun tryParseExchangeNotEnoughAllowanceError(error: ExpressError): DataError {
val currentAllowance = error.value?.currentAllowance ?: return DataError.UnknownErrorWithCode(error.code)
return DataError.ExchangeNotEnoughAllowanceError(
code = error.code,
currentAllowance = currentAllowance,
)
}
private fun tryParseExchangeInvalidFromDecimalsError(error: ExpressError): DataError {
val receivedFromDecimals = error.value?.receivedFromDecimals ?: return DataError.UnknownErrorWithCode(
code = error.code,
)
val expressFromDecimals = error.value?.expressFromDecimals ?: return DataError.UnknownErrorWithCode(error.code)
return DataError.ExchangeInvalidFromDecimalsError(
code = error.code,
receivedFromDecimals = receivedFromDecimals,
expressFromDecimals = expressFromDecimals,
)
}
}

View file

@ -13,7 +13,7 @@ internal class ExchangeStatusConverter : Converter<ExchangeStatusResponse, Excha
it.name.lowercase() == value.externalStatus.name.lowercase()
},
txId = value.externalTxId,
txUrl = value.externalTxUrl,
txExternalUrl = value.externalTxUrl,
)
}
}

View file

@ -1,39 +1,47 @@
package com.tangem.feature.swap.converters
import com.tangem.datasource.api.express.models.response.ExchangeDataResponse
import com.tangem.datasource.api.express.models.response.ExchangeDataResponseWithTxDetails
import com.tangem.datasource.api.express.models.response.TxDetails
import com.tangem.datasource.api.express.models.response.TxType
import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
import com.tangem.feature.swap.domain.models.domain.ExpressTransactionModel
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.utils.converter.Converter
class ExpressDataConverter : Converter<ExchangeDataResponse, SwapDataModel> {
internal class ExpressDataConverter : Converter<ExchangeDataResponseWithTxDetails, SwapDataModel> {
override fun convert(value: ExchangeDataResponse): SwapDataModel {
override fun convert(value: ExchangeDataResponseWithTxDetails): SwapDataModel {
val data = value.dataResponse
return SwapDataModel(
toTokenAmount = createFromAmountWithOffset(value.toAmount, value.toDecimals),
transaction = convertTransaction(value),
toTokenAmount = createFromAmountWithOffset(data.toAmount, data.toDecimals),
transaction = convertTransaction(value.txDetails, data),
)
}
private fun convertTransaction(transactionDto: ExchangeDataResponse): ExpressTransactionModel {
private fun convertTransaction(
transactionDto: TxDetails,
dataResponse: ExchangeDataResponse,
): ExpressTransactionModel {
return if (transactionDto.txType == TxType.SWAP) {
ExpressTransactionModel.DEX(
fromAmount = createFromAmountWithOffset(transactionDto.fromAmount, transactionDto.fromDecimals),
toAmount = createFromAmountWithOffset(transactionDto.toAmount, transactionDto.toDecimals),
txId = transactionDto.txId,
fromAmount = createFromAmountWithOffset(dataResponse.fromAmount, dataResponse.fromDecimals),
toAmount = createFromAmountWithOffset(dataResponse.toAmount, dataResponse.toDecimals),
txId = dataResponse.txId,
txTo = transactionDto.txTo,
txFrom = requireNotNull(transactionDto.txFrom),
txData = requireNotNull(transactionDto.txData),
)
} else {
ExpressTransactionModel.CEX(
fromAmount = createFromAmountWithOffset(transactionDto.fromAmount, transactionDto.fromDecimals),
toAmount = createFromAmountWithOffset(transactionDto.toAmount, transactionDto.toDecimals),
txId = transactionDto.txId,
fromAmount = createFromAmountWithOffset(dataResponse.fromAmount, dataResponse.fromDecimals),
toAmount = createFromAmountWithOffset(dataResponse.toAmount, dataResponse.toDecimals),
txId = dataResponse.txId,
txTo = transactionDto.txTo,
externalTxId = requireNotNull(transactionDto.externalTxId),
externalTxUrl = requireNotNull(transactionDto.externalTxUrl),
txExtraIdName = transactionDto.txExtraIdName,
txExtraId = transactionDto.txExtraId,
)
}
}

View file

@ -2,18 +2,20 @@ package com.tangem.feature.swap.converters
import com.tangem.datasource.api.express.models.response.*
import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo
import com.tangem.feature.swap.domain.models.domain.PairsWithProviders
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.utils.converter.Converter
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType as ExchangeProviderTypeDomain
import com.tangem.feature.swap.domain.models.domain.SwapPairLeast as SwapPairDomain
import com.tangem.feature.swap.domain.models.domain.SwapProvider as SwapPairProviderDomain
class SwapPairInfoConverter : Converter<SwapPairsWithProviders, List<SwapPairDomain>> {
class SwapPairInfoConverter : Converter<SwapPairsWithProviders, PairsWithProviders> {
private val rateTypeConverter = RateTypeConverter()
override fun convert(value: SwapPairsWithProviders): List<SwapPairDomain> {
override fun convert(value: SwapPairsWithProviders): PairsWithProviders {
val providersAdditionalMap = value.providers.associateBy { it.id }
return value.swapPair.map { pair ->
val pairs = value.swapPair.map { pair ->
SwapPairDomain(
from = LeastTokenInfo(
contractAddress = pair.from.contractAddress,
@ -28,6 +30,22 @@ class SwapPairInfoConverter : Converter<SwapPairsWithProviders, List<SwapPairDom
},
)
}
return PairsWithProviders(
pairs = pairs,
allProviders = value.providers.map { convertLeastProvider(it) },
)
}
private fun convertLeastProvider(exchangeProvider: ExchangeProvider): SwapProvider {
return SwapProvider(
providerId = exchangeProvider.id,
rateTypes = emptyList(),
name = exchangeProvider.name,
type = convertExchangeType(exchangeProvider.type),
imageLarge = exchangeProvider.imageLargeUrl,
termsOfUse = exchangeProvider.termsOfUse,
privacyPolicy = exchangeProvider.privacyPolicy,
)
}
private fun convertProvider(
@ -41,6 +59,8 @@ class SwapPairInfoConverter : Converter<SwapPairsWithProviders, List<SwapPairDom
name = additionalProvider.name,
type = convertExchangeType(additionalProvider.type),
imageLarge = additionalProvider.imageLargeUrl,
termsOfUse = additionalProvider.termsOfUse,
privacyPolicy = additionalProvider.privacyPolicy,
)
}

View file

@ -3,18 +3,17 @@ package com.tangem.feature.swap.di
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.datasource.api.oneinch.OneInchApiFactory
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.feature.swap.SwapRepositoryImpl
import com.tangem.feature.swap.converters.ErrorsDataConverter
import com.tangem.feature.swap.DefaultSwapTransactionRepository
import com.tangem.feature.swap.domain.SwapRepository
import com.tangem.feature.swap.DefaultSwapRepository
import com.tangem.feature.swap.converters.ErrorsDataConverter
import com.tangem.feature.swap.domain.SwapTransactionRepository
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -31,22 +30,22 @@ internal class SwapDataModule {
internal fun provideSwapRepository(
tangemTechApi: TangemTechApi,
tangemExpressApi: TangemExpressApi,
oneInchApiFactory: OneInchApiFactory,
coroutineDispatcher: CoroutineDispatcherProvider,
configManager: ConfigManager,
dataSignature: DataSignatureVerifier,
walletManagerFacade: WalletManagersFacade,
walletsStateHolder: WalletsStateHolder,
errorsDataConverter: ErrorsDataConverter,
@NetworkMoshi moshi: Moshi,
): SwapRepository {
return SwapRepositoryImpl(
return DefaultSwapRepository(
tangemTechApi = tangemTechApi,
tangemExpressApi = tangemExpressApi,
oneInchApiFactory = oneInchApiFactory,
coroutineDispatcher = coroutineDispatcher,
configManager = configManager,
walletManagersFacade = walletManagerFacade,
walletsStateHolder = walletsStateHolder,
errorsDataConverter = errorsDataConverter,
dataSignatureVerifier = dataSignature,
moshi = moshi,
)
}

1
features/swap/domain/api/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,18 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.feature.swap.domain.api"
}
dependencies {
implementation(projects.features.swap.domain.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(deps.arrow.core)
}

View file

@ -1,15 +1,19 @@
package com.tangem.feature.swap.domain
package com.tangem.feature.swap.domain.api
import arrow.core.Either
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.domain.*
import java.math.BigDecimal
interface SwapRepository {
suspend fun getPairs(initialCurrency: LeastTokenInfo, currencyList: List<CryptoCurrency>): List<SwapPairLeast>
suspend fun getPairs(initialCurrency: LeastTokenInfo, currencyList: List<CryptoCurrency>): PairsWithProviders
/** Express getPairs request variant without providers request */
suspend fun getPairsOnly(initialCurrency: LeastTokenInfo, currencyList: List<CryptoCurrency>): PairsWithProviders
suspend fun getRates(currencyId: String, tokenIds: List<String>): Map<String, Double>
@ -31,20 +35,7 @@ interface SwapRepository {
toDecimals: Int,
providerId: String,
rateType: RateType,
): AggregatedSwapDataModel<QuoteModel>
/**
* Returns address of 1inch router that must be trusted
*
* @return address
*/
suspend fun addressForTrust(networkId: String): String
/**
* Returns a tangem fee for swap in percents
* Example: 0.35%
*/
fun getTangemFee(): Double
): Either<DataError, QuoteModel>
@Suppress("LongParameterList")
@Throws(IllegalStateException::class)
@ -80,7 +71,9 @@ interface SwapRepository {
providerId: String,
rateType: RateType,
toAddress: String,
): AggregatedSwapDataModel<SwapDataModel>
): Either<DataError, SwapDataModel>
fun getNativeTokenForNetwork(networkId: String): CryptoCurrency
suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal?
}

View file

@ -28,6 +28,9 @@ dependencies {
implementation(projects.domain.demo)
implementation(projects.domain.card)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.txhistory.models)
implementation(projects.features.swap.domain.api)
implementation(projects.features.swap.domain.models)
/** Core modules */
implementation(projects.core.utils)
@ -37,7 +40,6 @@ dependencies {
implementation(projects.features.wallet.api)
/** Other Libraries **/
implementation(deps.kotlin.serialization)
implementation(deps.kotlin.coroutines)
implementation(deps.arrow.core)
implementation(deps.timber)

View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,23 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
id("configuration")
}
android {
namespace = "com.tangem.feature.swap.domain.models"
}
dependencies {
/** Domain */
implementation(projects.domain.tokens.models)
/** Core modules */
implementation(projects.core.utils)
/** Other Libraries **/
implementation(deps.kotlin.serialization)
implementation(deps.arrow.core)
}

View file

@ -30,6 +30,14 @@ sealed class DataError {
val expressFromDecimals: Int,
) : DataError()
data class UnknownErrorWithCode(override val code: Int) : DataError()
data class InvalidSignatureError(override val code: Int = 990) : DataError()
data class InvalidRequestIdError(override val code: Int = 991) : DataError()
data class InvalidPayoutAddressError(override val code: Int = 992) : DataError()
object UnknownError : DataError() {
override val code: Int = -1
}

View file

@ -0,0 +1,3 @@
package com.tangem.feature.swap.domain.models
class ExpressException(val dataError: DataError) : Exception()

View file

@ -4,7 +4,7 @@ data class ExchangeStatusModel(
val providerId: String,
val status: ExchangeStatus? = null,
val txId: String? = null,
val txUrl: String? = null,
val txExternalUrl: String? = null,
)
enum class ExchangeStatus {
@ -17,4 +17,5 @@ enum class ExchangeStatus {
Sending,
Finished,
Refunded,
Cancelled,
}

View file

@ -25,5 +25,7 @@ sealed class ExpressTransactionModel {
override val txTo: String,
val externalTxId: String,
val externalTxUrl: String,
val txExtraIdName: String?,
val txExtraId: String?,
) : ExpressTransactionModel()
}

View file

@ -0,0 +1,6 @@
package com.tangem.feature.swap.domain.models.domain
data class PairsWithProviders(
val pairs: List<SwapPairLeast>,
val allProviders: List<SwapProvider>,
)

Some files were not shown because too many files have changed in this diff Show more