Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-06 13:15:40 +03:00
parent 7c92156d19
commit 680794d447
12 changed files with 357 additions and 5 deletions

View file

@ -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
*/

View file

@ -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" }
}
}

View file

@ -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"
}
}
}

View file

@ -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,
)

View file

@ -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
}
}