Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-05 17:52:37 +03:00
parent d68ad52a45
commit 13b1bbc486
10 changed files with 154 additions and 23 deletions

View file

@ -2,11 +2,20 @@ package com.tangem.datasource.api.gasless
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.gasless.models.GaslessServiceResponse
import com.tangem.datasource.api.gasless.models.GaslessSignedTransactionResult
import com.tangem.datasource.api.gasless.models.GaslessSupportedTokens
import com.tangem.datasource.api.gasless.models.GaslessTransactionRequest
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
interface GaslessTxServiceApi {
@GET("api/v1/tokens")
suspend fun getSupportedTokens(): ApiResponse<GaslessServiceResponse<GaslessSupportedTokens>>
@POST("api/v1/sign")
suspend fun signGaslessTransaction(
@Body transaction: GaslessTransactionRequest,
): ApiResponse<GaslessServiceResponse<GaslessSignedTransactionResult>>
}

View file

@ -0,0 +1,23 @@
package com.tangem.datasource.api.gasless.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Result of gasless transaction signing.
* Contains the signed transaction data and gas parameters.
*/
@JsonClass(generateAdapter = true)
data class GaslessSignedTransactionResult(
@Json(name = "signedTransaction")
val signedTransaction: String,
@Json(name = "gasLimit")
val gasLimit: String,
@Json(name = "maxFeePerGas")
val maxFeePerGas: String,
@Json(name = "maxPriorityFeePerGas")
val maxPriorityFeePerGas: String,
)

View file

@ -0,0 +1,93 @@
package com.tangem.datasource.api.gasless.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Request body for gasless transaction submission.
* Represents complete transaction with fee delegation metadata.
*/
@JsonClass(generateAdapter = true)
data class GaslessTransactionRequest(
@Json(name = "gaslessTransaction")
val gaslessTransaction: GaslessTransactionData,
@Json(name = "signature")
val signature: String,
@Json(name = "userAddress")
val userAddress: String,
@Json(name = "chainId")
val chainId: Int,
@Json(name = "eip7702auth")
val eip7702Auth: Eip7702Authorization? = null,
)
@JsonClass(generateAdapter = true)
data class GaslessTransactionData(
@Json(name = "transaction")
val transaction: TransactionData,
@Json(name = "fee")
val fee: FeeData,
@Json(name = "nonce")
val nonce: String,
)
@JsonClass(generateAdapter = true)
data class TransactionData(
@Json(name = "to")
val to: String,
@Json(name = "value")
val value: String,
@Json(name = "data")
val data: String,
)
@JsonClass(generateAdapter = true)
data class FeeData(
@Json(name = "feeToken")
val feeToken: String,
@Json(name = "maxTokenFee")
val maxTokenFee: String,
@Json(name = "coinPriceInToken")
val coinPriceInToken: String,
@Json(name = "feeTransferGasLimit")
val feeTransferGasLimit: String,
@Json(name = "baseGas")
val baseGas: String,
)
/**
* EIP-7702 authorization for account abstraction.
* Optional field, used only when EOA delegation is required.
*/
@JsonClass(generateAdapter = true)
data class Eip7702Authorization(
@Json(name = "chainId")
val chainId: Int,
@Json(name = "address")
val address: String,
@Json(name = "nonce")
val nonce: String,
@Json(name = "yParity")
val yParity: Int,
@Json(name = "r")
val r: String,
@Json(name = "s")
val s: String,
)

View file

@ -127,7 +127,7 @@ class ResponseCryptoCurrenciesFactory @Inject constructor(
}
}
private fun createToken(blockchain: Blockchain, sdkToken: Token, network: Network): CryptoCurrency.Token {
fun createToken(blockchain: Blockchain, sdkToken: Token, network: Network): CryptoCurrency.Token {
val id = getTokenId(network, sdkToken)
return CryptoCurrency.Token(

View file

@ -24,6 +24,9 @@ dependencies {
implementation(projects.core.datasource)
implementation(projects.core.utils)
/** Common */
implementation(projects.data.common)
/** Domain */
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.legacy)

View file

@ -1,8 +1,9 @@
package com.tangem.data.transaction
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.transaction.converters.GaslessTokenDtoToCryptoCurrencyConverter
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.gasless.GaslessTxServiceApi
import com.tangem.domain.models.currency.CryptoCurrency
@ -17,9 +18,9 @@ import java.math.BigInteger
class DefaultGaslessTransactionRepository(
private val gaslessTxServiceApi: GaslessTxServiceApi,
private val coroutineDispatcherProvider: CoroutineDispatcherProvider,
private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
) : GaslessTransactionRepository {
private val gaslessTokenDtoToCryptoCurrency = GaslessTokenDtoToCryptoCurrencyConverter()
private val supportedTokensState = MutableStateFlow<Set<CryptoCurrency>?>(null)
override fun isNetworkSupported(network: Network): Boolean {
@ -27,16 +28,27 @@ class DefaultGaslessTransactionRepository(
return SUPPORTED_BLOCKCHAINS.contains(blockchain)
}
override suspend fun getSupportedTokens(): Set<CryptoCurrency> {
override suspend fun getSupportedTokens(network: Network): Set<CryptoCurrency> {
return withContext(coroutineDispatcherProvider.io) {
val storedTokens = supportedTokensState.value
if (storedTokens != null) {
if (storedTokens != null && storedTokens.isNotEmpty()) {
return@withContext storedTokens
}
val supportedTokensData = gaslessTxServiceApi.getSupportedTokens().getOrThrow()
if (supportedTokensData.isSuccess) {
val supportedTokens = supportedTokensData.result.tokens.map {
gaslessTokenDtoToCryptoCurrency.convert(it)
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()
// update local cache
supportedTokensState.update { supportedTokens }

View file

@ -1,12 +0,0 @@
package com.tangem.data.transaction.converters
import com.tangem.datasource.api.gasless.models.GaslessTokenDTO
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.utils.converter.Converter
internal class GaslessTokenDtoToCryptoCurrencyConverter : Converter<GaslessTokenDTO, CryptoCurrency> {
override fun convert(value: GaslessTokenDTO): CryptoCurrency {
TODO("implement conversion logic here")
}
}

View file

@ -8,7 +8,7 @@ interface GaslessTransactionRepository {
fun isNetworkSupported(network: Network): Boolean
suspend fun getSupportedTokens(): Set<CryptoCurrency>
suspend fun getSupportedTokens(network: Network): Set<CryptoCurrency>
fun getTokenFeeReceiverAddress(): String

View file

@ -44,7 +44,7 @@ class GetAvailableFeeTokensUseCase(
return@either listOf(nativeCurrencyStatus)
}
val gaslessTokens = getGaslessTokens(userCurrenciesStatuses)
val gaslessTokens = getGaslessTokens(network, userCurrenciesStatuses)
buildList {
add(nativeCurrencyStatus)
addAll(gaslessTokens)
@ -73,9 +73,10 @@ class GetAvailableFeeTokensUseCase(
}
private suspend fun getGaslessTokens(
network: Network,
userCurrenciesStatuses: List<CryptoCurrencyStatus>,
): List<CryptoCurrencyStatus> {
val supportedGaslessTokens = gaslessTransactionRepository.getSupportedTokens()
val supportedGaslessTokens = gaslessTransactionRepository.getSupportedTokens(network)
return userCurrenciesStatuses
.asSequence()
.filter { currencyStatus ->

View file

@ -149,7 +149,9 @@ class GetFeeForGaslessUseCase(
error = "only Fee.Ethereum supported, but was ${initialTxFee.normal::class.qualifiedName}",
)
val supportedGaslessTokens = gaslessTransactionRepository.getSupportedTokens()
val supportedGaslessTokens = gaslessTransactionRepository.getSupportedTokens(
network = nativeCurrencyStatus.currency.network,
)
val supportedGaslessTokensStatusesSortedByBalanceDesc = networkCurrenciesStatuses
.filterNot { it.value.amount == BigDecimal.ZERO || it.currency !is CryptoCurrency.Token }
.sortedByDescending { it.value.amount }