Skip to main content

MetaMask SDK documentation

Seamlessly connect to the MetaMask extension and mobile app.

Upgrade an EOA to a smart account

This tutorial walks you through upgrading a MetaMask externally owned account (EOA) to a MetaMask smart account via EIP-7702, and sending an atomic batch transaction via EIP-5792. You will use a provided template, which sets up MetaMask SDK with a Next.js and Wagmi dapp.

Prerequisites

Steps

1. Set up the project

  1. Clone the MetaMask/7702-livestream-demo repository:

    git clone git@github.com:MetaMask/7702-livestream-demo.git
  2. Switch to the feat/mm-sdk branch:

    cd 7702-livestream-demo && git switch feat/mm-sdk
  3. Install dependencies:

    npm install
  4. Run the development server and navigate to http://localhost:3000:

    npm run dev

    The initial template displays with non-functional buttons:

SDK 7702 initial template

2. Configure the MetaMask connector

In the root directory, create a .env.local file. Add a NEXT_PUBLIC_INFURA_API_KEY environment variable, replacing <YOUR-API-KEY> with your Infura API key:

.env.local
NEXT_PUBLIC_INFURA_API_KEY=<YOUR-API-KEY>

In src/providers/AppProvider.tsx, configure the Wagmi MetaMask SDK connector using your Infura API key:

AppProvider.tsx
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createConfig, http, WagmiProvider } from "wagmi";
import { sepolia } from "viem/chains";
import { ReactNode } from "react";
import { metaMask } from "wagmi/connectors";

- export const connectors = []
+ export const connectors = [
+ metaMask({
+ infuraAPIKey: process.env.NEXT_PUBLIC_INFURA_API_KEY,
+ }),
+ ];
// ...

3. Create a connect and disconnect button

In src/app/page.tsx, use the useAccount, useConnect, and useDisconnect hooks from Wagmi, along with the MetaMask SDK connector, to create a button to connect and disconnect your MetaMask wallet, and display the connection status:

page.tsx
"use client";

import Image from "next/image";
+ import { metaMask } from "wagmi/connectors";
+ import { useAccount, useConnect, useDisconnect } from "wagmi";

export default function Home() {
+ const { connect } = useConnect();
+ const { disconnect } = useDisconnect();
+ const { address, isConnected } = useAccount();

+ const formatAddress = (addr: string) => {
+ return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
+ };

return (
// ...
- <ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
- <li className="mb-2 tracking-[-.01em]">
- Get started by editing{" "}
- <code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
- src/app/page.tsx
- </code>
- .
- </li>
- <li className="tracking-[-.01em]">
- Save and see your changes instantly.
- </li>
- </ol>

- <div className="flex gap-4 items-center flex-col sm:flex-row">
+ {/* Wallet connection section */}
+ <div className="bg-gray-50 dark:bg-gray-900 p-6 rounded-lg w-full">
+ <h2 className="text-xl font-semibold mb-4">Wallet Connection</h2>

+ {/* Connection status */}
+ <div className="mb-6">
+ {isConnected ? (
+ <div className="flex items-center gap-2 text-green-600">
+ <div className="w-2 h-2 bg-green-500 rounded-full"></div>
+ <span>Connected to {formatAddress(address!)}</span>
+ </div>
+ ) : (
+ <div className="flex items-center gap-2 text-red-600">
+ <div className="w-2 h-2 bg-red-500 rounded-full"></div>
+ <span>Not connected</span>
+ </div>
+ )}
+ </div>

- <button
- className="rounded-full border border-solid px-4 py-2 cursor-pointer hover:bg-gray-100"
- onClick={() => {
- console.log("clicked");
- }}
- >
- Connect Wallet
- </button>
+ {/* Connect/disconnect button */}
+ <button
+ className={`w-full rounded-lg border border-solid px-6 py-3 font-medium transition-colors ${
+ isConnected
+ ? "bg-red-50 hover:bg-red-100 text-red-700 border-red-300 cursor-pointer"
+ : "bg-blue-50 hover:bg-blue-100 text-blue-700 border-blue-300 cursor-pointer"
+ }`}
+ onClick={() => {
+ if (isConnected) {
+ disconnect();
+ } else {
+ connect({ connector: metaMask() });
+ }
+ }}
+ >
+ {isConnected ? "Disconnect Wallet" : "Connect with MetaMask"}
+ </button>
// ...

In the development server, test that the button works to connect and disconnect from your MetaMask wallet. When connected, the interface displays your connected wallet address:

SDK disconnected
SDK connected

4. Handle and send batch transactions

In src/app/page.tsx, use the useSendCalls hook from Wagmi to handle and send atomic batch transactions. Also use React's useState hook to handle the transaction state. The following example sends 0.001 and 0.0001 ETH in a batch transaction. Replace <YOUR-RECIPIENT-ADDRESS> with recipient addresses of your choice:

page.tsx
"use client";

import Image from "next/image";
import { metaMask } from "wagmi/connectors";
- import { useAccount, useConnect, useDisconnect } from "wagmi";
+ import { useAccount, useConnect, useDisconnect, useSendCalls } from "wagmi";
+ import { useState } from "react";
+ import { parseEther } from "viem";

export default function Home() {
const { connect } = useConnect();
const { disconnect } = useDisconnect();
const { address, isConnected } = useAccount();
+ const { sendCalls, error, isPending, isSuccess, data, reset } = useSendCalls();
+ const [transactionHash, setTransactionHash] = useState<string | null>(null);
+ const [statusError, setStatusError] = useState<string | null>(null);

+ const handleSendTransaction = () => {
+ if (!isConnected) return;
+
+ // Reset previous states
+ setTransactionHash(null);
+ setStatusError(null);
+ reset();
+
+ sendCalls({
+ calls: [
+ {
+ to: "<YOUR-RECIPIENT-ADDRESS>",
+ value: parseEther("0.001"),
+ },
+ {
+ to: "<YOUR-RECIPIENT-ADDRESS>",
+ value: parseEther("0.0001"),
+ },
+ ],
+ });
+ };
// ...

Then, create a button to send batch transactions, and display the transaction state (pending, success, or error). Also, update the connect/disconnect button to reset states when disconnected:

page.tsx
// ...
{/* Connect/disconnect button */}
<button
// ...
onClick={() => {
if (isConnected) {
disconnect();
+ // Reset previous states
+ setTransactionHash(null);
+ setStatusError(null);
+ reset();
// ...
>
{isConnected ? "Disconnect Wallet" : "Connect with MetaMask"}
</button>
- <button
- className="rounded-full border border-solid px-4 py-2 cursor-pointer hover:bg-gray-100"
- onClick={() => {
- console.log("clicked");
- }}
- >
- Send Batch Transaction
- </button>
</div>

+ {/* Batch transaction section */}
+ <div className="bg-gray-50 dark:bg-gray-900 p-6 rounded-lg w-full">
+ <h2 className="text-xl font-semibold mb-4">Send Batch Transaction</h2>

+ {/* Send batch transaction button */}
+ <button
+ className={`w-full rounded-lg border border-solid px-6 py-3 font-medium transition-colors mb-4 ${
+ !isConnected || isPending
+ ? "bg-gray-100 text-gray-400 border-gray-300 cursor-not-allowed"
+ : "bg-green-50 hover:bg-green-100 text-green-700 border-green-300 cursor-pointer"
+ }`}
+ onClick={handleSendTransaction}
+ disabled={!isConnected || isPending}
+ >
+ {isPending ? "Sending Transaction..." : "Send Batch Transaction"}
+ </button>

+ {/* Transaction state */}
+ {isPending && (
+ <div className="flex items-center gap-2 text-blue-600 mb-4">
+ <div className="w-4 h-4 border-2 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
+ <span>Transaction pending...</span>
+ </div>
+ )}
+
+ {isSuccess && data && (
+ <div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-4">
+ <div className="flex items-center gap-2 text-green-700 mb-2">
+ <div className="w-2 h-2 bg-green-500 rounded-full"></div>
+ <span className="font-medium">
+ Transaction submitted successfully!
+ </span>
+ </div>
+ <div className="text-sm text-gray-600">
+ <p>
+ Data ID:{" "}
+ <code className="bg-gray-100 px-1 rounded">{data.id}</code>
+ </p>
+ </div>
+ </div>
+ )}
+
+ {error && (
+ <div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
+ <div className="text-red-700 font-medium">Transaction Error</div>
+ <div className="text-sm text-red-600 mt-1">{error.message}</div>
+ </div>
+ )}
+ </div>
// ...

In the development server, test that the button works to send batch transactions from your wallet. Ensure you are connected to the Sepolia network in MetaMask. MetaMask prompts you to upgrade your EOA to a smart account in order to send a batch transaction:

SDK send batch transactions button
SDK upgrade EOA to smart account

5. Get the status of batch transactions

In src/app/page.tsx, use the getCallsStatus action from Wagmi to get the status of sent batch transactions:

page.tsx
"use client";

import Image from "next/image";
import { metaMask } from "wagmi/connectors";
import { useAccount, useConnect, useDisconnect, useSendCalls } from "wagmi";
import { useState } from "react";
import { parseEther } from "viem";
+ import { getCallsStatus } from "@wagmi/core";
+ import { wagmiConfig as config } from "@/providers/AppProvider";

export default function Home() {
const { connect } = useConnect();
const { disconnect } = useDisconnect();
const { address, isConnected } = useAccount();
const { sendCalls, error, isPending, isSuccess, data, reset } = useSendCalls();
const [transactionHash, setTransactionHash] = useState<string | null>(null);
const [statusError, setStatusError] = useState<string | null>(null);
+ const [statusLoading, setStatusLoading] = useState(false);

+ const handleGetCallsStatus = async () => {
+ if (!data?.id) return;
+
+ setStatusLoading(true);
+ setStatusError(null);
+
+ try {
+ const status = await getCallsStatus(config, { id: data.id });
+ console.log("Transaction status:", status);
+
+ if (
+ status.status === "success" &&
+ status.receipts?.[0]?.transactionHash
+ ) {
+ setTransactionHash(status.receipts[0].transactionHash);
+ } else if (status.status === "failure") {
+ setStatusError("Transaction failed");
+ }
+ } catch (err) {
+ console.error("Error getting call status:", err);
+ setStatusError(
+ err instanceof Error ? err.message : "Failed to get transaction status"
+ );
+ } finally {
+ setStatusLoading(false);
+ }
+ };
// ...

Then, create a button to check the batch transaction status:

page.tsx
// ...

{/* Transaction state */}
// ...

+ {/* Check transaction status button */}
+ {data && (
+ <button
+ className={`w-full rounded-lg border border-solid px-6 py-3 font-medium transition-colors ${
+ statusLoading
+ ? "bg-gray-100 text-gray-400 border-gray-300 cursor-not-allowed"
+ : "bg-purple-50 hover:bg-purple-100 text-purple-700 border-purple-300 cursor-pointer"
+ }`}
+ onClick={handleGetCallsStatus}
+ disabled={statusLoading || !data.id}
+ >
+ {statusLoading
+ ? "Checking Status..."
+ : "Check Transaction Status"}
+ </button>
+ )}

+ {/* Status error */}
+ {statusError && (
+ <div className="bg-red-50 border border-red-200 rounded-lg p-4 mt-4">
+ <div className="text-red-700 font-medium">Status Check Error</div>
+ <div className="text-sm text-red-600 mt-1">{statusError}</div>
+ </div>
+ )}

+ {/* Transaction hash */}
+ {transactionHash && (
+ <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mt-4">
+ <div className="text-blue-700 font-medium mb-2">
+ Transaction Confirmed!
+ </div>
+ <div className="text-sm">
+ <a
+ href={`https://sepolia.etherscan.io/tx/${transactionHash}`}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="text-blue-600 hover:text-blue-800 hover:underline break-all"
+ >
+ View on Etherscan: {transactionHash}
+ </a>
+ </div>
+ </div>
+ )}
// ...

In the development server, when you send a successful batch transaction, the success state and the Check Transaction Status button appear. When you select the Check Transaction Status button, if the transaction is confirmed, a link to Etherscan with your transaction hash appears:

SDK successful 7702 transaction
SDK check transaction status

You have successfully used the SDK to upgrade a MetaMask EOA to a MetaMask smart account, send an atomic batch transaction, and check its status!

Resources