Updated on 2026-08-14
This commit is contained in:
parent
3baec6cc7e
commit
8564f21d01
9 changed files with 369 additions and 83 deletions
|
|
@ -26,7 +26,7 @@ class DefaultGaslessTransactionRepository(
|
|||
private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
|
||||
) : GaslessTransactionRepository {
|
||||
|
||||
private val supportedTokensState = MutableStateFlow<Set<CryptoCurrency>?>(null)
|
||||
private val supportedTokensState = MutableStateFlow<Map<Network.ID, Set<CryptoCurrency>>>(hashMapOf())
|
||||
private val gaslessTransactionRequestBuilder = GaslessTransactionRequestBuilder()
|
||||
private val signedTransactionResultConverter = GaslessSignedTransactionResultConverter()
|
||||
|
||||
|
|
@ -37,28 +37,37 @@ class DefaultGaslessTransactionRepository(
|
|||
|
||||
override suspend fun getSupportedTokens(network: Network): Set<CryptoCurrency> {
|
||||
return withContext(coroutineDispatcherProvider.io) {
|
||||
val storedTokens = supportedTokensState.value
|
||||
val storedTokens = supportedTokensState.value[network.id]
|
||||
if (storedTokens != null && storedTokens.isNotEmpty()) {
|
||||
return@withContext storedTokens
|
||||
}
|
||||
|
||||
val supportedTokensData = gaslessTxServiceApi.getSupportedTokens().getOrThrow()
|
||||
if (supportedTokensData.isSuccess) {
|
||||
val supportedTokens = supportedTokensData.result.tokens.mapNotNull { token ->
|
||||
val blockchain = Blockchain.fromChainId(token.chainId) ?: return@mapNotNull null
|
||||
responseCryptoCurrenciesFactory.createToken(
|
||||
blockchain = blockchain,
|
||||
sdkToken = Token(
|
||||
contractAddress = token.tokenAddress,
|
||||
name = token.tokenName,
|
||||
symbol = token.tokenSymbol,
|
||||
decimals = token.decimals,
|
||||
),
|
||||
network = network,
|
||||
)
|
||||
}.toSet()
|
||||
val networkBlockchain = Blockchain.fromNetworkId(network.backendId)
|
||||
?: error("Cannot determine blockchain for network id: ${network.backendId}")
|
||||
val supportedTokens = supportedTokensData.result.tokens
|
||||
.filter {
|
||||
it.chainId == networkBlockchain.getChainId()
|
||||
}
|
||||
.map { token ->
|
||||
responseCryptoCurrenciesFactory.createToken(
|
||||
blockchain = networkBlockchain,
|
||||
sdkToken = Token(
|
||||
contractAddress = token.tokenAddress,
|
||||
name = token.tokenName,
|
||||
symbol = token.tokenSymbol,
|
||||
decimals = token.decimals,
|
||||
),
|
||||
network = network,
|
||||
)
|
||||
}.toSet()
|
||||
// update local cache
|
||||
supportedTokensState.update { supportedTokens }
|
||||
supportedTokensState.update { current ->
|
||||
val newMap = current.toMutableMap()
|
||||
newMap[network.id] = supportedTokens
|
||||
newMap
|
||||
}
|
||||
return@withContext supportedTokens
|
||||
} else {
|
||||
error("Gasless service returned unsuccessful response")
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class GaslessTxDataToGaslessRequestConverter : Converter<GaslessTransactionData,
|
|||
)
|
||||
}
|
||||
|
||||
private fun convertTransaction(transaction: com.tangem.domain.transaction.models.Transaction): TransactionData {
|
||||
private fun convertTransaction(transaction: GaslessTransactionData.Transaction): TransactionData {
|
||||
return TransactionData(
|
||||
to = transaction.to,
|
||||
value = transaction.value.toString(),
|
||||
|
|
@ -31,7 +31,7 @@ class GaslessTxDataToGaslessRequestConverter : Converter<GaslessTransactionData,
|
|||
)
|
||||
}
|
||||
|
||||
private fun convertFee(fee: com.tangem.domain.transaction.models.Fee): FeeData {
|
||||
private fun convertFee(fee: GaslessTransactionData.Fee): FeeData {
|
||||
return FeeData(
|
||||
feeToken = fee.feeToken,
|
||||
maxTokenFee = fee.maxTokenFee.toString(),
|
||||
|
|
|
|||
|
|
@ -13,54 +13,55 @@ data class GaslessTransactionData(
|
|||
val fee: Fee,
|
||||
/** Nonce from user's contract */
|
||||
val nonce: BigInteger,
|
||||
)
|
||||
|
||||
/**
|
||||
* Core transaction data.
|
||||
*
|
||||
* @property to destination address
|
||||
* @property value transaction value in wei (currently always 0 for gasless)
|
||||
* @property data encoded transaction data (contract call)
|
||||
*/
|
||||
data class Transaction(
|
||||
val to: String,
|
||||
val value: BigInteger,
|
||||
val data: ByteArray,
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Transaction
|
||||
/**
|
||||
* Core transaction data.
|
||||
*
|
||||
* @property to destination address
|
||||
* @property value transaction value in wei (currently always 0 for gasless)
|
||||
* @property data encoded transaction data (contract call)
|
||||
*/
|
||||
data class Transaction(
|
||||
val to: String,
|
||||
val value: BigInteger,
|
||||
val data: ByteArray,
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
if (to != other.to) return false
|
||||
if (value != other.value) return false
|
||||
if (!data.contentEquals(other.data)) return false
|
||||
other as Transaction
|
||||
|
||||
return true
|
||||
if (to != other.to) return false
|
||||
if (value != other.value) return false
|
||||
if (!data.contentEquals(other.data)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = to.hashCode()
|
||||
result = 31 * result + value.hashCode()
|
||||
result = 31 * result + data.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = to.hashCode()
|
||||
result = 31 * result + value.hashCode()
|
||||
result = 31 * result + data.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fee configuration for gasless transaction.
|
||||
*
|
||||
* @property feeToken token address used for fee payment
|
||||
* @property maxTokenFee maximum fee amount in fee token units
|
||||
* @property coinPriceInToken price of native coin in fee token units
|
||||
* @property feeTransferGasLimit gas limit for fee token transfer
|
||||
* @property baseGas base gas cost (currently constant BASE_GAS, may vary in future)
|
||||
*/
|
||||
data class Fee(
|
||||
val feeToken: String,
|
||||
val maxTokenFee: BigInteger,
|
||||
val coinPriceInToken: BigInteger,
|
||||
val feeTransferGasLimit: BigInteger,
|
||||
val baseGas: BigInteger,
|
||||
)
|
||||
/**
|
||||
* Fee configuration for gasless transaction.
|
||||
*
|
||||
* @property feeToken token address used for fee payment
|
||||
* @property maxTokenFee maximum fee amount in fee token units
|
||||
* @property coinPriceInToken price of native coin in fee token units
|
||||
* @property feeTransferGasLimit gas limit for fee token transfer
|
||||
* @property baseGas base gas cost (currently constant BASE_GAS, may vary in future)
|
||||
*/
|
||||
data class Fee(
|
||||
val feeToken: String,
|
||||
val maxTokenFee: BigInteger,
|
||||
val coinPriceInToken: BigInteger,
|
||||
val feeTransferGasLimit: BigInteger,
|
||||
val baseGas: BigInteger,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,11 +1,131 @@
|
|||
package com.tangem.domain.transaction.usecase.gasless
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
|
||||
import com.tangem.blockchain.blockchains.ethereum.gasless.EthereumGaslessDataProvider
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplySendCallData
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.domain.transaction.models.GaslessTransactionData
|
||||
import com.tangem.domain.transaction.models.TransactionFeeExtended
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import java.math.BigInteger
|
||||
|
||||
class CreateAndSendGaslessTransactionUseCase {
|
||||
class CreateAndSendGaslessTransactionUseCase(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
|
||||
) {
|
||||
|
||||
operator fun invoke(transactionData: TransactionData, fee: TransactionFeeExtended) {
|
||||
// Implementation goes here
|
||||
@Suppress("UnusedPrivateProperty")
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
transactionData: TransactionData,
|
||||
fee: TransactionFeeExtended,
|
||||
): Either<SendTransactionError, Unit> {
|
||||
return either {
|
||||
catch(
|
||||
block = {
|
||||
transactionData as? TransactionData.Uncompiled ?: error("Uncompiled transaction data required")
|
||||
|
||||
val tokenForFeeStatus = getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync(
|
||||
userWalletId,
|
||||
fee.feeTokenId,
|
||||
).getOrNull() ?: error("Token for fee not found")
|
||||
|
||||
val blockchain = Blockchain.fromId(tokenForFeeStatus.currency.network.rawId)
|
||||
|
||||
val gaslessTransactionData = createGaslessTransactionData(
|
||||
userWalletId = userWalletId,
|
||||
transactionData = transactionData,
|
||||
txFee = fee,
|
||||
tokenFeeStatus = tokenForFeeStatus,
|
||||
)
|
||||
|
||||
val chainId =
|
||||
blockchain.getChainId() ?: error("ChainId not found for blockchain ${blockchain.name}")
|
||||
|
||||
val eip712Data = Eip712TypedDataBuilder.build(
|
||||
gaslessTransaction = gaslessTransactionData,
|
||||
chainId = chainId,
|
||||
verifyingContract = transactionData.sourceAddress,
|
||||
)
|
||||
|
||||
val eip712HashToSign = EthereumUtils.makeTypedDataHash(eip712Data)
|
||||
},
|
||||
catch = {
|
||||
raise(SendTransactionError.DataError(it.message))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createGaslessTransactionData(
|
||||
userWalletId: UserWalletId,
|
||||
transactionData: TransactionData.Uncompiled,
|
||||
txFee: TransactionFeeExtended,
|
||||
tokenFeeStatus: CryptoCurrencyStatus,
|
||||
): GaslessTransactionData {
|
||||
val bigIntegerAmount =
|
||||
transactionData.amount.value?.movePointRight(transactionData.amount.decimals)?.toBigInteger()
|
||||
?: error("Amount value is null")
|
||||
val txData = (transactionData.extras as? EthereumTransactionExtras)?.callData ?: error("Call data required")
|
||||
|
||||
val transaction = GaslessTransactionData.Transaction(
|
||||
to = getDestinationAddress(transactionData),
|
||||
value = bigIntegerAmount,
|
||||
data = txData.data,
|
||||
)
|
||||
|
||||
val tokenForFee = tokenFeeStatus.currency as? CryptoCurrency.Token
|
||||
?: error("only CryptoCurrency.Token supported for fee")
|
||||
|
||||
val txFeeInTokenCurrency = txFee.transactionFee.normal as? Fee.Ethereum.TokenCurrency ?: error(
|
||||
"only Fee.Ethereum.TokenCurrency supported for gasless fee",
|
||||
)
|
||||
val fee = GaslessTransactionData.Fee(
|
||||
feeToken = tokenForFee.contractAddress,
|
||||
maxTokenFee = txFeeInTokenCurrency.gasLimit,
|
||||
coinPriceInToken = txFeeInTokenCurrency.coinPriceInToken,
|
||||
feeTransferGasLimit = txFeeInTokenCurrency.feeTransferGasLimit,
|
||||
baseGas = txFeeInTokenCurrency.baseGas,
|
||||
)
|
||||
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, tokenForFee.network)
|
||||
?: error("WalletManager not found for network ${tokenForFee.network.id}")
|
||||
val gaslessDataProvider = walletManager as? EthereumGaslessDataProvider ?: error(
|
||||
"WalletManager for network ${tokenForFee.network.id} does not support gasless transactions",
|
||||
)
|
||||
|
||||
val nonceResult = gaslessDataProvider.getGaslessContractNonce(
|
||||
userAddress = transactionData.sourceAddress,
|
||||
)
|
||||
val nonce = when (nonceResult) {
|
||||
is com.tangem.blockchain.extensions.Result.Failure -> BigInteger.ZERO
|
||||
is com.tangem.blockchain.extensions.Result.Success -> nonceResult.data
|
||||
}
|
||||
|
||||
return GaslessTransactionData(
|
||||
transaction = transaction,
|
||||
fee = fee,
|
||||
nonce = nonce,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDestinationAddress(txData: TransactionData.Uncompiled): String {
|
||||
val ethereumCallData = (txData.extras as? EthereumTransactionExtras)?.callData
|
||||
return if (ethereumCallData is EthereumYieldSupplySendCallData) {
|
||||
ethereumCallData.destinationAddress
|
||||
} else {
|
||||
txData.destinationAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
package com.tangem.domain.transaction.usecase.gasless
|
||||
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.domain.transaction.models.GaslessTransactionData
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* Builder for creating EIP-712 typed data JSON for gasless transaction signing.
|
||||
*
|
||||
* The EIP-712 standard allows users to sign typed, structured data instead of raw bytes.
|
||||
* This provides better UX as wallets can show users exactly what they're signing.
|
||||
*
|
||||
* Example usage:
|
||||
* ```kotlin
|
||||
* val typedDataJson = Eip712TypedDataBuilder.build(
|
||||
* gaslessTransaction = gaslessTransactionData,
|
||||
* chainId = 1,
|
||||
* verifyingContract = "0x1234..."
|
||||
* )
|
||||
* val signature = wallet.signTypedData(typedDataJson)
|
||||
* ```
|
||||
*/
|
||||
object Eip712TypedDataBuilder {
|
||||
|
||||
private const val DOMAIN_NAME = "Tangem7702GaslessExecutor"
|
||||
private const val DOMAIN_VERSION = "1"
|
||||
private const val PRIMARY_TYPE = "GaslessTransaction"
|
||||
|
||||
/**
|
||||
* Builds EIP-712 typed data JSON for gasless transaction.
|
||||
*
|
||||
* @param gaslessTransaction domain model with transaction and fee data
|
||||
* @param chainId blockchain network chain ID
|
||||
* @param verifyingContract address of the deployed gasless executor contract
|
||||
* @return JSON string ready for EIP-712 signing
|
||||
*/
|
||||
fun build(gaslessTransaction: GaslessTransactionData, chainId: Int, verifyingContract: String): String {
|
||||
val typedData = JSONObject().apply {
|
||||
put("types", buildTypes())
|
||||
put("primaryType", PRIMARY_TYPE)
|
||||
put("domain", buildDomain(chainId, verifyingContract))
|
||||
put("message", buildMessage(gaslessTransaction))
|
||||
}
|
||||
return typedData.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the type definitions for all structures.
|
||||
* This schema is fixed and defines the structure of the data being signed.
|
||||
*/
|
||||
@Suppress("NestedScopeFunctions")
|
||||
private fun buildTypes(): JSONObject {
|
||||
return JSONObject().apply {
|
||||
put("EIP712Domain", JSONArray().apply {
|
||||
put(typeProperty("name", "string"))
|
||||
put(typeProperty("version", "string"))
|
||||
put(typeProperty("chainId", "uint256"))
|
||||
put(typeProperty("verifyingContract", "address"))
|
||||
})
|
||||
put("Transaction", JSONArray().apply {
|
||||
put(typeProperty("to", "address"))
|
||||
put(typeProperty("value", "uint256"))
|
||||
put(typeProperty("data", "bytes"))
|
||||
})
|
||||
put("Fee", JSONArray().apply {
|
||||
put(typeProperty("feeToken", "address"))
|
||||
put(typeProperty("maxTokenFee", "uint256"))
|
||||
put(typeProperty("coinPriceInToken", "uint256"))
|
||||
put(typeProperty("feeTransferGasLimit", "uint256"))
|
||||
put(typeProperty("baseGas", "uint256"))
|
||||
})
|
||||
put("GaslessTransaction", JSONArray().apply {
|
||||
put(typeProperty("transaction", "Transaction"))
|
||||
put(typeProperty("fee", "Fee"))
|
||||
put(typeProperty("nonce", "uint256"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a type property JSON object.
|
||||
*/
|
||||
private fun typeProperty(name: String, type: String): JSONObject {
|
||||
return JSONObject().apply {
|
||||
put("name", name)
|
||||
put("type", type)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the domain separator.
|
||||
*/
|
||||
private fun buildDomain(chainId: Int, verifyingContract: String): JSONObject {
|
||||
return JSONObject().apply {
|
||||
put("name", DOMAIN_NAME)
|
||||
put("version", DOMAIN_VERSION)
|
||||
put("chainId", chainId)
|
||||
put("verifyingContract", verifyingContract)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the message data from gasless transaction.
|
||||
*/
|
||||
@Suppress("NestedScopeFunctions")
|
||||
private fun buildMessage(gaslessTransaction: GaslessTransactionData): JSONObject {
|
||||
return JSONObject().apply {
|
||||
put("transaction", JSONObject().apply {
|
||||
put("to", gaslessTransaction.transaction.to)
|
||||
put("value", gaslessTransaction.transaction.value.toString())
|
||||
put("data", gaslessTransaction.transaction.data.toHexString())
|
||||
})
|
||||
put("fee", JSONObject().apply {
|
||||
put("feeToken", gaslessTransaction.fee.feeToken)
|
||||
put("maxTokenFee", gaslessTransaction.fee.maxTokenFee.toString())
|
||||
put("coinPriceInToken", gaslessTransaction.fee.coinPriceInToken.toString())
|
||||
put("feeTransferGasLimit", gaslessTransaction.fee.feeTransferGasLimit.toString())
|
||||
put("baseGas", gaslessTransaction.fee.baseGas.toString())
|
||||
})
|
||||
put("nonce", gaslessTransaction.nonce.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -77,12 +77,17 @@ class GetAvailableFeeTokensUseCase(
|
|||
userCurrenciesStatuses: List<CryptoCurrencyStatus>,
|
||||
): List<CryptoCurrencyStatus> {
|
||||
val supportedGaslessTokens = gaslessTransactionRepository.getSupportedTokens(network)
|
||||
.mapNotNull {
|
||||
(it as? CryptoCurrency.Token)?.contractAddress?.lowercase()
|
||||
}.toSet()
|
||||
return userCurrenciesStatuses
|
||||
.asSequence()
|
||||
.filter { it.currency.network.id == network.id }
|
||||
.filter { currencyStatus ->
|
||||
currencyStatus.currency is CryptoCurrency.Token &&
|
||||
val token = currencyStatus.currency
|
||||
token is CryptoCurrency.Token &&
|
||||
currencyStatus.value.amount?.let { amount -> amount > BigDecimal.ZERO } == true &&
|
||||
supportedGaslessTokens.contains(currencyStatus.currency)
|
||||
supportedGaslessTokens.contains(token.contractAddress.lowercase())
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,11 +151,17 @@ class GetFeeForGaslessUseCase(
|
|||
|
||||
val supportedGaslessTokens = gaslessTransactionRepository.getSupportedTokens(
|
||||
network = nativeCurrencyStatus.currency.network,
|
||||
)
|
||||
).mapNotNull { currency ->
|
||||
(currency as? CryptoCurrency.Token)?.contractAddress
|
||||
}.toSet()
|
||||
|
||||
val supportedGaslessTokensStatusesSortedByBalanceDesc = networkCurrenciesStatuses
|
||||
.filterNot { it.value.amount == BigDecimal.ZERO || it.currency !is CryptoCurrency.Token }
|
||||
.sortedByDescending { it.value.amount }
|
||||
.filter { it.currency in supportedGaslessTokens }
|
||||
.filter { status ->
|
||||
val token = status.currency as? CryptoCurrency.Token ?: return@filter false
|
||||
token.contractAddress.lowercase() in supportedGaslessTokens
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects token with highest balance to maximize chances of successful fee payment.
|
||||
|
|
@ -171,11 +177,4 @@ class GetFeeForGaslessUseCase(
|
|||
initialFee = initialFee,
|
||||
).bind()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
/** Amount in token units for fee transfer */
|
||||
const val FEE_TRANSFER_AMOUNT = 10000
|
||||
/** Gas price safety multiplier for fee calculation */
|
||||
const val GAS_PRICE_MULTIPLIER = 2
|
||||
}
|
||||
}
|
||||
|
|
@ -101,13 +101,21 @@ internal class TokenFeeCalculator(
|
|||
|
||||
val nativeFiatRate = nativeCurrencyStatus.value.fiatRate ?: raiseIllegalStateError("fiatRate is null")
|
||||
val tokenFiatRate = tokenForPayFeeStatus.value.fiatRate ?: raiseIllegalStateError("fiatRate is null")
|
||||
val coinPriceInToken = nativeFiatRate.divide(
|
||||
|
||||
val coinPriceInTokenInBigDecimal = nativeFiatRate.divide(
|
||||
tokenFiatRate,
|
||||
maxOf(nativeCurrencyStatus.currency.decimals, tokenForPayFee.decimals),
|
||||
RoundingMode.DOWN,
|
||||
RoundingMode.UP,
|
||||
)
|
||||
|
||||
val feeInTokenCurrency = coinPriceInToken.multiply(feeInNativeCurrency.toBigDecimal())
|
||||
val coinPriceInTokenBigInt = coinPriceInTokenInBigDecimal
|
||||
.movePointRight(tokenForPayFee.decimals)
|
||||
.increaseByPercent(PERCENT_TO_INCREASE_TOKEN_PRICE)
|
||||
.toBigInteger()
|
||||
|
||||
val feeInTokenCurrency = coinPriceInTokenInBigDecimal.multiply(
|
||||
feeInNativeCurrency.toBigDecimal(),
|
||||
)
|
||||
|
||||
val tokenBalance = tokenForPayFeeStatus.value.amount ?: BigDecimal.ZERO
|
||||
if (tokenBalance < feeInTokenCurrency) {
|
||||
|
|
@ -119,7 +127,7 @@ internal class TokenFeeCalculator(
|
|||
val fee = Fee.Ethereum.TokenCurrency(
|
||||
amount = amount,
|
||||
gasLimit = maxTokenFeeGas,
|
||||
coinPriceInToken = coinPriceInToken,
|
||||
coinPriceInToken = coinPriceInTokenBigInt,
|
||||
feeTransferGasLimit = feeTransferGasLimit,
|
||||
baseGas = baseGas,
|
||||
)
|
||||
|
|
@ -151,8 +159,28 @@ internal class TokenFeeCalculator(
|
|||
|
||||
private companion object {
|
||||
/** Amount in token units for fee transfer */
|
||||
const val FEE_TRANSFER_AMOUNT = 10000
|
||||
const val FEE_TRANSFER_AMOUNT = 0.01 // calculate using decimals
|
||||
/** Gas price safety multiplier for fee calculation */
|
||||
const val GAS_PRICE_MULTIPLIER = 2
|
||||
const val PERCENT_TO_INCREASE_TOKEN_PRICE = 1
|
||||
|
||||
/**
|
||||
* Increases BigDecimal value by specified percentage.
|
||||
*
|
||||
* @param percent percentage to increase by (e.g., 1 for 1%, 10 for 10%)
|
||||
* @return value increased by specified percentage (value * (1 + percent/100))
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* BigDecimal("100").increaseByPercent(1) // 101
|
||||
* BigDecimal("100").increaseByPercent(10) // 110
|
||||
* BigDecimal("100").increaseByPercent(50) // 150
|
||||
* ```
|
||||
*/
|
||||
private fun BigDecimal.increaseByPercent(percent: Int): BigDecimal {
|
||||
require(percent >= 0) { "Percent must be non-negative" }
|
||||
val multiplier = BigDecimal.ONE.add(BigDecimal(percent).divide(BigDecimal("100")))
|
||||
return this.multiply(multiplier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
# https://github.com/tangem/tangem-sdk-android/
|
||||
# https://github.com/tangem/vico
|
||||
|
||||
tangemBlockchainSdk = "develop-1340"
|
||||
tangemBlockchainSdk = "develop-1346"
|
||||
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
||||
tangemCardSdk = "develop-573"
|
||||
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue