Skip to content

Enabling Gasless Transactions with 0xGasless

Why Gasless Transactions?

One of the primary challenges in blockchain development has been the requirement for users to hold native tokens (like ETH or GLMR) to pay transaction fees. This traditional EOA-based model creates unnecessary friction, particularly when onboarding users who expect Web2-like experiences.

Gasless transactions can help solve this through Account Abstraction (ERC-4337), implementing meta-transactions that separate user actions from fee payment. This architecture allows dApps or third-party paymasters to cover gas costs on behalf of users while smart contract wallets handle the transaction execution. 0xGasless leverages these principles in its SDK, enabling Moonbeam developers to implement sophisticated features like social logins, transaction batching, and custom wallet controls – all while abstracting away the complexity of gas management from end users.

In the following tutorial, we'll go through the end-to-end steps of setting up a paymaster on 0xGasless and dispatching a gasless transaction to modify the state of a smart contract on Moonbeam.

Create and Fund a Paymaster

First, you'll need to register for an account on 0xGasless. Then, create a Paymaster for the Moonbeam Network by pressing Create Paymaster and then taking the following steps:

  1. Enter a name for your paymaster
  2. Select Moonbeam as the chain
  3. Press Create

Create Paymaster

Your paymaster needs funds to cover gas fees for sponsored transactions. To deposit GLMR into your paymaster, take the following steps:

  1. Enter the amount you would like to deposit
  2. Press Deposit and confirm the transaction in your wallet

Fund Paymaster

Your deposited funds remain flexible - use them to sponsor gasless transactions or withdraw them whenever needed.

Dispatching a Gasless Transaction

In the following section, we'll create a script demonstrating how to dispatch a gasless transaction.

Prerequisites

Create a .env file in your project's root directory with the following:

PRIVATE_KEY=INSERT_PRIVATE_KEY
RPC_URL=https://rpc.api.moonbeam.network

Why are we specifying a private key in the .env? While this transaction will be gasless, you still need a private key to sign the transaction. The account associated with this private key:

  • Does not need any GLMR tokens
  • Will not pay for gas fees
  • Is only used for transaction signing

Note

Never commit your .env file or share your private key. Add .env to your .gitignore file.

Also, make sure you have installed the 0xGasless SDK and supporting ethers and dotenv packages:

npm install ethers dotenv @0xgasless/smart-account

First, we'll import the required packages as follows:

require('dotenv').config();
const ethers = require('ethers');
const {
  PaymasterMode,
  createSmartAccountClient,
} = require('@0xgasless/smart-account');

Next, we'll set the critical constants. We must define the CHAIN_ID, BUNDLER_URL, and PAYMASTER_URL. You can get your unique paymaster URL from the paymaster on your 0xGasless Dashboard.

The contract address we've defined here is the address of an Incrementer contract on Moonbeam, on which we'll call the increment function specified by the function selector. This simple contract will allow us to easily see if the gasless transaction has been dispatched successfully.

const CHAIN_ID = 1284; // Moonbeam mainnet
const BUNDLER_URL = `https://bundler.0xgasless.com/${CHAIN_ID}`;
const PAYMASTER_URL =
  'https://paymaster.0xgasless.com/v1/1284/rpc/INSERT_API_KEY';
const CONTRACT_ADDRESS = '0x3aE26f2c909EB4F1EdF97bf60B36529744b09213';
const FUNCTION_SELECTOR = '0xd09de08a';

Warning

The Paymaster URL format has recently changed. Use:

https://paymaster.0xgasless.com/v1/1284/rpc/INSERT_API_KEY

Do not use the deprecated format:

https://paymaster.0xgasless.com/api/v1/1284/rpc/INSERT_API_KEY

The difference is that /api has been removed from the path. Make sure your code uses the current format.

Sending the Transaction

To send a gasless transaction using the 0xGasless smart account, you can call smartWallet.sendTransaction() with two parameters:

  • The transaction object containing the contract interaction details
  • A configuration object specifying paymasterServiceData with SPONSORED mode. This indicates that the 0xGasless paymaster will use the gas tank to pay for the gas.

The function returns a UserOperation response containing a hash. Wait for the transaction receipt using the waitForUserOpReceipt() helper function, which polls for completion with a configurable timeout (default 60 seconds).

const userOpResponse = await smartWallet.sendTransaction(transaction, {
  paymasterServiceData: { mode: PaymasterMode.SPONSORED },
});

const receipt = await waitForUserOpReceipt(userOpResponse, 60000);

Putting it all together and adding plenty of logging and error handling for easy debugging, the full script is as follows:

Dispatch a gasless transaction
require('dotenv').config();
const ethers = require('ethers');
const {
  PaymasterMode,
  createSmartAccountClient,
} = require('@0xgasless/smart-account');

const CHAIN_ID = 1284; // Moonbeam mainnet
const BUNDLER_URL = `https://bundler.0xgasless.com/${CHAIN_ID}`;
const PAYMASTER_URL =
  'https://paymaster.0xgasless.com/v1/1284/rpc/INSERT_API_KEY';
const CONTRACT_ADDRESS = '0x3aE26f2c909EB4F1EdF97bf60B36529744b09213';
const FUNCTION_SELECTOR = '0xd09de08a';

async function main() {
  console.log('Starting the script...');
  try {
    // Set up provider and wallet
    console.log('Setting up provider and wallet...');
    const provider = new ethers.providers.JsonRpcProvider(process.env.RPC_URL);
    const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);

    // Check connection and balance
    console.log('Checking network connection...');
    const network = await provider.getNetwork();
    console.log(
      `Connected to network: ${network.name} (Chain ID: ${network.chainId})`
    );
    const balance = await provider.getBalance(wallet.address);
    console.log(`Wallet balance: ${ethers.utils.formatEther(balance)} GLMR`);

    // Initialize smart account
    console.log('Initializing smart account...');
    const smartWallet = await createSmartAccountClient({
      signer: wallet,
      paymasterUrl: PAYMASTER_URL,
      bundlerUrl: BUNDLER_URL,
      chainId: CHAIN_ID,
    });
    const smartWalletAddress = await smartWallet.getAddress();
    console.log('Smart Account Address:', smartWalletAddress);

    // Create a transaction for contract interaction
    console.log('Creating contract transaction...');
    const transaction = {
      to: CONTRACT_ADDRESS,
      value: '0', // No native token transfer
      data: FUNCTION_SELECTOR, // The function selector for the method we want to call
    };

    // Send the transaction
    console.log('Sending transaction...');
    const userOpResponse = await smartWallet.sendTransaction(transaction, {
      paymasterServiceData: { mode: PaymasterMode.SPONSORED },
    });
    console.log('UserOp Hash:', userOpResponse.hash);

    console.log('Waiting for transaction receipt...');
    const userOpReceipt = await waitForUserOpReceipt(userOpResponse, 60000); // Wait for up to 60 seconds

    if (userOpReceipt.success) {
      console.log('Transaction successful!');
      console.log('Transaction hash:', userOpReceipt.receipt.transactionHash);
    } else {
      console.log('Transaction failed');
      console.log('Receipt:', userOpReceipt);
    }
  } catch (error) {
    console.error('An error occurred:');
    console.error(error);
  }
}

async function waitForUserOpReceipt(userOpResponse, timeoutMs = 60000) {
  return new Promise((resolve, reject) => {
    const startTime = Date.now();
    const checkReceipt = async () => {
      try {
        const receipt = await userOpResponse.wait();
        resolve(receipt);
      } catch (error) {
        if (Date.now() - startTime > timeoutMs) {
          reject(new Error(`Transaction wait timeout after ${timeoutMs}ms`));
        } else {
          setTimeout(checkReceipt, 5000); // Retry every 5 seconds
        }
      }
    };
    checkReceipt();
  });
}

main().catch((error) => {
  console.error('Unhandled error in main function:');
  console.error(error);
});

Verifying Completion

Upon running the script, you'll see output that looks like the following:

0xgasless % node dispatch.js Starting the script... Setting up provider and wallet... Checking network connection... Connected to network: unknown (Chain ID: 1284) Wallet balance: 8.781249287153010128 GLMR Initializing smart account... Smart Account Address: 0xbBf77D3B43d81D426c4c3D200a76F4D3a914ccE3 Creating contract transaction... Sending transaction... UserOp Hash: undefined Waiting for transaction receipt... Transaction successful! Transaction hash: 0x9cb49cc0acc21abc364c13dd52b3f65c206ec61c57a13c23b635f59e1919cf7c

Since the gasless transaction we initiated interacts with an Incrementer smart contract on Moonbeam, it's easy to check to see if the transaction was initiated successfully. You can return to Read Contract section of the Incrementer contract on Moonscan and check the number stored in the contract. Alternatively, you can head to the Internal Transactions tab and toggle advanced mode ON to see the contract call incrementing the contract.

For more information about integrating support for gasless transactions into your dApp, be sure to check out the 0xGasless docs.

This tutorial is for educational purposes only. As such, any contracts or code created in this tutorial should not be used in production.
The information presented herein has been provided by third parties and is made available solely for general information purposes. Moonbeam does not endorse any project listed and described on the Moonbeam Doc Website (https://docs.moonbeam.network/). Moonbeam Foundation does not warrant the accuracy, completeness or usefulness of this information. Any reliance you place on such information is strictly at your own risk. Moonbeam Foundation disclaims all liability and responsibility arising from any reliance placed on this information by you or by anyone who may be informed of any of its contents. All statements and/or opinions expressed in these materials are solely the responsibility of the person or entity providing those materials and do not necessarily represent the opinion of Moonbeam Foundation. The information should not be construed as professional or financial advice of any kind. Advice from a suitably qualified professional should always be sought in relation to any particular matter or circumstance. The information herein may link to or integrate with other websites operated or content provided by third parties, and such other websites may link to this website. Moonbeam Foundation has no control over any such other websites or their content and will have no liability arising out of or related to such websites or their content. The existence of any such link does not constitute an endorsement of such websites, the content of the websites, or the operators of the websites. These links are being provided to you only as a convenience and you release and hold Moonbeam Foundation harmless from any and all liability arising from your use of this information or the information provided by any third-party website or service.
Last update: November 27, 2024
| Created: November 27, 2024