Skip to content

Latest commit

 

History

History
711 lines (546 loc) · 22.2 KB

File metadata and controls

711 lines (546 loc) · 22.2 KB

@gelatocloud/gasless

npm version

All-in-one solution for gasless transactions.

Features

  • Three abstraction levels - Choose the optimal integration model for your application::
    • Turbo Relayer: The fastest, most efficient way to submit transactions on-chain with zero gas overhead, ideal for latency-sensitive workflows.
    • Turbo Relayer with Smart Account - Leverage Smart Accounts for streamlined transaction encoding and signing while retaining Turbo-level performance.
    • ERC-4337 Bundler - A fully compliant ERC-4337 bundler for native Account Abstraction flows.
  • Sponsorship via Gas Tank - Support for sponsored transactions using your Gas Tank.
  • 2D nonce support - Advanced nonce management using both nonce and nonceKey for parallelized execution.
  • Type-safe - Implemented on top of viem, offering complete TypeScript type safety and developer ergonomics.
  • Synchronous methods: Send transaction and get the receipt in a single call
  • WebSockets: Live notifications and updates via WebSockets

Learn more in our docs

Installation

pnpm install viem @gelatocloud/gasless

Peer dependencies: viem

Quick Start

  1. Get your API key at https://app.gelato.cloud/
  2. Set your environment variable:
export GELATO_API_KEY="your-api-key"

Usage

Turbo Relayer

Direct gasless transaction relay without smart accounts. Best for simple sponsored transactions.

Synchronous:

import { createGelatoEvmRelayerClient } from '@gelatocloud/gasless';
import { baseSepolia } from 'viem/chains';

const relayer = createGelatoEvmRelayerClient({
  apiKey: process.env.GELATO_API_KEY,
  testnet: true
});

// Send and wait for inclusion in one call
const receipt = await relayer.sendTransactionSync({
  chainId: baseSepolia.id,
  to: '0xTargetContract...',
  data: '0xCalldata...'
});

console.log(`Transaction hash: ${receipt.transactionHash}`);

Asynchronous:

import { createGelatoEvmRelayerClient } from '@gelatocloud/gasless';
import { baseSepolia } from 'viem/chains';

const relayer = createGelatoEvmRelayerClient({
  apiKey: process.env.GELATO_API_KEY,
  testnet: true
});

// Send transaction (returns immediately with ID)
const id = await relayer.sendTransaction({
  chainId: baseSepolia.id,
  to: '0xTargetContract...',
  data: '0xCalldata...'
});

// Poll for status separately
const receipt = await relayer.waitForReceipt({ id });

console.log(`Transaction hash: ${receipt.transactionHash}`);

Account (Gelato Smart Account)

Gelato's smart account implementation with ERC-7821 delegation pattern.

Synchronous:

import {
  createGelatoSmartAccountClient,
  toGelatoSmartAccount } from '@gelatocloud/gasless';
import { createPublicClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { baseSepolia } from 'viem/chains';

const owner = privateKeyToAccount('0xYourPrivateKey...');

const publicClient = createPublicClient({
  chain: baseSepolia,
  transport: http()
});

// Create a Gelato smart account
const account = toGelatoSmartAccount({ client: publicClient, owner });

// Create the smart account client
const client = await createGelatoSmartAccountClient({
  account,
  apiKey: process.env.GELATO_API_KEY
});

// Send and wait for inclusion in one call
const receipt = await client.sendTransactionSync({
  calls: [
    { to: '0xContract1...', data: '0xCalldata1...' },
    { to: '0xContract2...', data: '0xCalldata2...' }
  ]// Optional: nonce or nonceKey for 2D nonce management
});

console.log(`Transaction hash: ${receipt.transactionHash}`);

Asynchronous:

import {
  createGelatoSmartAccountClient,
  toGelatoSmartAccount } from '@gelatocloud/gasless';

// ... same setup as above ...

// Send transaction (returns immediately with ID)
const id = await client.sendTransaction({
  calls: [{ to: '0xContract...', data: '0xCalldata...' }] });

// Poll for status separately
const receipt = await client.waitForReceipt({ id });

console.log(`Transaction hash: ${receipt.transactionHash}`);

Bundler (ERC-4337)

Compatible with any ERC-4337 smart account.

import { createGelatoBundlerClient } from '@gelatocloud/gasless';
import { to7702SimpleSmartAccount } from 'permissionless/accounts';
import { createPublicClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { baseSepolia } from 'viem/chains';

const owner = privateKeyToAccount('0xYourPrivateKey...');

const client = createPublicClient({
  chain: baseSepolia,
  transport: http()
});

// Create a smart account 
const account = await to7702SimpleSmartAccount({
  client,
  owner
});

// Create the Gelato bundler client
const bundler = await createGelatoBundlerClient({
  account,
  client,
  apiKey: process.env.GELATO_API_KEY,
  sponsored: true
});

// Send a user operation
const hash = await bundler.sendUserOperation({
  calls: [{ to: '0x...', data: '0x...' }]
});

// Wait for the receipt
const { receipt } = await bundler.waitForUserOperationReceipt({ hash });

console.log(`Transaction hash: ${receipt.transactionHash}`);

WebSocket Subscriptions

WebSockets are enabled by default. Methods automatically race WebSocket notifications against HTTP polling for the fastest result. To disable:

const relayer = createGelatoEvmRelayerClient({
  apiKey: process.env.GELATO_API_KEY,
  ws: { disable: true }
});

Relayer — single transaction:

const id = await relayer.sendTransaction({
  chainId: baseSepolia.id,
  to: '0xTargetContract...',
  data: '0xCalldata...'
});

const subscription = await relayer.ws.subscribe({ id });

subscription.on('success', (data) => {
  console.log(`Included in block ${data.receipt.blockNumber}`);
});

subscription.on('reverted', (data) => {
  console.log(`Reverted: ${data.receipt.blockNumber}`);
});

// Cleanup when done
await relayer.ws.unsubscribe(subscription.subscriptionId);
relayer.ws.disconnect();

Relayer — global (all transactions):

const subscription = await relayer.ws.subscribe();

subscription.on('submitted', (data) => console.log(`${data.id} submitted`));
subscription.on('success', (data) => console.log(`${data.id} success`));
subscription.on('rejected', (data) => console.log(`${data.id} rejected`));

Bundler — same patterns apply via bundler.ws:

const subscription = await bundler.ws.subscribe({ id: userOpHash });
subscription.on('success', (data) => console.log(data.receipt));

Events:

Event Description
pending Transaction pending
submitted Submitted to network
success Successfully included on-chain
rejected Rejected by relayer
reverted Reverted on-chain

Cleanup:

await relayer.ws.unsubscribe(subscription.subscriptionId);
relayer.ws.disconnect();

Get Balance

Check your Gas Tank balance. Available on all client types.

const { balance, decimals, unit } = await relayer.getBalance();
// Also: client.getBalance(), bundler.getBalance()

Sync vs Async

The SDK offers two ways to send transactions:

Sync (sendTransactionSync) Async (sendTransaction + WS)
Returns Final TransactionReceipt ID (Hex) immediately
Lifecycle events None (handled internally) All: pending, submitted, success, rejected, reverted
Tx hash on (re)submission Not exposed hash field on every submitted event
Control Minimal — fire and forget Full — react to each status change via WS subscription
Reorg Protection None Yes, a new update event is published

Sync — call sendTransactionSync to send the transaction and get the receipt in the same call. If the transaction is not included, the SDK will handle it internally and return a final TransactionReceipt by racing http polls and ws updates. Simplest approach when you just need the result:

const receipt = await relayer.sendTransactionSync({ chainId, to, data });

Async — call sendTransaction to get a ID immediately, then subscribe via WebSocket for granular status updates. WS subscriptions give you full control over the transaction lifecycle — you can react to resubmissions, display tx hashes to users in real time, handle rejections immediately, and more. The submitted event includes the transaction hash each time the relay (re)submits the transaction (e.g. with bumped gas):

const id = await relayer.sendTransaction({ chainId, to, data });
const subscription = await relayer.ws.subscribe({ id });

subscription.on('submitted', (data) => {
  console.log(`Tx hash: ${data.hash}`); // available on every (re)submission
});

subscription.on('success', (data) => {
  console.log(`Confirmed in block ${data.receipt.blockNumber}`);
});

API Reference

Relayer API

createGelatoEvmRelayerClient(config)

Creates a low-level relayer client for direct transaction submission.

const client = createGelatoEvmRelayerClient({
  apiKey: string,    // Your Gelato API key
  testnet: boolean   // true for testnets, false for mainnet
});

Methods:

Method Parameters Returns Description
sendTransaction { chainId, to, data, authorizationList?, context? } Promise<Hex> Submit a transaction
sendTransactionSync { chainId, to, data, timeout?, pollingInterval?, throwOnReverted?, ... } Promise<TransactionReceipt> Send and wait for receipt
getStatus { id: string } Promise<Status> Get transaction status
waitForReceipt { id: string, timeout?, pollingInterval?, throwOnReverted? } Promise<TransactionReceipt> Wait for receipt, throws on failure
getBalance - Promise<Balance> Get Gas Tank balance
getCapabilities - Promise<Capabilities> Get supported chains
getFeeData { chainId, gas, l1Fee? } Promise<FeeData> Get network fee data

Account API

toGelatoSmartAccount(params)

Creates a Gelato smart account using ERC-7821 delegation.

const account = toGelatoSmartAccount({
  client: Client,   // viem public client
  owner: Account    // EOA that owns the smart account
});

createGelatoSmartAccountClient(config)

Creates a client for interacting with a Gelato smart account.

const client = await createGelatoSmartAccountClient({
  apiKey: string,              // Your Gelato API key
  account: SmartAccount        // From toGelatoSmartAccount()
});

Methods:

Method Parameters Returns Description
sendTransaction { calls, nonce?, nonceKey?} Promise<Hex> Send transaction(s)
sendTransactionSync { calls, nonce?, nonceKey?, timeout?, pollingInterval?, throwOnReverted?, ... } Promise<TransactionReceipt> Send and wait for receipt
getStatus { id: string } Promise<Status> Get transaction status
waitForReceipt { id: string, timeout?, pollingInterval?, throwOnReverted? } Promise<TransactionReceipt> Wait for receipt, throws on failure
getBalance - Promise<Balance> Get Gas Tank balance
getCapabilities - Promise<Capabilities> Get supported chains

Nonce Options:

  • nonce: Explicit nonce value
  • nonceKey: Key for 2D nonce (allows parallel transactions)

Polling Configuration:

All synchronous methods (sendTransactionSync, sendUserOperationSync, waitForReceipt) support customizable polling behavior:

  • timeout (optional): Maximum wait time in milliseconds
    • Default: 120000 (2 minutes)
    • Must not exceed 600000 (10 minutes)
  • pollingInterval (optional): Frequency to check status in milliseconds
    • Default: 1000 (1 second)

Example:

// Wait up to 30 seconds, checking every 500ms
const receipt = await relayer.sendTransactionSync({
  chainId: baseSepolia.id,
  to: '0xTargetContract...',
  data: '0xCalldata...',
  timeout: 30000,
  pollingInterval: 500
});

Bundler API

createGelatoBundlerClient(config)

Creates an ERC-4337 bundler client. Extends viem's BundlerClient.

const bundler = await createGelatoBundlerClient({
  account: SmartAccount,       // Any ERC-4337 smart account
  client: Client,              // viem public client
  apiKey: string,              // Your Gelato API key
  sponsored: boolean,          // Whether to use sponsored payment via Gas Tank
  pollingInterval?: number     // Polling interval in ms
});

Methods:

Method Parameters Returns Description
sendUserOperation { calls } Promise<Hex> Send a user operation
sendUserOperationSync { calls, timeout?, pollingInterval? } Promise<UserOperationReceipt> Send and wait for receipt
waitForUserOperationReceipt { hash } Promise<{ receipt }> Wait for receipt
getBalance - Promise<Balance> Get Gas Tank balance
estimateUserOperationGas UserOperationParams Promise<GasEstimate> Estimate gas
prepareUserOperation UserOperationParams Promise<UserOperation> Prepare operation
getUserOperationGasPrice - Promise<GasPrice> Get current gas prices
getUserOperationQuote UserOperationParams Promise<Quote> Get fee quote

Types

StatusCode

enum StatusCode {
  Pending = 100,    // Transaction pending
  Submitted = 110,  // Submitted to network
  Success = 200,   // Successfully included
  Rejected = 400,   // Rejected by relayer
  Reverted = 500    // Reverted on-chain
}

ErrorCode

enum ErrorCode {
  // JSON-RPC
  ParseError = -32700,
  InvalidRequest = -32600,
  MethodNotFound = -32601,
  InvalidParams = -32602,
  InternalError = -32603,
  TimeoutError = -32070,

  // Relayer
  Unauthorized = 4100,
  UnsupportedPaymentToken = 4202,
  InsufficientPayment = 4200,
  InsufficientBalance = 4205,
  UnsupportedChain = 4206,
  UnknownTransactionId = 4208,
  InvalidAuthorizationList = 4210,
  SimulationFailed = 4211,

  // Bundler
  ValidationFailed = -32500,
  PaymasterValidationFailed = -32501,
  InvalidSignature = -32507,
  ExecutionFailed = -32521
}

Call

type Call = {
  to: Address;      // Target contract address
  data?: Hex;       // Calldata (optional)
  value?: bigint;   // ETH value (optional)
};

Status Handling

const status = await client.getStatus({ id: hash });

switch (status.status) {
  case StatusCode.Success:
    console.log('Success:', status.receipt.transactionHash);
    break;
  case StatusCode.Rejected:
    console.log('Rejected:', status.message);
    break;
  case StatusCode.Reverted:
    console.log('Reverted:', status.data);
    break;
}

Error Handling

Timeout Errors

Synchronous methods (sendTransactionSync, waitForReceipt) throw TimeoutError when operations don't complete within the configured timeout:

import { TimeoutError } from '@gelatocloud/gasless';

try {
  const receipt = await relayer.sendTransactionSync({
    chainId: baseSepolia.id,
    to: '0xTargetContract...',
    data: '0xCalldata...',
    timeout: 10000
  });
} catch (error) {
  if (error instanceof TimeoutError) {
    console.error('Transaction timed out:', error.message);
    // Transaction may still be pending - you can retry with longer timeout
    // or use async methods to check status manually
  } else {
    console.error('Other error:', error);
  }
}

Revert Handling

By default, sendTransactionSync and waitForReceipt return the receipt even when a transaction reverts on-chain. Set throwOnReverted: true to throw a TransactionRevertedError instead:

import { TransactionRevertedError } from '@gelatocloud/gasless';

try {
  const receipt = await relayer.sendTransactionSync({
    chainId: baseSepolia.id,
    to: '0xTargetContract...',
    data: '0xCalldata...',
    throwOnReverted: true
  });
} catch (error) {
  if (error instanceof TransactionRevertedError) {
    console.error('Transaction reverted:', error.receipt.transactionHash);
    console.error('Error message:', error.erroMessage);
    console.error('Error data:', error.errorData);
    // Also available: error.id, error.chainId, error.createdAt
  }
}

Available on both relayer and account clients via sendTransactionSync and waitForReceipt. Properties on TransactionRevertedError: receipt, id, chainId, createdAt, errorData, errorMessage.

Simulation Errors

When the relayer simulates a transaction before submission and the simulation reverts, a SimulationFailedRpcError is thrown with error code 4211. This happens before the transaction is sent on-chain:

import { SimulationFailedRpcError } from '@gelatocloud/gasless';

try {
  const receipt = await relayer.sendTransactionSync({
    chainId: baseSepolia.id,
    to: '0xTargetContract...',
    data: '0xCalldata...'
  });
} catch (error) {
  if (error instanceof SimulationFailedRpcError) {
    console.error('Simulation reverted:', error.message);
    console.error('Revert data:', error.revertData);
    // error.params contains the original { to, chainId, data } sent
  }
}

Properties on SimulationFailedRpcError: revertData, params, code (4211).

Automatic Retries

Relayer methods (sendTransaction, sendTransactionSync) support automatic retries when specific error codes are encountered. This is useful for transient failures like simulation errors (4211) that may succeed on a subsequent attempt.

const receipt = await relayer.sendTransactionSync(
  {
    chainId: baseSepolia.id,
    to: '0xTargetContract...',
    data: '0xCalldata...'
  },
  {
    retries: {
      max: 3,          // Retry up to 3 times (default: 0, max: 5)
      delay: 1000,     // Wait 1s between retries (default: 200ms)
      backoff: 'fixed', // 'exponential' (default) or 'fixed'
      errorCodes: [4211] // Error codes that trigger a retry (default: [4211])
    }
  }
);

Options:

Option Type Default Description
max number 0 Maximum number of retries (0–5). Clamped to 5.
delay number 200 Base delay in milliseconds before each retry.
backoff 'fixed' | 'exponential' 'exponential' Backoff strategy. 'fixed' keeps constant delay; 'exponential' doubles each retry (delay × 2^attempt).
maxDelay number 10000 Maximum delay in ms. Caps exponential backoff so it never exceeds this value.
errorCodes number[] [4211] RPC error codes that trigger a retry. Default is SimulationFailedRpcError.

Works with both async and sync relayer methods:

// Async
const id = await relayer.sendTransaction(
  { chainId, to, data },
  { retries: { max: 3 } }
);

// Sync
const receipt = await relayer.sendTransactionSync(
  { chainId, to, data },
  { retries: { max: 3 } }
);

Only errors with a matching code property are retried. All other errors are thrown immediately.

Automatic Fallback on Timeout

When sendTransactionSync rpc request times out, the SDK automatically falls back to polling for the transaction status. If you see a warning message like:

Transaction 0x... sync call timed out, falling back to polling for completion. DO NOT RETRY this transaction.

This means your transaction was successfully submitted but the sync method timed out. The SDK will continue polling for completion automatically. Do not retry the operation as this could result in duplicate transactions.

Recovery Strategies

If a timeout occurs:

  1. Wait for automatic fallback: sendTransactionSync automatically polls after timeout
  2. Check status manually: Use getStatus({ id }) to check if transaction is still processing if needed
  3. Retry with longer timeout: Increase timeout and call waitForReceipt again
  4. Use async methods: Switch to async pattern for more control

Configuration Limits

The SDK enforces the following limits to prevent denial of service:

import {
  TimeoutError,
  TransactionRevertedError,
  SimulationFailedRpcError,
  MIN_TIMEOUT,
  MAX_TIMEOUT,
  MIN_POLLING_INTERVAL,
  MAX_POLLING_INTERVAL
} from '@gelatocloud/gasless';

console.log(MIN_TIMEOUT); // 1000ms (1 second)
console.log(MAX_TIMEOUT); // 600000ms (10 minutes)
console.log(MIN_POLLING_INTERVAL); // 100ms
console.log(MAX_POLLING_INTERVAL); // 300000ms (5 minutes)

You can set default timeout and polling interval at the client level:

const relayer = createGelatoEvmRelayerClient({
  apiKey: process.env.GELATO_API_KEY,
  timeout: 30000, // Default 30 second timeout
  pollingInterval: 500 // Default 500ms polling interval
});

// Methods use client defaults unless overridden
const receipt = await relayer.sendTransactionSync({
  chainId: baseSepolia.id,
  to: '0xTargetContract...',
  data: '0xCalldata...',
  // timeout: 60000 // Optional: override client default
});

Requirements

  • Node.js >= 23
  • Peer dependencies: viem, zod
  • Optional: typescript

Examples

See the /examples directory for complete working examples:

Run an example:

cd examples/account/sponsored
npm install
GELATO_API_KEY=your-key npm run dev

Links

License

MIT