Skip to main content
See full example here which uses Pimlico services for bundling, paymaster, gas estimation, etc.

Basic Implementation

1

Create app with the React Quickstart

Follow the React Quickstart Custom setup path: Ethereum (EVM) with Wagmi and viem. Use the quickstart’s Vite scaffold, or scaffold React with your own tooling and install the same packages. In the Dynamic dashboard, enable Ethereum under Chains & Networks and add your dev origin under SecurityAllowed Origins.
2

Install Delegation Toolkit

After the app is created, install the Delegation Toolkit:
npm install @metamask/delegation-toolkit
3

Create Smart Account Hook

src/hooks/useSmartAccount.ts
import {
  Implementation,
  MetaMaskSmartAccount,
  toMetaMaskSmartAccount,
} from "@metamask/delegation-toolkit";
import { useEffect, useState } from "react";
import { useAccount, usePublicClient, useWalletClient } from "wagmi";

export default function useSmartAccount(): {
  smartAccount: MetaMaskSmartAccount | null;
} {
  const { address } = useAccount();
  const publicClient = usePublicClient();
  const { data: walletClient } = useWalletClient();
  const [smartAccount, setSmartAccount] = useState<MetaMaskSmartAccount | null>(
    null
  );

  useEffect(() => {
    if (!address || !walletClient || !publicClient) return;

    console.log("Creating smart account");

    toMetaMaskSmartAccount({
      client: publicClient,
      implementation: Implementation.Hybrid,
      deployParams: [address, [], [], []],
      deploySalt: "0x",
      signatory: { walletClient },
    }).then((smartAccount) => {
      setSmartAccount(smartAccount);
    });
  }, [address, walletClient, publicClient]);

  return { smartAccount };
}
4

Create Bundler Client Hook

src/hooks/useBundlerClient.ts
import { createBundlerClient } from "viem/account-abstraction";
import { useState, useEffect } from "react";
import { usePublicClient } from "wagmi";
import { http } from "viem";

export function useBundlerClient() {
    const [bundlerClient, setBundlerClient] = useState();
    const publicClient = usePublicClient();

    useEffect(() => {
        if (!publicClient) return;
        setBundlerClient(createBundlerClient({
            client: publicClient,
            transport: http("https://your-bundler-rpc.com"),
        }));
    }, [publicClient]);

    return { bundlerClient };
}
5

Send User Operation

src/components/SendUserOperation.tsx
import { parseEther } from "viem";
import useBundlerClient from "/hooks/useBundlerClient";
import useSmartAccount from "../hooks/useSmartAccount";

const SendUserOperation = () => {
    const { bundlerClient } = useBundlerClient();
    const { smartAccount } = useSmartAccount();

    // Appropriate fee per gas must be determined for the specific bundler being used.
    const maxFeePerGas = 1n;
    const maxPriorityFeePerGas = 1n;

    const handleSendUserOperation = async () => {
        const userOperationHash = await bundlerClient.sendUserOperation({
            account: smartAccount,
            calls: [
                {
                    to: "0x1234567890123456789012345678901234567890",
                    value: parseEther("1"),
                },
            ],
            maxFeePerGas,
            maxPriorityFeePerGas,
        });
        // You may want to handle the result here, e.g., show a notification
        console.log("User Operation Hash:", userOperationHash);
    };

    return (
        <button onClick={handleSendUserOperation}>
            Send User Operation
        </button>
    );
}

References