MetaTxGateway
The MetaTx Gateway enables gasless transactions and meta-transaction functionality for the IXFI Protocol. It allows users to execute transactions without holding native tokens for gas fees, improving accessibility and user experience.
Overview
The MetaTx Gateway provides:
Gasless Transactions: Execute transactions without native tokens
EIP-2771 Compliance: Standard meta-transaction support
Flexible Fee Models: Multiple payment options for transaction fees
Relayer Network: Decentralized network of transaction relayers
Batch Execution: Execute multiple transactions in a single meta-transaction
Smart Contract Interface
Core Functions
executeMetaTransaction
Execute a meta-transaction on behalf of a user.
function executeMetaTransaction(
address user,
address target,
bytes calldata functionSignature,
uint256 nonce,
bytes calldata signature
) external returns (bool success, bytes memory returnData)
Parameters:
user
: Address of the user initiating the transactiontarget
: Target contract addressfunctionSignature
: Encoded function call datanonce
: User's current nonce for meta-transactionssignature
: User's signature for the meta-transaction
Returns:
success
: Whether the meta-transaction executed successfullyreturnData
: Return data from the target function call
Usage Example:
const metaTxGateway = new ethers.Contract(gatewayAddress, gatewayABI, relayerSigner);
// Prepare meta-transaction
const functionSignature = targetContract.interface.encodeFunctionData("transfer", [
recipientAddress,
ethers.parseEther("100")
]);
const nonce = await metaTxGateway.getNonce(userAddress);
// Create signature
const domain = {
name: "IXFI MetaTx Gateway",
version: "1",
chainId: await provider.getNetwork().then(n => n.chainId),
verifyingContract: gatewayAddress
};
const types = {
MetaTransaction: [
{ name: "user", type: "address" },
{ name: "target", type: "address" },
{ name: "functionSignature", type: "bytes" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" }
]
};
const values = {
user: userAddress,
target: targetContractAddress,
functionSignature: functionSignature,
nonce: nonce,
deadline: Math.floor(Date.now() / 1000) + 3600 // 1 hour
};
const signature = await userSigner._signTypedData(domain, types, values);
// Execute meta-transaction
const result = await metaTxGateway.executeMetaTransaction(
userAddress,
targetContractAddress,
functionSignature,
nonce,
signature
);
console.log("Meta-transaction executed:", result.success);
executeMetaTransactionWithCredits
Execute a meta-transaction using pre-paid gas credits.
function executeMetaTransactionWithCredits(
address user,
address target,
bytes calldata functionSignature,
uint256 nonce,
uint256 maxGasPrice,
bytes calldata signature
) external returns (bool success, bytes memory returnData)
Parameters:
user
: User addresstarget
: Target contract addressfunctionSignature
: Encoded function callnonce
: User's noncemaxGasPrice
: Maximum gas price user agrees to paysignature
: User's signature
batchExecuteMetaTransactions
Execute multiple meta-transactions in a single call.
function batchExecuteMetaTransactions(
MetaTransactionData[] calldata transactions
) external returns (BatchExecutionResult memory)
Parameters:
struct MetaTransactionData {
address user;
address target;
bytes functionSignature;
uint256 nonce;
bytes signature;
}
struct BatchExecutionResult {
bool[] successes;
bytes[] returnData;
uint256 totalGasUsed;
uint256 failedCount;
}
Usage Example:
const transactions = [
{
user: userAddress,
target: tokenContract.address,
functionSignature: tokenContract.interface.encodeFunctionData("approve", [spenderAddress, amount1]),
nonce: nonce1,
signature: signature1
},
{
user: userAddress,
target: dexContract.address,
functionSignature: dexContract.interface.encodeFunctionData("swap", [swapParams]),
nonce: nonce2,
signature: signature2
}
];
const batchResult = await metaTxGateway.batchExecuteMetaTransactions(transactions);
console.log("Batch execution results:");
batchResult.successes.forEach((success, index) => {
console.log(`Transaction ${index}: ${success ? 'Success' : 'Failed'}`);
});
console.log(`Total gas used: ${batchResult.totalGasUsed}`);
console.log(`Failed transactions: ${batchResult.failedCount}`);
Gas Credit Management
addGasCredits
Add gas credits for a user account.
function addGasCredits(
address user,
uint256 amount
) external payable
Parameters:
user
: User address to creditamount
: Amount of gas credits to add (in wei equivalent)
getGasCredits
Get the current gas credit balance for a user.
function getGasCredits(address user) external view returns (uint256)
withdrawGasCredits
Allow users to withdraw unused gas credits.
function withdrawGasCredits(uint256 amount) external
sponsorGasCredits
Allow third parties to sponsor gas credits for users.
function sponsorGasCredits(
address user,
uint256 amount,
string memory sponsorMessage
) external payable
Relayer Management
registerRelayer
Register a new relayer in the network.
function registerRelayer(
address relayerAddress,
uint256 stake,
string memory endpoint
) external
Parameters:
relayerAddress
: Address of the relayerstake
: Amount of tokens to stakeendpoint
: API endpoint for the relayer
updateRelayerStatus
Update relayer status (active/inactive).
function updateRelayerStatus(
address relayer,
bool isActive
) external onlyOwner
slashRelayer
Slash a relayer's stake for misbehavior.
function slashRelayer(
address relayer,
uint256 slashAmount,
string memory reason
) external onlyOwner
View Functions
getNonce
Get the current nonce for a user's meta-transactions.
function getNonce(address user) external view returns (uint256)
isValidSignature
Verify if a meta-transaction signature is valid.
function isValidSignature(
address user,
address target,
bytes calldata functionSignature,
uint256 nonce,
bytes calldata signature
) external view returns (bool)
getRelayerInfo
Get information about a registered relayer.
function getRelayerInfo(address relayer) external view returns (RelayerInfo memory)
Returns:
struct RelayerInfo {
bool isActive;
uint256 stake;
string endpoint;
uint256 successfulTxs;
uint256 failedTxs;
uint256 lastActive;
}
estimateMetaTxGas
Estimate gas cost for a meta-transaction.
function estimateMetaTxGas(
address user,
address target,
bytes calldata functionSignature
) external view returns (uint256)
JavaScript SDK Integration
MetaTxGateway Class
class MetaTxGateway {
constructor(config) {
this.contract = new ethers.Contract(
config.contractAddress,
config.abi,
config.provider
);
this.relayerEndpoint = config.relayerEndpoint;
this.domain = {
name: "IXFI MetaTx Gateway",
version: "1",
chainId: config.chainId,
verifyingContract: config.contractAddress
};
this.types = {
MetaTransaction: [
{ name: "user", type: "address" },
{ name: "target", type: "address" },
{ name: "functionSignature", type: "bytes" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" }
]
};
}
async executeGaslessTransaction(userSigner, target, functionSignature, options = {}) {
try {
const userAddress = await userSigner.getAddress();
const nonce = await this.contract.getNonce(userAddress);
const deadline = options.deadline || Math.floor(Date.now() / 1000) + 3600;
// Create meta-transaction data
const metaTxData = {
user: userAddress,
target: target,
functionSignature: functionSignature,
nonce: nonce,
deadline: deadline
};
// Sign the meta-transaction
const signature = await userSigner._signTypedData(
this.domain,
this.types,
metaTxData
);
// Submit to relayer
const response = await this.submitToRelayer({
...metaTxData,
signature
});
return {
success: true,
transactionHash: response.transactionHash,
relayerUsed: response.relayerAddress
};
} catch (error) {
throw new Error(`Gasless transaction failed: ${error.message}`);
}
}
async batchGaslessTransactions(userSigner, transactions, options = {}) {
try {
const userAddress = await userSigner.getAddress();
let nonce = await this.contract.getNonce(userAddress);
const deadline = options.deadline || Math.floor(Date.now() / 1000) + 3600;
// Sign each transaction
const signedTransactions = [];
for (const tx of transactions) {
const metaTxData = {
user: userAddress,
target: tx.target,
functionSignature: tx.functionSignature,
nonce: nonce++,
deadline: deadline
};
const signature = await userSigner._signTypedData(
this.domain,
this.types,
metaTxData
);
signedTransactions.push({
...metaTxData,
signature
});
}
// Submit batch to relayer
const response = await this.submitBatchToRelayer(signedTransactions);
return {
success: true,
transactionHash: response.transactionHash,
batchResults: response.results
};
} catch (error) {
throw new Error(`Batch gasless transaction failed: ${error.message}`);
}
}
async addGasCredits(userSigner, amount, beneficiary = null) {
try {
const userAddress = await userSigner.getAddress();
const target = beneficiary || userAddress;
const tx = await this.contract.connect(userSigner).addGasCredits(target, amount, {
value: amount
});
await tx.wait();
return {
success: true,
transactionHash: tx.hash,
creditsAdded: ethers.formatEther(amount)
};
} catch (error) {
throw new Error(`Failed to add gas credits: ${error.message}`);
}
}
async getGasCreditsBalance(userAddress) {
try {
const balance = await this.contract.getGasCredits(userAddress);
return {
balance: ethers.formatEther(balance),
balanceWei: balance.toString()
};
} catch (error) {
throw new Error(`Failed to get gas credits: ${error.message}`);
}
}
async estimateGaslessTransactionCost(target, functionSignature) {
try {
const gasEstimate = await this.contract.estimateMetaTxGas(
ethers.ZeroAddress, // Placeholder address
target,
functionSignature
);
// Get current gas price from relayer
const gasPriceResponse = await fetch(`${this.relayerEndpoint}/gas-price`);
const { gasPrice } = await gasPriceResponse.json();
const totalCost = gasEstimate * BigInt(gasPrice);
return {
gasEstimate: gasEstimate.toString(),
gasPrice: gasPrice,
totalCost: ethers.formatEther(totalCost),
totalCostWei: totalCost.toString()
};
} catch (error) {
throw new Error(`Failed to estimate cost: ${error.message}`);
}
}
async submitToRelayer(metaTxData) {
const response = await fetch(`${this.relayerEndpoint}/submit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(metaTxData)
});
if (!response.ok) {
throw new Error(`Relayer error: ${response.statusText}`);
}
return await response.json();
}
async submitBatchToRelayer(transactions) {
const response = await fetch(`${this.relayerEndpoint}/submit-batch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ transactions })
});
if (!response.ok) {
throw new Error(`Batch relayer error: ${response.statusText}`);
}
return await response.json();
}
async getRelayerStatus() {
try {
const response = await fetch(`${this.relayerEndpoint}/status`);
return await response.json();
} catch (error) {
throw new Error(`Failed to get relayer status: ${error.message}`);
}
}
async waitForTransaction(transactionHash, timeout = 60000) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
const response = await fetch(`${this.relayerEndpoint}/transaction/${transactionHash}`);
const txData = await response.json();
if (txData.status === 'confirmed') {
return txData;
} else if (txData.status === 'failed') {
throw new Error(`Transaction failed: ${txData.error}`);
}
// Wait 2 seconds before checking again
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (error) {
if (Date.now() - startTime >= timeout) {
throw new Error('Transaction timeout');
}
}
}
throw new Error('Transaction timeout');
}
}
React Hook
import { useState, useCallback } from 'react';
import { MetaTxGateway } from '@ixfi/sdk';
export function useMetaTxGateway(config) {
const [metaTxGateway] = useState(() => new MetaTxGateway(config));
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const executeGaslessTransaction = useCallback(async (userSigner, target, functionSignature, options) => {
setLoading(true);
setError(null);
try {
const result = await metaTxGateway.executeGaslessTransaction(
userSigner,
target,
functionSignature,
options
);
return result;
} catch (err) {
setError(err.message);
throw err;
} finally {
setLoading(false);
}
}, [metaTxGateway]);
const batchGaslessTransactions = useCallback(async (userSigner, transactions, options) => {
setLoading(true);
setError(null);
try {
const result = await metaTxGateway.batchGaslessTransactions(
userSigner,
transactions,
options
);
return result;
} catch (err) {
setError(err.message);
throw err;
} finally {
setLoading(false);
}
}, [metaTxGateway]);
const addGasCredits = useCallback(async (userSigner, amount, beneficiary) => {
setLoading(true);
setError(null);
try {
const result = await metaTxGateway.addGasCredits(userSigner, amount, beneficiary);
return result;
} catch (err) {
setError(err.message);
throw err;
} finally {
setLoading(false);
}
}, [metaTxGateway]);
const getGasCreditsBalance = useCallback(async (userAddress) => {
try {
return await metaTxGateway.getGasCreditsBalance(userAddress);
} catch (err) {
setError(err.message);
throw err;
}
}, [metaTxGateway]);
const estimateGaslessTransactionCost = useCallback(async (target, functionSignature) => {
try {
return await metaTxGateway.estimateGaslessTransactionCost(target, functionSignature);
} catch (err) {
setError(err.message);
throw err;
}
}, [metaTxGateway]);
return {
executeGaslessTransaction,
batchGaslessTransactions,
addGasCredits,
getGasCreditsBalance,
estimateGaslessTransactionCost,
loading,
error,
gateway: metaTxGateway
};
}
// Usage in React component
function GaslessTransactionInterface() {
const {
executeGaslessTransaction,
getGasCreditsBalance,
addGasCredits,
loading,
error
} = useMetaTxGateway({
contractAddress: '0x...',
abi: metaTxGatewayABI,
provider: provider,
relayerEndpoint: 'https://relayer.ixfi.com'
});
const [creditsBalance, setCreditsBalance] = useState(null);
const handleGaslessSwap = async () => {
try {
const functionSignature = swapContract.interface.encodeFunctionData("swap", [
tokenIn,
tokenOut,
amountIn,
minAmountOut,
userAddress
]);
const result = await executeGaslessTransaction(
userSigner,
swapContract.address,
functionSignature
);
console.log('Gasless swap executed:', result.transactionHash);
} catch (err) {
console.error('Gasless swap failed:', err);
}
};
const handleAddCredits = async () => {
try {
const amount = ethers.parseEther('0.1'); // 0.1 ETH worth of credits
const result = await addGasCredits(userSigner, amount);
console.log('Credits added:', result);
// Refresh balance
const newBalance = await getGasCreditsBalance(userAddress);
setCreditsBalance(newBalance);
} catch (err) {
console.error('Failed to add credits:', err);
}
};
return (
<div>
<div>
<h3>Gas Credits Balance</h3>
<p>{creditsBalance ? creditsBalance.balance : 'Loading...'} ETH</p>
<button onClick={handleAddCredits} disabled={loading}>
Add Credits
</button>
</div>
<div>
<h3>Gasless Transaction</h3>
<button onClick={handleGaslessSwap} disabled={loading}>
{loading ? 'Executing...' : 'Execute Gasless Swap'}
</button>
</div>
{error && <div className="error">Error: {error}</div>}
</div>
);
}
Advanced Features
Custom Relayer Integration
class CustomRelayer {
constructor(config) {
this.gateway = new MetaTxGateway(config.gateway);
this.relayerWallet = new ethers.Wallet(config.privateKey, config.provider);
this.endpoint = config.endpoint;
this.gasOracle = config.gasOracle;
this.pendingTransactions = new Map();
}
async startRelayerService() {
// Express.js server for relayer API
const express = require('express');
const app = express();
app.use(express.json());
app.post('/submit', this.handleMetaTransaction.bind(this));
app.post('/submit-batch', this.handleBatchMetaTransactions.bind(this));
app.get('/status', this.getStatus.bind(this));
app.get('/transaction/:hash', this.getTransactionStatus.bind(this));
app.get('/gas-price', this.getGasPrice.bind(this));
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Relayer service running on port ${port}`);
});
}
async handleMetaTransaction(req, res) {
try {
const { user, target, functionSignature, nonce, signature, deadline } = req.body;
// Validate the meta-transaction
const isValid = await this.gateway.contract.isValidSignature(
user,
target,
functionSignature,
nonce,
signature
);
if (!isValid) {
return res.status(400).json({ error: 'Invalid signature' });
}
// Check if user has sufficient gas credits
const gasEstimate = await this.gateway.contract.estimateMetaTxGas(
user,
target,
functionSignature
);
const gasPrice = await this.gasOracle.getGasPrice();
const gasCost = gasEstimate * gasPrice;
const userCredits = await this.gateway.contract.getGasCredits(user);
if (userCredits < gasCost) {
return res.status(400).json({
error: 'Insufficient gas credits',
required: ethers.formatEther(gasCost),
available: ethers.formatEther(userCredits)
});
}
// Execute the meta-transaction
const tx = await this.gateway.contract.connect(this.relayerWallet).executeMetaTransactionWithCredits(
user,
target,
functionSignature,
nonce,
gasPrice,
signature,
{
gasLimit: gasEstimate + BigInt(50000), // Add buffer
gasPrice: gasPrice
}
);
// Track the transaction
this.pendingTransactions.set(tx.hash, {
user,
target,
status: 'pending',
submittedAt: Date.now()
});
res.json({
success: true,
transactionHash: tx.hash,
relayerAddress: this.relayerWallet.address
});
// Wait for confirmation in background
this.waitForConfirmation(tx.hash);
} catch (error) {
console.error('Meta-transaction execution failed:', error);
res.status(500).json({ error: error.message });
}
}
async handleBatchMetaTransactions(req, res) {
try {
const { transactions } = req.body;
// Validate all transactions
for (const tx of transactions) {
const isValid = await this.gateway.contract.isValidSignature(
tx.user,
tx.target,
tx.functionSignature,
tx.nonce,
tx.signature
);
if (!isValid) {
return res.status(400).json({
error: `Invalid signature for transaction with nonce ${tx.nonce}`
});
}
}
// Execute batch
const metaTxData = transactions.map(tx => ({
user: tx.user,
target: tx.target,
functionSignature: tx.functionSignature,
nonce: tx.nonce,
signature: tx.signature
}));
const batchTx = await this.gateway.contract.connect(this.relayerWallet).batchExecuteMetaTransactions(
metaTxData
);
// Track batch transaction
this.pendingTransactions.set(batchTx.hash, {
type: 'batch',
count: transactions.length,
status: 'pending',
submittedAt: Date.now()
});
res.json({
success: true,
transactionHash: batchTx.hash,
batchSize: transactions.length
});
// Wait for confirmation
this.waitForConfirmation(batchTx.hash);
} catch (error) {
console.error('Batch execution failed:', error);
res.status(500).json({ error: error.message });
}
}
async waitForConfirmation(transactionHash) {
try {
const receipt = await this.relayerWallet.provider.waitForTransaction(transactionHash);
const pendingTx = this.pendingTransactions.get(transactionHash);
if (pendingTx) {
pendingTx.status = receipt.status === 1 ? 'confirmed' : 'failed';
pendingTx.confirmedAt = Date.now();
pendingTx.gasUsed = receipt.gasUsed.toString();
}
} catch (error) {
const pendingTx = this.pendingTransactions.get(transactionHash);
if (pendingTx) {
pendingTx.status = 'failed';
pendingTx.error = error.message;
}
}
}
getStatus(req, res) {
const relayerBalance = this.relayerWallet.provider.getBalance(this.relayerWallet.address);
res.json({
relayerAddress: this.relayerWallet.address,
balance: ethers.formatEther(relayerBalance),
pendingTransactions: this.pendingTransactions.size,
uptime: process.uptime(),
chainId: this.relayerWallet.provider.network.chainId
});
}
getTransactionStatus(req, res) {
const { hash } = req.params;
const transaction = this.pendingTransactions.get(hash);
if (!transaction) {
return res.status(404).json({ error: 'Transaction not found' });
}
res.json(transaction);
}
async getGasPrice(req, res) {
try {
const gasPrice = await this.gasOracle.getGasPrice();
res.json({ gasPrice: gasPrice.toString() });
} catch (error) {
res.status(500).json({ error: 'Failed to get gas price' });
}
}
}
Gas Credits Management System
class GasCreditsManager {
constructor(metaTxGateway) {
this.gateway = metaTxGateway;
this.subscriptions = new Map();
this.refillThresholds = new Map();
}
async createSubscription(userAddress, monthlyLimit, autoRefill = true) {
const subscriptionId = `sub_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const subscription = {
id: subscriptionId,
user: userAddress,
monthlyLimit: monthlyLimit,
currentUsage: 0,
autoRefill: autoRefill,
createdAt: Date.now(),
lastRefill: Date.now()
};
this.subscriptions.set(subscriptionId, subscription);
if (autoRefill) {
await this.gateway.addGasCredits(userAddress, monthlyLimit);
}
return subscriptionId;
}
async checkAndRefillCredits(userAddress) {
const userSubscriptions = Array.from(this.subscriptions.values())
.filter(sub => sub.user === userAddress && sub.autoRefill);
for (const subscription of userSubscriptions) {
const currentBalance = await this.gateway.getGasCreditsBalance(userAddress);
const threshold = this.refillThresholds.get(userAddress) || ethers.parseEther('0.01');
if (BigInt(currentBalance.balanceWei) < threshold) {
const refillAmount = subscription.monthlyLimit / 4; // Quarter of monthly limit
await this.gateway.addGasCredits(userAddress, refillAmount);
subscription.currentUsage += Number(ethers.formatEther(refillAmount));
subscription.lastRefill = Date.now();
console.log(`Auto-refilled ${ethers.formatEther(refillAmount)} ETH for user ${userAddress}`);
}
}
}
setRefillThreshold(userAddress, threshold) {
this.refillThresholds.set(userAddress, threshold);
}
async getUsageStats(userAddress, period = 30) {
const subscription = Array.from(this.subscriptions.values())
.find(sub => sub.user === userAddress);
if (!subscription) {
return null;
}
const periodStart = Date.now() - (period * 24 * 60 * 60 * 1000);
return {
subscriptionId: subscription.id,
monthlyLimit: ethers.formatEther(subscription.monthlyLimit),
currentUsage: subscription.currentUsage,
usagePercentage: (subscription.currentUsage / Number(ethers.formatEther(subscription.monthlyLimit))) * 100,
daysUntilReset: Math.ceil((subscription.createdAt + (30 * 24 * 60 * 60 * 1000) - Date.now()) / (24 * 60 * 60 * 1000)),
lastRefill: new Date(subscription.lastRefill).toISOString()
};
}
async sponsorUserCredits(sponsorSigner, userAddress, amount, message = '') {
try {
const result = await this.gateway.contract.connect(sponsorSigner).sponsorGasCredits(
userAddress,
amount,
message,
{ value: amount }
);
return {
success: true,
transactionHash: result.hash,
sponsoredAmount: ethers.formatEther(amount),
beneficiary: userAddress,
message: message
};
} catch (error) {
throw new Error(`Failed to sponsor gas credits: ${error.message}`);
}
}
}
Best Practices
Security Considerations
Signature Validation: Always validate signatures before execution
Nonce Management: Prevent replay attacks with proper nonce handling
Gas Limit Checks: Prevent gas griefing attacks
Rate Limiting: Implement rate limits for relayer endpoints
Performance Optimization
Batch Transactions: Group multiple operations when possible
Gas Price Optimization: Use dynamic gas pricing
Caching: Cache gas estimates and nonce values
Load Balancing: Distribute load across multiple relayers
User Experience
Clear Error Messages: Provide actionable error feedback
Progress Indicators: Show transaction status updates
Fallback Options: Offer regular transactions as backup
Cost Transparency: Display gas costs and credits clearly
Resources
Last updated