Documentation

Batua Smart-Account Wallet Tutorial

Step-by-step setup and configuration for a Next.js app integrating Batua’s smart-account SDK and Wagmi/RainbowKit.

What is Batua?

Batua.sh is an easy-to-integrate, embedded smart-account secured by passkeys. It offers built-in support for sponsoring transactions, batching multiple calls, and gives you complete ownership of the code. Thanks to shadcn/ui, it embeds seamlessly into your app’s theme, and it works out of the box with Wagmi, Viem, Ethers, Privy, Dynamic, and more.

In this tutorial, you’ll scaffold a Next.js 13 TypeScript app, install and configure Batua’s smart-account SDK alongside Wagmi & RainbowKit, and build a simple interface to connect your passkey-secured wallet and send batch ERC-20 transactions.

1. Create your Next.js App

You’ll start by scaffolding a new Next.js 13 TypeScript project.

npx create-next-app@latest batua-demo --typescript
cd batua-demo

2. Install Core Dependencies

Install React-Query, Wagmi, RainbowKit, and utilities:

npm install wagmi viem @tanstack/react-query @rainbow-me/rainbowkit @radix-ui/react-accordion @radix-ui/react-dialog @radix-ui/react-separator @radix-ui/react-slot @radix-ui/react-tooltip class-variance-authority clsx lucide-react zod zustand

These packages power wallet connections, on-chain RPC calls, state management, and UI primitives.

3. Add Batua & UI Primitives

Run the installation helper to pull in Batua’s SDK hooks and UI:

npx shadcn@latest add https://batua.sh/install

This command installs:

  • @batua/react hooks for smart-account creation & batching
  • shadcn/ui primitives for consistent styling

4. App Layout & Providers

You need to wrap your application in Wagmi, React-Query, and RainbowKit providers. Create or replace app/layout.tsx with:

"use client";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { WagmiProvider } from "wagmi";
import { RainbowKitProvider } from "@rainbow-me/rainbowkit";
import { config } from "@/lib/utils/wagmiConfig";
import "@rainbow-me/rainbowkit/styles.css";
const queryClient = new QueryClient();
const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] });
const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
        <WagmiProvider config={config}>
          <QueryClientProvider client={queryClient}>
            <RainbowKitProvider>{children}</RainbowKitProvider>
          </QueryClientProvider>
        </WagmiProvider>
      </body>
    </html>
  );
}
  • WagmiProvider: manages blockchain connections
  • QueryClientProvider: handles server/state caching
  • RainbowKitProvider: adds wallet UI components

5. Wagmi Configuration

Create lib/utils/wagmiConfig.ts to define your wallet & RPC settings:

"use client";
import { createConfig, http } from "wagmi";
import { injected } from "wagmi/connectors";
import { sepolia } from "wagmi/chains";
 
export const config = createConfig({
  chains: [sepolia],
  connectors: [injected()],
  transports: {
    [sepolia.id]: http("https://1rpc.io/sepolia"),
  },
});
  • Batua only supports Sepolia at the moment.
  • injected() lets users connect any injected wallet including the Batua Smart Account Wallet (MetaMask, etc.).
  • RPC transport points at a public Sepolia endpoint.

Batua Wallet Preview

6. Batua SDK Initialization

Create lib/utils/batuaClient.ts to bootstrap the Batua smart-account client:

"use client";
import { Batua } from "@/lib/batua";
import { http } from "viem";
import { sepolia } from "viem/chains";
const PIMLICO_KEY  = process.env.NEXT_PUBLIC_PIMLICO_API_KEY!;
export function setupBatua() {
  {
    const res = Batua.create({
      rpc: {
        transports: {
          [sepolia.id]: http("https://1rpc.io/sepolia"),
        },
      },
      paymaster: {
        transports: {
          [sepolia.id]: http(
            `https://api.pimlico.io/v2/${sepolia.id}/rpc?apikey=${pimlicoApiKey}`
          ),
        },
      },
      bundler: {
        transports: {
          [sepolia.id]: http(
            `https://api.pimlico.io/v2/${sepolia.id}/rpc?apikey=${pimlicoApiKey}`
          ),
        },
      },
    });
    console.log(`Batua Created \n`, res);
  }
}

Be sure to set the following in your .env.local:

NEXT_PUBLIC_RAINBOWKIT_PROJECT_ID = your_rainbowkit_project_id
NEXT_PUBLIC_PIMLICO_API_KEY=your_pimlico_key

7. Build the Main App Page

Now we’ll implement the app/page.tsx to:

  1. Initialize Batua on mount
  2. Connect your batua via RainbowKit
  3. Batch-send ERC20 calls (approve + transfer)

lib/constants.ts

export const TEST_ERC20_TOKEN_ADDRESS =
  "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
export const randomAddressOne = "0xE38fcB5145397B0c723222CDaDa3987425EFc0e5";
export const randomAddressTwo = "0x13AF61A3ec244456645fcE7881E0795E826CE1bD";

app/page.tsx

Refer to the constants file for the TEST_ERC20_TOKEN_ADDRESS and randomAddressOne values or define custom ones here.

"use client";
 
import { setupBatua } from "@/lib/utils/batuaClient";
import { useAccount } from "wagmi";
import { useEffect, useState } from "react";
import { useSendCalls, useWaitForCallsStatus } from "wagmi";
import { encodeFunctionData, parseUnits } from "viem";
import { TEST_ERC20_TOKEN_ADDRESS, randomAddressOne } from "@/lib/constants";
import { ConnectButton } from "@rainbow-me/rainbowkit";
import { config } from "@/lib/utils/wagmiConfig";
import { erc20Abi } from "viem";
import Link from "next/link";
 
export default function BatuaPage() {
  const account = useAccount();
  const { data: callStatus, sendCallsAsync } = useSendCalls({ config });
  const { data: callReceipts } = useWaitForCallsStatus({ id: callStatus?.id });
 
  const [hasMounted, setHasMounted] = useState(false);
  const [batchTxUrl, setBatchTxUrl] = useState("");
 
  useEffect(() => {
    setupBatua();
    setHasMounted(true);
  }, []);
 
  if (!hasMounted) return null;
 
  const executeBatchTx = async () => {
    const res = await sendCallsAsync({
      account: account.address,
      chainId: account.chainId as 1 | 11155111,
      calls: [
        {
          to: TEST_ERC20_TOKEN_ADDRESS,
          data: encodeFunctionData({
            abi: erc20Abi,
            functionName: "approve",
            args: [randomAddressOne, parseUnits("1", 6)],
          }),
        },
        {
          to: TEST_ERC20_TOKEN_ADDRESS,
          data: encodeFunctionData({
            abi: erc20Abi,
            functionName: "transfer",
            args: [randomAddressOne, parseUnits("1", 6)],
          }),
        },
      ] as any,
    });
 
    if (callReceipts?.status === "success" && callReceipts.receipts) {
      setBatchTxUrl(
        `https://sepolia.etherscan.io/tx/${callReceipts.receipts[0].transactionHash}`
      );
    }
  };
 
  return (
    <main className="flex min-h-screen flex-col items-center justify-center p-24 space-y-8">
      <h1 className="text-4xl">Welcome to the Batua Dapp</h1>
 
      {!account.isConnected ? (
        <ConnectButton />
      ) : (
        <div className="flex flex-col items-center space-y-4">
          <ConnectButton />
          <button
            onClick={executeBatchTx}
            className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition"
          >
            Send Batch Transaction
          </button>
 
          {callReceipts?.status === "pending" && (
            <p className="text-yellow-500">Executing...</p>
          )}
          {callReceipts?.status === "failure" && (
            <p className="text-red-500">Execution Failed</p>
          )}
          {batchTxUrl && (
            <p className="text-green-500">
              Execution Successful!  
              <br />
              <Link
                href={batchTxUrl}
                target="_blank"
                className="font-bold underline break-words"
              >
                {batchTxUrl}
              </Link>
            </p>
          )}
        </div>
      )}
    </main>
  );
}
  • useEffect calls setupBatua() once the component mounts.
  • useSendCalls & useWaitForCallsStatus handle batching & status polling.
  • Two ERC20 calls are batched: approve then transfer.
  • Transaction result shown via Etherscan link.

8. Run the App

Start your development server:

npm run dev

After running the command, open http://localhost:3000 to see your Batua Dapp in action.

  • Initial Dapp view before connecting a wallet. Dapp Landing Page
  • Sign in with a Passkey-enabled smart account. Passkey Login
  • View after connecting the Batua smart account. Wallet Connected View
  • Prepared approve + transfer calls queued for batch execution. Batch Transaction Preview
  • Transaction being sent via Batua. Sending Transaction
  • Batch transaction completed successfully. Transaction Successful
  • Inspect the user-operation tx on Etherscan. Etherscan View
  • Inspect the user-operation event emissions on Etherscan. Etherscan Events

Conclusion

In this tutorial, you learned how to:

  • Embed the Batua smart-account wallet into your Next.js app
  • Initialize the Batua SDK with passkey and sponsorship support
  • Batch multiple ERC-20 calls in a single transaction

Check out the source code on GitHub here