Updated on 2026-08-14

This commit is contained in:
Tangem 2023-01-09 13:56:57 +03:00
parent 30c34a9e14
commit 01e5b6d95a
15 changed files with 322 additions and 124 deletions

View file

@ -11,6 +11,7 @@ import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.*
//todo move extensions to utils
fun BigDecimal.toFormattedString(
decimals: Int, roundingMode: RoundingMode = RoundingMode.DOWN, locale: Locale = Locale.US
): String {

View file

@ -25,6 +25,7 @@ val navigationMiddleware: Middleware<AppState> = { _, state ->
screen = action.screen,
addToBackstack = action.addToBackstack,
fgShareTransition = action.fragmentShareTransition,
bundle = action.bundle,
)
}
is NavigationAction.PopBackTo -> {

View file

@ -33,7 +33,6 @@ class TransactionManagerImpl(
private val appStateHolder: AppStateHolder,
) : TransactionManager {
@Throws(IllegalStateException::class)
override suspend fun sendTransaction(
networkId: String,
amountToSend: BigDecimal,
@ -63,6 +62,10 @@ class TransactionManagerImpl(
return handleSendResult(sendResult)
}
override fun getNativeTokenDecimals(networkId: String): Int {
return Blockchain.fromNetworkId(networkId)?.decimals() ?: error("blockchain not found")
}
@Throws(IllegalStateException::class)
override suspend fun getFee(
networkId: String,

View file

@ -0,0 +1,21 @@
package com.tangem.utils
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.*
fun BigDecimal.toFormattedString(
decimals: Int, roundingMode: RoundingMode = RoundingMode.DOWN, locale: Locale = Locale.US,
): String {
val symbols = DecimalFormatSymbols(locale)
val df = DecimalFormat().apply {
decimalFormatSymbols = symbols
maximumFractionDigits = decimals
minimumFractionDigits = 0
isGroupingUsed = false
this.roundingMode = roundingMode
}
return df.format(this)
}

View file

@ -2,6 +2,7 @@ package com.tangem.feature.swap.converters
import com.tangem.datasource.api.oneinch.models.QuoteResponse
import com.tangem.feature.swap.domain.models.data.SwapState.QuoteModel
import com.tangem.feature.swap.domain.models.data.createFromAmountWithOffset
import com.tangem.utils.converter.Converter
import javax.inject.Inject
@ -9,8 +10,10 @@ class QuotesConverter @Inject constructor() : Converter<QuoteResponse, QuoteMode
override fun convert(value: QuoteResponse): QuoteModel {
return QuoteModel(
fromTokenAmount = value.fromTokenAmount,
toTokenAmount = value.toTokenAmount,
fromTokenAmount = createFromAmountWithOffset(value.fromTokenAmount, value.fromToken.decimals),
toTokenAmount = createFromAmountWithOffset(value.toTokenAmount, value.toToken.decimals),
fromTokenAddress = value.fromToken.address,
toTokenAddress = value.toToken.address,
estimatedGas = value.estimatedGas,
)
}

View file

@ -1,8 +1,8 @@
package com.tangem.feature.swap.domain
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.SwapAmount
import com.tangem.feature.swap.domain.models.data.SwapState
import java.math.BigDecimal
interface SwapInteractor {
@ -10,15 +10,18 @@ interface SwapInteractor {
suspend fun getTokenBalance(tokenId: String): String
suspend fun getFee(): BigDecimal
@Throws(IllegalStateException::class)
suspend fun givePermissionToSwap(tokenToApprove: Currency)
@Throws(IllegalStateException::class)
suspend fun findBestQuote(
fromToken: Currency,
toToken: Currency,
amount: String,
amount: SwapAmount,
): SwapState
@Throws(IllegalStateException::class)
suspend fun onSwap(): SwapState
fun getTokenDecimals(token: Currency): Int
}

View file

@ -4,8 +4,10 @@ import com.tangem.feature.swap.domain.cache.SwapDataCache
import com.tangem.feature.swap.domain.converters.CryptoCurrencyConverter
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.DataError
import com.tangem.feature.swap.domain.models.data.SwapAmount
import com.tangem.feature.swap.domain.models.data.SwapState
import com.tangem.feature.swap.domain.models.data.TransactionModel
import com.tangem.feature.swap.domain.models.data.toStringWithRightOffset
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
import java.math.BigDecimal
@ -34,23 +36,6 @@ internal class SwapInteractorImpl @Inject constructor(
TODO("Not yet implemented")
}
override suspend fun getFee(): BigDecimal {
return transactionManager.getFee(
networkId = cache.getNetworkId() ?: error("no networkId found, call getTokensToSwap first"),
amountToSend = cache.getAmountToSwap() ?: error("no amount found, call findBestQuote first"),
currencyToSend = cryptoCurrencyConverter.convert(
cache.getExchangeCurrencies()?.fromCurrency ?: error(
"no fromCurrency found, call findBestQuote first",
),
),
destinationAddress = getTokenAddress(
cache.getExchangeCurrencies()?.toCurrency ?: error(
"no toCurrency found, call findBestQuote first",
),
),
).value
}
override suspend fun givePermissionToSwap(tokenToApprove: Currency) {
cache.getNetworkId()?.let { networkId ->
if (tokenToApprove is Currency.NonNativeToken) {
@ -71,11 +56,10 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
override suspend fun findBestQuote(fromToken: Currency, toToken: Currency, amount: String): SwapState {
override suspend fun findBestQuote(fromToken: Currency, toToken: Currency, amount: SwapAmount): SwapState {
val networkId = cache.getNetworkId()
val fromTokenAddress = getTokenAddress(fromToken)
val toTokenAddress = getTokenAddress(toToken)
val bigDecimalAmount = amount.toBigDecimalOrNull() ?: error("wrong amount format, use only digits")
val isAllowedToSpend = if (networkId.isNullOrEmpty()) {
error("no networkId found, please call getTokensToSwap first")
} else {
@ -91,13 +75,13 @@ internal class SwapInteractorImpl @Inject constructor(
networkId = networkId,
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
amount = amount,
amount = amount.toStringWithRightOffset(),
).let { quotes ->
val quoteDataModel = quotes.dataModel
if (quoteDataModel != null) {
cache.cacheSwapParams(
quoteModel = quoteDataModel.copy(isAllowedToSpend = isAllowedToSpend),
amount = bigDecimalAmount,
amount = amount,
fromCurrency = fromToken,
toCurrency = toToken,
)
@ -115,9 +99,9 @@ internal class SwapInteractorImpl @Inject constructor(
if (quoteModel != null && amountToSwap != null && networkId != null) {
repository.prepareSwapTransaction(
networkId = networkId,
fromTokenAddress = quoteModel.fromTokenAmount,
toTokenAddress = quoteModel.toTokenAmount,
amount = amountToSwap.toPlainString(),
fromTokenAddress = quoteModel.fromTokenAddress,
toTokenAddress = quoteModel.toTokenAddress,
amount = amountToSwap.toStringWithRightOffset(),
slippage = DEFAULT_SLIPPAGE,
fromWalletAddress = getWalletAddress(networkId),
).let {
@ -137,6 +121,14 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
override fun getTokenDecimals(token: Currency): Int {
return if (token is Currency.NonNativeToken) {
token.decimalCount
} else {
transactionManager.getNativeTokenDecimals(token.networkId)
}
}
private fun getWalletAddress(networkId: String): String {
return userWalletManager.getWalletAddress(networkId)
}

View file

@ -2,17 +2,17 @@ package com.tangem.feature.swap.domain.cache
import com.tangem.feature.swap.domain.models.ExchangeCurrencies
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.SwapAmount
import com.tangem.feature.swap.domain.models.data.SwapState.QuoteModel
import java.math.BigDecimal
interface SwapDataCache {
fun cacheNetworkId(networkId: String)
fun cacheSwapParams(quoteModel: QuoteModel, amount: BigDecimal, fromCurrency: Currency, toCurrency: Currency)
fun cacheSwapParams(quoteModel: QuoteModel, amount: SwapAmount, fromCurrency: Currency, toCurrency: Currency)
fun cacheAvailableToSwapTokens(networkId: String, tokens: List<Currency>)
fun getAvailableTokens(networkId: String): List<Currency>
fun getLastQuote(): QuoteModel?
fun getAmountToSwap(): BigDecimal?
fun getAmountToSwap(): SwapAmount?
fun getNetworkId(): String?
fun getExchangeCurrencies(): ExchangeCurrencies?
}

View file

@ -3,8 +3,8 @@ package com.tangem.feature.swap.domain.cache
import com.tangem.feature.swap.domain.models.ExchangeCurrencies
import com.tangem.feature.swap.domain.models.SwapDataHolder
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.SwapAmount
import com.tangem.feature.swap.domain.models.data.SwapState.QuoteModel
import java.math.BigDecimal
class SwapDataCacheImpl : SwapDataCache {
@ -13,7 +13,7 @@ class SwapDataCacheImpl : SwapDataCache {
override fun cacheSwapParams(
quoteModel: QuoteModel,
amount: BigDecimal,
amount: SwapAmount,
fromCurrency: Currency,
toCurrency: Currency,
) {
@ -52,7 +52,7 @@ class SwapDataCacheImpl : SwapDataCache {
return lastDataForSwap.quoteModel
}
override fun getAmountToSwap(): BigDecimal? {
override fun getAmountToSwap(): SwapAmount? {
return lastDataForSwap.amountToSwap
}
}

View file

@ -1,12 +1,13 @@
package com.tangem.feature.swap.domain.models
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.SwapAmount
import com.tangem.feature.swap.domain.models.data.SwapState.QuoteModel
import java.math.BigDecimal
data class SwapDataHolder(
val quoteModel: QuoteModel? = null,
val amountToSwap: BigDecimal? = null,
val amountToSwap: SwapAmount? = null,
val networkId: String? = null,
val exchangeCurrencies: ExchangeCurrencies? = null,
)

View file

@ -0,0 +1,41 @@
package com.tangem.feature.swap.domain.models.data
import com.tangem.utils.toFormattedString
import java.math.BigDecimal
data class SwapAmount(
val value: BigDecimal,
val decimals: Int,
)
fun SwapAmount.formatToUIRepresentation(): String {
return value.toFormattedString(decimals = decimals)
}
fun createFromAmountWithoutOffset(amountWithoutOffset: String, decimals: Int): SwapAmount {
return SwapAmount(
value = requireNotNull(
amountWithoutOffset.toBigDecimalOrNull(),
) { "wrong amount format, use only digits" },
decimals = decimals,
)
}
fun createFromAmountWithOffset(amountWithOffset: String, decimals: Int): SwapAmount {
return SwapAmount(
value = requireNotNull(
amountWithOffset.toBigDecimalOrNull()?.movePointLeft(decimals),
) { "wrong amount format, use only digits" },
decimals = decimals,
)
}
@Throws(IllegalStateException::class)
fun SwapAmount.toStringWithRightOffset(): String {
return value.movePointRight(decimals).toPlainString()
}
@Throws(IllegalStateException::class)
fun SwapAmount.toStringWithLeftOffset(): String {
return value.movePointLeft(decimals).toPlainString()
}

View file

@ -3,15 +3,17 @@ package com.tangem.feature.swap.domain.models.data
sealed interface SwapState {
data class QuoteModel(
val fromTokenAmount: String,
val toTokenAmount: String,
val fromTokenAmount: SwapAmount,
val toTokenAmount: SwapAmount,
val fromTokenAddress: String,
val toTokenAddress: String,
val estimatedGas: Int,
val isAllowedToSpend: Boolean = false,
) : SwapState
data class SwapSuccess(
val fromTokenAmount: String,
val toTokenAmount: String,
val fromTokenAmount: SwapAmount,
val toTokenAmount: SwapAmount,
) : SwapState
data class SwapError(

View file

@ -0,0 +1,110 @@
package com.tangem.feature.swap.ui
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.SwapState
import com.tangem.feature.swap.domain.models.data.formatToUIRepresentation
import com.tangem.feature.swap.models.FeeState
import com.tangem.feature.swap.models.SwapButton
import com.tangem.feature.swap.models.SwapCardData
import com.tangem.feature.swap.models.SwapStateHolder
import com.tangem.feature.swap.models.SwapWarning
import com.tangem.feature.swap.models.TransactionCardType
/**
* State builder creates a specific states for SwapScreen
*/
class StateBuilder {
fun createInitialLoadingState(networkCurrency: String, onAmountChanged: (String) -> Unit): SwapStateHolder {
return SwapStateHolder(
sendCardData = SwapCardData(
type = TransactionCardType.SendCard("", true, onAmountChanged),
amount = null,
amountEquivalent = null,
tokenIconUrl = "",
tokenCurrency = "",
canSelectAnotherToken = false,
),
receiveCardData = SwapCardData(
type = TransactionCardType.ReceiveCard(),
amount = null,
amountEquivalent = null,
tokenIconUrl = "",
tokenCurrency = "",
canSelectAnotherToken = false,
),
fee = FeeState.Loading,
networkCurrency = networkCurrency,
swapButton = SwapButton(enabled = false, loading = true, onClick = {}),
onRefresh = {},
onBackClicked = {},
onChangeCardsClicked = {},
)
}
fun createQuotesLoadingState(
uiStateHolder: SwapStateHolder,
fromToken: Currency,
toToken: Currency,
mainTokenId: String,
): SwapStateHolder {
return uiStateHolder.copy(
sendCardData = SwapCardData(
type = (uiStateHolder.sendCardData.type as TransactionCardType.SendCard),
amount = uiStateHolder.sendCardData.amount,
amountEquivalent = uiStateHolder.sendCardData.amountEquivalent,
tokenIconUrl = fromToken.logoUrl,
tokenCurrency = fromToken.symbol,
canSelectAnotherToken = mainTokenId != fromToken.id,
),
receiveCardData = SwapCardData(
type = TransactionCardType.ReceiveCard(),
amount = uiStateHolder.receiveCardData.amount,
amountEquivalent = uiStateHolder.receiveCardData.amountEquivalent,
tokenIconUrl = toToken.logoUrl,
tokenCurrency = toToken.symbol,
canSelectAnotherToken = mainTokenId != toToken.id,
),
fee = FeeState.Loading,
swapButton = SwapButton(enabled = false, loading = true, onClick = {}),
)
}
fun createQuotesLoadedState(
uiStateHolder: SwapStateHolder,
quoteModel: SwapState.QuoteModel,
fromToken: Currency,
onSwapClick: () -> Unit,
): SwapStateHolder {
return uiStateHolder.copy(
sendCardData = SwapCardData(
type = (uiStateHolder.sendCardData.type as TransactionCardType.SendCard)
.copy(permissionIsGiven = quoteModel.isAllowedToSpend),
amount = quoteModel.fromTokenAmount.formatToUIRepresentation(),
amountEquivalent = uiStateHolder.sendCardData.amountEquivalent,
tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl,
tokenCurrency = uiStateHolder.sendCardData.tokenCurrency,
canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken,
),
receiveCardData = SwapCardData(
type = TransactionCardType.ReceiveCard(),
amount = quoteModel.toTokenAmount.formatToUIRepresentation(),
amountEquivalent = uiStateHolder.receiveCardData.amountEquivalent,
tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl,
tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency,
canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken,
),
warnings = if (!quoteModel.isAllowedToSpend) {
listOf(SwapWarning.PermissionNeeded(fromToken.symbol))
} else {
emptyList()
},
fee = FeeState.Loaded(quoteModel.estimatedGas.toString()),
swapButton = SwapButton(
enabled = quoteModel.isAllowedToSpend,
loading = false,
onClick = onSwapClick,
),
)
}
}

View file

@ -4,38 +4,44 @@ import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.feature.swap.models.ApprovePermissionButton
import com.tangem.feature.swap.models.CancelPermissionButton
import com.tangem.feature.swap.models.FeeState
import com.tangem.feature.swap.models.SwapButton
import com.tangem.feature.swap.models.SwapCardData
import com.tangem.feature.swap.models.SwapPermissionStateHolder
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.SwapState
import com.tangem.feature.swap.domain.models.data.createFromAmountWithoutOffset
import com.tangem.feature.swap.models.SwapStateHolder
import com.tangem.feature.swap.models.SwapSuccessStateHolder
import com.tangem.feature.swap.models.SwapWarning
import com.tangem.feature.swap.models.TokenBalanceData
import com.tangem.feature.swap.models.TokenToSelect
import com.tangem.feature.swap.models.TransactionCardType
import com.tangem.feature.swap.presentation.R
import com.tangem.feature.swap.presentation.SwapFragment
import com.tangem.feature.swap.router.SwapRouter
import com.tangem.feature.swap.router.SwapScreen
import com.tangem.feature.swap.ui.StateBuilder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import javax.inject.Inject
import kotlin.properties.Delegates
@HiltViewModel
internal class SwapViewModel @Inject constructor(
private val referralInteractor: SwapInteractor,
private val swapInteractor: SwapInteractor,
private val dispatchers: CoroutineDispatcherProvider,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
var uiState: SwapStateHolder by mutableStateOf(createInitialUiState())
private val stateBuilder = StateBuilder()
private val currency = Json.decodeFromString<Currency>(
savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY]
?: error("no expected parameter Currency found"),
)
var uiState: SwapStateHolder by mutableStateOf(
stateBuilder.createInitialLoadingState(currency.symbol)
{ /** [REDACTED_TODO_COMMENT]*/ },
)
private set
private var swapRouter: SwapRouter by Delegates.notNull()
@ -52,78 +58,87 @@ internal class SwapViewModel @Inject constructor(
)
}
private fun createInitialUiState(): SwapStateHolder { // TODO: create using real state
return createTestUiState()
}
// region temp for test
init {
viewModelScope.launch {
viewModelScope.launch(dispatchers.main) {
runCatching {
val tokens = referralInteractor.getTokensToSwap("binance-smart-chain")
val token = tokens[1]
val token2 = tokens[2]
}.onFailure {
Log.e("SwapViewModel", it.message ?: it.cause.toString())
withContext(dispatchers.io) {
swapInteractor.getTokensToSwap(currency.networkId)
}
}
.onSuccess { tokens ->
if (tokens.size > MIN_TOKENS_IN_LIST) {
tokens.firstOrNull { token ->
token.id == currency.id
}?.let {
loadQuotes(it, tokens[1], INITIAL_AMOUNT)
}
}
}
.onFailure {
Log.e("SwapViewModel", it.message ?: it.cause.toString())
}
}
}
private fun createTestUiState(): SwapStateHolder {
val sendCard = SwapCardData(
type = TransactionCardType.SendCard("123", false, {}),
amount = "1 000 000 000 000 000 000",
amountEquivalent = "1 000 000 000 000 000 000",
tokenIconUrl = "",
tokenCurrency = "DAI",
networkIconRes = R.drawable.img_polygon_22,
)
val receiveCard = SwapCardData(
type = TransactionCardType.ReceiveCard(),
amount = "1 000 000",
amountEquivalent = "1 000 000",
tokenIconUrl = "",
tokenCurrency = "DAI",
networkIconRes = R.drawable.img_polygon_22,
canSelectAnotherToken = true,
)
val permissionState = SwapPermissionStateHolder(
currency = "DAI",
amount = "",
walletAddress = "",
spenderAddress = "",
fee = "2,14$",
approveButton = ApprovePermissionButton(true) {},
cancelButton = CancelPermissionButton(true) {},
)
val token = TokenToSelect(
id = "USDC",
name = "USDC",
symbol = "USDC",
iconUrl = "",
addedTokenBalanceData = TokenBalanceData(amount = "15 000 $", amountEquivalent = "15 000 USDT"),
)
val selectTokenState = SwapSelectTokenStateHolder(
listOf(token, token, token, token, token, token, token, token, token),
listOf(token, token, token, token, token, token, token, token, token), {}, {},
)
return SwapStateHolder(
sendCardData = sendCard,
receiveCardData = receiveCard,
fee = FeeState.Loaded(fee = "0.155 MATIC (0.14 $)"),
warnings = listOf(SwapWarning.PermissionNeeded("DAI")),
networkCurrency = "MATIC",
swapButton = SwapButton(enabled = true, loading = false, onClick = {}),
onRefresh = {}, onBackClicked = {}, onChangeCardsClicked = {},
permissionState = permissionState,
successState = SwapSuccessStateHolder("Success"),
selectTokenState = selectTokenState,
)
private fun loadQuotes(fromToken: Currency, toToken: Currency, amount: String) {
uiState = stateBuilder.createQuotesLoadingState(uiState, fromToken, toToken, currency.id)
viewModelScope.launch(dispatchers.main) {
runCatching {
withContext(dispatchers.io) {
swapInteractor.findBestQuote(
fromToken = fromToken,
toToken = toToken,
amount = createFromAmountWithoutOffset(
amountWithoutOffset = amount,
decimals = swapInteractor.getTokenDecimals(fromToken),
),
)
}
}
.onSuccess { swapState ->
when (swapState) {
is SwapState.QuoteModel -> {
uiState = stateBuilder.createQuotesLoadedState(uiState, swapState, fromToken) {
onSwapClick(toToken, swapState)
}
}
is SwapState.SwapSuccess -> {
//todo implement
}
is SwapState.SwapError -> {
}
}
}
.onFailure { }
}
}
private fun onSwapClick(toToken: Currency, quoteModel: SwapState.QuoteModel) {
viewModelScope.launch(dispatchers.main) {
runCatching {
if (!quoteModel.isAllowedToSpend) {
swapInteractor.givePermissionToSwap(toToken)
} else {
swapInteractor.onSwap()
}
}
.onSuccess {
if (it is SwapState) {
when (it) {
is SwapState.SwapSuccess -> {
}
is SwapState.SwapError -> {
}
else -> {}
}
}
}
.onFailure { }
}
}
companion object {
private const val MIN_TOKENS_IN_LIST = 2
private const val INITIAL_AMOUNT = "1"
}
// endregion temp for test
}

View file

@ -7,6 +7,7 @@ import java.math.BigDecimal
interface TransactionManager {
@Throws(IllegalStateException::class)
suspend fun sendTransaction(
networkId: String,
amountToSend: BigDecimal,
@ -16,6 +17,7 @@ interface TransactionManager {
dataToSign: String,
): SendTxResult
@Throws(IllegalStateException::class)
suspend fun getFee(
networkId: String,
amountToSend: BigDecimal,
@ -24,4 +26,7 @@ interface TransactionManager {
): ProxyAmount
fun getNativeAddress(networkId: String): String
@Throws(IllegalStateException::class)
fun getNativeTokenDecimals(networkId: String): Int
}