Updated on 2026-08-14
This commit is contained in:
parent
7c92156d19
commit
680794d447
12 changed files with 357 additions and 5 deletions
|
|
@ -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<GaslessServiceResponse<GaslessSignedTransactionResult>>
|
||||
): ApiResponse<GaslessServiceResponse<GaslessSignedTransactionResultDTO>>
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
||||
|
|
@ -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,
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Set<CryptoCurrency>?>(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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<GaslessSignedTransactionResultDTO, GaslessSignedTransactionResult> {
|
||||
|
||||
override fun convert(value: GaslessSignedTransactionResultDTO): GaslessSignedTransactionResult {
|
||||
return GaslessSignedTransactionResult(
|
||||
signedTransaction = value.signedTransaction,
|
||||
gasLimit = value.gasLimit.toBigInteger(),
|
||||
maxFeePerGas = value.maxFeePerGas.toBigInteger(),
|
||||
maxPriorityFeePerGas = value.maxPriorityFeePerGas.toBigInteger(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<GaslessTransactionData, GaslessTransactionDataDTO> {
|
||||
|
||||
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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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 <a href="https://eips.ethereum.org/EIPS/eip-7702">EIP-7702 Specification</a>
|
||||
*/
|
||||
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" }
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue