Documentation

Executing Token Swap on Uniswap V3 with Pimlico's ERC20 Paymaster & Alto Bundler

Pimlico Plugin tailored for Account Abstraction (AA) with a performant ERC-4337 bundler and Pimlico Paymasters.

Introduction

This tutorial guides you through integrating Pimlico Paymaster with their ERC-4337 compatible Alto bundler for account abstraction in blockchain applications using the Pimlico plugin. Pimlico Alto simplifies bundling user operations into transactions and submitting them to the blockchain via standard JSON-RPC requests. It supports permissionless.js, enabling seamless Web3 application development and testing. Additionally it also supports Paymaster services allowing the users to perform gasless transactions, and paying for gas in ERC20s instead


Why Test Pimlico AA with BuildBear Sandbox?

✅ Seamless Integration

Pimlico Plugin is designed for effortless integration with BuildBear Sandboxes, making it easy to experiment with Account Abstraction (AA) features.

✅ Permissionless.js Support

  • High-level API for managing ERC-4337 smart accounts, bundlers, and paymasters.
  • Built on Viem for modularity and extensibility.

✅ JSON-RPC Support for Bundler

Interact with the bundler using standard methods:

  • eth_sendUserOperation
  • eth_estimateUserOperationGas
  • eth_getUserOperationReceipt
  • eth_getUserOperationByHash
  • eth_supportedEntryPoints
  • pimlico_getUserOperationGasPrice
  • pimlico_getUserOperationStatus

✅ ERC20 Paymaster Support

Choose from a list of supported tokens to pay for transactions in ERC20 instead of native tokens.

Supported ERC20s

✅ Unlocked Faucets

BuildBear provides unlimited access to native and ERC-20 tokens, ensuring smooth development and testing.


Step 1: Setting Up Pimlico

1. Create a BuildBear Sandbox

  • Navigate to BuildBear and create a new Sandbox or use an existing one.
  • Open the Plugins tab in your Sandbox dashboard.

2. Install the Pimlico Plugin

  • Locate Pimlico in the plugin marketplace.
  • Click Install to add it to your environment. Install Pimlico Plugin
  • Verify installation in the Installed Plugins tab.

3. Retrieve the RPC URL

  • Open your BuildBear Sandbox dashboard.
  • Copy the RPC URL, which also serves as the Pimlico Client API endpoint.
BuildBear Sandbox RPC URL: https://rpc.buildbear.io/{Sandbox-ID}
Pimlico Client API: https://rpc.buildbear.io/{Sandbox-ID}

4. Install Dependencies

Ensure all necessary dependencies are installed. Example package.json:

{
  "name": "pimlico-tutorial-template",
  "version": "1.0.0",
  "description": "A template repository for Pimlico tutorials (https://docs.pimlico.io/tutorial)",
  "main": "index.ts",
  "type": "module",
  "scripts": {
    "start": "tsx index.ts"
  },
  "author": "",
  "license": "MIT",
  "dependencies": {
    "dotenv": "^16.3.1",
    "ethers": "^6.13.5",
    "permissionless": "^0.2.0",
    "viem": "^2.20.0"
  },
  "devDependencies": {
    "@types/node": "^20.11.10",
    "tsx": "^3.13.0"
  }
}

Step 2: Configuring BuildBear

Note: For this walkthrough, use a sandbox forked from Polygon Mainnet

1. Define the Sandbox URL

const buildbearSandboxUrl = "https://rpc.buildbear.io/{SANDBOX-ID}";

2. Set Up BuildBear Sandbox Network

const BBSandboxNetwork = /*#__PURE__*/ defineChain({
  id: SANDBOX-CHAIN-ID,
  name: "BuildBear x Polygon Mainnet Sandbox",
  nativeCurrency: { name: "BBETH", symbol: "BBETH", decimals: 18 },
  rpcUrls: {
    default: {
      http: [buildbearSandboxUrl],
    },
  },
  blockExplorers: {
    default: {
      name: "BuildBear x Polygon Mainnet Scan",
      url: "https://explorer.buildbear.io/{SANDBOX-ID}",
      apiUrl: "https://api.buildbear.io/{SANDBOX-ID}/api",
    },
  },
});

3. Generate a Private Key

const privateKey = (process.env.PRIVATE_KEY as Hex)
  ? (process.env.PRIVATE_KEY as Hex)
  : (() => {
      const pk = generatePrivateKey();
      appendFileSync(".env", `\nPRIVATE_KEY=${pk}`);
      return pk;
    })();

4. Set Up Public Client

export const publicClient = createPublicClient({
  chain: BBSandboxNetwork,
  transport: http(buildbearSandboxUrl), //@>>> Put in buildbear rpc
});

Step 3: Configuring Pimlico Client & Smart Accounts

1. Set Up Pimlico Client

const pimlicoClient = createPimlicoClient({
  transport: http(buildbearSandboxUrl),
  entryPoint: {
    address: entryPoint07Address,
    version: "0.7",
  },
});

2. Create Smart Account & Client

const signer = privateKeyToAccount(privateKey);
 
const account = await toSafeSmartAccount({
  client: publicClient,
  owners: [signer],
  entryPoint: {
    address: entryPoint07Address,
    version: "0.7",
  },
  version: "1.4.1",
});
 
const smartAccountClient = createSmartAccountClient({
  account,
  chain: BBSandboxNetwork,
  bundlerTransport: http(buildbearSandboxUrl),
  paymaster: pimlicoClient,
  userOperation: {
    estimateFeesPerGas: async () => {
      return (await pimlicoClient.getUserOperationGasPrice()).fast;
    },
  },
});

Step 4: Funding & Executing Transactions

1. Check Account Balance

let balance = await publicClient.getBalance({ address: account.address }); // Get the balance of the sender
let daiBalanceBefore = await getDAIBalance();
let usdtBalanceBefore = await getUSDTBalance();
 
if (+daiBalanceBefore.toString() <= 0) {
  console.log("====================================");
  console.log(
    `⚠️⚠️Fund your Account with DAI tokens from your BuildBear Sandbox Faucet and try running the script again.\nSmart Account Address: ${account.address}\nSigner Address: ${signer.address}\n`
  );
  console.log("====================================");
  exit();
} else {
  console.log("====================================");
  console.log(`Smart Account Address: ${account.address}`);
  console.log(`Signer Address: ${signer.address}`);
 
  console.log("====================================");
}

2. Fund Smart Account

Native Token Faucet

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "buildbear_nativeFaucet",
  "params": [{
    "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
    "balance": "10000"
  }]
}

ERC Token Faucet

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "buildbear_ERC20Faucet",
  "params": [{
    "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
    "token": "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063",
    "balance": "10000"
  }]
}

3. Setup Helper Functions

async function getUSDTBalance(): Promise<string> {
  let res = await publicClient.readContract({
    address: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
    abi: ERC20Abi,
    functionName: "balanceOf",
    args: [account.address],
  });
  return formatUnits(res as bigint, 6).toString();
}
 
async function getDAIBalance(): Promise<string> {
  let res = await publicClient.readContract({
    address: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063",
    abi: ERC20Abi,
    functionName: "balanceOf",
    args: [account.address],
  });
  return formatUnits(res as bigint, 18).toString();
}

4. Overriding the Paymaster Deposit

With recent updates to Pimlico Paymasters, the paymaster balance needs to be overriden, to prevent failures in UserOp execution due to low funds or no funds in Pimlico Paymaster You can refer to the SingletonPaymasterAbi here

async function overrideDeposit(paymaster: any) {
  const client = createWalletClient({
    account: signer,
    chain: BBSandboxNetwork, // or any other chain like goerli, polygon, etc.
    transport: http(buildbearSandboxUrl),
  });
  const singletonPaymaster = getContract({
    address: paymaster,
    abi: SingletonPaymasterAbi,
    client: client,
  });
 
  await singletonPaymaster.write.deposit({
    value: parseEther("500"),
  });
}
 
overrideDeposit(swapParams.paymasterV7Address).catch(e => {
  console.error(e);
});

5. Initialize Swap Params & Estimate Cost

let swapParams = {
  tokenIn: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063",
  tokenOut: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
  fee: 3000,
  recipient: account.address,
  deadline: BigInt(Math.floor(Date.now() / 1000) + 120),
  amountIn: parseEther("1"),
  amountOutMinimum: BigInt(0),
  sqrtPriceLimitX96: BigInt(0),
  v3Router: "0xE592427A0AEce92De3Edee1F18E0157C05861564",
  paymasterV7Address: "0x0000000000000039cd5e8ae05257ce51c473ddd1",
};
 
console.log("🟠 Calculating UserOp Cost in DAI....");
 
const quotes = await pimlicoClient.getTokenQuotes({
  chain: BBSandboxNetwork,
  tokens: [swapParams.tokenIn],
});
 
const { postOpGas, exchangeRate, paymaster } = quotes[0];
 
const userOperation = await smartAccountClient.prepareUserOperation({
  calls: [
    {
      to: swapParams.tokenIn,
      abi: parseAbi(["function approve(address,uint)"]),
      functionName: "approve",
      args: [swapParams.v3Router, parseEther("1")],
    },
    {
      to: swapParams.v3Router,
      abi: parseAbi([
        `function exactInputSingle(
        (   address, 
            address, 
            uint24, 
            address, 
            uint256,
            uint256, 
            uint256, 
            uint160)
            ) 
         external payable returns (uint256 amountOut)`,
      ]),
      functionName: "exactInputSingle",
      args: [[
        swapParams.tokenIn,
        swapParams.tokenOut,
        swapParams.fee,
        swapParams.recipient,
        swapParams.deadline,
        swapParams.amountIn,
        swapParams.amountOutMinimum,
        swapParams.sqrtPriceLimitX96,
      ]],
    },
  ],
});
 
const userOperationMaxGas =
  userOperation.preVerificationGas +
  userOperation.callGasLimit +
  userOperation.verificationGasLimit +
  (userOperation.paymasterPostOpGasLimit || 0n) +
  (userOperation.paymasterVerificationGasLimit || 0n);
 
const userOperationMaxCost = userOperationMaxGas * userOperation.maxFeePerGas;
 
const maxCostInToken =
  ((userOperationMaxCost + postOpGas * userOperation.maxFeePerGas) * exchangeRate) /
  BigInt(1e18);

6. Send Transaction

const txHash = await smartAccountClient.sendUserOperation({
  account,
  calls: [
    {
      to: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063" as `0x${string}`, //DAI
      abi: parseAbi(["function approve(address,uint)"]),
      functionName: "approve",
      args: [swapParams.paymasterV7Address, maxCostInToken],
    },
    {
      to: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063" as `0x${string}`, //DAI
      abi: parseAbi(["function approve(address,uint)"]),
      functionName: "approve",
      args: [swapParams.v3Router, parseEther("1")],
    },
    {
      to: swapParams.v3Router, //UniV3 Router
      abi: parseAbi([
        "function exactInputSingle((address, address , uint24 , address , uint256 , uint256 , uint256 , uint160)) external payable returns (uint256 amountOut)",
      ]),
      functionName: "exactInputSingle",
      args: [
        [
          swapParams.tokenIn,
          swapParams.tokenOut,
          swapParams.fee,
          swapParams.recipient,
          swapParams.deadline,
          swapParams.amountIn,
          swapParams.amountOutMinimum,
          swapParams.sqrtPriceLimitX96,
        ],
      ],
    },
  ],
  paymasterContext: {
    token: swapParams.tokenIn,
  },
});
 
console.log("🟠 Swapping DAI....");
 
let { receipt } = await smartAccountClient.waitForUserOperationReceipt({
  hash: txHash,
  retryCount: 7,
  pollingInterval: 2000,
});
 
console.log(
  `🟢User operation included: https://explorer.buildbear.io/pretty-medusa-192c3f8e/tx/${receipt.transactionHash}`
);
 
balance = await publicClient.getBalance({ address: account.address }); // Get the balance of the sender
let daiBalanceAfter = await getDAIBalance();
let usdtBalanceAfter = await getUSDTBalance();
 
console.log(
  `🟢 Yay!! 🎉🎉 Swapped ${formatUnits(swapParams.amountIn, 18)} DAI to ${
    +usdtBalanceAfter - +usdtBalanceBefore
  } USDT`
);
 
console.log("🟢 Balance after transaction: ", formatEther(balance));
console.log("🟢 DAI Balance after transaction: ", daiBalanceAfter);
console.log("🟢 USDT Balance after transaction: ", usdtBalanceAfter);
console.log("🟢 Max DAI Estimate for UserOp: ", formatEther(maxCostInToken));
 
console.log(
  `🟢 DAI charged for UserOp: ${formatUnits(
    (
      BigInt(parseUnits(daiBalanceBefore, 18)) -
      BigInt(parseUnits(daiBalanceAfter, 18)) -
      BigInt(parseUnits(`1`, 18))
    ).toString(),
    18
  )}`
);
 
exit();
Expand to See the Complete Tutorial Script
import "dotenv/config";
import { appendFileSync } from "fs";
import { toSafeSmartAccount } from "permissionless/accounts";
import {
  Hex,
  concat,
  createPublicClient,
  defineChain,
  formatEther,
  formatUnits,
  http,
  keccak256,
  maxUint256,
  pad,
  parseAbi,
  createTestClient,
  createWalletClient,
  toHex,
  getContract,
} from "viem";
 
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import { createPimlicoClient } from "permissionless/clients/pimlico";
import { entryPoint07Address, UserOperation } from "viem/account-abstraction";
import { createSmartAccountClient } from "permissionless";
import { parseEther, parseUnits } from "ethers";
import { exit } from "process";
import { simulateContract, writeContract } from "viem/actions";
 
import SingletonPaymasterAbi from "./utils/ABIs/SingletonPaymaster.json";
import ERC20Abi from "./utils/ABIs/ERC20.json";
 
const buildbearSandboxUrl = "https://rpc.buildbear.io/pretty-medusa-192c3f8e";
 
const BBSandboxNetwork = /*#__PURE__*/ defineChain({
  id: 137, // IMPORTANT : replace this with your sandbox's chain id
  name: "BuildBear x Polygon Mainnet Sandbox", // name your network
  nativeCurrency: { name: "BBETH", symbol: "BBETH", decimals: 18 }, // native currency of forked network
  rpcUrls: {
    default: {
      http: [buildbearSandboxUrl],
    },
  },
  blockExplorers: {
    default: {
      name: "BuildBear x Polygon Mainnet Scan", // block explorer for network
      url: "https://explorer.buildbear.io/pretty-medusa-192c3f8e",
      apiUrl: "https://api.buildbear.io/pretty-medusa-192c3f8e/api",
    },
  },
});
 
const privateKey = (process.env.PRIVATE_KEY as Hex)
  ? (process.env.PRIVATE_KEY as Hex)
  : (() => {
      const pk = generatePrivateKey();
      appendFileSync(".env", `\nPRIVATE_KEY=${pk}`);
      return pk;
    })();
 
export const publicClient = createPublicClient({
  chain: BBSandboxNetwork,
  transport: http(buildbearSandboxUrl), //@>>> Put in buildbear rpc
});
 
const pimlicoClient = createPimlicoClient({
  transport: http(buildbearSandboxUrl),
  entryPoint: {
    address: entryPoint07Address,
    version: "0.7",
  },
});
 
const signer = privateKeyToAccount(privateKey);
 
const account = await toSafeSmartAccount({
  client: publicClient,
  owners: [signer],
 
  entryPoint: {
    address: entryPoint07Address,
    version: "0.7",
  }, // global entrypoint
  version: "1.4.1",
});
 
const smartAccountClient = createSmartAccountClient({
  account,
  chain: BBSandboxNetwork,
  bundlerTransport: http(buildbearSandboxUrl), //sending the tx to buildbear
  paymaster: pimlicoClient,
  userOperation: {
    estimateFeesPerGas: async () => {
      return (await pimlicoClient.getUserOperationGasPrice()).fast;
    },
  },
});
 
let balance = await publicClient.getBalance({ address: account.address }); // Get the balance of the sender
let daiBalanceBefore = await getDAIBalance();
let usdtBalanceBefore = await getUSDTBalance();
 
if (+daiBalanceBefore.toString() <= 0) {
  console.log("====================================");
  console.log(
    `⚠️⚠️Fund your Account with DAI tokens from your BuildBear Sandbox Faucet and try running the script again.\nSmart Account Address: ${account.address}\nSigner Address: ${signer.address}\n`
  );
  console.log("====================================");
  exit();
} else {
  console.log("====================================");
  console.log(`Smart Account Address: ${account.address}`);
  console.log(`Signer Address: ${signer.address}`);
 
  console.log("====================================");
}
 
console.log("====================================");
 
console.log(
  "-------- UserOp to Swap DAI to USDT on Uniswap V3 with Alto ---------"
);
console.log("🟠 Balance before transaction: ", formatEther(balance));
console.log("🟠 DAI Balance before transaction: ", daiBalanceBefore);
console.log("🟠 USDT Balance before transaction: ", usdtBalanceBefore);
console.log("====================================");
 
const swapParams = {
  tokenIn: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063" as `0x${string}`, // DAI
  tokenOut: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F" as `0x${string}`, // USDT
  fee: 3000 as number, //fee
  recipient: account.address, // recipient
  deadline: (Math.floor(Date.now() / 1000) + 60 * 2) as unknown as bigint, // expiration
  amountIn: parseEther("1") as bigint, //amountIn
  amountOutMinimum: 0 as unknown as bigint, //amountOutMinimum
  sqrtPriceLimitX96: 0 as unknown as bigint, //sqrtPriceLimitX96
  v3Router: "0xE592427A0AEce92De3Edee1F18E0157C05861564" as `0x${string}`,
  paymasterV7Address:
    "0x0000000000000039cd5e8ae05257ce51c473ddd1" as `0x${string}`,
};
 
async function overrideDeposit(paymaster: any) {
  const client = createWalletClient({
    account: signer,
    chain: BBSandboxNetwork, // or any other chain like goerli, polygon, etc.
    transport: http(buildbearSandboxUrl),
  });
  const singletonPaymaster = getContract({
    address: paymaster,
    abi: SingletonPaymasterAbi,
    client: client,
  });
 
  await singletonPaymaster.write.deposit({
    value: parseEther("500"),
  });
}
 
overrideDeposit(swapParams.paymasterV7Address).catch(e => {
  console.error(e);
});
 
// await overWritePaymasterSigner();
 
console.log("🟠 Approving DAI....");
console.log("====================================");
 
console.log("🟠 Calculating UserOp Cost in DAI....");
 
// Get quotes for tokens in array on given network
const quotes = await pimlicoClient.getTokenQuotes({
  chain: BBSandboxNetwork,
  tokens: [swapParams.tokenIn],
});
 
// extract post op gas, exchange rate and paymaster from quotes
const { postOpGas, exchangeRate, paymaster } = quotes[0];
 
// prepare user operation & calculating the estimate
const userOperation: UserOperation<"0.7"> =
  await smartAccountClient.prepareUserOperation({
    calls: [
      {
        to: swapParams.paymasterV7Address as `0x${string}`, //DAI
        abi: parseAbi(["function deposit() payable"]),
        functionName: "deposit",
        args: [],
        value: parseEther("1"),
      },
      {
        to: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063" as `0x${string}`, //DAI
        abi: parseAbi(["function approve(address,uint)"]),
        functionName: "approve",
        args: [swapParams.v3Router, parseEther("1")],
      },
      {
        to: swapParams.v3Router, //UniV3 Router
        abi: parseAbi([
          "function exactInputSingle((address, address , uint24 , address , uint256 , uint256 , uint256 , uint160)) external payable returns (uint256 amountOut)",
        ]),
        functionName: "exactInputSingle",
        args: [
          [
            swapParams.tokenIn,
            swapParams.tokenOut,
            swapParams.fee,
            swapParams.recipient,
            swapParams.deadline,
            swapParams.amountIn,
            swapParams.amountOutMinimum,
            swapParams.sqrtPriceLimitX96,
          ],
        ],
      },
    ],
  });
 
// calculate max cost in token
const userOperationMaxGas =
  userOperation.preVerificationGas +
  userOperation.callGasLimit +
  userOperation.verificationGasLimit +
  (userOperation.paymasterPostOpGasLimit || 0n) +
  (userOperation.paymasterVerificationGasLimit || 0n);
 
// calculate max cost in token
const userOperationMaxCost = userOperationMaxGas * userOperation.maxFeePerGas;
 
// using formula here https://github.com/pimlicolabs/singleton-paymaster/blob/main/src/base/BaseSingletonPaymaster.sol#L334-L341
const maxCostInToken =
  ((userOperationMaxCost + postOpGas * userOperation.maxFeePerGas) *
    exchangeRate) /
  BigInt(1e18);
console.log("====================================");
 
const txHash = await smartAccountClient.sendUserOperation({
  account,
  calls: [
    {
      to: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063" as `0x${string}`, //DAI
      abi: parseAbi(["function approve(address,uint)"]),
      functionName: "approve",
      args: [swapParams.paymasterV7Address, maxCostInToken],
    },
    {
      to: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063" as `0x${string}`, //DAI
      abi: parseAbi(["function approve(address,uint)"]),
      functionName: "approve",
      args: [swapParams.v3Router, parseEther("1")],
    },
    {
      to: swapParams.v3Router, //UniV3 Router
      abi: parseAbi([
        "function exactInputSingle((address, address , uint24 , address , uint256 , uint256 , uint256 , uint160)) external payable returns (uint256 amountOut)",
      ]),
      functionName: "exactInputSingle",
      args: [
        [
          swapParams.tokenIn,
          swapParams.tokenOut,
          swapParams.fee,
          swapParams.recipient,
          swapParams.deadline,
          swapParams.amountIn,
          swapParams.amountOutMinimum,
          swapParams.sqrtPriceLimitX96,
        ],
      ],
    },
  ],
  paymasterContext: {
    token: swapParams.tokenIn,
  },
});
console.log("🟠 Swapping DAI....");
 
let { receipt } = await smartAccountClient.waitForUserOperationReceipt({
  hash: txHash,
  retryCount: 7,
  pollingInterval: 2000,
});
 
console.log(
  `🟢User operation included: https://explorer.buildbear.io/pretty-medusa-192c3f8e/tx/${receipt.transactionHash}`
);
 
balance = await publicClient.getBalance({ address: account.address }); // Get the balance of the sender
let daiBalanceAfter = await getDAIBalance();
let usdtBalanceAfter = await getUSDTBalance();
 
console.log(
  `🟢 Yay!! 🎉🎉 Swapped ${formatUnits(swapParams.amountIn, 18)} DAI to ${
    +usdtBalanceAfter - +usdtBalanceBefore
  } USDT`
);
 
console.log("🟢 Balance after transaction: ", formatEther(balance));
console.log("🟢 DAI Balance after transaction: ", daiBalanceAfter);
console.log("🟢 USDT Balance after transaction: ", usdtBalanceAfter);
console.log("🟢 Max DAI Estimate for UserOp: ", formatEther(maxCostInToken));
 
console.log(
  `🟢 DAI charged for UserOp: ~${formatUnits(
    (
      BigInt(parseUnits(daiBalanceBefore, 18)) -
      BigInt(parseUnits(daiBalanceAfter, 18)) -
      BigInt(parseUnits(`1`, 18))
    ).toString(),
    18
  )}`
);
 
exit();
 
// Helper Functions
 
// get USDT Balance of Smart Account
async function getUSDTBalance(): Promise<string> {
  let res = await publicClient.readContract({
    address: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
    abi: ERC20Abi,
    functionName: "balanceOf",
    args: [account.address],
  });
  return formatUnits(res as bigint, 6).toString();
}
 
// get DAI Balance of Smart Account
async function getDAIBalance(): Promise<string> {
  let res = await publicClient.readContract({
    address: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063",
    abi: ERC20Abi,
    functionName: "balanceOf",
    args: [account.address],
  });
  return formatUnits(res as bigint, 18).toString();
}

Execute the script with npm start and the swap should go through producing the following output:

====================================
Smart Account Address: 0xa03Af1e5A78F70d8c7aCDb0ddaa2731E4A56E8FB
====================================
-------- UserOp to Swap DAI to USDT on Uniswap V3 with Alto ---------
🟠 Balance before transaction:  100.99956781271324068
🟠 DAI Balance before transaction:  85.99999999999986006
🟠 USDT Balance before transaction:  14.970922
====================================
🟠 Approving DAI....
====================================
🟠 Calculating UserOp Cost in DAI....
====================================
🟠 Swapping DAI....
🟢 Yay!! 🎉🎉 Swapped 1 DAI to 0.9979220000000009 USDT
🟢 Balance after transaction:  100.99956781271324068
🟢 DAI Balance after transaction:  84.999999999999848441
🟢 USDT Balance after transaction:  15.968844
🟢 Max DAI Estimate for UserOp:  1.056731379494445588
🟢 DAI charged for UserOp:  0.000000000000014211

Execution Output Screenshot


Step 5: Debugging and Deep Dive into UserOp with Sentio

Clicking on View On Sentio will open a Sentio debugger for the transaction, where you can observe:

1. Fund Flow

  • Depicts the flow of funds among different contracts and their order of execution

Fund Flow

2. Call Trace

  • Shows the entire call trace for the transaction, including:
    • Contract calls
    • Functions called within the contract
    • Inputs and outputs

Call Trace

3. Call Graph

  • Visual graph showing top-level overview of contract interactions

Call Graph

4. Gas Profiler

  • Displays gas usage per function and gas metrics:
    • Limits, actual usage, initial gas

Gas Profiler

5. Debugging With Sentio

  • Access the Debugger Tab:
    • View inputs, gas metrics & return values for every call
  • Contracts Tab:
    • Deeper inspection of smart contracts and functions
  • Events Tab:
    • All event emissions from the transaction
  • State Tab:
    • State of funds before & after the transaction

Debugger Tab Contracts Tab Events Tab

✅ Conclusion

By following this tutorial, you have successfully:

  • ✅ Set up Pimlico on BuildBear
  • ✅ Configured a smart account using permissionless.js
  • ✅ Set up ERC20 Paymaster with Pimlico Client & Alto Bundler
  • ✅ Funded the smart account using BuildBear faucets
  • ✅ Executed a UserOperation to swap tokens on Uniswap V3 using ERC20 gas payments (DAI)

For full code and demos, check out the GitHub repository!