Updated on 2026-08-14
This commit is contained in:
commit
cf782b8e13
174 changed files with 2953 additions and 1544 deletions
|
|
@ -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
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@ enum class ExchangeStatus {
|
|||
|
||||
@Json(name = "verifying")
|
||||
VERIFYING,
|
||||
|
||||
@Json(name = "expired")
|
||||
CANCELLED,
|
||||
}
|
||||
|
||||
data class ExchangeStatusError(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.datasource.crypto
|
||||
|
||||
interface DataSignatureVerifier {
|
||||
|
||||
fun verifySignature(signature: String, data: String): Boolean
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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]
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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> */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,4 +15,5 @@ enum class ExchangeAnalyticsStatus(val value: String) {
|
|||
Fail("Fail"),
|
||||
KYC("KYC"),
|
||||
Refunded("Refunded"),
|
||||
Cancelled("Canceled"),
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 provider’s 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 provider’s 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 provider’s %s</string>
|
||||
<string name="express_legal_two_placeholders">By using swap functionality, you agree with provider’s %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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
BIN
core/ui/src/main/res/drawable/img_swap_promo.webp
Normal file
BIN
core/ui/src/main/res/drawable/img_swap_promo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -7,5 +7,7 @@ interface ReduxStateHolder {
|
|||
|
||||
fun dispatch(action: Action)
|
||||
|
||||
suspend fun dispatchWithMain(action: Action)
|
||||
|
||||
suspend fun onUserWalletSelected(userWallet: UserWallet)
|
||||
}
|
||||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,5 @@ package com.tangem.domain.transaction.error
|
|||
|
||||
sealed class GetFeeError {
|
||||
data class DataError(val cause: Throwable?) : GetFeeError()
|
||||
|
||||
object UnknownError : GetFeeError()
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import com.tangem.crypto.bip39.Wordlist
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultMnemonicRepository(
|
||||
class DefaultMnemonicRepository(
|
||||
private val bip39Wordlist: Wordlist,
|
||||
) : MnemonicRepository {
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
1
features/swap/domain/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
18
features/swap/domain/api/build.gradle.kts
Normal file
18
features/swap/domain/api/build.gradle.kts
Normal 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)
|
||||
|
||||
}
|
||||
|
|
@ -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?
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
1
features/swap/domain/models/.gitignore
vendored
Normal file
1
features/swap/domain/models/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
23
features/swap/domain/models/build.gradle.kts
Normal file
23
features/swap/domain/models/build.gradle.kts
Normal 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)
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.tangem.feature.swap.domain.models
|
||||
|
||||
class ExpressException(val dataError: DataError) : Exception()
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -25,5 +25,7 @@ sealed class ExpressTransactionModel {
|
|||
override val txTo: String,
|
||||
val externalTxId: String,
|
||||
val externalTxUrl: String,
|
||||
val txExtraIdName: String?,
|
||||
val txExtraId: String?,
|
||||
) : ExpressTransactionModel()
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue