Updated on 2026-08-14
This commit is contained in:
commit
6303e0b5af
168 changed files with 8245 additions and 2347 deletions
|
|
@ -36,7 +36,6 @@ internal object TransactionDomainModule {
|
|||
isDemoCardUseCase = isDemoCardUseCase,
|
||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -75,9 +75,10 @@ interface FragmentOnBackPressedHandler {
|
|||
|
||||
@SuppressLint("FragmentBackPressedCallback")
|
||||
fun Fragment.addBackPressHandler(handler: FragmentOnBackPressedHandler) {
|
||||
requireActivity().onBackPressedDispatcher.addCallback {
|
||||
handler.handleOnBackPressed()
|
||||
}
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
owner = this,
|
||||
onBackPressed = { handler.handleOnBackPressed() },
|
||||
)
|
||||
|
||||
view?.findViewById<Toolbar>(R.id.toolbar)?.setNavigationOnClickListener {
|
||||
handler.handleOnBackPressed()
|
||||
|
|
|
|||
|
|
@ -9,11 +9,8 @@ import com.tangem.common.extensions.guard
|
|||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.domain.common.extensions.toCoinId
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.model.NetworkAddress
|
||||
import com.tangem.feature.swap.presentation.SwapFragment
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
|
|
@ -26,7 +23,6 @@ import com.tangem.tap.common.extensions.dispatchOnMain
|
|||
import com.tangem.tap.common.extensions.dispatchOpenUrl
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.tokens.getIconUrl
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||
import com.tangem.tap.features.send.redux.PrepareSendScreen
|
||||
|
|
@ -41,9 +37,6 @@ import com.tangem.tap.proxy.redux.DaggerGraphState
|
|||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency as SwapCurrency
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class TradeCryptoMiddleware {
|
||||
|
|
@ -58,17 +51,12 @@ class TradeCryptoMiddleware {
|
|||
is TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen(action)
|
||||
is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
|
||||
is TradeCryptoAction.Swap -> {
|
||||
openSwap(
|
||||
currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency(),
|
||||
derivationPath = store.state.walletState.selectedWalletData?.currency?.derivationPath,
|
||||
)
|
||||
// todo remove old flow
|
||||
}
|
||||
is TradeCryptoAction.New.Buy -> proceedNewBuyAction(state, action)
|
||||
is TradeCryptoAction.New.Sell -> proceedNewSellAction(action)
|
||||
is TradeCryptoAction.New.Swap -> openSwap(
|
||||
currency = action.cryptoCurrency.toSwapCurrency(),
|
||||
derivationPath = action.cryptoCurrency.network.derivationPath.value,
|
||||
network = action.cryptoCurrency.network,
|
||||
currency = action.cryptoCurrency,
|
||||
)
|
||||
is TradeCryptoAction.New.SendToken -> handleNewSendToken(action = action)
|
||||
is TradeCryptoAction.New.SendCoin -> handleNewSendCoin(action = action)
|
||||
|
|
@ -274,70 +262,14 @@ class TradeCryptoMiddleware {
|
|||
)?.let { store.dispatchOpenUrl(it) }
|
||||
}
|
||||
|
||||
private fun openSwap(currency: SwapCurrency?, derivationPath: String?, network: Network? = null) {
|
||||
private fun openSwap(currency: CryptoCurrency) {
|
||||
val bundle = bundleOf(
|
||||
SwapFragment.CURRENCY_BUNDLE_KEY to Json.encodeToString(currency),
|
||||
SwapFragment.DERIVATION_PATH to derivationPath,
|
||||
SwapFragment.NETWORK to network,
|
||||
SwapFragment.CURRENCY_BUNDLE_KEY to currency,
|
||||
)
|
||||
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle))
|
||||
}
|
||||
|
||||
private fun CryptoCurrency.toSwapCurrency(): SwapCurrency {
|
||||
val blockchain = Blockchain.fromId(network.id.value)
|
||||
|
||||
return when (this) {
|
||||
is CryptoCurrency.Coin -> {
|
||||
SwapCurrency.NativeToken(
|
||||
id = blockchain.toCoinId(),
|
||||
name = name,
|
||||
symbol = symbol,
|
||||
networkId = blockchain.toNetworkId(),
|
||||
// no need to set logoUrl for blockchain cause
|
||||
// error when form url with coinId, coinId of eth and arbitrum the same
|
||||
logoUrl = "",
|
||||
)
|
||||
}
|
||||
is CryptoCurrency.Token -> {
|
||||
SwapCurrency.NonNativeToken(
|
||||
id = id.rawCurrencyId ?: "",
|
||||
name = name,
|
||||
symbol = symbol,
|
||||
networkId = blockchain.toNetworkId(),
|
||||
logoUrl = getIconUrl(id.rawCurrencyId ?: ""),
|
||||
contractAddress = contractAddress,
|
||||
decimalCount = decimals,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Currency.toSwapCurrency(): SwapCurrency {
|
||||
return when (this) {
|
||||
is Currency.Blockchain -> {
|
||||
SwapCurrency.NativeToken(
|
||||
id = blockchain.toCoinId(),
|
||||
name = this.currencyName,
|
||||
symbol = this.currencySymbol,
|
||||
networkId = this.blockchain.toNetworkId(),
|
||||
// no need to set logoUrl for blockchain cause
|
||||
// error when form url with coinId, coinId of eth and arbitrum the same
|
||||
logoUrl = "",
|
||||
)
|
||||
}
|
||||
is Currency.Token -> SwapCurrency.NonNativeToken(
|
||||
id = this.token.id ?: "",
|
||||
name = this.currencyName,
|
||||
symbol = this.currencySymbol,
|
||||
networkId = this.blockchain.toNetworkId(),
|
||||
logoUrl = getIconUrl(this.token.id ?: ""),
|
||||
contractAddress = this.token.contractAddress,
|
||||
decimalCount = decimals,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNewSendToken(action: TradeCryptoAction.New.SendToken) {
|
||||
val currency = action.tokenCurrency
|
||||
val blockchain = Blockchain.fromId(currency.network.id.value)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.tap.network.auth
|
||||
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.lib.auth.ExpressAuthProvider
|
||||
import com.tangem.lib.auth.sessionId.ExpressSessionIdGenerator
|
||||
import java.util.UUID
|
||||
|
||||
class ExpressAuthProviderImpl(
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val configManager: ConfigManager,
|
||||
) : ExpressAuthProvider, ExpressSessionIdGenerator {
|
||||
|
||||
private var uuid = UUID.randomUUID()
|
||||
|
||||
override fun getApiKey(): String {
|
||||
return configManager.config.tangemExpressApiKey
|
||||
}
|
||||
|
||||
override fun getUserId(): String {
|
||||
return userWalletsStore.selectedUserWalletOrNull?.walletId?.stringValue ?: ""
|
||||
}
|
||||
|
||||
override fun getSessionId(): String {
|
||||
return uuid.toString()
|
||||
}
|
||||
|
||||
override fun generateNewSessionId() {
|
||||
uuid = UUID.randomUUID()
|
||||
}
|
||||
}
|
||||
|
|
@ -224,20 +224,18 @@ class TransactionManagerImpl(
|
|||
// for not EVM blockchains set gasLimit ZERO for now
|
||||
when (fee.data) {
|
||||
is TransactionFee.Single -> {
|
||||
val fee = (fee.data as TransactionFee.Single).normal
|
||||
val normalFee = (fee.data as TransactionFee.Single).normal
|
||||
val singleFee = ProxyFee(
|
||||
gasLimit = BigInteger.ZERO,
|
||||
fee = convertToProxyAmount(amount = fee.amount),
|
||||
fee = convertToProxyAmount(amount = normalFee.amount),
|
||||
)
|
||||
ProxyFees(
|
||||
minFee = singleFee,
|
||||
normalFee = singleFee,
|
||||
priorityFee = singleFee,
|
||||
ProxyFees.SingleFee(
|
||||
singleFee = singleFee,
|
||||
)
|
||||
}
|
||||
is TransactionFee.Choosable -> {
|
||||
val choosableFee = fee.data as TransactionFee.Choosable
|
||||
ProxyFees(
|
||||
ProxyFees.MultipleFees(
|
||||
minFee = ProxyFee(
|
||||
gasLimit = BigInteger.ZERO,
|
||||
fee = convertToProxyAmount(amount = choosableFee.minimum.amount),
|
||||
|
|
@ -280,7 +278,7 @@ class TransactionManagerImpl(
|
|||
).increaseBigIntegerByPercents(increaseBy)
|
||||
return when (val gasPrice = walletManager.getGasPrice()) {
|
||||
is Result.Success -> {
|
||||
createProxyFees(gasPrice = gasPrice.data, gasLimit = gasLimit, blockchain = blockchain)
|
||||
createMultipleProxyFees(gasPrice = gasPrice.data, gasLimit = gasLimit, blockchain = blockchain)
|
||||
}
|
||||
is Result.Failure -> {
|
||||
error(gasPrice.error.message ?: gasPrice.error.customMessage)
|
||||
|
|
@ -316,7 +314,7 @@ class TransactionManagerImpl(
|
|||
fee = convertToProxyAmount(amount = choosableFee.priority.amount),
|
||||
)
|
||||
|
||||
ProxyFees(
|
||||
ProxyFees.MultipleFees(
|
||||
minFee = minProxyFee,
|
||||
normalFee = normalProxyFee,
|
||||
priorityFee = priorityProxyFee,
|
||||
|
|
@ -456,7 +454,7 @@ class TransactionManagerImpl(
|
|||
* @param gasLimit
|
||||
* @param blockchain
|
||||
*/
|
||||
private fun createProxyFees(gasPrice: BigInteger, gasLimit: BigInteger, blockchain: Blockchain): ProxyFees {
|
||||
private fun createMultipleProxyFees(gasPrice: BigInteger, gasLimit: BigInteger, blockchain: Blockchain): ProxyFees {
|
||||
val gasPriceNormal = gasPrice.increaseBigIntegerByPercents(MULTIPLIER_GAS_PRICE_FOR_NORMAL_FEE)
|
||||
val gasPricePriority = gasPrice.increaseBigIntegerByPercents(MULTIPLIER_GAS_PRICE_FOR_PRIORITY_FEE)
|
||||
val feeMin = gasLimit.multiply(gasPrice).toBigDecimal(
|
||||
|
|
@ -495,7 +493,7 @@ class TransactionManagerImpl(
|
|||
decimals = blockchain.decimals(),
|
||||
),
|
||||
)
|
||||
return ProxyFees(
|
||||
return ProxyFees.MultipleFees(
|
||||
minFee = minFee,
|
||||
normalFee = normalFee,
|
||||
priorityFee = priorityFee,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/send_fee_label"
|
||||
android:text="@string/common_fee_label"
|
||||
android:textColor="@color/text_tertiary"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/onboarding_balance_title"
|
||||
android:text="@string/common_balance_title"
|
||||
android:textColor="@color/text_tertiary"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
android:elevation="0dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:letterSpacing="0.036"
|
||||
android:text="@string/onboarding_balance_title"
|
||||
android:text="@string/common_balance_title"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,11 @@ sealed class ApiResponseError : Exception() {
|
|||
* @property code The HTTP status code.
|
||||
* @property message A human-readable message describing the error.
|
||||
*/
|
||||
data class HttpException(val code: Code, override val message: String?) : ApiResponseError() {
|
||||
data class HttpException(
|
||||
val code: Code,
|
||||
override val message: String?,
|
||||
val errorBody: String?,
|
||||
) : ApiResponseError() {
|
||||
|
||||
// region Error Codes
|
||||
enum class Code(val code: Int) {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ internal fun <T : Any> Response<T>.toSafeApiResponse(): ApiResponse<T> {
|
|||
val e = if (code == null) {
|
||||
ApiResponseError.UnknownException(IllegalArgumentException("Unknown error status code: ${code()}"))
|
||||
} else {
|
||||
ApiResponseError.HttpException(code, message())
|
||||
ApiResponseError.HttpException(code, message(), errorBody()?.string())
|
||||
}
|
||||
|
||||
apiError(e)
|
||||
|
|
|
|||
|
|
@ -8,13 +8,12 @@ import retrofit2.http.Body
|
|||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Interface of Tangem Express API (new swap mechanism)
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
interface ExpressApi {
|
||||
interface TangemExpressApi {
|
||||
|
||||
@POST("assets")
|
||||
suspend fun getAssets(@Body body: AssetsRequestBody): ApiResponse<List<Asset>>
|
||||
|
|
@ -31,9 +30,11 @@ interface ExpressApi {
|
|||
@Query("fromNetwork") fromNetwork: String,
|
||||
@Query("toContractAddress") toContractAddress: String,
|
||||
@Query("toNetwork") toNetwork: String,
|
||||
@Query("fromAmount") fromAmount: BigDecimal,
|
||||
@Query("providerId") providerId: Int,
|
||||
@Query("rateType") rateType: RateType,
|
||||
@Query("fromAmount") fromAmount: String,
|
||||
@Query("fromDecimals") fromDecimals: Int,
|
||||
@Query("toDecimals") toDecimals: Int,
|
||||
@Query("providerId") providerId: String,
|
||||
@Query("rateType") rateType: String,
|
||||
): ApiResponse<ExchangeQuoteResponse>
|
||||
|
||||
@GET("exchange-data")
|
||||
|
|
@ -42,12 +43,14 @@ interface ExpressApi {
|
|||
@Query("fromNetwork") fromNetwork: String,
|
||||
@Query("toContractAddress") toContractAddress: String,
|
||||
@Query("toNetwork") toNetwork: String,
|
||||
@Query("fromAmount") fromAmount: BigDecimal,
|
||||
@Query("providerId") providerId: Int,
|
||||
@Query("rateType") rateType: RateType,
|
||||
@Query("fromAmount") fromAmount: String,
|
||||
@Query("fromDecimals") fromDecimals: Int,
|
||||
@Query("toDecimals") toDecimals: Int,
|
||||
@Query("providerId") providerId: String,
|
||||
@Query("rateType") rateType: String,
|
||||
@Query("toAddress") toAddress: String,
|
||||
): ApiResponse<ExchangeDataResponse>
|
||||
|
||||
@GET("exchange-result")
|
||||
suspend fun getExchangeResults(@Query("txId") txId: String): ApiResponse<ExchangeResultsResponse>
|
||||
@GET("exchange-status")
|
||||
suspend fun getExchangeStatus(@Query("txId") txId: String): ApiResponse<ExchangeStatusResponse>
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.datasource.api.express.models
|
||||
|
||||
object TangemExpressValues {
|
||||
const val EMPTY_CONTRACT_ADDRESS_VALUE = "0"
|
||||
}
|
||||
|
|
@ -3,5 +3,5 @@ package com.tangem.datasource.api.express.models.request
|
|||
import com.squareup.moshi.Json
|
||||
|
||||
data class AssetsRequestBody(
|
||||
@Json(name = "filter") val filter: List<LeastTokenInfo>?,
|
||||
@Json(name = "tokensList") val tokensList: List<LeastTokenInfo>?,
|
||||
)
|
||||
|
|
@ -9,21 +9,6 @@ data class Asset(
|
|||
@Json(name = "network")
|
||||
val network: String,
|
||||
|
||||
@Json(name = "token")
|
||||
val token: String,
|
||||
|
||||
@Json(name = "name")
|
||||
val name: String,
|
||||
|
||||
@Json(name = "symbol")
|
||||
val symbol: String,
|
||||
|
||||
@Json(name = "decimals")
|
||||
val decimals: Int,
|
||||
|
||||
@Json(name = "isActive")
|
||||
val isActive: Boolean,
|
||||
|
||||
@Json(name = "exchangeAvailable")
|
||||
val exchangeAvailable: Boolean,
|
||||
)
|
||||
|
|
@ -4,8 +4,17 @@ import com.squareup.moshi.Json
|
|||
import java.math.BigDecimal
|
||||
|
||||
data class ExchangeDataResponse(
|
||||
@Json(name = "fromAmount")
|
||||
val fromAmount: String,
|
||||
|
||||
@Json(name = "fromDecimals")
|
||||
val fromDecimals: Int,
|
||||
|
||||
@Json(name = "toAmount")
|
||||
val toAmount: BigDecimal,
|
||||
val toAmount: String,
|
||||
|
||||
@Json(name = "toDecimals")
|
||||
val toDecimals: Int,
|
||||
|
||||
@Json(name = "txType")
|
||||
val txType: TxType,
|
||||
|
|
|
|||
|
|
@ -4,19 +4,19 @@ import com.squareup.moshi.Json
|
|||
|
||||
data class ExchangeProvider(
|
||||
@Json(name = "id")
|
||||
val id: Int,
|
||||
val id: String,
|
||||
|
||||
@Json(name = "name")
|
||||
val name: String,
|
||||
|
||||
@Json(name = "id")
|
||||
@Json(name = "type")
|
||||
val type: ExchangeProviderType,
|
||||
|
||||
@Json(name = "imageLarge")
|
||||
val imageLargeUrl: Int,
|
||||
val imageLargeUrl: String,
|
||||
|
||||
@Json(name = "imageSmall")
|
||||
val imageSmallUrl: Int,
|
||||
val imageSmallUrl: String,
|
||||
)
|
||||
|
||||
enum class ExchangeProviderType {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,23 @@ import com.squareup.moshi.Json
|
|||
import java.math.BigDecimal
|
||||
|
||||
data class ExchangeQuoteResponse(
|
||||
|
||||
@Json(name = "fromAmount")
|
||||
val fromAmount: String,
|
||||
|
||||
@Json(name = "fromDecimals")
|
||||
val fromDecimals: Int,
|
||||
|
||||
@Json(name = "toAmount")
|
||||
val toAmount: BigDecimal,
|
||||
val toAmount: String,
|
||||
|
||||
@Json(name = "toDecimals")
|
||||
val toDecimals: Int,
|
||||
|
||||
@Json(name = "allowanceContract")
|
||||
val allowanceContract: String?,
|
||||
|
||||
@Json(name = "minAmount")
|
||||
val minAmount: BigDecimal,
|
||||
|
||||
)
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
package com.tangem.datasource.api.express.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
|
||||
data class ExchangeResultsResponse(
|
||||
@Json(name = "status")
|
||||
val status: ExchangeResultsStatus,
|
||||
|
||||
@Json(name = "externalStatus")
|
||||
val externalStatus: String,
|
||||
|
||||
@Json(name = "externalTxUrl")
|
||||
val externalTxUrl: String,
|
||||
|
||||
@Json(name = "error")
|
||||
val error: ExchangeResultsError?,
|
||||
)
|
||||
|
||||
enum class ExchangeResultsStatus {
|
||||
@Json(name = "processing")
|
||||
PROCESSING,
|
||||
|
||||
@Json(name = "done")
|
||||
DONE,
|
||||
|
||||
@Json(name = "failed")
|
||||
FAILED,
|
||||
|
||||
@Json(name = "refunded")
|
||||
REFUNDED,
|
||||
|
||||
@Json(name = "verificationRequired")
|
||||
VERIFICATION_REQUIRED,
|
||||
}
|
||||
|
||||
data class ExchangeResultsError(
|
||||
@Json(name = "code")
|
||||
val code: Int,
|
||||
|
||||
@Json(name = "description")
|
||||
val description: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.datasource.api.express.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
|
||||
data class ExchangeStatusResponse(
|
||||
|
||||
@Json(name = "providerId")
|
||||
val providerId: String,
|
||||
|
||||
@Json(name = "externalTxId")
|
||||
val externalTxId: String,
|
||||
|
||||
@Json(name = "externalTxStatus")
|
||||
val externalStatus: ExchangeStatus,
|
||||
|
||||
@Json(name = "externalTxUrl")
|
||||
val externalTxUrl: String,
|
||||
|
||||
@Json(name = "error")
|
||||
val error: ExchangeStatusError?,
|
||||
)
|
||||
|
||||
enum class ExchangeStatus {
|
||||
|
||||
@Json(name = "new")
|
||||
NEW,
|
||||
|
||||
@Json(name = "waiting")
|
||||
WAITING,
|
||||
|
||||
@Json(name = "confirming")
|
||||
CONFIRMING,
|
||||
|
||||
@Json(name = "exchanging")
|
||||
EXCHANGING,
|
||||
|
||||
@Json(name = "sending")
|
||||
SENDING,
|
||||
|
||||
@Json(name = "finished")
|
||||
FINISHED,
|
||||
|
||||
@Json(name = "failed")
|
||||
FAILED,
|
||||
|
||||
@Json(name = "refunded")
|
||||
REFUNDED,
|
||||
|
||||
@Json(name = "verifying")
|
||||
VERIFYING,
|
||||
}
|
||||
|
||||
data class ExchangeStatusError(
|
||||
@Json(name = "code")
|
||||
val code: Int,
|
||||
|
||||
@Json(name = "description")
|
||||
val description: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.datasource.api.express.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class ExpressErrorResponse(
|
||||
@Json(name = "error")
|
||||
val error: ExpressError,
|
||||
)
|
||||
|
||||
data class ExpressError(
|
||||
@Json(name = "code")
|
||||
val code: Int,
|
||||
|
||||
@Json(name = "description")
|
||||
val description: String?,
|
||||
|
||||
@Json(name = "value")
|
||||
val value: ExpressErrorValue?,
|
||||
)
|
||||
|
||||
data class ExpressErrorValue(
|
||||
@Json(name = "minAmount")
|
||||
val minAmount: String?,
|
||||
|
||||
@Json(name = "decimals")
|
||||
val decimals: Int?,
|
||||
|
||||
@Json(name = "currentAllowance")
|
||||
val currentAllowance: BigDecimal?,
|
||||
|
||||
@Json(name = "receivedFromDecimals")
|
||||
val receivedFromDecimals: Int?,
|
||||
|
||||
@Json(name = "expressFromDecimals")
|
||||
val expressFromDecimals: Int?,
|
||||
)
|
||||
|
|
@ -17,10 +17,10 @@ data class SwapPair(
|
|||
|
||||
data class SwapPairProvider(
|
||||
@Json(name = "providerId")
|
||||
val providerId: Int,
|
||||
val providerId: String,
|
||||
|
||||
@Json(name = "rateType")
|
||||
val rateType: RateType,
|
||||
@Json(name = "rateTypes")
|
||||
val rateTypes: List<RateType>,
|
||||
)
|
||||
|
||||
enum class RateType {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.datasource.api.express.models.response
|
||||
|
||||
class SwapPairsWithProviders(
|
||||
val swapPair: List<SwapPair>,
|
||||
val providers: List<ExchangeProvider>,
|
||||
)
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.token.DefaultUserMarketCoinsStore
|
||||
import com.tangem.datasource.local.token.UserMarketCoinsStore
|
||||
import com.tangem.datasource.local.token.DefaultAssetsStore
|
||||
import com.tangem.datasource.local.token.AssetsStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -11,11 +11,11 @@ import javax.inject.Singleton
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object MarketCoinsStoreModule {
|
||||
internal object AssetsStoreModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideUserMarketCoinsStore(): UserMarketCoinsStore {
|
||||
return DefaultUserMarketCoinsStore(dataStore = RuntimeDataStore())
|
||||
fun provideAssetsStore(): AssetsStore {
|
||||
return DefaultAssetsStore(dataStore = RuntimeDataStore())
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ package com.tangem.datasource.di
|
|||
import android.content.Context
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
|
||||
import com.tangem.datasource.api.express.ExpressApi
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.datasource.api.promotion.PromotionApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.utils.RequestHeader.*
|
||||
|
|
@ -32,7 +32,7 @@ class NetworkModule {
|
|||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
expressAuthProvider: ExpressAuthProvider,
|
||||
): ExpressApi {
|
||||
): TangemExpressApi {
|
||||
return Retrofit.Builder()
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
|
||||
|
|
@ -44,7 +44,7 @@ class NetworkModule {
|
|||
.build(),
|
||||
)
|
||||
.build()
|
||||
.create(ExpressApi::class.java)
|
||||
.create(TangemExpressApi::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.swaptx.DefaultSwapTransactionStatusStore
|
||||
import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object SwapTransactionStatusStoreModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSwapTransactionStatusStore(): SwapTransactionStatusStore {
|
||||
return DefaultSwapTransactionStatusStore(
|
||||
dataStore = RuntimeDataStore(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -33,11 +33,15 @@ object PreferencesKeys {
|
|||
|
||||
val BALANCE_HIDING_SETTINGS_KEY by lazy { stringPreferencesKey(name = "balanceHidingSettings") }
|
||||
|
||||
val SWAP_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "swapTransactions") }
|
||||
|
||||
val WALLETS_SCROLL_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "walletsScrollPreview") }
|
||||
|
||||
val SENT_ONE_TIME_EVENTS_KEY by lazy { stringPreferencesKey(name = "sentOneTimeEvents") }
|
||||
|
||||
val WALLETS_BALANCES_STATES_KEY by lazy { stringPreferencesKey(name = "walletsBalancesStates") }
|
||||
|
||||
val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") }
|
||||
}
|
||||
|
||||
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.datasource.local.swaptx
|
||||
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
|
||||
|
||||
internal class DefaultSwapTransactionStatusStore(
|
||||
private val dataStore: StringKeyDataStore<ExchangeAnalyticsStatus>,
|
||||
) : SwapTransactionStatusStore, StringKeyDataStore<ExchangeAnalyticsStatus> by dataStore {
|
||||
|
||||
override suspend fun getTransactionStatus(txId: String) = getSyncOrNull(txId)
|
||||
|
||||
override suspend fun setTransactionStatus(txId: String, status: ExchangeAnalyticsStatus) = store(txId, status)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.datasource.local.swaptx
|
||||
|
||||
/**
|
||||
* Runtime cache for storing swap transactions statuses sent to analytics
|
||||
*/
|
||||
interface SwapTransactionStatusStore {
|
||||
suspend fun getTransactionStatus(txId: String): ExchangeAnalyticsStatus?
|
||||
|
||||
suspend fun setTransactionStatus(txId: String, status: ExchangeAnalyticsStatus)
|
||||
}
|
||||
|
||||
enum class ExchangeAnalyticsStatus(val value: String) {
|
||||
InProgress("In Progress"),
|
||||
Done("Done"),
|
||||
Fail("Fail"),
|
||||
KYC("KYC"),
|
||||
Refunded("Refunded"),
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.datasource.local.token
|
||||
|
||||
import com.tangem.datasource.api.express.models.response.Asset
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
interface AssetsStore {
|
||||
|
||||
suspend fun getSyncOrNull(userWalletId: UserWalletId): List<Asset>?
|
||||
|
||||
suspend fun store(userWalletId: UserWalletId, item: List<Asset>)
|
||||
}
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
package com.tangem.datasource.local.token
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.datasource.api.express.models.response.Asset
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
internal class DefaultUserMarketCoinsStore(
|
||||
private val dataStore: StringKeyDataStore<CoinsResponse>,
|
||||
) : UserMarketCoinsStore {
|
||||
internal class DefaultAssetsStore(
|
||||
private val dataStore: StringKeyDataStore<List<Asset>>,
|
||||
) : AssetsStore {
|
||||
|
||||
override suspend fun getSyncOrNull(userWalletId: UserWalletId): CoinsResponse? {
|
||||
override suspend fun getSyncOrNull(userWalletId: UserWalletId): List<Asset>? {
|
||||
return dataStore.getSyncOrNull(userWalletId.stringValue)
|
||||
}
|
||||
|
||||
override suspend fun store(userWalletId: UserWalletId, item: CoinsResponse) {
|
||||
override suspend fun store(userWalletId: UserWalletId, item: List<Asset>) {
|
||||
dataStore.store(userWalletId.stringValue, item)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package com.tangem.datasource.local.token
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
interface UserMarketCoinsStore {
|
||||
|
||||
suspend fun getSyncOrNull(userWalletId: UserWalletId): CoinsResponse?
|
||||
|
||||
suspend fun store(userWalletId: UserWalletId, item: CoinsResponse)
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
<string name="common_save_changes">Änderungen speichern</string>
|
||||
<string name="common_send">Absenden</string>
|
||||
<string name="common_success">Erfolg</string>
|
||||
<string name="common_fee_label">Gebühr</string>
|
||||
<string name="details_manage_security_access_code">Zugangscode</string>
|
||||
<string name="details_manage_security_access_code_description">Sie müssen den richtigen Zugangscode eingeben, bevor Sie die Karte scannen.</string>
|
||||
<string name="details_manage_security_long_tap">Langes Tippen</string>
|
||||
|
|
@ -40,7 +41,6 @@
|
|||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_include_description">inkl. Gebühr</string>
|
||||
<string name="send_fee_label">Gebühr</string>
|
||||
<string name="send_fee_picker_low">Niedrig</string>
|
||||
<string name="send_fee_picker_normal">Normal</string>
|
||||
<string name="send_fee_picker_priority">Priorität</string>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
<string name="common_save_changes">Sauvegarder les modifications</string>
|
||||
<string name="common_send">Envoyer</string>
|
||||
<string name="common_success">Avec succès</string>
|
||||
<string name="common_fee_label">Commissions</string>
|
||||
<string name="details_manage_security_access_code">Code d\'accès</string>
|
||||
<string name="details_manage_security_access_code_description">Vous devrez entrer le mot de passe correct avant de scanner la carte</string>
|
||||
<string name="details_manage_security_long_tap">Tenez la carte fermement</string>
|
||||
|
|
@ -40,7 +41,6 @@
|
|||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_include_description">Inclure les commissions</string>
|
||||
<string name="send_fee_label">Commissions</string>
|
||||
<string name="send_fee_picker_low">Bas</string>
|
||||
<string name="send_fee_picker_normal">Normal</string>
|
||||
<string name="send_fee_picker_priority">Priorité</string>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
<string name="common_save_changes">Mantieni le modifiche</string>
|
||||
<string name="common_send">Invia</string>
|
||||
<string name="common_success">Con successo</string>
|
||||
<string name="common_fee_label">Commissione</string>
|
||||
<string name="details_manage_security_access_code">Codice di accesso</string>
|
||||
<string name="details_manage_security_access_code_description">Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto</string>
|
||||
<string name="details_manage_security_long_tap">Mantenimento della carta</string>
|
||||
|
|
@ -40,7 +41,6 @@
|
|||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_include_description">Includi commissione</string>
|
||||
<string name="send_fee_label">Commissione</string>
|
||||
<string name="send_fee_picker_low">Insufficiente</string>
|
||||
<string name="send_fee_picker_normal">Normale</string>
|
||||
<string name="send_fee_picker_priority">Prioritario</string>
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@
|
|||
<string name="common_approval">Одобрение</string>
|
||||
<string name="common_attention">Внимание</string>
|
||||
<string name="common_balance">Баланс: %s</string>
|
||||
<string name="common_balance_title">Баланс</string>
|
||||
<string name="common_biometric_authentication">биометрическую аутентификацию</string>
|
||||
<string name="common_biometrics">биометрией</string>
|
||||
<string name="common_buy">Купить</string>
|
||||
|
|
@ -85,9 +86,11 @@
|
|||
<string name="common_enabled">Включено</string>
|
||||
<string name="common_error">Ошибка</string>
|
||||
<string name="common_exchange">Обменять</string>
|
||||
<string name="common_explore_transaction_history">Посмотреть историю транзакций</string>
|
||||
<string name="common_explore">Обозреватель</string>
|
||||
<string name="common_explore_history">Посмотреть историю</string>
|
||||
<string name="common_explore_transaction_history">Посмотреть историю транзакций</string>
|
||||
<string name="common_explorer">Обозреватель</string>
|
||||
<string name="common_fee_label">Комиссия</string>
|
||||
<string name="common_fee_selector_footer">Сетевые комиссии за транзакции используются для поддержки безопасности сети, поощрения валидаторов, выделения ресурсов и определения приоритета транзакции.</string>
|
||||
<string name="common_fee_selector_option_custom">Свое</string>
|
||||
<string name="common_fee_selector_option_fast">Быстро</string>
|
||||
|
|
@ -191,9 +194,19 @@
|
|||
<string name="exchange_tokens_available_tokens_header">Мои токены</string>
|
||||
<string name="exchange_tokens_empty_tokens">У вас нет добавленных токенов. Добавьте токены для обмена</string>
|
||||
<string name="exchange_tokens_unavailable_tokens_header">Недоступен для обмена с %s</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_exchange_notification_verification_text">Посетите сайт провайдера для проверки</string>
|
||||
<string name="express_exchange_notification_verification_title">Провайдер запрашивает прохождение верификации</string>
|
||||
<string name="express_exchange_token_list_subtitle">Список токенов в вашем кошельке</string>
|
||||
<string name="express_fetch_best_rates">Получение наилучших курсов...</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="feedback_data_collection_message">Информация ниже не является обязательной. Вы можете стереть её, если хотите.</string>
|
||||
<string name="feedback_preface_rate_negative">Расскажите, каких функций вам не хватает, и мы постараемся вам помочь.</string>
|
||||
|
|
@ -267,7 +280,6 @@
|
|||
<string name="onboarding_access_codes_doesnt_match">Введенные коды доступа не совпадают</string>
|
||||
<string name="onboarding_alert_message_not_max_backup_cards_added">Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить?</string>
|
||||
<string name="onboarding_backup_exit_warning">Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас.</string>
|
||||
<string name="onboarding_balance_title">Баланс</string>
|
||||
<string name="onboarding_button_add_backup_card">Добавить резервную карту</string>
|
||||
<string name="onboarding_button_backup_card_format">Сканировать карту #%d</string>
|
||||
<string name="onboarding_button_backup_now">Создать резервную копию</string>
|
||||
|
|
@ -418,7 +430,6 @@
|
|||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_include_description">Включая комиссию</string>
|
||||
<string name="send_fee_label">Комиссия</string>
|
||||
<string name="send_fee_picker_low">Низкая</string>
|
||||
<string name="send_fee_picker_normal">Нормальная</string>
|
||||
<string name="send_fee_picker_priority">Приоритетная</string>
|
||||
|
|
@ -629,12 +640,18 @@
|
|||
<string name="warning_button_like_it">Нравится</string>
|
||||
<string name="warning_button_ok">Понятно!</string>
|
||||
<string name="warning_button_really_cool">Очень круто!</string>
|
||||
<string name="warning_button_refresh">Обновить</string>
|
||||
<string name="warning_demo_mode_message">Вы находитесь в режиме демо</string>
|
||||
<string name="warning_demo_mode_title">Демо режим включен</string>
|
||||
<string name="warning_developer_card_message">Отсканированная вами карта является картой разработчика. Не используйте ее для создания своего кошелька.</string>
|
||||
<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_no_exchangeable_coins_description">У вас в списке нет монет доступных для обмена с %s</string>
|
||||
<string name="warning_express_no_exchangeable_coins_title">Нет доступных токенов для обмена</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>
|
||||
<string name="warning_failed_to_verify_card_message">Возможно, данная карта - образец или подделка</string>
|
||||
<string name="warning_failed_to_verify_card_title">Ошибка проверки подлинности</string>
|
||||
<string name="warning_low_signatures_message">На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства.</string>
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@
|
|||
<string name="common_submit">提交</string>
|
||||
<string name="common_success">成功</string>
|
||||
<string name="common_swap">交換</string>
|
||||
<string name="common_fee_label">費用</string>
|
||||
<string name="common_terms_and_conditions">條款和條件</string>
|
||||
<string name="common_transactions">交易</string>
|
||||
<string name="common_understand">我了解</string>
|
||||
|
|
@ -178,7 +179,7 @@
|
|||
<string name="onboarding_access_codes_doesnt_match">輸入的訪問密碼與初始訪問密碼不匹配</string>
|
||||
<string name="onboarding_alert_message_not_max_backup_cards_added">您已添加一張備用卡。備份過程完成後,您將無法添加更多備份卡。如果您還有一張卡,請將其添加到備份中。您想繼續備份過程嗎?</string>
|
||||
<string name="onboarding_backup_exit_warning">備份過程已部分完成。你現在不能退出</string>
|
||||
<string name="onboarding_balance_title">餘額</string>
|
||||
<string name="common_balance_title">餘額</string>
|
||||
<string name="onboarding_button_add_backup_card">添加備用卡</string>
|
||||
<string name="onboarding_button_backup_card_format">掃描卡片 #%d</string>
|
||||
<string name="onboarding_button_backup_now">立即備份</string>
|
||||
|
|
@ -294,7 +295,6 @@
|
|||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_include_description">包含費用</string>
|
||||
<string name="send_fee_label">費用</string>
|
||||
<string name="send_fee_picker_low">低</string>
|
||||
<string name="send_fee_picker_normal">正常</string>
|
||||
<string name="send_fee_picker_priority">優先</string>
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@
|
|||
<string name="common_approval">Approval</string>
|
||||
<string name="common_attention">Attention</string>
|
||||
<string name="common_balance">Balance: %s</string>
|
||||
<string name="common_balance_title">Balance</string>
|
||||
<string name="common_biometric_authentication">biometric authentication</string>
|
||||
<string name="common_biometrics">biometrics</string>
|
||||
<string name="common_buy">Buy</string>
|
||||
|
|
@ -84,8 +85,10 @@
|
|||
<string name="common_error">Error</string>
|
||||
<string name="common_exchange">Exchange</string>
|
||||
<string name="common_explore">Explore</string>
|
||||
<string name="common_explore_history">Explore history</string>
|
||||
<string name="common_explore_transaction_history">Explore transaction history</string>
|
||||
<string name="common_explorer">Explorer</string>
|
||||
<string name="common_fee_label">Fee</string>
|
||||
<string name="common_fee_selector_footer">Network transaction fees are used to support network security, incentivize validators, allocate resources, and determine transaction priority.</string>
|
||||
<string name="common_fee_selector_option_custom">Custom</string>
|
||||
<string name="common_fee_selector_option_fast">Fast</string>
|
||||
|
|
@ -185,14 +188,46 @@
|
|||
<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_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 don\'t have any added tokens yet. Add tokens via Market to swap</string>
|
||||
<string name="exchange_tokens_unavailable_tokens_header">Unavailable for swap from %s</string>
|
||||
<string name="express_choose_providers_subtitle">Providers facilitate transactions, ensuring smooth and efficient token exchanges</string>
|
||||
<string name="express_choose_providers_title">Choose Provider</string>
|
||||
<string name="express_provider_best_rate">Best Rate</string>
|
||||
<string name="exchange_tokens_empty_tokens">You haven\'t added any tokens yet. Add tokens via Market to swap</string>
|
||||
<string name="exchange_tokens_unavailable_tokens_header">Cannot be swapped for %s</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_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_confirmed">Confirmed</string>
|
||||
<string name="express_exchange_status_confirming">Confirming</string>
|
||||
<string name="express_exchange_status_confirming_active">Confirming…</string>
|
||||
<string name="express_exchange_status_exchanged">Exchanged</string>
|
||||
<string name="express_exchange_status_exchanging">Exchanging</string>
|
||||
<string name="express_exchange_status_exchanging_active">Exchanging…</string>
|
||||
<string name="express_exchange_status_failed">Failed</string>
|
||||
<string name="express_exchange_status_received">Deposit received</string>
|
||||
<string name="express_exchange_status_receiving">Awaiting deposit</string>
|
||||
<string name="express_exchange_status_receiving_active">Awaiting deposit…</string>
|
||||
<string name="express_exchange_status_refunded">Refunded</string>
|
||||
<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_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_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="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>
|
||||
|
|
@ -267,7 +302,6 @@
|
|||
<string name="onboarding_access_codes_doesnt_match">Entered access code didn\'t match the initial access code</string>
|
||||
<string name="onboarding_alert_message_not_max_backup_cards_added">You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process?</string>
|
||||
<string name="onboarding_backup_exit_warning">The backup process is partly complete. You can\'t exit it now.</string>
|
||||
<string name="onboarding_balance_title">Balance</string>
|
||||
<string name="onboarding_button_add_backup_card">Add a backup card</string>
|
||||
<string name="onboarding_button_backup_card_format">Scan the card #%d</string>
|
||||
<string name="onboarding_button_backup_now">Backup now</string>
|
||||
|
|
@ -413,9 +447,7 @@
|
|||
<string name="send_extras_error_invalid_memo">Invalid Memo. It won\'t be added to the transaction.</string>
|
||||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_insufficient_funds">Insufficient funds for transfer</string>
|
||||
<string name="send_fee_include_description">Include fee</string>
|
||||
<string name="send_fee_label">Fee</string>
|
||||
<string name="send_fee_picker_low">Low</string>
|
||||
<string name="send_fee_picker_normal">Normal</string>
|
||||
<string name="send_fee_picker_priority">Priority</string>
|
||||
|
|
@ -490,7 +522,7 @@
|
|||
<string name="story_meet_title">Meet Tangem</string>
|
||||
<string name="story_web3_description">Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services</string>
|
||||
<string name="story_web3_title">Web 3.0 Compatible</string>
|
||||
<string name="swapping_approve_information_text">Approvals are considered an industry standard across all decentralized exchanges and protect your wallet from being accessed by a smart contract without your permission. By design, smart contracts can\'t access your tokens unless you approve access from your end. By \"unlocking\" your tokens, you are give permission to the 1inch smart contract to spend your assets. The miners of the network are compensated with a gas fee (paid by you) to record this action on the blockchain. Once permission has been granted you will be able to swap your token.</string>
|
||||
<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_generic_error">There was an error. Please try again.</string>
|
||||
|
|
@ -498,23 +530,25 @@
|
|||
<string name="swapping_high_price_impact">High price impact!</string>
|
||||
<string name="swapping_high_price_impact_description">Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome.</string>
|
||||
<string name="swapping_insufficient_funds">Insufficient funds</string>
|
||||
<string name="swapping_not_enough_funds_for_fee">Not enough funds for fee in your %1$s wallet to create a transaction. Top up your %2$s wallet first.</string>
|
||||
<string name="swapping_not_enough_funds_for_fee">Insufficient funds in your %1$s wallet to cover fees. Top up your %2$s wallet first.</string>
|
||||
<string name="swapping_pending_transaction_subtitle">Transaction in progress...</string>
|
||||
<string name="swapping_pending_transaction_title">Waiting</string>
|
||||
<string name="swapping_permission_buttons_approve">Approve</string>
|
||||
<string name="swapping_permission_current_transaction">Current transaction</string>
|
||||
<string name="swapping_permission_fee_footer">The token approval network fee will be charged to confirm that you are the one allowing your token to be used for the exchange.</string>
|
||||
<string name="swapping_permission_fee_footer">The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap.</string>
|
||||
<string name="swapping_permission_header">Give Permission</string>
|
||||
<string name="swapping_permission_policy_type_footer">Specify the approve limit for the selected token</string>
|
||||
<string name="swapping_permission_rows_amount">Amount %s</string>
|
||||
<string name="swapping_permission_rows_spender">Spender</string>
|
||||
<string name="swapping_permission_rows_your_wallet">Your Wallet</string>
|
||||
<string name="swapping_permission_subheader">To continue you need to allow 1inch smart contracts to use your %s</string>
|
||||
<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 of %s to</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_token_list_other_tokens">Other tokens</string>
|
||||
<string name="swapping_token_list_title">Choose token</string>
|
||||
|
|
@ -636,12 +670,20 @@
|
|||
<string name="warning_button_like_it">Like it</string>
|
||||
<string name="warning_button_ok">Ok, Got it!</string>
|
||||
<string name="warning_button_really_cool">Really cool!</string>
|
||||
<string name="warning_button_refresh">Refresh</string>
|
||||
<string name="warning_demo_mode_message">You are currently in the Demo mode</string>
|
||||
<string name="warning_demo_mode_title">Demo mode active</string>
|
||||
<string name="warning_developer_card_message">The card you scanned is a developer card. Do not use it to create your wallet.</string>
|
||||
<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_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_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>
|
||||
<string name="warning_failed_to_verify_card_title">Authenticity check failed</string>
|
||||
<string name="warning_low_signatures_message">Only %s signatures are left on this card. You must withdraw all of your funds.</string>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.SubcomposeLayout
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
|
|
@ -18,10 +17,12 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import com.tangem.core.ui.components.atoms.text.BoundCounter
|
||||
|
||||
/**
|
||||
* https://stackoverflow.com/questions/69083061/how-to-make-middle-ellipsis-in-text-with-jetpack-compose
|
||||
*/
|
||||
@Deprecated("Use EllipsisText with TextEllipsis.Middle ellipsis instead")
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun MiddleEllipsisText(
|
||||
|
|
@ -138,38 +139,4 @@ fun MiddleEllipsisText(
|
|||
|
||||
private const val ELLIPSIS_CHARACTERS_COUNT = 3
|
||||
private const val ELLIPSIS_CHARACTER = '.'
|
||||
private val ellipsisText = List(ELLIPSIS_CHARACTERS_COUNT) { ELLIPSIS_CHARACTER }.joinToString(separator = "")
|
||||
|
||||
private class BoundCounter(
|
||||
private val text: String,
|
||||
private val textLayoutResult: TextLayoutResult,
|
||||
private val charPosition: (Int) -> Int,
|
||||
) {
|
||||
var string = ""
|
||||
private set
|
||||
var width = 0f
|
||||
private set
|
||||
|
||||
private var _nextCharWidth: Float? = null
|
||||
private var invalidCharsCount = 0
|
||||
|
||||
fun widthWithNextChar(): Float = width + nextCharWidth()
|
||||
|
||||
private fun nextCharWidth(): Float = _nextCharWidth ?: run {
|
||||
var boundingBox: Rect
|
||||
// invalidCharsCount fixes this bug: https://issuetracker.google.com/issues/197146630
|
||||
invalidCharsCount--
|
||||
do {
|
||||
boundingBox = textLayoutResult
|
||||
.getBoundingBox(charPosition(string.count() + ++invalidCharsCount))
|
||||
} while (boundingBox.right == 0f)
|
||||
_nextCharWidth = boundingBox.width
|
||||
boundingBox.width
|
||||
}
|
||||
|
||||
fun addNextChar() {
|
||||
string += text[charPosition(string.count())]
|
||||
width += nextCharWidth()
|
||||
_nextCharWidth = null
|
||||
}
|
||||
}
|
||||
private val ellipsisText = List(ELLIPSIS_CHARACTERS_COUNT) { ELLIPSIS_CHARACTER }.joinToString(separator = "")
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
package com.tangem.core.ui.components
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Screen for showing result
|
||||
*
|
||||
* @param resultMessage message to show
|
||||
* @param title title to show
|
||||
* @param resultColor color which will tint the round icon of the result
|
||||
* @param icon icon to show in the middle of the round icon
|
||||
* @param secondaryButtonIcon icon to show in the secondary button
|
||||
* @param secondaryButtonText label of the secondary button
|
||||
* @param onSecondaryButtonClick action on clicking secondary button
|
||||
* @param onButtonClick action on clicking "Done" button
|
||||
*
|
||||
* @see <a href =
|
||||
* "https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?node-id=1123%3A3863&t=wwR84h5IsMaMsDhq-1"
|
||||
* >Figma component</a>
|
||||
*/
|
||||
@Composable
|
||||
fun ResultScreenContent(
|
||||
resultMessage: AnnotatedString,
|
||||
onButtonClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@StringRes title: Int = R.string.common_success,
|
||||
resultColor: Color = TangemTheme.colors.icon.accent,
|
||||
@DrawableRes icon: Int = R.drawable.ic_check_24,
|
||||
@DrawableRes secondaryButtonIcon: Int? = null,
|
||||
@StringRes secondaryButtonText: Int? = null,
|
||||
onSecondaryButtonClick: (() -> Unit)? = null,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.background(TangemTheme.colors.background.secondary)
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing32,
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
SpacerHHalf()
|
||||
SuccessImage(resultColor = resultColor, icon = icon)
|
||||
SpacerH50()
|
||||
Text(
|
||||
text = stringResource(id = title),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
SpacerH12()
|
||||
Text(
|
||||
text = resultMessage,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
SpacerHHalf()
|
||||
if (onSecondaryButtonClick != null && secondaryButtonText != null) {
|
||||
SecondaryButtonForResultScreen(
|
||||
secondaryButtonText = secondaryButtonText,
|
||||
secondaryButtonIcon = secondaryButtonIcon,
|
||||
onSecondaryButtonClick = onSecondaryButtonClick,
|
||||
)
|
||||
SpacerH12()
|
||||
}
|
||||
PrimaryButton(
|
||||
text = stringResource(id = R.string.common_close),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
onClick = { onButtonClick() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SuccessImage(resultColor: Color, @DrawableRes icon: Int) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = resultColor.copy(alpha = 0.2f),
|
||||
shape = CircleShape,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(TangemTheme.dimens.spacing24)
|
||||
.background(
|
||||
color = resultColor,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.height(TangemTheme.dimens.size93)
|
||||
.width(TangemTheme.dimens.size93),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = icon),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.primary2,
|
||||
modifier = Modifier.size(TangemTheme.dimens.size40),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SecondaryButtonForResultScreen(
|
||||
@StringRes secondaryButtonText: Int,
|
||||
onSecondaryButtonClick: () -> Unit,
|
||||
@DrawableRes secondaryButtonIcon: Int? = null,
|
||||
) {
|
||||
if (secondaryButtonIcon != null) {
|
||||
SecondaryButtonIconStart(
|
||||
text = stringResource(id = secondaryButtonText),
|
||||
iconResId = secondaryButtonIcon,
|
||||
onClick = onSecondaryButtonClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
} else {
|
||||
SecondaryButton(
|
||||
text = stringResource(id = secondaryButtonText),
|
||||
onClick = onSecondaryButtonClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region preview
|
||||
|
||||
@Composable
|
||||
private fun SuccessScreenPreview() {
|
||||
ResultScreenContent(
|
||||
resultMessage = AnnotatedString("Swap of 1 000 DAI to 1 131,46 MATIC"),
|
||||
secondaryButtonText = R.string.swapping_success_view_explorer_button_title,
|
||||
onSecondaryButtonClick = {},
|
||||
onButtonClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun Preview_SuccessScreenContent_InLightTheme() {
|
||||
TangemTheme(isDark = false) {
|
||||
SuccessScreenPreview()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun Preview_SuccessScreenContent_InDarkTheme() {
|
||||
TangemTheme(isDark = true) {
|
||||
SuccessScreenPreview()
|
||||
}
|
||||
}
|
||||
|
||||
// endregion preview
|
||||
|
|
@ -3,14 +3,13 @@ package com.tangem.core.ui.components.appbar
|
|||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
|
|
@ -49,7 +48,11 @@ fun AppBarWithBackButton(
|
|||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(size = TangemTheme.dimens.size24)
|
||||
.clickable { onBackClick() },
|
||||
.clickable(
|
||||
indication = rememberRipple(bounded = false),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onBackClick,
|
||||
),
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
)
|
||||
if (!text.isNullOrBlank()) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.core.ui.components.atoms.text
|
||||
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
|
||||
internal class BoundCounter(
|
||||
private val text: String,
|
||||
private val textLayoutResult: TextLayoutResult,
|
||||
private val charPosition: (Int) -> Int,
|
||||
) {
|
||||
var string = ""
|
||||
private set
|
||||
var width = 0f
|
||||
private set
|
||||
|
||||
private var _nextCharWidth: Float? = null
|
||||
private var invalidCharsCount = 0
|
||||
|
||||
fun widthWithNextChar(): Float = width + nextCharWidth()
|
||||
|
||||
private fun nextCharWidth(): Float = _nextCharWidth ?: run {
|
||||
var boundingBox: Rect
|
||||
// invalidCharsCount fixes this bug: https://issuetracker.google.com/issues/197146630
|
||||
invalidCharsCount--
|
||||
do {
|
||||
boundingBox = textLayoutResult
|
||||
.getBoundingBox(charPosition(string.count() + ++invalidCharsCount))
|
||||
} while (boundingBox.right == 0f)
|
||||
_nextCharWidth = boundingBox.width
|
||||
boundingBox.width
|
||||
}
|
||||
|
||||
fun addNextChar() {
|
||||
string += text[charPosition(string.count())]
|
||||
width += nextCharWidth()
|
||||
_nextCharWidth = null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
package com.tangem.core.ui.components.atoms.text
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.SubcomposeLayout
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
sealed class TextEllipsis {
|
||||
|
||||
object Middle : TextEllipsis()
|
||||
|
||||
object End : TextEllipsis()
|
||||
|
||||
data class OffsetEnd(
|
||||
val offsetEnd: Int = 0,
|
||||
val hasSeparator: Boolean = true,
|
||||
) : TextEllipsis()
|
||||
}
|
||||
|
||||
/**
|
||||
* https://stackoverflow.com/questions/69083061/how-to-make-middle-ellipsis-in-text-with-jetpack-compose
|
||||
*
|
||||
* Customized Text with ellipsis. Ellipsis can be placed in: Middle, End or OffsetEnd (OffsetEnd with separator).
|
||||
*
|
||||
* * OffsetEnd can be useful to display big amounts with currency symbol. OffsetEnd 0 is equal to End.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun EllipsisText(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = Color.Unspecified,
|
||||
fontSize: TextUnit = TextUnit.Unspecified,
|
||||
fontStyle: FontStyle? = null,
|
||||
fontWeight: FontWeight? = null,
|
||||
fontFamily: FontFamily? = null,
|
||||
letterSpacing: TextUnit = TextUnit.Unspecified,
|
||||
textDecoration: TextDecoration? = null,
|
||||
textAlign: TextAlign? = null,
|
||||
lineHeight: TextUnit = TextUnit.Unspecified,
|
||||
softWrap: Boolean = true,
|
||||
onTextLayout: (TextLayoutResult) -> Unit = {},
|
||||
style: TextStyle = LocalTextStyle.current,
|
||||
ellipsis: TextEllipsis = TextEllipsis.End,
|
||||
) {
|
||||
val ellipsisText = remember(text) {
|
||||
if (ellipsis is TextEllipsis.OffsetEnd && ellipsis.hasSeparator) {
|
||||
ELLIPSIS_TEXT_WITH_SEPARATOR
|
||||
} else {
|
||||
ELLIPSIS_TEXT
|
||||
}
|
||||
}
|
||||
|
||||
// some letters, like "r", will have less width when placed right before "."
|
||||
// adding a space to prevent such case
|
||||
val layoutText = remember(text) { "$text $ellipsisText" }
|
||||
val textLayoutResultState = remember(layoutText) {
|
||||
mutableStateOf<TextLayoutResult?>(null)
|
||||
}
|
||||
SubcomposeLayout(modifier) { constraints ->
|
||||
// result is ignored - we only need to fill our textLayoutResult
|
||||
subcompose("measure") {
|
||||
Text(
|
||||
text = layoutText,
|
||||
color = color,
|
||||
fontSize = fontSize,
|
||||
fontStyle = fontStyle,
|
||||
fontWeight = fontWeight,
|
||||
fontFamily = fontFamily,
|
||||
letterSpacing = letterSpacing,
|
||||
textDecoration = textDecoration,
|
||||
textAlign = textAlign,
|
||||
lineHeight = lineHeight,
|
||||
softWrap = softWrap,
|
||||
maxLines = 1,
|
||||
onTextLayout = { textLayoutResultState.value = it },
|
||||
style = style,
|
||||
)
|
||||
}.first().measure(Constraints())
|
||||
// to allow smart cast
|
||||
val textLayoutResult = textLayoutResultState.value
|
||||
?: // shouldn't happen - onTextLayout is called before subcompose finishes
|
||||
return@SubcomposeLayout layout(0, 0) {}
|
||||
val placeable = subcompose("visible") {
|
||||
val finalText = remember(text, textLayoutResult, constraints.maxWidth) {
|
||||
if (
|
||||
text.isEmpty() ||
|
||||
textLayoutResult.getBoundingBox(text.indices.last).right <= constraints.maxWidth
|
||||
) {
|
||||
// text not including ellipsis fits on the first line.
|
||||
return@remember text
|
||||
}
|
||||
|
||||
var ellipsisWidth = 0f
|
||||
layoutText.indices.toList()
|
||||
.takeLast(ellipsisText.length)
|
||||
.forEach widthLet@{
|
||||
ellipsisWidth += textLayoutResult.getBoundingBox(it).width
|
||||
}
|
||||
|
||||
val availableWidth = constraints.maxWidth - ellipsisWidth
|
||||
val startCounter = BoundCounter(text, textLayoutResult) { it }
|
||||
val endCounter = BoundCounter(text, textLayoutResult) { text.indices.last - it }
|
||||
|
||||
when (ellipsis) {
|
||||
TextEllipsis.Middle -> {
|
||||
middleEllipsisText(
|
||||
availableWidth,
|
||||
startCounter,
|
||||
endCounter,
|
||||
)
|
||||
}
|
||||
TextEllipsis.End -> {
|
||||
offsetEndEllipsisText(
|
||||
availableWidth = availableWidth,
|
||||
startCounter = startCounter,
|
||||
endCounter = endCounter,
|
||||
)
|
||||
}
|
||||
is TextEllipsis.OffsetEnd -> {
|
||||
offsetEndEllipsisText(
|
||||
availableWidth = availableWidth,
|
||||
startCounter = startCounter,
|
||||
endCounter = endCounter,
|
||||
offsetEnd = ellipsis.offsetEnd,
|
||||
withSeparator = ellipsis.hasSeparator,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = finalText,
|
||||
color = color,
|
||||
fontSize = fontSize,
|
||||
fontStyle = fontStyle,
|
||||
fontWeight = fontWeight,
|
||||
fontFamily = fontFamily,
|
||||
letterSpacing = letterSpacing,
|
||||
textDecoration = textDecoration,
|
||||
textAlign = textAlign,
|
||||
lineHeight = lineHeight,
|
||||
softWrap = softWrap,
|
||||
onTextLayout = onTextLayout,
|
||||
style = style,
|
||||
)
|
||||
}[0].measure(constraints)
|
||||
layout(placeable.width, placeable.height) {
|
||||
placeable.place(0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val ELLIPSIS_SEPARATOR = " "
|
||||
private const val ELLIPSIS_TEXT = "..."
|
||||
private const val ELLIPSIS_TEXT_WITH_SEPARATOR = ELLIPSIS_TEXT.plus(ELLIPSIS_SEPARATOR)
|
||||
|
||||
private fun middleEllipsisText(availableWidth: Float, startCounter: BoundCounter, endCounter: BoundCounter): String {
|
||||
while (availableWidth - startCounter.width - endCounter.width > 0) {
|
||||
val possibleEndWidth = endCounter.widthWithNextChar()
|
||||
if (
|
||||
startCounter.width >= possibleEndWidth &&
|
||||
availableWidth - startCounter.width - possibleEndWidth >= 0
|
||||
) {
|
||||
endCounter.addNextChar()
|
||||
} else if (availableWidth - startCounter.widthWithNextChar() - endCounter.width >= 0) {
|
||||
startCounter.addNextChar()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return startCounter.string.trimEnd() + ELLIPSIS_TEXT + endCounter.string.reversed().trimStart()
|
||||
}
|
||||
|
||||
private fun offsetEndEllipsisText(
|
||||
availableWidth: Float,
|
||||
startCounter: BoundCounter,
|
||||
endCounter: BoundCounter,
|
||||
offsetEnd: Int = 0,
|
||||
withSeparator: Boolean = false,
|
||||
): String {
|
||||
while (availableWidth - startCounter.width - endCounter.width > 0) {
|
||||
val possibleEndWidth = endCounter.widthWithNextChar()
|
||||
if (
|
||||
offsetEnd > endCounter.string.length &&
|
||||
availableWidth - startCounter.width - possibleEndWidth >= 0
|
||||
) {
|
||||
endCounter.addNextChar()
|
||||
} else if (availableWidth - startCounter.widthWithNextChar() - endCounter.width >= 0) {
|
||||
startCounter.addNextChar()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
val ellipsis = if (withSeparator) ELLIPSIS_TEXT_WITH_SEPARATOR else ELLIPSIS_TEXT
|
||||
|
||||
return startCounter.string.trimEnd() + ellipsis + endCounter.string.reversed().trimStart()
|
||||
}
|
||||
|
||||
//region Preview
|
||||
@Preview(widthDp = 200)
|
||||
@Composable
|
||||
private fun EllipsisTexPreview(@PreviewParameter(EllipsisTexPreviewParameterProvider::class) ellipsis: TextEllipsis) {
|
||||
TangemTheme {
|
||||
EllipsisText(
|
||||
text = "11111111111111111111111111111111111111111111111111 END",
|
||||
ellipsis = ellipsis,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class EllipsisTexPreviewParameterProvider : PreviewParameterProvider<TextEllipsis> {
|
||||
override val values: Sequence<TextEllipsis>
|
||||
get() = sequenceOf(
|
||||
TextEllipsis.Middle,
|
||||
TextEllipsis.End,
|
||||
TextEllipsis.OffsetEnd("TEXT".length),
|
||||
TextEllipsis.OffsetEnd("TEXT".length, false),
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
|
|
@ -33,7 +33,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
|
|||
sheetState = sheetState,
|
||||
containerColor = contentColor,
|
||||
shape = TangemTheme.shapes.bottomSheetLarge,
|
||||
dragHandle = { TangemBottomSheetDraggableHeader() },
|
||||
dragHandle = { TangemBottomSheetDraggableHeader(contentColor) },
|
||||
) {
|
||||
content(config.content)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,162 @@
|
|||
package com.tangem.core.ui.components.inputrow
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.components.atoms.text.TextEllipsis
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* [Input Row Approx](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2207-810&mode=design&t=fM1ZU6zQF6g3CaTv-4)
|
||||
*
|
||||
* @param leftIcon left token state
|
||||
* @param leftTitle left token title
|
||||
* @param leftSubtitle left token subtitle
|
||||
* @param rightIcon right token state
|
||||
* @param rightTitle right token title
|
||||
* @param rightSubtitle right token subtitle
|
||||
* @param modifier composable modifier
|
||||
* @param showDivider show divider
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun InputRowApprox(
|
||||
leftIcon: TokenIconState,
|
||||
leftTitle: TextReference,
|
||||
leftSubtitle: TextReference,
|
||||
rightIcon: TokenIconState,
|
||||
rightTitle: TextReference,
|
||||
rightSubtitle: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
leftTitleEllipsisOffset: Int = 0,
|
||||
rightTitleEllipsisOffset: Int = 0,
|
||||
showDivider: Boolean = false,
|
||||
) {
|
||||
DividerContainer(
|
||||
showDivider = showDivider,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(TangemTheme.dimens.spacing12)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
InputRowApproxItem(
|
||||
iconState = leftIcon,
|
||||
title = leftTitle,
|
||||
subtitle = leftSubtitle,
|
||||
titleEllipsisOffset = leftTitleEllipsisOffset,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_approx_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing4,
|
||||
vertical = TangemTheme.dimens.spacing10,
|
||||
),
|
||||
)
|
||||
InputRowApproxItem(
|
||||
iconState = rightIcon,
|
||||
title = rightTitle,
|
||||
subtitle = rightSubtitle,
|
||||
titleEllipsisOffset = rightTitleEllipsisOffset,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InputRowApproxItem(
|
||||
iconState: TokenIconState,
|
||||
title: TextReference,
|
||||
subtitle: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
titleEllipsisOffset: Int = 0,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
) {
|
||||
TokenIcon(
|
||||
state = iconState,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size36),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
) {
|
||||
EllipsisText(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
ellipsis = TextEllipsis.OffsetEnd(titleEllipsisOffset),
|
||||
)
|
||||
EllipsisText(
|
||||
text = subtitle.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing2),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//region Preview
|
||||
@Preview
|
||||
@Composable
|
||||
private fun InputRowApproxPreview_Light() {
|
||||
TangemTheme {
|
||||
InputRowApprox(
|
||||
leftIcon = TokenIconState.Loading,
|
||||
leftTitle = TextReference.Str("Left title USD"),
|
||||
leftSubtitle = TextReference.Str("Left subtitle USD"),
|
||||
leftTitleEllipsisOffset = 3,
|
||||
rightIcon = TokenIconState.Loading,
|
||||
rightTitle = TextReference.Str("Right title Right title Right title Right title Right title USD"),
|
||||
rightSubtitle = TextReference.Str("Right subtitle Right subtitle Right subtitle USD"),
|
||||
rightTitleEllipsisOffset = 3,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun InputRowApproxPreview_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
InputRowApprox(
|
||||
leftIcon = TokenIconState.Loading,
|
||||
leftTitle = TextReference.Str("Left title Left title Left title Left title Left title USD"),
|
||||
leftSubtitle = TextReference.Str("Left subtitle Left subtitle Left subtitle USD"),
|
||||
leftTitleEllipsisOffset = 3,
|
||||
rightIcon = TokenIconState.Loading,
|
||||
rightTitle = TextReference.Str("Right title USD"),
|
||||
rightSubtitle = TextReference.Str("Right subtitle USD"),
|
||||
rightTitleEllipsisOffset = 3,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
package com.tangem.core.ui.components.inputrow
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* [Input Row Best Rate](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-889&mode=dev)
|
||||
*
|
||||
* @param imageUrl image source url
|
||||
* @param title title
|
||||
* @param titleExtra title extra
|
||||
* @param subtitle subtitle
|
||||
* @param modifier composable modifier
|
||||
* @param showTag show tag
|
||||
* @param showDivider show divider
|
||||
* @param onIconClick icon click
|
||||
*/
|
||||
@Composable
|
||||
fun InputRowBestRate(
|
||||
imageUrl: String,
|
||||
title: TextReference,
|
||||
titleExtra: TextReference,
|
||||
subtitle: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
showTag: Boolean = false,
|
||||
showDivider: Boolean = false,
|
||||
onIconClick: (() -> Unit)? = null,
|
||||
) {
|
||||
DividerContainer(
|
||||
showDivider = showDivider,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
InnerIcon(imageUrl = imageUrl)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
InnerTitle(
|
||||
title = title,
|
||||
titleExtra = titleExtra,
|
||||
showTag = showTag,
|
||||
)
|
||||
Text(
|
||||
text = subtitle.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
|
||||
)
|
||||
}
|
||||
SpacerWMax()
|
||||
onIconClick?.let {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_chevron_right_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing10)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false),
|
||||
onClick = onIconClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InnerTitle(title: TextReference, titleExtra: TextReference, showTag: Boolean = false) {
|
||||
Row {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
text = titleExtra.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing4),
|
||||
)
|
||||
if (showTag) {
|
||||
Text(
|
||||
text = stringResource(R.string.express_provider_best_rate),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.icon.accent,
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing4)
|
||||
.background(
|
||||
color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius20),
|
||||
)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing6),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InnerIcon(imageUrl: String) {
|
||||
SubcomposeAsyncImage(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size40),
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(imageUrl)
|
||||
.crossfade(enable = true)
|
||||
.allowHardware(enable = false)
|
||||
.build(),
|
||||
loading = { LoadingIcon() },
|
||||
error = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.tertiary,
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
//region preview
|
||||
@Preview
|
||||
@Composable
|
||||
private fun InputRowBestRatePreview_Light(
|
||||
@PreviewParameter(InputRowBestRatePreviewDataProvider::class) data: InputRowBestRatePreviewData,
|
||||
) {
|
||||
TangemTheme {
|
||||
InputRowBestRate(
|
||||
imageUrl = "",
|
||||
title = data.title,
|
||||
titleExtra = data.titleExtra,
|
||||
subtitle = data.subtitle,
|
||||
showTag = data.showTag,
|
||||
onIconClick = data.iconClick,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun InputRowBestRatePreview_Dark(
|
||||
@PreviewParameter(InputRowBestRatePreviewDataProvider::class) data: InputRowBestRatePreviewData,
|
||||
) {
|
||||
TangemTheme(isDark = true) {
|
||||
InputRowBestRate(
|
||||
imageUrl = "",
|
||||
title = data.title,
|
||||
titleExtra = data.titleExtra,
|
||||
subtitle = data.subtitle,
|
||||
showTag = data.showTag,
|
||||
onIconClick = data.iconClick,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class InputRowBestRatePreviewData(
|
||||
val title: TextReference,
|
||||
val titleExtra: TextReference,
|
||||
val showTag: Boolean,
|
||||
val subtitle: TextReference,
|
||||
val iconClick: (() -> Unit)?,
|
||||
)
|
||||
|
||||
private class InputRowBestRatePreviewDataProvider : PreviewParameterProvider<InputRowBestRatePreviewData> {
|
||||
override val values: Sequence<InputRowBestRatePreviewData>
|
||||
get() = sequenceOf(
|
||||
InputRowBestRatePreviewData(
|
||||
title = TextReference.Str("1inch"),
|
||||
titleExtra = TextReference.Str("DEX"),
|
||||
subtitle = TextReference.Str("0,64554846 DAI ≈ 1 MATIC "),
|
||||
showTag = true,
|
||||
iconClick = {},
|
||||
),
|
||||
InputRowBestRatePreviewData(
|
||||
title = TextReference.Str("ChangeNow"),
|
||||
titleExtra = TextReference.Str("CEX"),
|
||||
subtitle = TextReference.Str("0,64554846 DAI ≈ 1 MATIC "),
|
||||
showTag = false,
|
||||
iconClick = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
|
|
@ -8,7 +8,9 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -42,8 +44,18 @@ import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsSta
|
|||
* >Figma component</a>
|
||||
*/
|
||||
@Composable
|
||||
fun Notification(config: NotificationConfig, modifier: Modifier = Modifier, iconTint: Color? = null) {
|
||||
BaseContainer(buttonsState = config.buttonsState, onClick = config.onClick, modifier = modifier) {
|
||||
fun Notification(
|
||||
config: NotificationConfig,
|
||||
modifier: Modifier = Modifier,
|
||||
containerColor: Color? = null,
|
||||
iconTint: Color? = null,
|
||||
) {
|
||||
BaseContainer(
|
||||
buttonsState = config.buttonsState,
|
||||
onClick = config.onClick,
|
||||
modifier = modifier,
|
||||
containerColor = containerColor,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.spacing12),
|
||||
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12),
|
||||
|
|
@ -71,9 +83,10 @@ private fun BaseContainer(
|
|||
buttonsState: NotificationConfig.ButtonsState?,
|
||||
onClick: (() -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
containerColor: Color? = null,
|
||||
content: @Composable BoxScope.() -> Unit,
|
||||
) {
|
||||
val containerColor by rememberUpdatedState(
|
||||
val tempContainerColor by rememberUpdatedState(
|
||||
newValue = if (buttonsState != null || onClick != null) {
|
||||
TangemTheme.colors.background.primary
|
||||
} else {
|
||||
|
|
@ -88,7 +101,7 @@ private fun BaseContainer(
|
|||
.fillMaxWidth(),
|
||||
enabled = onClick != null,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
color = containerColor,
|
||||
color = containerColor ?: tempContainerColor,
|
||||
) {
|
||||
Box(content = content)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
package com.tangem.core.ui.components.rows
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH28
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Simple clickable action row, without input and icon
|
||||
*
|
||||
* https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=Ygv5sohTTHYAQcBS-4
|
||||
*/
|
||||
@Composable
|
||||
fun SimpleActionRow(title: String, description: String, modifier: Modifier = Modifier, isClickable: Boolean = true) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(color = TangemTheme.colors.background.action)
|
||||
.height(TangemTheme.dimens.size44)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(end = TangemTheme.dimens.spacing48)
|
||||
.align(Alignment.CenterStart),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
AnimatedContent(targetState = title, label = "") {
|
||||
Text(
|
||||
text = it,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
AnimatedContent(targetState = description, label = "") {
|
||||
Text(
|
||||
text = it,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isClickable) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_chevron_right_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(alignment = Alignment.CenterEnd)
|
||||
.padding(end = TangemTheme.dimens.spacing12),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SimpleActionRowPreview() {
|
||||
Column {
|
||||
TangemTheme(isDark = false) {
|
||||
SimpleActionRow(
|
||||
title = "Title",
|
||||
description = "Description",
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH28()
|
||||
|
||||
TangemTheme(isDark = false) {
|
||||
SimpleActionRow(
|
||||
title = "Title",
|
||||
description = "Description",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
package com.tangem.core.ui.components.rows
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
fun SelectorRowItem(
|
||||
@StringRes titleRes: Int,
|
||||
@DrawableRes iconRes: Int,
|
||||
onSelect: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
preEllipsize: TextReference? = null,
|
||||
postEllipsize: TextReference? = null,
|
||||
isSelected: Boolean = false,
|
||||
showDivider: Boolean = true,
|
||||
) {
|
||||
val iconTint by animateColorAsState(
|
||||
targetValue = if (isSelected) {
|
||||
TangemTheme.colors.icon.accent
|
||||
} else {
|
||||
TangemTheme.colors.icon.informative
|
||||
},
|
||||
label = "Selector icon tint change",
|
||||
)
|
||||
|
||||
val textStyle = if (isSelected) {
|
||||
TangemTheme.typography.subtitle2
|
||||
} else {
|
||||
TangemTheme.typography.body2
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onSelect() },
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Icon(
|
||||
painter = painterResource(iconRes),
|
||||
tint = iconTint,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(titleRes),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing8,
|
||||
top = TangemTheme.dimens.spacing14,
|
||||
bottom = TangemTheme.dimens.spacing14,
|
||||
),
|
||||
)
|
||||
if (preEllipsize != null && postEllipsize != null) {
|
||||
SelectorValueContent(
|
||||
amount = preEllipsize,
|
||||
symbol = postEllipsize,
|
||||
textStyle = textStyle,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (showDivider) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size1)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12)
|
||||
.background(TangemTheme.colors.stroke.primary)
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.SelectorValueContent(amount: TextReference, symbol: TextReference, textStyle: TextStyle) {
|
||||
Text(
|
||||
text = amount.resolveReference(),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.End,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing4,
|
||||
top = TangemTheme.dimens.spacing14,
|
||||
bottom = TangemTheme.dimens.spacing14,
|
||||
),
|
||||
)
|
||||
Text(
|
||||
text = symbol.resolveReference(),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing1,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
top = TangemTheme.dimens.spacing14,
|
||||
bottom = TangemTheme.dimens.spacing14,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SelectorRowItemPreview_Light() {
|
||||
TangemTheme {
|
||||
SelectorRowItem(
|
||||
titleRes = R.string.common_fee_selector_option_slow,
|
||||
iconRes = R.drawable.ic_tortoise_24,
|
||||
preEllipsize = TextReference.Str("1000"),
|
||||
postEllipsize = TextReference.Str("$"),
|
||||
isSelected = true,
|
||||
onSelect = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SelectorRowItemPreview_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
SelectorRowItem(
|
||||
titleRes = R.string.common_fee_selector_option_slow,
|
||||
iconRes = R.drawable.ic_tortoise_24,
|
||||
preEllipsize = TextReference.Str("1000"),
|
||||
postEllipsize = TextReference.Str("$"),
|
||||
isSelected = true,
|
||||
onSelect = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -104,6 +104,16 @@ fun combinedReference(refs: WrappedList<TextReference>): TextReference {
|
|||
return TextReference.Combined(refs)
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines multiple [TextReference] instances into a single [TextReference].
|
||||
*
|
||||
* @param refs Vararg of [TextReference] instances to be combined.
|
||||
* @return A [TextReference] representing the combined text references.
|
||||
*/
|
||||
fun combinedReference(vararg refs: TextReference): TextReference {
|
||||
return TextReference.Combined(WrappedList(listOf(*refs)))
|
||||
}
|
||||
|
||||
/** Resolve [TextReference] as [String] */
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ data class TangemDimens internal constructor(
|
|||
val size0: Dp = 0.dp,
|
||||
val size0_5: Dp = 0.5.dp,
|
||||
val size1: Dp = 1.dp,
|
||||
val size1_5: Dp = 1.5.dp,
|
||||
val size2: Dp = 2.dp,
|
||||
val size4: Dp = 4.dp,
|
||||
val size5: Dp = 5.dp,
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ private fun darkThemeColors(): TangemColors {
|
|||
),
|
||||
button = TangemColors.Button(
|
||||
primary = TangemColorPalette.Light4,
|
||||
secondary = TangemColorPalette.Dark5,
|
||||
secondary = TangemColorPalette.Dark4,
|
||||
disabled = TangemColorPalette.Dark5,
|
||||
positiveDisabled = TangemColorPalette.DarkGreen,
|
||||
),
|
||||
|
|
|
|||
10
core/ui/src/main/res/drawable/ic_alert_triangle_20.xml
Normal file
10
core/ui/src/main/res/drawable/ic_alert_triangle_20.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M9.135,2.328L1.702,15.165C1.316,15.832 1.797,16.667 2.568,16.667H17.432C18.202,16.667 18.684,15.832 18.298,15.165L10.865,2.328C10.48,1.663 9.52,1.663 9.135,2.328ZM10.833,11.667H9.167V6.667H10.833V11.667ZM10.833,15H9.167V13.333H10.833V15Z"
|
||||
android:fillColor="#FFB71B"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
9
core/ui/src/main/res/drawable/ic_approx_24.xml
Normal file
9
core/ui/src/main/res/drawable/ic_approx_24.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M14.153,11.13C12.544,11.13 11.142,9.695 9.731,9.695C8.625,9.695 7.825,10.153 7.25,10.798V9.076C7.816,8.457 8.652,8 9.847,8C11.456,8 12.867,9.435 14.269,9.435C15.366,9.435 16.175,8.978 16.75,8.332V10.054C16.184,10.673 15.348,11.13 14.153,11.13ZM14.153,16C12.544,16 11.142,14.565 9.731,14.565C8.625,14.565 7.825,15.022 7.25,15.668V13.946C7.816,13.327 8.652,12.87 9.847,12.87C11.456,12.87 12.867,14.305 14.269,14.305C15.366,14.305 16.175,13.856 16.75,13.202V14.924C16.184,15.543 15.348,16 14.153,16Z"
|
||||
android:fillColor="#909090"/>
|
||||
</vector>
|
||||
12
core/ui/src/main/res/drawable/ic_exclamation_24.xml
Normal file
12
core/ui/src/main/res/drawable/ic_exclamation_24.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M11,13H13V7H11V13Z"
|
||||
android:fillColor="#FFB71B"/>
|
||||
<path
|
||||
android:pathData="M11,17H13V15H11V17Z"
|
||||
android:fillColor="#FFB71B"/>
|
||||
</vector>
|
||||
9
core/ui/src/main/res/drawable/ic_forward_24.xml
Normal file
9
core/ui/src/main/res/drawable/ic_forward_24.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M4.16,11V13H16.16L10.66,18.5L12.08,19.92L20,12L12.08,4.08L10.66,5.5L16.16,11H4.16Z"
|
||||
android:fillColor="#1E1E1E"/>
|
||||
</vector>
|
||||
10
core/ui/src/main/res/drawable/ic_no_token_44.xml
Normal file
10
core/ui/src/main/res/drawable/ic_no_token_44.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="44dp"
|
||||
android:height="44dp"
|
||||
android:viewportWidth="44"
|
||||
android:viewportHeight="44">
|
||||
<path
|
||||
android:pathData="M6.31,26.499C6.508,25.666 6.773,24.813 7.102,23.949C8.493,26.484 10.368,29 12.684,31.316C15.001,33.632 17.516,35.507 20.051,36.898C19.187,37.227 18.334,37.492 17.501,37.69C13.387,38.669 10.043,37.991 8.026,35.974C6.009,33.957 5.331,30.613 6.31,26.499ZM6.862,37.139C3.507,33.784 3.448,27.963 6.13,22C3.448,16.037 3.507,10.216 6.862,6.862C10.216,3.507 16.037,3.448 22,6.13C27.963,3.448 33.784,3.507 37.139,6.862C40.493,10.216 40.552,16.037 37.87,22C40.553,27.963 40.493,33.784 37.139,37.139C33.784,40.493 27.963,40.552 22,37.87C16.037,40.552 10.216,40.493 6.862,37.139ZM13.849,30.152C11.273,27.576 9.292,24.765 7.948,22C9.292,19.235 11.273,16.424 13.849,13.849C16.424,11.273 19.235,9.292 22,7.948C24.765,9.292 27.576,11.273 30.152,13.849C32.728,16.424 34.708,19.235 36.052,22C34.708,24.765 32.727,27.576 30.152,30.152C27.576,32.727 24.765,34.708 22,36.052C19.235,34.708 16.425,32.727 13.849,30.152ZM12.684,12.684C15,10.368 17.515,8.493 20.051,7.102C19.187,6.773 18.334,6.508 17.502,6.31C13.388,5.331 10.044,6.009 8.026,8.026C6.009,10.043 5.331,13.387 6.31,17.501C6.509,18.334 6.773,19.187 7.102,20.051C8.493,17.515 10.368,15 12.684,12.684ZM31.316,12.684C29,10.368 26.485,8.493 23.949,7.102C24.813,6.773 25.666,6.508 26.499,6.31C30.613,5.331 33.957,6.009 35.974,8.026C37.991,10.043 38.669,13.387 37.69,17.501C37.492,18.334 37.227,19.187 36.898,20.051C35.507,17.515 33.632,15 31.316,12.684ZM31.316,31.316C33.632,29 35.507,26.484 36.898,23.949C37.227,24.813 37.492,25.666 37.69,26.499C38.67,30.613 37.992,33.957 35.974,35.974C33.957,37.991 30.613,38.669 26.499,37.69C25.666,37.492 24.813,37.227 23.949,36.898C26.485,35.507 29,33.632 31.316,31.316ZM22.539,19.035L21.919,14.838L21.299,19.035C21.112,20.303 20.116,21.299 18.848,21.486L14.65,22.106L18.848,22.726C20.116,22.914 21.112,23.91 21.299,25.178L21.919,29.375L22.539,25.178C22.726,23.91 23.722,22.914 24.991,22.726L29.188,22.106L24.991,21.486C23.722,21.299 22.726,20.303 22.539,19.035Z"
|
||||
android:fillColor="#EBEBEB"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
|
|
@ -25,7 +25,7 @@ fun BigDecimal.toFormattedString(
|
|||
@Suppress("MagicNumber")
|
||||
fun BigDecimal.toFormattedCurrencyString(
|
||||
decimals: Int,
|
||||
currency: String,
|
||||
currency: String? = null,
|
||||
roundingMode: RoundingMode = RoundingMode.DOWN,
|
||||
limitNumberOfDecimals: Boolean = true,
|
||||
): String {
|
||||
|
|
@ -38,7 +38,8 @@ fun BigDecimal.toFormattedCurrencyString(
|
|||
decimals = decimalsForRounding,
|
||||
roundingMode = roundingMode,
|
||||
)
|
||||
return "$formattedAmount $currency"
|
||||
val formattedCurrency = currency?.let { " $it " } ?: ""
|
||||
return "$formattedAmount$formattedCurrency"
|
||||
}
|
||||
|
||||
fun BigDecimal.toFiatString(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.feature.swap.viewmodels
|
||||
package com.tangem.utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -2,11 +2,12 @@ package com.tangem.data.tokens.di
|
|||
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.tokens.repository.*
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.network.NetworksStatusesStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.quote.QuotesStore
|
||||
import com.tangem.datasource.local.token.UserMarketCoinsStore
|
||||
import com.tangem.datasource.local.token.AssetsStore
|
||||
import com.tangem.datasource.local.token.UserTokensStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.tokens.repository.*
|
||||
|
|
@ -26,17 +27,19 @@ internal object TokensDataModule {
|
|||
@Singleton
|
||||
fun provideCurrenciesRepository(
|
||||
tangemTechApi: TangemTechApi,
|
||||
tangemExpressApi: TangemExpressApi,
|
||||
userTokensStore: UserTokensStore,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
userMarketCoinsStore: UserMarketCoinsStore,
|
||||
assetsStore: AssetsStore,
|
||||
cacheRegistry: CacheRegistry,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): CurrenciesRepository {
|
||||
return DefaultCurrenciesRepository(
|
||||
tangemTechApi = tangemTechApi,
|
||||
tangemExpressApi = tangemExpressApi,
|
||||
userTokensStore = userTokensStore,
|
||||
userWalletsStore = userWalletsStore,
|
||||
userMarketCoinsStore = userMarketCoinsStore,
|
||||
assetsStore = assetsStore,
|
||||
cacheRegistry = cacheRegistry,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
|
@ -82,10 +85,8 @@ internal object TokensDataModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDefaultMarketCoinsRepository(
|
||||
userMarketCoinsStore: UserMarketCoinsStore,
|
||||
): MarketCryptoCurrencyRepository {
|
||||
return DefaultMarketCryptoCurrencyRepository(userMarketCoinsStore)
|
||||
fun provideDefaultMarketCoinsRepository(assetsStore: AssetsStore): MarketCryptoCurrencyRepository {
|
||||
return DefaultMarketCryptoCurrencyRepository(assetsStore)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -5,9 +5,14 @@ import com.tangem.data.common.api.safeApiCall
|
|||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.tokens.utils.*
|
||||
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.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
|
||||
import com.tangem.datasource.api.express.models.request.AssetsRequestBody
|
||||
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.local.token.UserMarketCoinsStore
|
||||
import com.tangem.datasource.local.token.AssetsStore
|
||||
import com.tangem.datasource.local.token.UserTokensStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.common.extensions.toCoinId
|
||||
|
|
@ -28,12 +33,13 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
@Suppress("LargeClass")
|
||||
@Suppress("LargeClass", "LongParameterList")
|
||||
internal class DefaultCurrenciesRepository(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val tangemExpressApi: TangemExpressApi,
|
||||
private val userTokensStore: UserTokensStore,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val userMarketCoinsStore: UserMarketCoinsStore,
|
||||
private val assetsStore: AssetsStore,
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : CurrenciesRepository {
|
||||
|
|
@ -88,12 +94,14 @@ internal class DefaultCurrenciesRepository(
|
|||
|
||||
val newCurrencies = (newCoins + filteredCurrencies).distinct()
|
||||
|
||||
val updatedResponse = savedCurrencies.copy(
|
||||
tokens = savedCurrencies.tokens + newCurrencies.map(userTokensResponseFactory::createResponseToken),
|
||||
)
|
||||
storeAndPushTokens(
|
||||
userWalletId = userWalletId,
|
||||
response = savedCurrencies.copy(
|
||||
tokens = savedCurrencies.tokens + newCurrencies.map(userTokensResponseFactory::createResponseToken),
|
||||
),
|
||||
response = updatedResponse,
|
||||
)
|
||||
fetchExchangeableUserMarketCoinsByIds(userWalletId, updatedResponse)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -374,14 +382,25 @@ internal class DefaultCurrenciesRepository(
|
|||
userTokens: UserTokensResponse,
|
||||
) {
|
||||
try {
|
||||
val networkIds = userTokens.tokens
|
||||
.distinctBy { it.networkId }
|
||||
.joinToString(separator = ",") { it.networkId }
|
||||
val response = tangemTechApi.getCoins(networkIds = networkIds, exchangeable = true)
|
||||
val tokensList = userTokens.tokens
|
||||
.map {
|
||||
LeastTokenInfo(
|
||||
contractAddress = it.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE,
|
||||
network = it.networkId,
|
||||
)
|
||||
}
|
||||
|
||||
userMarketCoinsStore.store(userWalletId, response)
|
||||
if (tokensList.isNotEmpty()) {
|
||||
val response = tangemExpressApi.getAssets(
|
||||
AssetsRequestBody(
|
||||
tokensList = tokensList,
|
||||
),
|
||||
)
|
||||
|
||||
assetsStore.store(userWalletId, response.getOrThrow())
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Unable to fetch user market coins for: ${userWalletId.stringValue}")
|
||||
Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,21 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.datasource.local.token.UserMarketCoinsStore
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
|
||||
import com.tangem.datasource.local.token.AssetsStore
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
class DefaultMarketCryptoCurrencyRepository(
|
||||
private val userMarketCoinsStore: UserMarketCoinsStore,
|
||||
private val assetsStore: AssetsStore,
|
||||
) : MarketCryptoCurrencyRepository {
|
||||
|
||||
override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Boolean {
|
||||
val blockchain = Blockchain.fromId(cryptoCurrencyId.rawNetworkId)
|
||||
val apiNetworkId = blockchain.toNetworkId()
|
||||
return userMarketCoinsStore.getSyncOrNull(userWalletId)?.coins
|
||||
?.firstOrNull { it.id == cryptoCurrencyId.rawCurrencyId }
|
||||
?.networks
|
||||
?.firstOrNull {
|
||||
if (it.contractAddress != null) {
|
||||
it.networkId == apiNetworkId && it.contractAddress == cryptoCurrencyId.contractAddress
|
||||
} else {
|
||||
it.networkId == apiNetworkId
|
||||
}
|
||||
}?.exchangeable ?: false
|
||||
override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
|
||||
val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE
|
||||
|
||||
return assetsStore.getSyncOrNull(userWalletId)?.find {
|
||||
it.network == cryptoCurrency.network.backendId &&
|
||||
it.contractAddress.equals(contractAddress, ignoreCase = true)
|
||||
}?.exchangeAvailable ?: false
|
||||
}
|
||||
}
|
||||
|
|
@ -67,6 +67,14 @@ internal class DefaultQuotesRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote {
|
||||
return withContext(dispatchers.io) {
|
||||
val quote = quotesStore.getSync(setOf(currencyId)).firstOrNull()
|
||||
requireNotNull(quote) { "Unable to get quote for $currencyId" }
|
||||
quotesConverter.convert(quote)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchExpiredQuotes(
|
||||
currenciesIds: Set<CryptoCurrency.ID>,
|
||||
appCurrencyId: String,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.data.tokens.utils
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.common.DerivationStyleProvider
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import timber.log.Timber
|
||||
|
||||
|
|
@ -21,9 +22,11 @@ internal fun getNetwork(
|
|||
|
||||
return Network(
|
||||
id = Network.ID(blockchain.id),
|
||||
backendId = blockchain.toNetworkId(),
|
||||
name = blockchain.fullName,
|
||||
isTestnet = blockchain.isTestnet(),
|
||||
derivationPath = getNetworkDerivationPath(blockchain, extraDerivationPath, derivationStyleProvider),
|
||||
currencySymbol = blockchain.currency,
|
||||
standardType = getNetworkStandardType(blockchain),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.blockchain.common.IconsUtil
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.common.extensions.toCoinId
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency.ID
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
|
|
@ -55,8 +54,7 @@ internal fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? {
|
|||
internal fun getCoinIconUrl(blockchain: Blockchain): String? {
|
||||
val coinId = when (blockchain) {
|
||||
Blockchain.Unknown -> null
|
||||
Blockchain.TerraV1, Blockchain.TerraV2, Blockchain.Near -> blockchain.toCoinId()
|
||||
else -> blockchain.toNetworkId()
|
||||
else -> blockchain.toCoinId()
|
||||
}
|
||||
|
||||
return coinId?.let(::getTokenIconUrlFromDefaultHost)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import kotlinx.parcelize.Parcelize
|
|||
* (e.g., ERC20, BEP20).
|
||||
*
|
||||
* @property id The unique identifier of the network.
|
||||
* @property backendId The name of this network in the Tangem backend.
|
||||
* @property name The human-readable name of the network, such as "Ethereum" or "Bitcoin".
|
||||
* @property derivationPath The path used to derive keys for this network.
|
||||
* @property isTestnet Indicates whether the network is a test network or a main network.
|
||||
|
|
@ -19,7 +20,9 @@ import kotlinx.parcelize.Parcelize
|
|||
@Parcelize
|
||||
data class Network(
|
||||
val id: ID,
|
||||
val backendId: String,
|
||||
val name: String,
|
||||
val currencySymbol: String,
|
||||
val derivationPath: DerivationPath,
|
||||
val isTestnet: Boolean,
|
||||
val standardType: StandardType,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.domain.tokens.models.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
|
||||
class TokenExchangeAnalyticsEvent(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent("Token", event, params, null) {
|
||||
|
||||
class CexTxStatusOpened(token: String) : TokenScreenAnalyticsEvent(
|
||||
event = "ChangeNow Status Opened",
|
||||
params = mapOf("Token" to token),
|
||||
)
|
||||
|
||||
class CexTxStatusChanged(token: String, status: String) : TokenScreenAnalyticsEvent(
|
||||
event = "ChangeNow Status",
|
||||
params = mapOf("Token" to token, "Status" to status),
|
||||
)
|
||||
|
||||
class GoToProviderStatus(token: String) : TokenScreenAnalyticsEvent(
|
||||
event = "Button - Go To Provider",
|
||||
params = mapOf("Token" to token, "Place" to "Status"),
|
||||
)
|
||||
|
||||
class GoToProviderKYC(token: String) : TokenScreenAnalyticsEvent(
|
||||
event = "Button - Go To Provider",
|
||||
params = mapOf("Token" to token, "Place" to "KYC"),
|
||||
)
|
||||
|
||||
class GoToProviderFail(token: String) : TokenScreenAnalyticsEvent(
|
||||
event = "Button - Go To Provider",
|
||||
params = mapOf("Token" to token, "Place" to "Fail"),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
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
|
||||
|
|
@ -55,6 +56,7 @@ class GetCryptoCurrencyActionsUseCase(
|
|||
userWalletId = userWallet.walletId,
|
||||
coinStatus = maybeCoinStatus.getOrNull(),
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
cardTypesResolver = userWallet.scanResponse.cardTypesResolver,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -66,11 +68,17 @@ class GetCryptoCurrencyActionsUseCase(
|
|||
userWalletId: UserWalletId,
|
||||
coinStatus: CryptoCurrencyStatus?,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
cardTypesResolver: CardTypesResolver,
|
||||
): TokenActionsState {
|
||||
return TokenActionsState(
|
||||
walletId = userWalletId,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
states = createListOfActions(userWalletId, coinStatus, cryptoCurrencyStatus),
|
||||
states = createListOfActions(
|
||||
userWalletId,
|
||||
coinStatus,
|
||||
cryptoCurrencyStatus,
|
||||
cardTypesResolver,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +90,7 @@ class GetCryptoCurrencyActionsUseCase(
|
|||
userWalletId: UserWalletId,
|
||||
coinStatus: CryptoCurrencyStatus?,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
cardTypesResolver: CardTypesResolver,
|
||||
): List<TokenActionsState.ActionState> {
|
||||
val cryptoCurrency = cryptoCurrencyStatus.currency
|
||||
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.MissedDerivation) {
|
||||
|
|
@ -107,8 +116,11 @@ class GetCryptoCurrencyActionsUseCase(
|
|||
activeList.add(TokenActionsState.ActionState.Send(true))
|
||||
}
|
||||
|
||||
val isMulticurrencyWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2()
|
||||
// swap
|
||||
if (marketCryptoCurrencyRepository.isExchangeable(userWalletId, cryptoCurrency.id)) {
|
||||
if (isMulticurrencyWallet &&
|
||||
marketCryptoCurrencyRepository.isExchangeable(userWalletId, cryptoCurrency)
|
||||
) {
|
||||
activeList.add(TokenActionsState.ActionState.Swap(true))
|
||||
} else {
|
||||
disabledList.add(TokenActionsState.ActionState.Swap(false))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.tokens.error.TokenListError
|
||||
import com.tangem.domain.tokens.error.mapper.mapToTokenListError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
||||
class GetCryptoCurrencyStatusSyncUseCase(
|
||||
internal val currenciesRepository: CurrenciesRepository,
|
||||
internal val quotesRepository: QuotesRepository,
|
||||
internal val networksRepository: NetworksRepository,
|
||||
internal val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencyId: CryptoCurrency.ID,
|
||||
): Either<TokenListError, CryptoCurrencyStatus> {
|
||||
val operations = CurrenciesStatusesOperations(
|
||||
userWalletId = userWalletId,
|
||||
currenciesRepository = currenciesRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
networksRepository = networksRepository,
|
||||
)
|
||||
|
||||
return operations.getCurrencyStatusSync(cryptoCurrencyId)
|
||||
.mapLeft { error -> error.mapToTokenListError() }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.tokens.error.TokenListError
|
||||
import com.tangem.domain.tokens.error.mapper.mapToTokenListError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
||||
class GetCryptoCurrencyStatusesSyncUseCase(
|
||||
internal val currenciesRepository: CurrenciesRepository,
|
||||
internal val quotesRepository: QuotesRepository,
|
||||
internal val networksRepository: NetworksRepository,
|
||||
internal val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId): Either<TokenListError, List<CryptoCurrencyStatus>> {
|
||||
val operations = CurrenciesStatusesOperations(
|
||||
userWalletId = userWalletId,
|
||||
currenciesRepository = currenciesRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
networksRepository = networksRepository,
|
||||
)
|
||||
|
||||
return operations.getCurrenciesStatusesSync()
|
||||
.mapLeft { error -> error.mapToTokenListError() }
|
||||
}
|
||||
}
|
||||
|
|
@ -67,6 +67,46 @@ internal class CurrenciesStatusesOperations(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun getCurrenciesStatusesSync(): Either<Error, List<CryptoCurrencyStatus>> {
|
||||
return either {
|
||||
catch(
|
||||
block = {
|
||||
val nonEmptyCurrencies =
|
||||
currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId).toNonEmptyListOrNull()
|
||||
?: return emptyList<CryptoCurrencyStatus>().right()
|
||||
val (networks, currenciesIds) = getIds(nonEmptyCurrencies)
|
||||
val quotes = quotesRepository.getQuotesSync(currenciesIds, false).right()
|
||||
val networkStatuses =
|
||||
networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right()
|
||||
return createCurrenciesStatuses(nonEmptyCurrencies, quotes, networkStatuses)
|
||||
},
|
||||
catch = { raise(Error.DataError(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getCurrencyStatusSync(cryptoCurrencyId: CryptoCurrency.ID): Either<Error, CryptoCurrencyStatus> {
|
||||
return either {
|
||||
catch(
|
||||
block = {
|
||||
val currency =
|
||||
currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, cryptoCurrencyId)
|
||||
val quotes = quotesRepository.getQuoteSync(cryptoCurrencyId).right()
|
||||
val networkStatuses =
|
||||
networksRepository.getNetworkStatusesSync(
|
||||
userWalletId,
|
||||
setOf(currency.network),
|
||||
false,
|
||||
).firstOrNull {
|
||||
it.network == currency.network
|
||||
}.right()
|
||||
return createCurrencyStatus(currency, quotes, networkStatuses)
|
||||
},
|
||||
catch = { raise(Error.DataError(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getCardCurrenciesStatusesFlow(): Flow<Either<Error, List<CryptoCurrencyStatus>>> {
|
||||
return flow {
|
||||
val nonEmptyCurrencies = recover(
|
||||
|
|
|
|||
|
|
@ -8,5 +8,5 @@ import com.tangem.domain.wallets.models.UserWalletId
|
|||
*/
|
||||
interface MarketCryptoCurrencyRepository {
|
||||
|
||||
suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Boolean
|
||||
suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean
|
||||
}
|
||||
|
|
@ -29,4 +29,6 @@ interface QuotesRepository {
|
|||
* @return A [Flow] emitting a set of quotes corresponding to the specified cryptocurrencies.
|
||||
*/
|
||||
suspend fun getQuotesSync(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Set<Quote>
|
||||
|
||||
suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote
|
||||
}
|
||||
|
|
@ -18,6 +18,8 @@ internal object MockNetworks {
|
|||
name = "Network One",
|
||||
isTestnet = false,
|
||||
standardType = Network.StandardType.ERC20,
|
||||
backendId = "network1",
|
||||
currencySymbol = "ETH",
|
||||
derivationPath = Network.DerivationPath.None,
|
||||
)
|
||||
|
||||
|
|
@ -26,6 +28,8 @@ internal object MockNetworks {
|
|||
name = "Network Two",
|
||||
isTestnet = false,
|
||||
standardType = Network.StandardType.ERC20,
|
||||
backendId = "network1",
|
||||
currencySymbol = "ETH",
|
||||
derivationPath = Network.DerivationPath.None,
|
||||
)
|
||||
|
||||
|
|
@ -34,6 +38,8 @@ internal object MockNetworks {
|
|||
name = "Network Three",
|
||||
isTestnet = false,
|
||||
standardType = Network.StandardType.ERC20,
|
||||
backendId = "network1",
|
||||
currencySymbol = "ETH",
|
||||
derivationPath = Network.DerivationPath.None,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -20,4 +20,9 @@ internal class MockQuotesRepository(
|
|||
override suspend fun getQuotesSync(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Set<Quote> {
|
||||
return getQuotesUpdates(currenciesIds).first()
|
||||
}
|
||||
|
||||
override suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote {
|
||||
return quotes.map { it.getOrElse { e -> throw e } }.first()
|
||||
.first { it.rawCurrencyId == currencyId.rawCurrencyId }
|
||||
}
|
||||
}
|
||||
|
|
@ -7,4 +7,13 @@ sealed class SendTransactionError {
|
|||
data class DataError(val message: String?) : SendTransactionError()
|
||||
|
||||
data class NetworkError(val message: String?) : SendTransactionError()
|
||||
|
||||
data class BlockchainSdkError(val code: Int, val cause: Throwable?) : SendTransactionError()
|
||||
object UserCancelledError : SendTransactionError()
|
||||
data class TangemSdkError(val code: Int, val cause: Throwable?) : SendTransactionError()
|
||||
data class UnknownError(val ex: Exception? = null) : SendTransactionError()
|
||||
|
||||
companion object {
|
||||
const val USER_CANCELLED_ERROR_CODE = 50002
|
||||
}
|
||||
}
|
||||
|
|
@ -3,13 +3,17 @@ package com.tangem.domain.transaction.usecase
|
|||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.blockchain.network.ResultChecker
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.domain.transaction.error.SendTransactionError.Companion.USER_CANCELLED_ERROR_CODE
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
|
||||
|
|
@ -52,10 +56,40 @@ class SendTransactionUseCase(
|
|||
ifRight = { result ->
|
||||
when (result) {
|
||||
is SimpleResult.Success -> true.right()
|
||||
is SimpleResult.Failure -> SendTransactionError.NetworkError(result.error.message).left()
|
||||
is SimpleResult.Failure -> handleError(result).left()
|
||||
}
|
||||
},
|
||||
ifLeft = { it.left() },
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleError(result: SimpleResult.Failure): SendTransactionError {
|
||||
if (ResultChecker.isNetworkError(result)) return SendTransactionError.NetworkError(result.error.message)
|
||||
val error = result.error as? BlockchainSdkError ?: return SendTransactionError.UnknownError()
|
||||
when (error) {
|
||||
is BlockchainSdkError.WrappedTangemError -> {
|
||||
val errorByCode = mapErrorByCode(error)
|
||||
if (errorByCode != null) {
|
||||
return errorByCode
|
||||
}
|
||||
val tangemSdkError = error.tangemError as? TangemSdkError ?: return SendTransactionError.UnknownError()
|
||||
if (tangemSdkError is TangemSdkError.UserCancelled) return SendTransactionError.UserCancelledError
|
||||
return SendTransactionError.TangemSdkError(tangemSdkError.code, tangemSdkError.cause)
|
||||
}
|
||||
else -> {
|
||||
return SendTransactionError.TangemSdkError(error.code, error.cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapErrorByCode(error: BlockchainSdkError.WrappedTangemError): SendTransactionError? {
|
||||
return when (error.code) {
|
||||
USER_CANCELLED_ERROR_CODE -> {
|
||||
return SendTransactionError.UserCancelledError
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ internal class SendAmountFieldConverter(
|
|||
),
|
||||
placeholder = TextReference.Str(DEFAULT_VALUE),
|
||||
isError = false,
|
||||
error = TextReference.Res(R.string.send_insufficient_funds),
|
||||
error = TextReference.Res(R.string.swapping_insufficient_funds),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,20 +8,25 @@ plugins {
|
|||
|
||||
dependencies {
|
||||
|
||||
/** AndroidX */
|
||||
implementation(deps.androidx.datastore)
|
||||
|
||||
/** Project*/
|
||||
implementation(project(":core:datasource"))
|
||||
implementation(project(":core:utils"))
|
||||
implementation(project(":features:swap:domain"))
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.features.swap.domain)
|
||||
|
||||
/** Network */
|
||||
implementation(deps.retrofit)
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.arrow.core)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/** Data */
|
||||
|
|
@ -32,5 +37,6 @@ dependencies {
|
|||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
package com.tangem.feature.swap
|
||||
|
||||
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.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.SavedLastSwappedCryptoCurrency
|
||||
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionModel
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class DefaultSwapTransactionRepository(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : SwapTransactionRepository {
|
||||
|
||||
override suspend fun storeTransaction(
|
||||
userWalletId: UserWalletId,
|
||||
fromCryptoCurrencyId: CryptoCurrency.ID,
|
||||
toCryptoCurrencyId: CryptoCurrency.ID,
|
||||
transaction: SavedSwapTransactionModel,
|
||||
) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
val savedTransactions: List<SavedSwapTransactionListModel>? = mutablePreferences.getObjectList(
|
||||
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
|
||||
)
|
||||
|
||||
val tokenTransactions = savedTransactions
|
||||
?.firstOrNull {
|
||||
it.checkId(
|
||||
checkUserWalletId = userWalletId,
|
||||
fromCurrencyId = fromCryptoCurrencyId,
|
||||
toCurrencyId = toCryptoCurrencyId,
|
||||
)
|
||||
}
|
||||
?.transactions
|
||||
?.addOrReplace(
|
||||
item = transaction,
|
||||
predicate = { it.txId == transaction.txId },
|
||||
) ?: listOf(transaction)
|
||||
|
||||
mutablePreferences.setObject(
|
||||
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
|
||||
value = savedTransactions?.updateList(
|
||||
userWalletId = userWalletId,
|
||||
fromCryptoCurrencyId = fromCryptoCurrencyId,
|
||||
toCryptoCurrencyId = toCryptoCurrencyId,
|
||||
transactions = tokenTransactions,
|
||||
) ?: listOf(
|
||||
SavedSwapTransactionListModel(
|
||||
userWalletId = userWalletId.stringValue,
|
||||
fromCryptoCurrencyId = fromCryptoCurrencyId.value,
|
||||
toCryptoCurrencyId = toCryptoCurrencyId.value,
|
||||
transactions = tokenTransactions,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTransactions(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencyId: CryptoCurrency.ID,
|
||||
): Flow<List<SavedSwapTransactionListModel>?> {
|
||||
return appPreferencesStore.getObjectList<SavedSwapTransactionListModel>(
|
||||
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
|
||||
).map { savedTransactions ->
|
||||
savedTransactions
|
||||
?.filter {
|
||||
it.userWalletId == userWalletId.stringValue &&
|
||||
(
|
||||
it.toCryptoCurrencyId == cryptoCurrencyId.value ||
|
||||
it.fromCryptoCurrencyId == cryptoCurrencyId.value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun removeTransaction(
|
||||
userWalletId: UserWalletId,
|
||||
fromCryptoCurrencyId: CryptoCurrency.ID,
|
||||
toCryptoCurrencyId: CryptoCurrency.ID,
|
||||
txId: String,
|
||||
) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
val savedList: List<SavedSwapTransactionListModel>? = mutablePreferences.getObjectList(
|
||||
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
|
||||
)
|
||||
val tokenTransactions = savedList
|
||||
?.first {
|
||||
it.checkId(
|
||||
checkUserWalletId = userWalletId,
|
||||
fromCurrencyId = fromCryptoCurrencyId,
|
||||
toCurrencyId = toCryptoCurrencyId,
|
||||
)
|
||||
}
|
||||
?.transactions
|
||||
?.filterNot { it.txId == txId }
|
||||
|
||||
val editedList =
|
||||
if (tokenTransactions.isNullOrEmpty()) {
|
||||
savedList?.filterNot {
|
||||
it.checkId(
|
||||
checkUserWalletId = userWalletId,
|
||||
fromCurrencyId = fromCryptoCurrencyId,
|
||||
toCurrencyId = toCryptoCurrencyId,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
savedList.updateList(
|
||||
userWalletId = userWalletId,
|
||||
fromCryptoCurrencyId = fromCryptoCurrencyId,
|
||||
toCryptoCurrencyId = toCryptoCurrencyId,
|
||||
transactions = tokenTransactions,
|
||||
)
|
||||
}
|
||||
|
||||
if (editedList.isNullOrEmpty()) {
|
||||
mutablePreferences.remove(key = PreferencesKeys.SWAP_TRANSACTIONS_KEY)
|
||||
} else {
|
||||
mutablePreferences.setObject(
|
||||
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
|
||||
value = editedList,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getLastSwappedCryptoCurrencyId(userWalletId: UserWalletId): String? {
|
||||
val lastSwappedCurrencies = appPreferencesStore.getObjectListSync<SavedLastSwappedCryptoCurrency>(
|
||||
key = PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY,
|
||||
)
|
||||
|
||||
return lastSwappedCurrencies.find { userWalletId.stringValue == it.userWalletId }?.cryptoCurrencyId
|
||||
}
|
||||
|
||||
override suspend fun storeLastSwappedCryptoCurrencyId(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencyId: CryptoCurrency.ID,
|
||||
) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
val lastSwappedCryptoCurrencies: List<SavedLastSwappedCryptoCurrency>? = mutablePreferences.getObjectList(
|
||||
key = PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY,
|
||||
)
|
||||
|
||||
val newList = if (lastSwappedCryptoCurrencies != null) {
|
||||
lastSwappedCryptoCurrencies.filter {
|
||||
it.userWalletId != userWalletId.stringValue
|
||||
} + SavedLastSwappedCryptoCurrency(userWalletId.stringValue, cryptoCurrencyId.value)
|
||||
} else {
|
||||
listOf(SavedLastSwappedCryptoCurrency(userWalletId.stringValue, cryptoCurrencyId.value))
|
||||
}
|
||||
|
||||
mutablePreferences.setObjectList(
|
||||
key = PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY,
|
||||
value = newList,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun SavedSwapTransactionListModel.checkId(
|
||||
checkUserWalletId: UserWalletId,
|
||||
fromCurrencyId: CryptoCurrency.ID,
|
||||
toCurrencyId: CryptoCurrency.ID,
|
||||
): Boolean {
|
||||
return userWalletId == checkUserWalletId.stringValue &&
|
||||
toCryptoCurrencyId == toCurrencyId.value &&
|
||||
fromCryptoCurrencyId == fromCurrencyId.value
|
||||
}
|
||||
|
||||
private fun List<SavedSwapTransactionListModel>.updateList(
|
||||
userWalletId: UserWalletId,
|
||||
fromCryptoCurrencyId: CryptoCurrency.ID,
|
||||
toCryptoCurrencyId: CryptoCurrency.ID,
|
||||
transactions: List<SavedSwapTransactionModel>,
|
||||
): List<SavedSwapTransactionListModel> {
|
||||
return addOrReplace(
|
||||
item = SavedSwapTransactionListModel(
|
||||
userWalletId = userWalletId.stringValue,
|
||||
fromCryptoCurrencyId = fromCryptoCurrencyId.value,
|
||||
toCryptoCurrencyId = toCryptoCurrencyId.value,
|
||||
transactions = transactions,
|
||||
),
|
||||
predicate = {
|
||||
it.checkId(
|
||||
checkUserWalletId = userWalletId,
|
||||
fromCurrencyId = fromCryptoCurrencyId,
|
||||
toCurrencyId = toCryptoCurrencyId,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +1,130 @@
|
|||
package com.tangem.feature.swap
|
||||
|
||||
import arrow.core.Either
|
||||
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 com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
|
||||
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.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.oneinch.OneInchErrorsHandler
|
||||
import com.tangem.datasource.api.oneinch.errors.OneIncResponseException
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
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.models.UserWallet
|
||||
import com.tangem.domain.wallets.legacy.WalletsStateHolder
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.swap.converters.QuotesConverter
|
||||
import com.tangem.feature.swap.converters.SwapConverter
|
||||
import com.tangem.feature.swap.converters.TokensConverter
|
||||
import com.tangem.feature.swap.converters.*
|
||||
import com.tangem.feature.swap.domain.SwapRepository
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
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.Currency
|
||||
import com.tangem.feature.swap.domain.models.domain.QuoteModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.mapErrors
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
import com.tangem.blockchain.common.Token as SdkToken
|
||||
import com.tangem.datasource.api.express.models.request.LeastTokenInfo as NetworkLeastTokenInfo
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class SwapRepositoryImpl @Inject constructor(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val tangemExpressApi: TangemExpressApi,
|
||||
private val oneInchApiFactory: OneInchApiFactory,
|
||||
private val oneInchErrorsHandler: OneInchErrorsHandler,
|
||||
private val coroutineDispatcher: CoroutineDispatcherProvider,
|
||||
private val configManager: ConfigManager,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val walletsStateHolder: WalletsStateHolder,
|
||||
private val errorsDataConverter: ErrorsDataConverter,
|
||||
) : SwapRepository {
|
||||
|
||||
private val tokensConverter = TokensConverter()
|
||||
private val quotesConverter = QuotesConverter()
|
||||
private val swapConverter = SwapConverter()
|
||||
private val expressDataConverter = ExpressDataConverter()
|
||||
private val leastTokenInfoConverter = LeastTokenInfoConverter()
|
||||
private val swapPairInfoConverter = SwapPairInfoConverter()
|
||||
private val cryptoCurrencyFactory = CryptoCurrencyFactory()
|
||||
private val exchangeStatusConverter = ExchangeStatusConverter()
|
||||
|
||||
override suspend fun getPairs(
|
||||
initialCurrency: LeastTokenInfo,
|
||||
currencyList: List<CryptoCurrency>,
|
||||
): List<SwapPairLeast> {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
val reversedPairs = async {
|
||||
getPairsInternal(
|
||||
from = currenciesList,
|
||||
to = arrayListOf(initial),
|
||||
)
|
||||
}
|
||||
|
||||
val allPairs = pairs.await() + reversedPairs.await()
|
||||
|
||||
val providers = tangemExpressApi.getProviders().getOrThrow()
|
||||
|
||||
return@withContext swapPairInfoConverter.convert(
|
||||
SwapPairsWithProviders(
|
||||
swapPair = allPairs,
|
||||
providers = providers,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getPairsInternal(
|
||||
from: List<NetworkLeastTokenInfo>,
|
||||
to: List<NetworkLeastTokenInfo>,
|
||||
): List<SwapPair> {
|
||||
return tangemExpressApi.getPairs(
|
||||
PairsRequestBody(
|
||||
from = from,
|
||||
to = to,
|
||||
),
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
override suspend fun getExchangeStatus(txId: String): Either<UnknownError, ExchangeStatusModel> {
|
||||
return withContext(coroutineDispatcher.io) {
|
||||
either {
|
||||
catch(
|
||||
{
|
||||
exchangeStatusConverter.convert(
|
||||
tangemExpressApi
|
||||
.getExchangeStatus(txId)
|
||||
.getOrThrow(),
|
||||
)
|
||||
},
|
||||
{
|
||||
raise(UnknownError(it.message))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getRates(currencyId: String, tokenIds: List<String>): Map<String, Double> {
|
||||
// workaround cause backend do not return arbitrum and optimism rates
|
||||
|
|
@ -83,23 +162,37 @@ internal class SwapRepositoryImpl @Inject constructor(
|
|||
}
|
||||
|
||||
override suspend fun findBestQuote(
|
||||
networkId: String,
|
||||
fromTokenAddress: String,
|
||||
toTokenAddress: String,
|
||||
amount: String,
|
||||
fromContractAddress: String,
|
||||
fromNetwork: String,
|
||||
toContractAddress: String,
|
||||
toNetwork: String,
|
||||
fromAmount: String,
|
||||
fromDecimals: Int,
|
||||
toDecimals: Int,
|
||||
providerId: String,
|
||||
rateType: RateType,
|
||||
): AggregatedSwapDataModel<QuoteModel> {
|
||||
return withContext(coroutineDispatcher.io) {
|
||||
try {
|
||||
val response = oneInchErrorsHandler.handleOneInchResponse(
|
||||
getOneInchApi(networkId).quote(
|
||||
fromTokenAddress = fromTokenAddress,
|
||||
toTokenAddress = toTokenAddress,
|
||||
amount = amount,
|
||||
val response = tangemExpressApi.getExchangeQuote(
|
||||
fromContractAddress = fromContractAddress,
|
||||
fromNetwork = fromNetwork,
|
||||
toContractAddress = toContractAddress,
|
||||
toNetwork = toNetwork,
|
||||
fromAmount = fromAmount,
|
||||
fromDecimals = fromDecimals,
|
||||
toDecimals = toDecimals,
|
||||
providerId = providerId,
|
||||
rateType = rateType.name.lowercase(),
|
||||
).getOrThrow()
|
||||
AggregatedSwapDataModel(
|
||||
dataModel = QuoteModel(
|
||||
toTokenAmount = createFromAmountWithOffset(response.toAmount, response.toDecimals),
|
||||
allowanceContract = response.allowanceContract,
|
||||
),
|
||||
)
|
||||
AggregatedSwapDataModel(dataModel = quotesConverter.convert(response))
|
||||
} catch (ex: OneIncResponseException) {
|
||||
AggregatedSwapDataModel(null, mapErrors(ex.data.description))
|
||||
} catch (ex: Exception) {
|
||||
AggregatedSwapDataModel(null, getDataError(ex))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -110,31 +203,37 @@ internal class SwapRepositoryImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun prepareSwapTransaction(
|
||||
networkId: String,
|
||||
fromTokenAddress: String,
|
||||
toTokenAddress: String,
|
||||
amount: String,
|
||||
fromWalletAddress: String,
|
||||
slippage: Int,
|
||||
override suspend fun getExchangeData(
|
||||
fromContractAddress: String,
|
||||
fromNetwork: String,
|
||||
toContractAddress: String,
|
||||
toNetwork: String,
|
||||
fromAmount: String,
|
||||
fromDecimals: Int,
|
||||
toDecimals: Int,
|
||||
providerId: String,
|
||||
rateType: RateType,
|
||||
toAddress: String,
|
||||
): AggregatedSwapDataModel<SwapDataModel> {
|
||||
return withContext(coroutineDispatcher.io) {
|
||||
try {
|
||||
val swapResponse = oneInchErrorsHandler.handleOneInchResponse(
|
||||
getOneInchApi(networkId).swap(
|
||||
fromTokenAddress = fromTokenAddress,
|
||||
toTokenAddress = toTokenAddress,
|
||||
amount = amount,
|
||||
fromAddress = fromWalletAddress,
|
||||
slippage = slippage,
|
||||
referrerAddress = configManager.config.swapReferrerAccount?.address,
|
||||
fee = configManager.config.swapReferrerAccount?.fee,
|
||||
),
|
||||
val response = tangemExpressApi.getExchangeData(
|
||||
fromContractAddress = fromContractAddress,
|
||||
fromNetwork = fromNetwork,
|
||||
toContractAddress = toContractAddress,
|
||||
toNetwork = toNetwork,
|
||||
fromAmount = fromAmount,
|
||||
fromDecimals = fromDecimals,
|
||||
toDecimals = toDecimals,
|
||||
providerId = providerId,
|
||||
rateType = rateType.name.lowercase(),
|
||||
toAddress = toAddress,
|
||||
).getOrThrow()
|
||||
AggregatedSwapDataModel(
|
||||
dataModel = expressDataConverter.convert(response),
|
||||
)
|
||||
|
||||
AggregatedSwapDataModel(swapConverter.convert(swapResponse))
|
||||
} catch (ex: OneIncResponseException) {
|
||||
AggregatedSwapDataModel(null, mapErrors(ex.data.description))
|
||||
} catch (ex: Exception) {
|
||||
AggregatedSwapDataModel(null, getDataError(ex))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -143,45 +242,13 @@ internal class SwapRepositoryImpl @Inject constructor(
|
|||
return configManager.config.swapReferrerAccount?.fee?.toDoubleOrNull() ?: 0.0
|
||||
}
|
||||
|
||||
override suspend fun getCryptoCurrency(
|
||||
userWallet: UserWallet,
|
||||
currency: Currency,
|
||||
network: Network,
|
||||
): CryptoCurrency? {
|
||||
val blockchain = Blockchain.fromNetworkId(currency.networkId) ?: return null
|
||||
val cryptoCurrencyFactory = CryptoCurrencyFactory()
|
||||
return when (currency) {
|
||||
is Currency.NativeToken -> {
|
||||
cryptoCurrencyFactory.createCoin(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = network.derivationPath.value,
|
||||
derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider,
|
||||
)
|
||||
}
|
||||
is Currency.NonNativeToken -> {
|
||||
val sdkToken = SdkToken(
|
||||
name = currency.name,
|
||||
symbol = currency.symbol,
|
||||
contractAddress = currency.contractAddress,
|
||||
decimals = currency.decimalCount,
|
||||
id = currency.id,
|
||||
)
|
||||
cryptoCurrencyFactory.createToken(
|
||||
sdkToken = sdkToken,
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = network.derivationPath.value,
|
||||
derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider,
|
||||
)
|
||||
}
|
||||
} as CryptoCurrency
|
||||
}
|
||||
|
||||
override suspend fun getAllowance(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: String,
|
||||
derivationPath: String?,
|
||||
tokenDecimalCount: Int,
|
||||
tokenAddress: String,
|
||||
spenderAddress: String,
|
||||
): BigDecimal {
|
||||
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
|
|
@ -189,7 +256,6 @@ internal class SwapRepositoryImpl @Inject constructor(
|
|||
blockchain = blockchain,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
val spenderAddress = addressForTrust(networkId)
|
||||
|
||||
val result = (walletManager as? Approver)?.getAllowance(
|
||||
spenderAddress,
|
||||
|
|
@ -210,8 +276,9 @@ internal class SwapRepositoryImpl @Inject constructor(
|
|||
userWalletId: UserWalletId,
|
||||
networkId: String,
|
||||
derivationPath: String?,
|
||||
currency: Currency,
|
||||
currency: CryptoCurrency,
|
||||
amount: BigDecimal?,
|
||||
spenderAddress: String,
|
||||
): String {
|
||||
val blockchain =
|
||||
requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
|
||||
|
|
@ -220,7 +287,6 @@ internal class SwapRepositoryImpl @Inject constructor(
|
|||
blockchain = blockchain,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
val spenderAddress = addressForTrust(networkId)
|
||||
|
||||
return (walletManager as? Approver)?.getApproveData(
|
||||
spenderAddress,
|
||||
|
|
@ -228,16 +294,16 @@ internal class SwapRepositoryImpl @Inject constructor(
|
|||
) ?: error("Cannot cast to Approver")
|
||||
}
|
||||
|
||||
private fun convertToAmount(amount: BigDecimal, currency: Currency, blockchain: Blockchain): Amount {
|
||||
private fun convertToAmount(amount: BigDecimal, currency: CryptoCurrency, blockchain: Blockchain): Amount {
|
||||
return when (currency) {
|
||||
is Currency.NativeToken -> {
|
||||
is CryptoCurrency.Token -> {
|
||||
Amount(value = amount, blockchain = blockchain)
|
||||
}
|
||||
is Currency.NonNativeToken -> {
|
||||
is CryptoCurrency.Coin -> {
|
||||
Amount(
|
||||
currencySymbol = currency.symbol,
|
||||
value = amount,
|
||||
decimals = currency.decimalCount,
|
||||
decimals = currency.decimals,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -247,6 +313,31 @@ internal class SwapRepositoryImpl @Inject constructor(
|
|||
return oneInchApiFactory.getApi(networkId)
|
||||
}
|
||||
|
||||
override fun getNativeTokenForNetwork(networkId: String): CryptoCurrency {
|
||||
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
|
||||
|
||||
return requireNotNull(
|
||||
cryptoCurrencyFactory.createCoin(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
derivationStyleProvider = requireNotNull(
|
||||
walletsStateHolder.userWalletsListManager
|
||||
?.selectedUserWalletSync
|
||||
?.scanResponse
|
||||
?.derivationStyleProvider,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDataError(ex: Exception): DataError {
|
||||
return if (ex is ApiResponseError.HttpException) {
|
||||
errorsDataConverter.convert(ex.errorBody ?: "")
|
||||
} else {
|
||||
DataError.UnknownError
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
// TODO("get this ids from blockchain enum later")
|
||||
private const val OPTIMISM_ID = "optimistic-ethereum"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
package com.tangem.feature.swap.converters
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
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
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class ErrorsDataConverter(
|
||||
private val jsonAdapter: JsonAdapter<ExpressErrorResponse>,
|
||||
) : Converter<String, DataError> {
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override fun convert(value: String): DataError {
|
||||
try {
|
||||
val error = jsonAdapter.fromJson(value)?.error ?: return DataError.UnknownError
|
||||
|
||||
return when (error.code) {
|
||||
2010 -> DataError.BadRequest(code = error.code)
|
||||
2210 -> DataError.ExchangeProviderNotFoundError(code = error.code)
|
||||
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),
|
||||
)
|
||||
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
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return DataError.UnknownError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.feature.swap.converters
|
||||
|
||||
import com.tangem.datasource.api.express.models.response.ExchangeStatusResponse
|
||||
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
|
||||
import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class ExchangeStatusConverter : Converter<ExchangeStatusResponse, ExchangeStatusModel> {
|
||||
override fun convert(value: ExchangeStatusResponse): ExchangeStatusModel {
|
||||
return ExchangeStatusModel(
|
||||
providerId = value.providerId,
|
||||
status = ExchangeStatus.values().firstOrNull {
|
||||
it.name.lowercase() == value.externalStatus.name.lowercase()
|
||||
},
|
||||
txId = value.externalTxId,
|
||||
txUrl = value.externalTxUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.feature.swap.converters
|
||||
|
||||
import com.tangem.datasource.api.express.models.response.ExchangeDataResponse
|
||||
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> {
|
||||
|
||||
override fun convert(value: ExchangeDataResponse): SwapDataModel {
|
||||
return SwapDataModel(
|
||||
toTokenAmount = createFromAmountWithOffset(value.toAmount, value.toDecimals),
|
||||
transaction = convertTransaction(value),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertTransaction(transactionDto: 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,
|
||||
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,
|
||||
txTo = transactionDto.txTo,
|
||||
externalTxId = requireNotNull(transactionDto.externalTxId),
|
||||
externalTxUrl = requireNotNull(transactionDto.externalTxUrl),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.feature.swap.converters
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
class LeastTokenInfoConverter : Converter<CryptoCurrency, LeastTokenInfo> {
|
||||
|
||||
override fun convert(value: CryptoCurrency): LeastTokenInfo {
|
||||
return LeastTokenInfo(
|
||||
contractAddress = (value as? CryptoCurrency.Token)?.contractAddress ?: "0",
|
||||
network = Blockchain.fromId(value.id.rawNetworkId).toNetworkId(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
package com.tangem.feature.swap.converters
|
||||
|
||||
import com.tangem.datasource.api.oneinch.models.QuoteResponse
|
||||
import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
|
||||
import com.tangem.feature.swap.domain.models.domain.QuoteModel
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
class QuotesConverter : Converter<QuoteResponse, QuoteModel> {
|
||||
|
||||
override fun convert(value: QuoteResponse): QuoteModel {
|
||||
return QuoteModel(
|
||||
toTokenAmount = createFromAmountWithOffset(value.toTokenAmount, value.toToken.decimals),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.feature.swap.converters
|
||||
|
||||
import com.tangem.datasource.api.express.models.response.RateType
|
||||
import com.tangem.utils.converter.TwoWayConverter
|
||||
import com.tangem.feature.swap.domain.models.domain.RateType as RateTypeDomain
|
||||
|
||||
class RateTypeConverter : TwoWayConverter<RateType, RateTypeDomain> {
|
||||
|
||||
override fun convert(value: RateType): RateTypeDomain {
|
||||
return when (value) {
|
||||
RateType.FIXED -> RateTypeDomain.FIXED
|
||||
RateType.FLOAT -> RateTypeDomain.FLOAT
|
||||
}
|
||||
}
|
||||
|
||||
override fun convertBack(value: RateTypeDomain): RateType {
|
||||
return when (value) {
|
||||
RateTypeDomain.FIXED -> RateType.FIXED
|
||||
RateTypeDomain.FLOAT -> RateType.FLOAT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package com.tangem.feature.swap.converters
|
||||
|
||||
import com.tangem.datasource.api.oneinch.models.SwapResponse
|
||||
import com.tangem.datasource.api.oneinch.models.TransactionDto
|
||||
import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.domain.TransactionModel
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
class SwapConverter : Converter<SwapResponse, SwapDataModel> {
|
||||
|
||||
override fun convert(value: SwapResponse): SwapDataModel {
|
||||
return SwapDataModel(
|
||||
toTokenAmount = createFromAmountWithOffset(value.toTokenAmount, value.toToken.decimals),
|
||||
transaction = convertTransaction(value.transaction),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertTransaction(transactionDto: TransactionDto): TransactionModel {
|
||||
return TransactionModel(
|
||||
fromWalletAddress = transactionDto.fromAddress,
|
||||
toWalletAddress = transactionDto.toAddress,
|
||||
data = transactionDto.data,
|
||||
value = transactionDto.value,
|
||||
gasPrice = transactionDto.gasPrice,
|
||||
gas = transactionDto.gas,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
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.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>> {
|
||||
|
||||
private val rateTypeConverter = RateTypeConverter()
|
||||
|
||||
override fun convert(value: SwapPairsWithProviders): List<SwapPairDomain> {
|
||||
val providersAdditionalMap = value.providers.associateBy { it.id }
|
||||
return value.swapPair.map { pair ->
|
||||
SwapPairDomain(
|
||||
from = LeastTokenInfo(
|
||||
contractAddress = pair.from.contractAddress,
|
||||
network = pair.from.network,
|
||||
),
|
||||
to = LeastTokenInfo(
|
||||
contractAddress = pair.to.contractAddress,
|
||||
network = pair.to.network,
|
||||
),
|
||||
providers = pair.providers.mapNotNull {
|
||||
convertProvider(it, providersAdditionalMap)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertProvider(
|
||||
swapPairProvider: SwapPairProvider,
|
||||
providerAdditional: Map<String, ExchangeProvider>,
|
||||
): SwapPairProviderDomain? {
|
||||
val additionalProvider = providerAdditional[swapPairProvider.providerId] ?: return null
|
||||
return SwapPairProviderDomain(
|
||||
providerId = swapPairProvider.providerId,
|
||||
rateTypes = swapPairProvider.rateTypes.map { rateTypeConverter.convert(it) },
|
||||
name = additionalProvider.name,
|
||||
type = convertExchangeType(additionalProvider.type),
|
||||
imageLarge = additionalProvider.imageLargeUrl,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertExchangeType(type: ExchangeProviderType): ExchangeProviderTypeDomain {
|
||||
return when (type) {
|
||||
ExchangeProviderType.DEX -> ExchangeProviderTypeDomain.DEX
|
||||
ExchangeProviderType.CEX -> ExchangeProviderTypeDomain.CEX
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,20 @@
|
|||
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.oneinch.OneInchErrorsHandler
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
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.domain.SwapTransactionRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -16,25 +24,44 @@ import javax.inject.Singleton
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class SwapDataModule {
|
||||
internal class SwapDataModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSwapRepository(
|
||||
internal fun provideSwapRepository(
|
||||
tangemTechApi: TangemTechApi,
|
||||
tangemExpressApi: TangemExpressApi,
|
||||
oneInchApiFactory: OneInchApiFactory,
|
||||
oneInchErrorsHandler: OneInchErrorsHandler,
|
||||
coroutineDispatcher: CoroutineDispatcherProvider,
|
||||
configManager: ConfigManager,
|
||||
walletManagerFacade: WalletManagersFacade,
|
||||
walletsStateHolder: WalletsStateHolder,
|
||||
errorsDataConverter: ErrorsDataConverter,
|
||||
): SwapRepository {
|
||||
return SwapRepositoryImpl(
|
||||
tangemTechApi = tangemTechApi,
|
||||
tangemExpressApi = tangemExpressApi,
|
||||
oneInchApiFactory = oneInchApiFactory,
|
||||
oneInchErrorsHandler = oneInchErrorsHandler,
|
||||
coroutineDispatcher = coroutineDispatcher,
|
||||
configManager = configManager,
|
||||
walletManagersFacade = walletManagerFacade,
|
||||
walletsStateHolder = walletsStateHolder,
|
||||
errorsDataConverter = errorsDataConverter,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSwapTransactionRepository(appPreferencesStore: AppPreferencesStore): SwapTransactionRepository {
|
||||
return DefaultSwapTransactionRepository(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
internal fun provideErrorsConverter(@NetworkMoshi moshi: Moshi): ErrorsDataConverter {
|
||||
val jsonAdapter = moshi.adapter(ExpressErrorResponse::class.java)
|
||||
return ErrorsDataConverter(jsonAdapter)
|
||||
}
|
||||
}
|
||||
|
|
@ -23,9 +23,15 @@ dependencies {
|
|||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.demo)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/** Feature Apis */
|
||||
implementation(projects.features.wallet.api)
|
||||
|
|
@ -35,4 +41,6 @@ dependencies {
|
|||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.tangem.blockchain)
|
||||
implementation(deps.moshi)
|
||||
}
|
||||
|
|
@ -1,12 +1,9 @@
|
|||
package com.tangem.feature.swap.domain
|
||||
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
import com.tangem.feature.swap.domain.models.domain.NetworkInfo
|
||||
|
||||
interface BlockchainInteractor {
|
||||
|
||||
fun getTokenDecimals(token: Currency): Int
|
||||
|
||||
/**
|
||||
* In app blockchain id, actual in blockchain sdk, not the same as networkId
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
package com.tangem.feature.swap.domain
|
||||
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
import com.tangem.feature.swap.domain.models.domain.NetworkInfo
|
||||
import com.tangem.lib.crypto.TransactionManager
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class BlockchainInteractorImpl @Inject constructor(
|
||||
internal class DefaultBlockchainInteractor @Inject constructor(
|
||||
private val transactionManager: TransactionManager,
|
||||
) : BlockchainInteractor {
|
||||
|
||||
|
|
@ -22,12 +21,4 @@ internal class BlockchainInteractorImpl @Inject constructor(
|
|||
override fun getExplorerTransactionLink(networkId: String, txAddress: String): String {
|
||||
return transactionManager.getExplorerTransactionLink(networkId, txAddress)
|
||||
}
|
||||
|
||||
override fun getTokenDecimals(token: Currency): Int {
|
||||
return if (token is Currency.NonNativeToken) {
|
||||
token.decimalCount
|
||||
} else {
|
||||
transactionManager.getNativeTokenDecimals(token.networkId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.feature.swap.domain
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class DefaultInitialToCurrencyResolver(
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val swapTransactionRepository: SwapTransactionRepository,
|
||||
) : InitialToCurrencyResolver {
|
||||
|
||||
override suspend fun tryGetFromCache(
|
||||
initialCryptoCurrency: CryptoCurrency,
|
||||
state: TokensDataStateExpress,
|
||||
): CryptoCurrencyStatus? {
|
||||
val selectedId = getSelectedWalletSyncUseCase().getOrNull() ?: return null
|
||||
val id = swapTransactionRepository.getLastSwappedCryptoCurrencyId(selectedId.walletId) ?: return null
|
||||
|
||||
return if (id != initialCryptoCurrency.id.value) {
|
||||
state.toGroup.available.find { it.currencyStatus.currency.id.value == id }?.currencyStatus
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun tryGetWithMaxAmount(state: TokensDataStateExpress): CryptoCurrencyStatus? {
|
||||
return state.toGroup.available.maxByOrNull {
|
||||
it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO
|
||||
}?.currencyStatus
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.feature.swap.domain
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress
|
||||
|
||||
interface InitialToCurrencyResolver {
|
||||
|
||||
suspend fun tryGetFromCache(
|
||||
initialCryptoCurrency: CryptoCurrency,
|
||||
state: TokensDataStateExpress,
|
||||
): CryptoCurrencyStatus?
|
||||
|
||||
fun tryGetWithMaxAmount(state: TokensDataStateExpress): CryptoCurrencyStatus?
|
||||
}
|
||||
|
|
@ -1,42 +1,18 @@
|
|||
package com.tangem.feature.swap.domain
|
||||
|
||||
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.wallets.models.UserWallet
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
import com.tangem.feature.swap.domain.models.domain.PermissionOptions
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import com.tangem.feature.swap.domain.models.ui.*
|
||||
import java.math.BigDecimal
|
||||
|
||||
interface SwapInteractor {
|
||||
|
||||
fun initDerivationPathAndNetwork(derivationPath: String?, network: Network?)
|
||||
suspend fun getTokensDataState(currency: CryptoCurrency): TokensDataStateExpress
|
||||
|
||||
/**
|
||||
* Init tokens to swap, load tokens list available to swap for given network
|
||||
*
|
||||
* @param initialCurrency currency which to swap or receive
|
||||
* @return [TokensDataState] that contains info about all available to swap tokens for networkId
|
||||
* and preselected tokens which initially select to swap
|
||||
*/
|
||||
suspend fun initTokensToSwap(initialCurrency: Currency): TokensDataState
|
||||
|
||||
/**
|
||||
* On search token, locally search tokens in previously loaded list to swap
|
||||
* searching in names and symbols
|
||||
*
|
||||
* @param networkId networkId for tokens
|
||||
* @param searchQuery string query for search
|
||||
* @return [FoundTokensState] that contains list of tokens matching condition query
|
||||
*/
|
||||
suspend fun searchTokens(networkId: String, searchQuery: String): FoundTokensState
|
||||
|
||||
/**
|
||||
* Find specific token by id, null if not found
|
||||
*
|
||||
* @param id token id
|
||||
* @return [Currency] or null
|
||||
*/
|
||||
fun findTokenById(id: String): Currency?
|
||||
fun initDerivationPathAndNetwork(derivationPath: String?, network: Network)
|
||||
|
||||
/**
|
||||
* Gives permission to swap, this starts scan card process
|
||||
|
|
@ -61,11 +37,12 @@ interface SwapInteractor {
|
|||
@Throws(IllegalStateException::class)
|
||||
suspend fun findBestQuote(
|
||||
networkId: String,
|
||||
fromToken: Currency,
|
||||
toToken: Currency,
|
||||
fromToken: CryptoCurrencyStatus,
|
||||
toToken: CryptoCurrencyStatus,
|
||||
providers: List<SwapProvider>,
|
||||
amountToSwap: String,
|
||||
selectedFee: FeeType = FeeType.NORMAL,
|
||||
): SwapState
|
||||
): Map<SwapProvider, SwapState>
|
||||
|
||||
/**
|
||||
* Starts swap transaction, perform sign transaction
|
||||
|
|
@ -81,30 +58,39 @@ interface SwapInteractor {
|
|||
@Suppress("LongParameterList")
|
||||
@Throws(IllegalStateException::class)
|
||||
suspend fun onSwap(
|
||||
swapProvider: SwapProvider,
|
||||
networkId: String,
|
||||
swapStateData: SwapStateData,
|
||||
currencyToSend: Currency,
|
||||
currencyToGet: Currency,
|
||||
swapData: SwapDataModel?,
|
||||
currencyToSend: CryptoCurrencyStatus,
|
||||
currencyToGet: CryptoCurrencyStatus,
|
||||
amountToSwap: String,
|
||||
includeFeeInAmount: IncludeFeeInAmount,
|
||||
fee: TxFee,
|
||||
): TxState
|
||||
|
||||
suspend fun updateQuotesStateWithSelectedFee(
|
||||
state: SwapState.QuotesLoadedState,
|
||||
selectedFee: FeeType,
|
||||
fromToken: CryptoCurrencyStatus,
|
||||
amountToSwap: String,
|
||||
networkId: String,
|
||||
): SwapState.QuotesLoadedState
|
||||
|
||||
/**
|
||||
* Returns token in wallet balance
|
||||
*
|
||||
* @param networkId
|
||||
* @param token
|
||||
*/
|
||||
fun getTokenBalance(networkId: String, token: Currency): SwapAmount
|
||||
fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount
|
||||
|
||||
fun isAvailableToSwap(networkId: String): Boolean
|
||||
|
||||
fun getSwapAmountForToken(amount: String, token: Currency): SwapAmount
|
||||
fun getSelectedWallet(): UserWallet?
|
||||
|
||||
suspend fun checkFeeIsEnough(
|
||||
fee: BigDecimal?,
|
||||
spendAmount: SwapAmount,
|
||||
networkId: String,
|
||||
fromToken: Currency,
|
||||
): Boolean
|
||||
suspend fun selectInitialCurrencyToSwap(
|
||||
initialCryptoCurrency: CryptoCurrency,
|
||||
state: TokensDataStateExpress,
|
||||
): CryptoCurrencyStatus?
|
||||
|
||||
fun getNativeToken(networkId: String): CryptoCurrency
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,26 +1,33 @@
|
|||
package com.tangem.feature.swap.domain
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
import com.tangem.feature.swap.domain.models.domain.QuoteModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
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 getRates(currencyId: String, tokenIds: List<String>): Map<String, Double>
|
||||
|
||||
suspend fun getExchangeableTokens(networkId: String): List<Currency>
|
||||
|
||||
suspend fun getExchangeStatus(txId: String): Either<UnknownError, ExchangeStatusModel>
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun findBestQuote(
|
||||
networkId: String,
|
||||
fromTokenAddress: String,
|
||||
toTokenAddress: String,
|
||||
amount: String,
|
||||
fromContractAddress: String,
|
||||
fromNetwork: String,
|
||||
toContractAddress: String,
|
||||
toNetwork: String,
|
||||
fromAmount: String,
|
||||
fromDecimals: Int,
|
||||
toDecimals: Int,
|
||||
providerId: String,
|
||||
rateType: RateType,
|
||||
): AggregatedSwapDataModel<QuoteModel>
|
||||
|
||||
/**
|
||||
|
|
@ -30,24 +37,13 @@ interface SwapRepository {
|
|||
*/
|
||||
suspend fun addressForTrust(networkId: String): String
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun prepareSwapTransaction(
|
||||
networkId: String,
|
||||
fromTokenAddress: String,
|
||||
toTokenAddress: String,
|
||||
amount: String,
|
||||
fromWalletAddress: String,
|
||||
slippage: Int,
|
||||
): AggregatedSwapDataModel<SwapDataModel>
|
||||
|
||||
/**
|
||||
* Returns a tangem fee for swap in percents
|
||||
* Example: 0.35%
|
||||
*/
|
||||
fun getTangemFee(): Double
|
||||
|
||||
suspend fun getCryptoCurrency(userWallet: UserWallet, currency: Currency, network: Network): CryptoCurrency?
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Throws(IllegalStateException::class)
|
||||
suspend fun getAllowance(
|
||||
userWalletId: UserWalletId,
|
||||
|
|
@ -55,14 +51,33 @@ interface SwapRepository {
|
|||
derivationPath: String?,
|
||||
tokenDecimalCount: Int,
|
||||
tokenAddress: String,
|
||||
spenderAddress: String,
|
||||
): BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Throws(IllegalStateException::class)
|
||||
suspend fun getApproveData(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: String,
|
||||
derivationPath: String?,
|
||||
currency: Currency,
|
||||
currency: CryptoCurrency,
|
||||
amount: BigDecimal?,
|
||||
spenderAddress: String,
|
||||
): String
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun getExchangeData(
|
||||
fromContractAddress: String,
|
||||
fromNetwork: String,
|
||||
toContractAddress: String,
|
||||
toNetwork: String,
|
||||
fromAmount: String,
|
||||
fromDecimals: Int,
|
||||
toDecimals: Int,
|
||||
providerId: String,
|
||||
rateType: RateType,
|
||||
toAddress: String,
|
||||
): AggregatedSwapDataModel<SwapDataModel>
|
||||
|
||||
fun getNativeTokenForNetwork(networkId: String): CryptoCurrency
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.feature.swap.domain
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface SwapTransactionRepository {
|
||||
|
||||
suspend fun storeTransaction(
|
||||
userWalletId: UserWalletId,
|
||||
fromCryptoCurrencyId: CryptoCurrency.ID,
|
||||
toCryptoCurrencyId: CryptoCurrency.ID,
|
||||
transaction: SavedSwapTransactionModel,
|
||||
)
|
||||
|
||||
fun getTransactions(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencyId: CryptoCurrency.ID,
|
||||
): Flow<List<SavedSwapTransactionListModel>?>
|
||||
|
||||
suspend fun removeTransaction(
|
||||
userWalletId: UserWalletId,
|
||||
fromCryptoCurrencyId: CryptoCurrency.ID,
|
||||
toCryptoCurrencyId: CryptoCurrency.ID,
|
||||
txId: String,
|
||||
)
|
||||
|
||||
suspend fun storeLastSwappedCryptoCurrencyId(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID)
|
||||
|
||||
suspend fun getLastSwappedCryptoCurrencyId(userWalletId: UserWalletId): String?
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.feature.swap.domain.cache
|
||||
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
import com.tangem.feature.swap.domain.models.ui.TokenWithBalance
|
||||
import java.math.BigDecimal
|
||||
|
||||
interface SwapDataCache {
|
||||
|
||||
fun cacheAvailableToSwapTokens(networkId: String, tokens: List<Currency>)
|
||||
fun cacheInWalletTokens(tokens: List<TokenWithBalance>)
|
||||
fun cacheLoadedTokens(tokens: List<TokenWithBalance>)
|
||||
fun cacheBalances(networkId: String, derivationPath: String?, balances: Map<String, SwapAmount>)
|
||||
fun cacheLastFeeForNetwork(fee: BigDecimal, networkId: String)
|
||||
fun getAvailableTokens(networkId: String): List<Currency>
|
||||
fun getInWalletTokens(): List<TokenWithBalance>
|
||||
fun getLoadedTokens(): List<TokenWithBalance>
|
||||
fun getBalanceForToken(networkId: String, derivationPath: String?, symbol: String): SwapAmount?
|
||||
fun getLastFeeForNetwork(networkId: String): BigDecimal?
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
package com.tangem.feature.swap.domain.cache
|
||||
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency
|
||||
import com.tangem.feature.swap.domain.models.ui.TokenWithBalance
|
||||
import java.math.BigDecimal
|
||||
|
||||
class SwapDataCacheImpl : SwapDataCache {
|
||||
|
||||
private val availableTokensForNetwork: MutableMap<String, List<Currency>> = mutableMapOf()
|
||||
private val feesForNetworks: MutableMap<String, BigDecimal> = mutableMapOf()
|
||||
private val tokensBalances: MutableMap<String, Map<String, SwapAmount>> = mutableMapOf()
|
||||
private val lastInWalletTokens = mutableListOf<TokenWithBalance>()
|
||||
private val lastLoadedTokens = mutableListOf<TokenWithBalance>()
|
||||
|
||||
override fun cacheLastFeeForNetwork(fee: BigDecimal, networkId: String) {
|
||||
feesForNetworks[networkId] = fee
|
||||
}
|
||||
|
||||
override fun cacheInWalletTokens(tokens: List<TokenWithBalance>) {
|
||||
lastInWalletTokens.clear()
|
||||
lastInWalletTokens.addAll(tokens)
|
||||
}
|
||||
|
||||
override fun cacheLoadedTokens(tokens: List<TokenWithBalance>) {
|
||||
lastLoadedTokens.clear()
|
||||
lastLoadedTokens.addAll(tokens)
|
||||
}
|
||||
|
||||
override fun getInWalletTokens(): List<TokenWithBalance> {
|
||||
return lastInWalletTokens
|
||||
}
|
||||
|
||||
override fun getLoadedTokens(): List<TokenWithBalance> {
|
||||
return lastLoadedTokens
|
||||
}
|
||||
|
||||
override fun getBalanceForToken(networkId: String, derivationPath: String?, symbol: String): SwapAmount? {
|
||||
return tokensBalances[createKeyFrom(networkId, derivationPath)]?.get(symbol)
|
||||
}
|
||||
|
||||
override fun cacheBalances(networkId: String, derivationPath: String?, balances: Map<String, SwapAmount>) {
|
||||
tokensBalances[createKeyFrom(networkId, derivationPath)] = balances
|
||||
}
|
||||
|
||||
override fun cacheAvailableToSwapTokens(networkId: String, tokens: List<Currency>) {
|
||||
availableTokensForNetwork[networkId] = tokens
|
||||
}
|
||||
|
||||
override fun getLastFeeForNetwork(networkId: String): BigDecimal? {
|
||||
return feesForNetworks[networkId]
|
||||
}
|
||||
|
||||
override fun getAvailableTokens(networkId: String): List<Currency> {
|
||||
return availableTokensForNetwork.getOrElse(networkId) { emptyList() }
|
||||
}
|
||||
|
||||
private fun createKeyFrom(networkId: String, derivationPath: String?): String {
|
||||
return "$networkId;$derivationPath"
|
||||
}
|
||||
}
|
||||
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