From 680794d447e6de5bc9cd3f541d65e9fcaec6d920 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 6 Jan 2026 13:15:40 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../api/gasless/GaslessTxServiceApi.kt | 4 +- ...t => GaslessSignedTransactionResultDTO.kt} | 2 +- .../models/GaslessTransactionRequest.kt | 4 +- .../DefaultGaslessTransactionRepository.kt | 35 ++++++++++ ...GaslessSignedTransactionResultConverter.kt | 22 +++++++ .../GaslessTransactionRequestBuilder.kt | 55 ++++++++++++++++ .../GaslessTxDataToGaslessRequestConverter.kt | 43 ++++++++++++ .../GaslessTransactionRepository.kt | 47 +++++++++++++ .../models/Eip7702Authorization.kt | 36 ++++++++++ .../models/GaslessSignedTransactionResult.kt | 37 +++++++++++ .../models/GaslessTransactionData.kt | 66 +++++++++++++++++++ .../CreateAndSendGaslessTransactionUseCase.kt | 11 ++++ 12 files changed, 357 insertions(+), 5 deletions(-) rename core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/{GaslessSignedTransactionResult.kt => GaslessSignedTransactionResultDTO.kt} (92%) create mode 100644 data/transaction/src/main/java/com/tangem/data/transaction/convertes/GaslessSignedTransactionResultConverter.kt create mode 100644 data/transaction/src/main/java/com/tangem/data/transaction/convertes/GaslessTransactionRequestBuilder.kt create mode 100644 data/transaction/src/main/java/com/tangem/data/transaction/convertes/GaslessTxDataToGaslessRequestConverter.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/models/Eip7702Authorization.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/models/GaslessSignedTransactionResult.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/models/GaslessTransactionData.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/CreateAndSendGaslessTransactionUseCase.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/gasless/GaslessTxServiceApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/gasless/GaslessTxServiceApi.kt index fcb9fe8c20..193dc620e8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/gasless/GaslessTxServiceApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/gasless/GaslessTxServiceApi.kt @@ -2,7 +2,7 @@ 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.GaslessSignedTransactionResultDTO import com.tangem.datasource.api.gasless.models.GaslessSupportedTokens import com.tangem.datasource.api.gasless.models.GaslessTransactionRequest import retrofit2.http.Body @@ -17,5 +17,5 @@ interface GaslessTxServiceApi { @POST("api/v1/sign") suspend fun signGaslessTransaction( @Body transaction: GaslessTransactionRequest, - ): ApiResponse> + ): ApiResponse> } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessSignedTransactionResult.kt b/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessSignedTransactionResultDTO.kt similarity index 92% rename from core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessSignedTransactionResult.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessSignedTransactionResultDTO.kt index 0a2fc092de..c8bd1b89b4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessSignedTransactionResult.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessSignedTransactionResultDTO.kt @@ -8,7 +8,7 @@ import com.squareup.moshi.JsonClass * Contains the signed transaction data and gas parameters. */ @JsonClass(generateAdapter = true) -data class GaslessSignedTransactionResult( +data class GaslessSignedTransactionResultDTO( @Json(name = "signedTransaction") val signedTransaction: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessTransactionRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessTransactionRequest.kt index f5f40bd9df..948567af63 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessTransactionRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessTransactionRequest.kt @@ -22,7 +22,7 @@ data class GaslessTransactionRequest( val chainId: Int, @Json(name = "eip7702auth") - val eip7702Auth: Eip7702Authorization? = null, + val eip7702Auth: Eip7702AuthorizationDTO? = null, ) @JsonClass(generateAdapter = true) @@ -72,7 +72,7 @@ data class FeeData( * Optional field, used only when EOA delegation is required. */ @JsonClass(generateAdapter = true) -data class Eip7702Authorization( +data class Eip7702AuthorizationDTO( @Json(name = "chainId") val chainId: Int, diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultGaslessTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultGaslessTransactionRepository.kt index 19822060a2..e68999f26f 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultGaslessTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultGaslessTransactionRepository.kt @@ -4,11 +4,16 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.transaction.convertes.GaslessSignedTransactionResultConverter +import com.tangem.data.transaction.convertes.GaslessTransactionRequestBuilder import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.gasless.GaslessTxServiceApi import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.transaction.GaslessTransactionRepository +import com.tangem.domain.transaction.models.Eip7702Authorization +import com.tangem.domain.transaction.models.GaslessSignedTransactionResult +import com.tangem.domain.transaction.models.GaslessTransactionData import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update @@ -22,6 +27,8 @@ class DefaultGaslessTransactionRepository( ) : GaslessTransactionRepository { private val supportedTokensState = MutableStateFlow?>(null) + private val gaslessTransactionRequestBuilder = GaslessTransactionRequestBuilder() + private val signedTransactionResultConverter = GaslessSignedTransactionResultConverter() override fun isNetworkSupported(network: Network): Boolean { val blockchain = Blockchain.fromNetworkId(network.backendId) ?: return false @@ -63,6 +70,34 @@ class DefaultGaslessTransactionRepository( return TOKEN_RECEIVER_ADDRESS } + override suspend fun sendGaslessTransaction( + gaslessTransactionData: GaslessTransactionData, + signature: String, + userAddress: String, + network: Network, + eip7702Auth: Eip7702Authorization?, + ): GaslessSignedTransactionResult = withContext(coroutineDispatcherProvider.io) { + val blockchain = Blockchain.fromNetworkId(network.backendId) + ?: error("Cannot determine blockchain for network id: ${network.backendId}") + + val transactionRequest = gaslessTransactionRequestBuilder.build( + gaslessTransaction = gaslessTransactionData, + signature = signature, + userAddress = userAddress, + chainId = blockchain.getChainId() ?: error("ChainId is null for blockchain: $blockchain"), + eip7702Auth = eip7702Auth, + ) + + val response = gaslessTxServiceApi.signGaslessTransaction(transactionRequest).getOrThrow() + + if (!response.isSuccess) { + error("Gasless service returned unsuccessful response") + } + + // Convert DTO to domain model + signedTransactionResultConverter.convert(response.result) + } + override fun getBaseGasForTransaction(): BigInteger { return BASE_GAS_FOR_TRANSACTION } diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/convertes/GaslessSignedTransactionResultConverter.kt b/data/transaction/src/main/java/com/tangem/data/transaction/convertes/GaslessSignedTransactionResultConverter.kt new file mode 100644 index 0000000000..4c8a6e62ef --- /dev/null +++ b/data/transaction/src/main/java/com/tangem/data/transaction/convertes/GaslessSignedTransactionResultConverter.kt @@ -0,0 +1,22 @@ +package com.tangem.data.transaction.convertes + +import com.tangem.datasource.api.gasless.models.GaslessSignedTransactionResultDTO as GaslessSignedTransactionResultDTO +import com.tangem.domain.transaction.models.GaslessSignedTransactionResult +import com.tangem.utils.converter.Converter + +/** + * Converts DTO GaslessSignedTransactionResult from API to domain model. + * Transforms string representations of gas parameters to BigInteger for type safety. + */ +class GaslessSignedTransactionResultConverter : + Converter { + + override fun convert(value: GaslessSignedTransactionResultDTO): GaslessSignedTransactionResult { + return GaslessSignedTransactionResult( + signedTransaction = value.signedTransaction, + gasLimit = value.gasLimit.toBigInteger(), + maxFeePerGas = value.maxFeePerGas.toBigInteger(), + maxPriorityFeePerGas = value.maxPriorityFeePerGas.toBigInteger(), + ) + } +} \ No newline at end of file diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/convertes/GaslessTransactionRequestBuilder.kt b/data/transaction/src/main/java/com/tangem/data/transaction/convertes/GaslessTransactionRequestBuilder.kt new file mode 100644 index 0000000000..b7caa4b13a --- /dev/null +++ b/data/transaction/src/main/java/com/tangem/data/transaction/convertes/GaslessTransactionRequestBuilder.kt @@ -0,0 +1,55 @@ +package com.tangem.data.transaction.convertes + +import com.tangem.datasource.api.gasless.models.GaslessTransactionRequest +import com.tangem.domain.transaction.models.Eip7702Authorization +import com.tangem.domain.transaction.models.GaslessTransactionData +import com.tangem.datasource.api.gasless.models.Eip7702AuthorizationDTO + +/** + * Builder for creating complete GaslessTransactionRequest from domain model. + * Combines transaction data with signature and user information. + */ +class GaslessTransactionRequestBuilder( + private val converter: GaslessTxDataToGaslessRequestConverter = GaslessTxDataToGaslessRequestConverter(), +) { + + /** + * Creates complete gasless transaction request. + * + * @param gaslessTransaction domain model of transaction + * @param signature transaction signature in hex format (with 0x prefix) + * @param userAddress user's Ethereum address + * @param chainId blockchain network chain ID + * @param eip7702Auth optional EIP-7702 authorization for account abstraction + * @return complete request ready for API submission + */ + fun build( + gaslessTransaction: GaslessTransactionData, + signature: String, + userAddress: String, + chainId: Int, + eip7702Auth: Eip7702Authorization? = null, + ): GaslessTransactionRequest { + return GaslessTransactionRequest( + gaslessTransaction = converter.convert(gaslessTransaction), + signature = signature, + userAddress = userAddress, + chainId = chainId, + eip7702Auth = eip7702Auth?.toDTO(), + ) + } + + /** + * Converts domain Eip7702Authorization to DTO. + */ + private fun Eip7702Authorization.toDTO(): Eip7702AuthorizationDTO { + return Eip7702AuthorizationDTO( + chainId = chainId, + address = address, + nonce = nonce.toString(), + yParity = yParity, + r = r, + s = s, + ) + } +} \ No newline at end of file diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/convertes/GaslessTxDataToGaslessRequestConverter.kt b/data/transaction/src/main/java/com/tangem/data/transaction/convertes/GaslessTxDataToGaslessRequestConverter.kt new file mode 100644 index 0000000000..16b1683a11 --- /dev/null +++ b/data/transaction/src/main/java/com/tangem/data/transaction/convertes/GaslessTxDataToGaslessRequestConverter.kt @@ -0,0 +1,43 @@ +package com.tangem.data.transaction.convertes + +import com.tangem.datasource.api.gasless.models.FeeData +import com.tangem.datasource.api.gasless.models.TransactionData +import com.tangem.domain.transaction.models.GaslessTransactionData +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.toHexString +import com.tangem.datasource.api.gasless.models.GaslessTransactionData as GaslessTransactionDataDTO + +/** + * Converts domain GaslessTransactionData to DTO for API requests. + * Note: This converter only handles the transaction data conversion. + * Additional fields (signature, userAddress, chainId) must be added separately + * to create complete GaslessTransactionRequest. + */ +class GaslessTxDataToGaslessRequestConverter : Converter { + + override fun convert(value: GaslessTransactionData): GaslessTransactionDataDTO { + return GaslessTransactionDataDTO( + transaction = convertTransaction(value.transaction), + fee = convertFee(value.fee), + nonce = value.nonce.toString(), + ) + } + + private fun convertTransaction(transaction: com.tangem.domain.transaction.models.Transaction): TransactionData { + return TransactionData( + to = transaction.to, + value = transaction.value.toString(), + data = transaction.data.toHexString(), + ) + } + + private fun convertFee(fee: com.tangem.domain.transaction.models.Fee): FeeData { + return FeeData( + feeToken = fee.feeToken, + maxTokenFee = fee.maxTokenFee.toString(), + coinPriceInToken = fee.coinPriceInToken.toString(), + feeTransferGasLimit = fee.feeTransferGasLimit.toString(), + baseGas = fee.baseGas.toString(), + ) + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/GaslessTransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/GaslessTransactionRepository.kt index 728a81f317..84074cf002 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/GaslessTransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/GaslessTransactionRepository.kt @@ -2,6 +2,9 @@ package com.tangem.domain.transaction import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network +import com.tangem.domain.transaction.models.Eip7702Authorization +import com.tangem.domain.transaction.models.GaslessSignedTransactionResult +import com.tangem.domain.transaction.models.GaslessTransactionData import java.math.BigInteger interface GaslessTransactionRepository { @@ -12,6 +15,50 @@ interface GaslessTransactionRepository { fun getTokenFeeReceiverAddress(): String + /** + * Sends gasless transaction to the gasless service for signing and returns the signed result. + * + * Flow: + * 1. User signs the gasless transaction data locally with their private key + * 2. This method submits the transaction + user signature to the gasless service + * 3. Service validates the signature and transaction data + * 4. Service adds its own signature for fee delegation (pays gas in tokens) + * 5. Service constructs the final EIP-1559 transaction with all signatures + * 6. Service returns the fully signed transaction ready for broadcasting + * + * The gasless service acts as a relayer that: + * - Pays network gas fees on behalf of the user + * - Receives payment in the specified token from the user's wallet + * - Ensures atomic execution (either both transfers succeed or both fail) + * + * @param gaslessTransactionData domain model containing: + * - transaction: target contract call data (to, value, data) + * - fee: token payment configuration (feeToken, maxTokenFee, etc.) + * - nonce: user's contract nonce to prevent replay attacks + * @param signature user's ECDSA signature of the gasless transaction in hex format (0x...) + * Signs keccak256 hash of the transaction data + * @param userAddress user's Ethereum address (EOA or contract wallet) + * @param network blockchain network (Ethereum, Polygon, BSC, etc.) + * Used to determine chainId for the request + * @param eip7702Auth optional EIP-7702 authorization for EOA delegation to smart contract + * Required only when user's EOA needs to temporarily act as contract wallet + * Contains signature authorizing delegation to entry point contract + * @return [GaslessSignedTransactionResult] containing: + * - signedTransaction: complete RLP-encoded transaction ready to broadcast + * - gasLimit: actual gas limit allocated by the service + * - maxFeePerGas: maximum fee per gas (base + priority) in wei + * - maxPriorityFeePerGas: tip for validators in wei (EIP-1559) + * @throws IllegalStateException if network is not supported or chainId cannot be determined + * @throws Exception if service returns error or network request fails + */ + suspend fun sendGaslessTransaction( + gaslessTransactionData: GaslessTransactionData, + signature: String, + userAddress: String, + network: Network, + eip7702Auth: Eip7702Authorization? = null, + ): GaslessSignedTransactionResult + /** * Hardcoded value as baseGas */ diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/models/Eip7702Authorization.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/models/Eip7702Authorization.kt new file mode 100644 index 0000000000..88e835c1cd --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/models/Eip7702Authorization.kt @@ -0,0 +1,36 @@ +package com.tangem.domain.transaction.models + +import java.math.BigInteger + +/** + * EIP-7702 authorization for account abstraction. + * Used for delegating EOA (Externally Owned Account) control to a smart contract. + * + * EIP-7702 allows EOAs to temporarily act as smart contract wallets by delegating + * their authority to a contract address for a specific transaction. + * + * @property chainId blockchain network chain ID + * @property address contract address to delegate authority to (entry point contract) + * @property nonce authorization nonce to prevent replay attacks + * @property yParity recovery ID for signature (0 or 1) + * @property r ECDSA signature component R + * @property s ECDSA signature component S + * + * @see EIP-7702 Specification + */ +data class Eip7702Authorization( + val chainId: Int, + val address: String, + val nonce: BigInteger, + val yParity: Int, + val r: String, + val s: String, +) { + init { + require(chainId > 0) { "Chain ID must be positive" } + require(address.isNotBlank()) { "Address must not be blank" } + require(yParity in 0..1) { "yParity must be 0 or 1" } + require(r.isNotBlank()) { "Signature component R must not be blank" } + require(s.isNotBlank()) { "Signature component S must not be blank" } + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/models/GaslessSignedTransactionResult.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/models/GaslessSignedTransactionResult.kt new file mode 100644 index 0000000000..3d41d103dd --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/models/GaslessSignedTransactionResult.kt @@ -0,0 +1,37 @@ +package com.tangem.domain.transaction.models + +import java.math.BigInteger + +/** + * Result of gasless transaction signing from the gasless service. + * Contains the fully signed transaction ready for broadcasting and gas parameters. + * + * After the user signs the gasless transaction data locally, the service: + * 1. Validates the signature + * 2. Adds its own signature for fee delegation + * 3. Constructs the final transaction + * 4. Returns this signed transaction with gas parameters + * + * @property signedTransaction complete signed transaction in RLP-encoded hex format (0x...) + * ready to be broadcast to the blockchain network + * @property gasLimit maximum amount of gas units allocated for the transaction + * @property maxFeePerGas maximum total fee per gas unit (base fee + priority fee) in wei + * @property maxPriorityFeePerGas maximum priority fee (tip) per gas unit in wei for EIP-1559 + */ +data class GaslessSignedTransactionResult( + val signedTransaction: String, + val gasLimit: BigInteger, + val maxFeePerGas: BigInteger, + val maxPriorityFeePerGas: BigInteger, +) { + init { + require(signedTransaction.isNotBlank()) { "Signed transaction must not be blank" } + require(signedTransaction.startsWith("0x")) { "Signed transaction must be in hex format with 0x prefix" } + require(gasLimit > BigInteger.ZERO) { "Gas limit must be positive" } + require(maxFeePerGas > BigInteger.ZERO) { "Max fee per gas must be positive" } + require(maxPriorityFeePerGas >= BigInteger.ZERO) { "Max priority fee per gas must be non-negative" } + require(maxPriorityFeePerGas <= maxFeePerGas) { + "Max priority fee per gas cannot exceed max fee per gas" + } + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/models/GaslessTransactionData.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/models/GaslessTransactionData.kt new file mode 100644 index 0000000000..c32dce0495 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/models/GaslessTransactionData.kt @@ -0,0 +1,66 @@ +package com.tangem.domain.transaction.models + +import java.math.BigInteger + +/** + * Domain model for gasless transaction. + * Represents complete transaction data with fee delegation metadata. + */ +data class GaslessTransactionData( + /** Transaction details */ + val transaction: Transaction, + /** Fee payment configuration */ + 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 + + 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 + } +} + +/** + * 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, +) \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/CreateAndSendGaslessTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/CreateAndSendGaslessTransactionUseCase.kt new file mode 100644 index 0000000000..941749629f --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/CreateAndSendGaslessTransactionUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.transaction.usecase.gasless + +import com.tangem.blockchain.common.TransactionData +import com.tangem.domain.transaction.models.TransactionFeeExtended + +class CreateAndSendGaslessTransactionUseCase { + + operator fun invoke(transactionData: TransactionData, fee: TransactionFeeExtended) { + // Implementation goes here + } +} \ No newline at end of file