Skip to main content
This guide covers building an iframe wrapper that connects browser wallets (MetaMask, injected) and derives a Safe smart account using permissionless for ERC-4337 UserOperations. Single-chain (Polygon) by default.
For the complete one-shot LLM prompt, see /llms-iframe-wagmi-viem.txt.

Tech Stack

  • React (18+) with TypeScript
  • Vite as build tool
  • Tailwind CSS for styling
  • wagmi for wallet connection
  • viem for Ethereum interactions
  • permissionless for ERC-4337 smart accounts
  • @tanstack/react-query (wagmi peer dependency)

Smart Account Constants

These constants are part of the Deframe protocol and must be used exactly as specified:
import { hexToBigInt, keccak256, stringToBytes } from 'viem'

// Default salt for Deframe smart accounts
// Produces: 66612165172041560534486737891040272261009897627722856013316623938731930217937n
const DEFRAME_SALT = hexToBigInt(keccak256(stringToBytes('deframe.io')))

// Safe 4337 module address
const SAFE_4337_MODULE_ADDRESS = '0xa581c4A4DB7175302464fF3C06380BC3270b4037' as const

// Default chain
const CHAIN_ID = 137

Smart Account Derivation

Use permissionless’s toSafeSmartAccount to derive the smart account address:
  • Client: a viem publicClient connected to Polygon via Alchemy RPC
  • EntryPoint: version 0.6 (entryPoint06Address from viem/account-abstraction)
  • Owners: the connected EOA wallet address
  • Salt: DEFRAME_SALT
  • Safe version: '1.4.1'
  • Safe 4337 module: SAFE_4337_MODULE_ADDRESS
The resulting safeAccount.address is passed to the iframe as smartAccountAddress.
The toSafeSmartAccount API shape may vary by permissionless version. The key parameters above are stable — consult the permissionless docs for the exact call signature.

Wallet Connection

Configure wagmi with:
  • Chain: Polygon (chain ID 137)
  • Connector: injected() (MetaMask, browser wallets)
  • Transport: Alchemy RPC (https://polygon-mainnet.g.alchemy.com/v2/{ALCHEMY_RPC_ID})
Wrap your app in WagmiProvider and QueryClientProvider. Use wagmi hooks:
  • useAccount() — get connected address and status
  • useConnect() — connect with the injected connector
  • useDisconnect() — disconnect wallet

UserOperation Flow

To process bytecodes into a signed ERC-4337 UserOperation:
  1. Get EOA from the browser wallet (window.ethereum)
  2. Create a wallet client (viem) using the browser wallet as transport
  3. Create a public client (viem) connected to Polygon via Alchemy RPC
  4. Create a paymaster client (viem) using the same Alchemy RPC
  5. Create the Safe smart account using toSafeSmartAccount with Deframe constants
  6. Create a smart account client (permissionless) with the safe account, paymaster, and bundler transport
  7. Transform bytecodes to calls: { to, data, value } — convert value strings to BigInt
  8. Prepare the UserOperation via the smart account client, passing the Alchemy policyId as paymaster context
  9. Sign the UserOperation using the Safe smart account signing method
  10. Submit via sendUserOperation on the bundler client
  11. Poll for the transaction receipt using getUserOperationReceipt and waitForUserOperationReceipt
The exact methods for steps 8-10 depend on your permissionless version. Some versions combine prepare + sign + send into a single call. Consult the docs for your version.

Environment Variables

VITE_WEBVIEW_URL=https://deframe-sdk-iframe.vercel.app
VITE_ALCHEMY_RPC_ID=your_alchemy_api_key
VITE_ALCHEMY_POLICY_ID=your_alchemy_gas_policy_id
VITE_DEFRAME_API_URL=https://api.deframe.com
VITE_DEFRAME_API_KEY=your_deframe_api_key
VITE_ALLOWED_ORIGINS=*
All variables use the VITE_ prefix for Vite’s environment variable exposure. Default chain is Polygon (137).

UI Requirements

  1. Header with Connect Wallet button (shows address + Disconnect when connected), Earn/Swap toggle
  2. Before wallet connection: placeholder message (“Connect your wallet to begin”)
  3. While deriving smart account: loading spinner
  4. After connection + SA derived: render the Deframe iframe with correct URL params
  5. Iframe: allow="clipboard-write", fills available space, not in DOM before wallet connection

Documentation References