diff --git a/apps/erc7715/.env.example b/apps/erc7715/.env.example
new file mode 100644
index 00000000..fa0aad61
--- /dev/null
+++ b/apps/erc7715/.env.example
@@ -0,0 +1,6 @@
+# Optional defaults for the permission request form (wired in `lib/env.ts` via `next.config.ts`).
+# Where you want to send the funds to
+SESSION_ACCOUNT_ADDRESS=
+
+# ERC20 Token Address
+TOKEN_ADDRESS=
diff --git a/apps/erc7715/.gitignore b/apps/erc7715/.gitignore
new file mode 100644
index 00000000..7b8da95f
--- /dev/null
+++ b/apps/erc7715/.gitignore
@@ -0,0 +1,42 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files (can opt-in for committing if needed)
+.env*
+!.env.example
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/apps/erc7715/README.md b/apps/erc7715/README.md
new file mode 100644
index 00000000..46aaa11e
--- /dev/null
+++ b/apps/erc7715/README.md
@@ -0,0 +1,48 @@
+# ERC-7715 Execution Permissions
+
+A Next.js web app for exploring [ERC-7715](https://eips.ethereum.org/EIPS/eip-7715) wallet execution permissions on Berachain. Connect a MetaMask wallet, probe supported permission types, request scoped execution permissions (native token or ERC-20), and redeem delegations on-chain via the returned `delegationManager`.
+
+
+
+**How it works:**
+
+1. **Probe** — calls `wallet_getSupportedExecutionPermissions` to discover which permission types, chains, and rule types the connected wallet supports.
+2. **Request** — builds a `PermissionRequest` and calls `wallet_requestExecutionPermissions`, returning a `PermissionResponse` with a `context` and `delegationManager` address.
+3. **Redeem** — submits a `redeemDelegations` transaction to the `delegationManager` contract (per ERC-7710), encoding either a native transfer or an ERC-20 `transfer`.
+
+## Requirements
+
+- [Node.js](https://nodejs.org/) v18+ (or [Bun](https://bun.sh/))
+- A browser with [MetaMask](https://metamask.io/) installed (must support ERC-7715 — e.g. MetaMask Flask)
+
+## Quickstart
+
+Install dependencies:
+
+```bash
+bun install
+```
+
+Copy the example env file and fill in your values:
+
+```bash
+cp .env.example .env
+```
+
+```
+# Where you want to send the funds to (session account)
+SESSION_ACCOUNT_ADDRESS=0x...
+
+# ERC-20 token address used as the default in the request form
+TOKEN_ADDRESS=0x...
+```
+
+Both variables are optional — they pre-populate form fields in the UI.
+
+Start the dev server:
+
+```bash
+bun run dev
+```
+
+The app runs at [http://localhost:3002](http://localhost:3002).
diff --git a/apps/erc7715/README/erc7715.png b/apps/erc7715/README/erc7715.png
new file mode 100644
index 00000000..f1f986b2
Binary files /dev/null and b/apps/erc7715/README/erc7715.png differ
diff --git a/apps/erc7715/app/favicon.ico b/apps/erc7715/app/favicon.ico
new file mode 100644
index 00000000..718d6fea
Binary files /dev/null and b/apps/erc7715/app/favicon.ico differ
diff --git a/apps/erc7715/app/globals.css b/apps/erc7715/app/globals.css
new file mode 100644
index 00000000..7513eb62
--- /dev/null
+++ b/apps/erc7715/app/globals.css
@@ -0,0 +1,28 @@
+@import "tailwindcss";
+
+:root {
+ --background: #0a0a0a;
+ --foreground: #fafafa;
+ --bera-gold: #f5a623;
+ --bera-gold-hover: #ffb84d;
+ --card: #1a1a1a;
+ --border: #2a2a2a;
+}
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --font-sans: var(--font-dm-sans);
+ --font-mono: var(--font-jetbrains-mono);
+}
+
+body {
+ background: var(--background);
+ color: var(--foreground);
+ font-family: var(--font-dm-sans), sans-serif;
+}
+
+.font-mono {
+ font-variant-ligatures: none;
+ font-feature-settings: "liga" 0;
+}
diff --git a/apps/erc7715/app/layout.tsx b/apps/erc7715/app/layout.tsx
new file mode 100644
index 00000000..57dae68a
--- /dev/null
+++ b/apps/erc7715/app/layout.tsx
@@ -0,0 +1,47 @@
+import type { Metadata } from "next";
+import { DM_Sans, JetBrains_Mono } from "next/font/google";
+import { headers } from "next/headers";
+import { cookieToInitialState } from "wagmi";
+import { Providers } from "./providers";
+import { config } from "@/lib/wagmi";
+import "./globals.css";
+
+const dmSans = DM_Sans({
+ variable: "--font-dm-sans",
+ subsets: ["latin"],
+ weight: ["400", "500", "700"],
+});
+
+const jetBrainsMono = JetBrains_Mono({
+ variable: "--font-jetbrains-mono",
+ subsets: ["latin"],
+ weight: ["400", "500", "700"],
+});
+
+export const metadata: Metadata = {
+ title: "ERC-7715 wallet support",
+ description:
+ "Check whether your wallet supports ERC-7715 execution permissions",
+};
+
+export default async function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ const initialState = cookieToInitialState(
+ config,
+ (await headers()).get("cookie") ?? undefined,
+ );
+
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/apps/erc7715/app/page.tsx b/apps/erc7715/app/page.tsx
new file mode 100644
index 00000000..c9be30c0
--- /dev/null
+++ b/apps/erc7715/app/page.tsx
@@ -0,0 +1,144 @@
+"use client";
+
+import { useCallback } from "react";
+import { RedeemSection } from "@/components/RedeemSection";
+import { RequestPermissionsSection } from "@/components/RequestPermissionsSection";
+import { SupportedPermissions } from "@/components/SupportedPermissions";
+import { UnsupportedBanner } from "@/components/UnsupportedBanner";
+import {
+ useStoredPermission,
+ type StoredGrant,
+} from "@/hooks/useStoredPermission";
+import { useWalletSupport } from "@/hooks/useWalletSupport";
+import { useConnect, useConnection, useDisconnect } from "wagmi";
+
+export default function HomePage() {
+ const { address, isConnected, status } = useConnection();
+ const { connect, connectors, isPending: isConnectPending } = useConnect();
+ const { disconnect } = useDisconnect();
+ const support = useWalletSupport();
+
+ const { grant, setGrant, clear: clearGrant } = useStoredPermission();
+
+ const handleGrantChange = useCallback(
+ (next: StoredGrant | null) => setGrant(next),
+ [setGrant],
+ );
+
+ const metaMask = connectors.find((c) => c.type === "metaMask");
+
+ const showPhase2Request =
+ isConnected &&
+ status === "connected" &&
+ !support.isLoading &&
+ !support.isUnsupported &&
+ support.data !== undefined;
+
+ return (
+
+
+
+
+ {!isConnected || status !== "connected" ? (
+
+ Connect MetaMask to automatically check execution permission support
+ on Berachain networks.
+
+ ) : (
+ <>
+ {support.isUnsupported && }
+
+ {support.isLoading && (
+
+ Querying the wallet for supported execution permissions…
+
+ )}
+
+ {support.isError && !support.isUnsupported && (
+
+
+ Could not read supported permissions
+
+ {support.errorMessage && (
+
+ {support.errorMessage}
+
+ )}
+ {support.errorCode !== undefined && (
+
+ RPC code: {support.errorCode}
+
+ )}
+
+ )}
+
+ {support.data && support.rawResponse !== undefined && (
+
+ )}
+
+ {showPhase2Request && support.data ? (
+
+ ) : null}
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/apps/erc7715/app/providers.tsx b/apps/erc7715/app/providers.tsx
new file mode 100644
index 00000000..5a4b9629
--- /dev/null
+++ b/apps/erc7715/app/providers.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { type ReactNode, useState } from "react";
+import { type State, WagmiProvider } from "wagmi";
+import { config } from "@/lib/wagmi";
+
+type ProvidersProps = {
+ children: ReactNode;
+ initialState?: State | undefined;
+};
+
+export function Providers({ children, initialState }: ProvidersProps) {
+ const [queryClient] = useState(() => new QueryClient());
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/erc7715/bun.lockb b/apps/erc7715/bun.lockb
new file mode 100755
index 00000000..5e6b4c3d
Binary files /dev/null and b/apps/erc7715/bun.lockb differ
diff --git a/apps/erc7715/components/PermissionSequenceDiagram.tsx b/apps/erc7715/components/PermissionSequenceDiagram.tsx
new file mode 100644
index 00000000..9616ada5
--- /dev/null
+++ b/apps/erc7715/components/PermissionSequenceDiagram.tsx
@@ -0,0 +1,164 @@
+"use client";
+
+import { useId } from "react";
+
+/**
+ * Phase 2 sequence: DApp → provider → wallet → user approval → response.
+ * Inline SVG (~300px tall) for use beside the request form.
+ */
+export function PermissionSequenceDiagram() {
+ const rid = useId().replace(/:/g, "");
+ const arrowId = `${rid}-arrowhead`;
+
+ return (
+
+
+
+
+
+
+
+
+
+ DApp
+
+
+
+
+ window.ethereum
+
+
+
+
+ Wallet
+
+
+
+
+ User
+
+
+
+
+
+ request()
+
+
+ method: wallet_requestExecutionPermissions
+
+
+ params: PermissionRequest[] (JSON-RPC array)
+
+
+
+
+ forward JSON-RPC
+
+
+
+
+ prompt: rules + permission + to
+
+
+
+
+ approve / reject (4001)
+
+
+
+
+ result: PermissionResponse[]
+
+
+
+
+ Promise resolves
+
+
+ + context, delegationManager, dependencies[]
+
+
+
+ Payload shape per ERC-7715 (chainId, to, permission, rules?)
+
+
+
+ );
+}
diff --git a/apps/erc7715/components/RedeemPermissionForm.tsx b/apps/erc7715/components/RedeemPermissionForm.tsx
new file mode 100644
index 00000000..6da48d2c
--- /dev/null
+++ b/apps/erc7715/components/RedeemPermissionForm.tsx
@@ -0,0 +1,385 @@
+"use client";
+
+import { type FormEvent, useEffect, useMemo, useState } from "react";
+import { getAddress, isAddress, parseEther, parseUnits } from "viem";
+import { berachain, berachainTestnetbArtio } from "viem/chains";
+import { useRedeemPermission } from "@/hooks/useRedeemPermission";
+import type { PermissionResponse } from "@/types/erc7715";
+
+const ERC20_DECIMALS = 18;
+
+const SUPPORTED_CHAINS = [berachain, berachainTestnetbArtio];
+
+function getExplorerTxUrl(chainIdHex: string, txHash: string): string | null {
+ const chainId = Number(chainIdHex);
+ const chain = SUPPORTED_CHAINS.find((c) => c.id === chainId);
+ const baseUrl = chain?.blockExplorers?.default?.url;
+ if (!baseUrl) return null;
+ return `${baseUrl}/tx/${txHash}`;
+}
+
+function formatTimestamp(ts: number): string {
+ return new Intl.DateTimeFormat(undefined, {
+ dateStyle: "medium",
+ timeStyle: "medium",
+ }).format(new Date(ts * 1000));
+}
+
+function truncateHex(hex: string, leading = 6, trailing = 4): string {
+ if (!hex.startsWith("0x") || hex.length <= leading + trailing + 2) return hex;
+ return `${hex.slice(0, leading + 2)}…${hex.slice(-trailing)}`;
+}
+
+function isPermissionResponse(value: unknown): value is PermissionResponse {
+ if (typeof value !== "object" || value === null) return false;
+ const v = value as Record;
+ return (
+ typeof v.context === "string" &&
+ typeof v.delegationManager === "string" &&
+ Array.isArray(v.dependencies) &&
+ typeof v.chainId === "string" &&
+ typeof v.to === "string" &&
+ typeof v.permission === "object" &&
+ v.permission !== null
+ );
+}
+
+export type RedeemPermissionFormProps = {
+ initialResponse?: PermissionResponse | null;
+ onClear?: () => void;
+};
+
+export function RedeemPermissionForm({
+ initialResponse,
+ onClear,
+}: RedeemPermissionFormProps) {
+ const redeem = useRedeemPermission();
+
+ const [responseJson, setResponseJson] = useState("");
+ const [targetAddress, setTargetAddress] = useState("");
+ const [amountInput, setAmountInput] = useState("0.001");
+
+ useEffect(() => {
+ if (initialResponse) {
+ setResponseJson(JSON.stringify(initialResponse, null, 2));
+ }
+ }, [initialResponse]);
+
+ const { parsed, parseError } = useMemo<{
+ parsed: PermissionResponse | null;
+ parseError: string | null;
+ }>(() => {
+ const trimmed = responseJson.trim();
+ if (!trimmed) return { parsed: null, parseError: null };
+ try {
+ const obj = JSON.parse(trimmed);
+ if (!isPermissionResponse(obj)) {
+ return {
+ parsed: null,
+ parseError:
+ "Missing required fields: context, delegationManager, dependencies, chainId, to, permission.",
+ };
+ }
+ return { parsed: obj, parseError: null };
+ } catch {
+ return {
+ parsed: null,
+ parseError: "Invalid JSON — paste a PermissionResponse object.",
+ };
+ }
+ }, [responseJson]);
+
+ const isErc20Type =
+ parsed?.permission.type.startsWith("erc20-token-") ?? false;
+
+ const amountValid = useMemo(() => {
+ try {
+ if (isErc20Type) {
+ parseUnits(amountInput.trim() || "0", ERC20_DECIMALS);
+ } else {
+ parseEther(amountInput.trim() || "0");
+ }
+ return true;
+ } catch {
+ return false;
+ }
+ }, [amountInput, isErc20Type]);
+
+ const data = (parsed?.permission.data ?? {}) as Record;
+ const tokenAddress =
+ typeof data.tokenAddress === "string" ? data.tokenAddress : null;
+ const periodAmount =
+ typeof data.periodAmount === "string" ? data.periodAmount : null;
+ const allowance = typeof data.allowance === "string" ? data.allowance : null;
+ const isNativeType =
+ parsed?.permission.type.startsWith("native-token-") ?? false;
+
+ const expiryRule = parsed?.rules?.find((r) => r.type === "expiry");
+ const expiryTs = (expiryRule?.data as { timestamp?: number } | undefined)
+ ?.timestamp;
+
+ const handleSubmit = (e: FormEvent) => {
+ e.preventDefault();
+ if (!parsed) return;
+ if (!isAddress(targetAddress)) return;
+ if (!amountValid) return;
+
+ const recipient = getAddress(targetAddress) as `0x${string}`;
+
+ if (isErc20Type && tokenAddress && isAddress(tokenAddress)) {
+ redeem.mutate({
+ kind: "erc20",
+ response: parsed,
+ tokenAddress: getAddress(tokenAddress) as `0x${string}`,
+ recipient,
+ amount: parseUnits(amountInput.trim(), ERC20_DECIMALS),
+ });
+ } else {
+ redeem.mutate({
+ kind: "native",
+ response: parsed,
+ recipient,
+ value: parseEther(amountInput.trim()),
+ });
+ }
+ };
+
+ const explorerUrl =
+ redeem.hash && parsed
+ ? getExplorerTxUrl(parsed.chainId, redeem.hash)
+ : null;
+
+ return (
+
+
+ Permission response (JSON)
+
+
+ {responseJson.trim() ? (
+
+ {initialResponse ? (
+
+
+ Loaded from storage
+
+ ) : null}
+ {
+ setResponseJson("");
+ redeem.reset();
+ onClear?.();
+ }}
+ >
+ Clear context
+
+
+ ) : null}
+
+ {parseError ?
{parseError}
: null}
+
+ {parsed ? (
+ <>
+
+
+ Parsed permission
+
+
+ Type
+
+
+ {parsed.permission.type}
+
+
+
+ Token
+
+ {isNativeType
+ ? "Native (BERA)"
+ : tokenAddress
+ ? truncateHex(tokenAddress)
+ : "—"}
+
+
+ {allowance ? (
+ <>
+ Allowance
+
+ {truncateHex(allowance, 10, 6)}
+
+ >
+ ) : null}
+
+ {periodAmount ? (
+ <>
+ Period amount
+
+ {truncateHex(periodAmount, 10, 6)}
+
+ >
+ ) : null}
+
+ {expiryTs ? (
+ <>
+ Expiry
+ {formatTimestamp(expiryTs)}
+ >
+ ) : null}
+
+ Context
+
+ {truncateHex(parsed.context, 10, 8)}
+
+
+ Delegation manager
+
+ {truncateHex(parsed.delegationManager)}
+
+
+
+
+
+
+ {redeem.hash ? (
+
+ ) : null}
+
+ {redeem.error ? (
+
+ {redeem.error.message}
+
+ ) : null}
+ >
+ ) : !parseError && !responseJson.trim() ? (
+
+ Paste a PermissionResponse{" "}
+ JSON above, or complete Phase 2 to auto-fill.
+
+ ) : null}
+
+ {/* TODO: Phase 4 — Revoke permission (call revokePermission on delegationManager with the context). */}
+
+ );
+}
diff --git a/apps/erc7715/components/RedeemSection.tsx b/apps/erc7715/components/RedeemSection.tsx
new file mode 100644
index 00000000..0edbce04
--- /dev/null
+++ b/apps/erc7715/components/RedeemSection.tsx
@@ -0,0 +1,64 @@
+"use client";
+
+import { RedeemPermissionForm } from "@/components/RedeemPermissionForm";
+import type { PermissionResponse } from "@/types/erc7715";
+
+export type RedeemSectionProps = {
+ initialResponse?: PermissionResponse | null;
+ onClear?: () => void;
+};
+
+export function RedeemSection({
+ initialResponse,
+ onClear,
+}: RedeemSectionProps) {
+ return (
+
+
+ Redeem permissions
+
+
+ Execute an on-chain action using the granted delegation context — no
+ additional wallet prompt required.
+
+
+
+
+
+ Redeeming calls{" "}
+ redeemDelegations on the{" "}
+ delegationManager{" "}
+ contract returned by the wallet in Phase 2.
+
+
+ You pass the context {" "}
+ (opaque permission proof), an{" "}
+ execution mode (default
+ single-call), and the encoded execution payload. The delegation
+ framework verifies the grant, enforces rules (expiry, allowance),
+ and forwards execution on behalf of the delegator — all without
+ another wallet signature.
+
+
+ For this demo the execution is a simple native-token transfer: pick
+ a target address and an amount, and the session account submits the
+ call through the delegation manager.
+
+
+ Paste a{" "}
+ PermissionResponse JSON
+ into the field on the right, or complete the{" "}
+ Request permissions {" "}
+ section above to auto-fill it.
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/erc7715/components/RequestPermissionsForm.tsx b/apps/erc7715/components/RequestPermissionsForm.tsx
new file mode 100644
index 00000000..c5685c94
--- /dev/null
+++ b/apps/erc7715/components/RequestPermissionsForm.tsx
@@ -0,0 +1,1252 @@
+"use client";
+
+import {
+ type FormEvent,
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+} from "react";
+import {
+ getAddress,
+ isAddress,
+ numberToHex,
+ parseEther,
+ parseUnits,
+ toHex,
+} from "viem";
+import { useChainId, useConnection } from "wagmi";
+import { UnsupportedBanner } from "@/components/UnsupportedBanner";
+import { useRequestPermissions } from "@/hooks/useRequestPermissions";
+import type { StoredGrant } from "@/hooks/useStoredPermission";
+import { env } from "@/lib/env";
+import type {
+ ERC20TokenAllowanceData,
+ ERC20TokenPeriodicData,
+ GetSupportedExecutionPermissionsResult,
+ NativeTokenAllowanceData,
+ PermissionRequest,
+ PermissionResponse,
+} from "@/types/erc7715";
+
+/** Phase-1 / wallet audit: ERC-20 types that share {@link ERC20TokenAllowanceData} (extend with `startsWith` below). */
+const ERC20_TOKEN_ALLOWANCE_LIKE_TYPES = [
+ "erc20-token-allowance",
+ "erc20-token-periodic",
+ "erc20-token-stream",
+ "erc20-token-revocation",
+] as const;
+
+/** Phase-1 audit: native types that share {@link NativeTokenAllowanceData} (single allowance; extend with `startsWith` below). */
+const NATIVE_TOKEN_ALLOWANCE_LIKE_TYPES = [
+ "native-token-allowance",
+ "native-token-periodic",
+ "native-token-stream",
+] as const;
+
+/**
+ * Same editor + `permission.data` as `erc20-token-allowance` (tokenAddress, allowance, periodAmount, periodDuration).
+ * Covers audited ids and any other `erc20-token-*` reported by the wallet.
+ */
+function isErc20TokenAllowanceLikeType(selectedType: string): boolean {
+ return (
+ (ERC20_TOKEN_ALLOWANCE_LIKE_TYPES as readonly string[]).includes(
+ selectedType,
+ ) || selectedType.startsWith("erc20-token-")
+ );
+}
+
+function isErc20AllowanceType(selectedType: string): boolean {
+ return selectedType === "erc20-token-allowance";
+}
+
+/**
+ * Same editor + `permission.data` as `native-token-allowance` (hex allowance only).
+ * Covers audited ids and any other `native-token-*` reported by the wallet.
+ */
+function isNativeTokenAllowanceLikeType(selectedType: string): boolean {
+ return (
+ (NATIVE_TOKEN_ALLOWANCE_LIKE_TYPES as readonly string[]).includes(
+ selectedType,
+ ) || selectedType.startsWith("native-token-")
+ );
+}
+
+/** Default ERC-20 form parsing: standard 18-decimal tokens. */
+const ERC20_FORM_DECIMALS = 18;
+const PERIOD_PRESETS = [
+ { label: "Hourly", seconds: 3600 },
+ { label: "Daily", seconds: 86400 },
+ { label: "Weekly", seconds: 604800 },
+ { label: "Biweekly", seconds: 1209600 },
+ { label: "Monthly", seconds: 2592000 },
+ { label: "Yearly", seconds: 31536000 },
+] as const;
+
+function uint256HexFromBigInt(value: bigint): `0x${string}` {
+ return toHex(value, { size: 32 });
+}
+
+function truncateAddress(address: string): string {
+ if (!address.startsWith("0x") || address.length < 12) return address;
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
+}
+
+function formatLocalDateTime(timestampSeconds: number): string {
+ return new Intl.DateTimeFormat(undefined, {
+ dateStyle: "medium",
+ timeStyle: "medium",
+ }).format(new Date(timestampSeconds * 1000));
+}
+
+function defaultExpiryAfterHours(hours: number): {
+ date: string;
+ time: string;
+} {
+ const d = new Date(Date.now() + hours * 3600000);
+ const pad = (n: number) => String(n).padStart(2, "0");
+ return {
+ date: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`,
+ time: `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`,
+ };
+}
+
+/** Combines native `date` + `time` (local) into unix seconds for the `expiry` rule. */
+function parseDateAndTimeToUnixSeconds(date: string, time: string): number {
+ if (!date?.trim() || !time?.trim()) {
+ return Math.floor(Date.now() / 1000) + 3600;
+ }
+ const ms = new Date(`${date.trim()}T${time.trim()}`).getTime();
+ if (Number.isNaN(ms)) {
+ return Math.floor(Date.now() / 1000) + 3600;
+ }
+ return Math.floor(ms / 1000);
+}
+
+function WalletAdjustedBadge() {
+ return (
+
+ adjusted
+
+ );
+}
+
+function PermissionResponseCard({
+ response,
+ submitted,
+}: {
+ response: PermissionResponse;
+ submitted: PermissionRequest;
+}) {
+ const chainAdjusted =
+ submitted.chainId.toLowerCase() !== response.chainId.toLowerCase();
+ const toAdjusted = submitted.to.toLowerCase() !== response.to.toLowerCase();
+ const fromAdjusted =
+ (submitted.from?.toLowerCase() ?? "") !==
+ (response.from?.toLowerCase() ?? "");
+
+ const permTypeAdjusted =
+ submitted.permission.type !== response.permission.type;
+ const permAdjAdjusted =
+ submitted.permission.isAdjustmentAllowed !==
+ response.permission.isAdjustmentAllowed;
+ const permDataAdjusted =
+ JSON.stringify(submitted.permission.data) !==
+ JSON.stringify(response.permission.data);
+
+ const subExpiry = submitted.rules?.find((r) => r.type === "expiry");
+ const resExpiry = response.rules?.find((r) => r.type === "expiry");
+ const expiryAdjusted =
+ JSON.stringify(subExpiry?.data ?? null) !==
+ JSON.stringify(resExpiry?.data ?? null);
+
+ return (
+
+
+ Granted permission
+
+
+ The wallet echoed your request and may have changed fields when{" "}
+ isAdjustmentAllowed was true.
+ Highlighted rows differ from what you submitted.
+
+
+
+
+
context
+
+ {response.context}
+
+
+
+
delegationManager
+
+ {response.delegationManager}
+
+
+
+
+ dependencies
+ {response.dependencies.length > 0 ? (
+
+ ({response.dependencies.length})
+
+ ) : null}
+
+
+ {response.dependencies.length === 0 ? (
+
+ None (accounts already deployed)
+
+ ) : (
+ response.dependencies.map((dep, i) => (
+
+
+ factory {dep.factory}
+
+
+ factoryData {" "}
+ {dep.factoryData.slice(0, 42)}…
+
+
+ ))
+ )}
+
+
+
+
+
+
+ Echoed permission
+
+
+
+ chainId
+ {response.chainId}
+ {chainAdjusted ? : null}
+
+
+ from
+ {response.from ?? "—"}
+ {fromAdjusted ? : null}
+
+
+ to
+ {response.to}
+ {toAdjusted ? : null}
+
+
+ permission.type
+ {response.permission.type}
+ {permTypeAdjusted ? : null}
+
+
+
+ permission.isAdjustmentAllowed
+
+ {String(response.permission.isAdjustmentAllowed)}
+ {permAdjAdjusted ? : null}
+
+
+ permission.data
+ {permDataAdjusted ? : null}
+
+ {JSON.stringify(response.permission.data, null, 2)}
+
+
+
+ rules (expiry)
+ {expiryAdjusted ? : null}
+
+ {JSON.stringify(response.rules ?? [], null, 2)}
+
+
+
+
+
+
+ Use the Redeem permissions section below to execute
+ against this grant.
+
+
+ );
+}
+
+export type RequestPermissionsFormProps = {
+ supported: GetSupportedExecutionPermissionsResult;
+ storedGrant?: StoredGrant | null;
+ onGrantChange?: (grant: StoredGrant | null) => void;
+};
+
+export function RequestPermissionsForm({
+ supported,
+ storedGrant,
+ onGrantChange,
+}: RequestPermissionsFormProps) {
+ const { address } = useConnection();
+ const chainIdNum = useChainId();
+ const requestMutation = useRequestPermissions();
+
+ const permissionKeys = useMemo(
+ () => Object.keys(supported).sort(),
+ [supported],
+ );
+
+ const [permissionType, setPermissionType] = useState(
+ () => permissionKeys[0] ?? "",
+ );
+ useEffect(() => {
+ if (permissionKeys.length === 0) return;
+ if (!permissionKeys.includes(permissionType)) {
+ setPermissionType(permissionKeys[0]!);
+ }
+ }, [permissionKeys, permissionType]);
+
+ const chainsForType = useMemo(
+ () => supported[permissionType]?.chainIds ?? [],
+ [supported, permissionType],
+ );
+
+ const defaultChainHex = useMemo(() => {
+ const wagmiHex = numberToHex(chainIdNum) as `0x${string}`;
+ const wagmiId = BigInt(wagmiHex);
+ const match = chainsForType.find((c) => BigInt(c) === wagmiId);
+ return (match ?? chainsForType[0] ?? wagmiHex) as `0x${string}`;
+ }, [chainsForType, chainIdNum]);
+
+ const [chainHex, setChainHex] = useState<`0x${string}`>(defaultChainHex);
+ useEffect(() => {
+ setChainHex(defaultChainHex);
+ }, [defaultChainHex]);
+
+ const [to, setTo] = useState(env.sessionAccountAddress);
+ const [isAdjustmentAllowed, setIsAdjustmentAllowed] = useState(true);
+
+ const [nativeAllowanceEth, setNativeAllowanceEth] = useState("0.1");
+ const [erc20TokenAddress, setErc20TokenAddress] = useState(env.tokenAddress);
+ const [erc20AllowanceUnits, setErc20AllowanceUnits] = useState("0.1");
+ const [erc20PeriodAmountUnits, setErc20PeriodAmountUnits] = useState("0.1");
+ const [erc20PeriodDuration, setErc20PeriodDuration] = useState(3600);
+ const [expiryMode, setExpiryMode] = useState<"periods" | "date">("periods");
+ const [numberOfPeriods, setNumberOfPeriods] = useState(5);
+
+ useEffect(() => {
+ if (isNativeTokenAllowanceLikeType(permissionType)) {
+ setNativeAllowanceEth("0.1");
+ }
+ if (isErc20TokenAllowanceLikeType(permissionType)) {
+ setErc20TokenAddress(env.tokenAddress);
+ setErc20AllowanceUnits("0.1");
+ setErc20PeriodAmountUnits("0.1");
+ setErc20PeriodDuration(3600);
+ }
+ }, [permissionType]);
+
+ const defaultExpiry = useMemo(() => defaultExpiryAfterHours(1), []);
+ const [expiryDate, setExpiryDate] = useState(defaultExpiry.date);
+ const [expiryTime, setExpiryTime] = useState(defaultExpiry.time);
+ const [lastSubmitted, setLastSubmitted] = useState(
+ null,
+ );
+ const [dismissedRejection, setDismissedRejection] = useState(false);
+
+ useEffect(() => {
+ const response = requestMutation.data?.[0] ?? null;
+ if (response && lastSubmitted) {
+ onGrantChange?.({ submitted: lastSubmitted, response });
+ }
+ }, [requestMutation.data, lastSubmitted, onGrantChange]);
+
+ useEffect(() => {
+ if (requestMutation.isPending) {
+ setDismissedRejection(false);
+ }
+ }, [requestMutation.isPending]);
+
+ const dateModeExpiryUnixSeconds = useMemo(
+ () => parseDateAndTimeToUnixSeconds(expiryDate, expiryTime),
+ [expiryDate, expiryTime],
+ );
+
+ const isNativeAllowanceLike = isNativeTokenAllowanceLikeType(permissionType);
+ const isErc20AllowanceLike = isErc20TokenAllowanceLikeType(permissionType);
+ const isPeriodicType = permissionType.includes("periodic");
+ const showExpiryModeToggle = isPeriodicType;
+ const effectiveExpiryMode: "periods" | "date" = showExpiryModeToggle
+ ? expiryMode
+ : "date";
+
+ const periodicDurationSeconds = useMemo(
+ () => Math.max(1, Math.floor(erc20PeriodDuration)),
+ [erc20PeriodDuration],
+ );
+ const numberOfPeriodsValid =
+ Number.isFinite(numberOfPeriods) && numberOfPeriods > 0;
+
+ const nowUnixSeconds = Math.floor(Date.now() / 1000);
+ const computedExpiryUnixSeconds = useMemo(() => {
+ if (effectiveExpiryMode === "periods") {
+ if (!numberOfPeriodsValid) return undefined;
+ return (
+ nowUnixSeconds + Math.floor(numberOfPeriods) * periodicDurationSeconds
+ );
+ }
+ return dateModeExpiryUnixSeconds;
+ }, [
+ dateModeExpiryUnixSeconds,
+ effectiveExpiryMode,
+ numberOfPeriods,
+ numberOfPeriodsValid,
+ nowUnixSeconds,
+ periodicDurationSeconds,
+ ]);
+
+ const expiryLocalPreview = useMemo(() => {
+ if (!computedExpiryUnixSeconds) return "—";
+ const d = new Date(computedExpiryUnixSeconds * 1000);
+ return Number.isNaN(d.getTime())
+ ? "—"
+ : d.toLocaleString(undefined, {
+ dateStyle: "medium",
+ timeStyle: "medium",
+ });
+ }, [computedExpiryUnixSeconds]);
+
+ const nativeAllowanceValid = useMemo(() => {
+ if (!isNativeTokenAllowanceLikeType(permissionType)) return true;
+ try {
+ parseEther(nativeAllowanceEth.trim() || "0");
+ return true;
+ } catch {
+ return false;
+ }
+ }, [nativeAllowanceEth, permissionType]);
+
+ const erc20FormValid = useMemo(() => {
+ if (!isErc20TokenAllowanceLikeType(permissionType)) return true;
+ if (!isAddress(erc20TokenAddress.trim())) return false;
+ try {
+ if (isErc20AllowanceType(permissionType)) {
+ parseUnits(erc20AllowanceUnits.trim() || "0", ERC20_FORM_DECIMALS);
+ }
+ parseUnits(erc20PeriodAmountUnits.trim() || "0", ERC20_FORM_DECIMALS);
+ } catch {
+ return false;
+ }
+ return Number.isFinite(erc20PeriodDuration) && erc20PeriodDuration > 0;
+ }, [
+ erc20AllowanceUnits,
+ erc20PeriodAmountUnits,
+ erc20PeriodDuration,
+ erc20TokenAddress,
+ permissionType,
+ ]);
+
+ const buildPermissionData = useCallback((): Record => {
+ if (isNativeTokenAllowanceLikeType(permissionType)) {
+ const wei = parseEther(nativeAllowanceEth.trim() || "0");
+ const data: NativeTokenAllowanceData = {
+ allowance: uint256HexFromBigInt(wei),
+ };
+ return data;
+ }
+ if (isErc20TokenAllowanceLikeType(permissionType)) {
+ const tokenAddress = getAddress(
+ erc20TokenAddress.trim(),
+ ) as `0x${string}`;
+ const periodWei = parseUnits(
+ erc20PeriodAmountUnits.trim() || "0",
+ ERC20_FORM_DECIMALS,
+ );
+ if (isErc20AllowanceType(permissionType)) {
+ const allowanceWei = parseUnits(
+ erc20AllowanceUnits.trim() || "0",
+ ERC20_FORM_DECIMALS,
+ );
+ const data: ERC20TokenAllowanceData = {
+ tokenAddress,
+ allowance: uint256HexFromBigInt(allowanceWei),
+ periodAmount: uint256HexFromBigInt(periodWei),
+ periodDuration: periodicDurationSeconds,
+ };
+ return data;
+ }
+ const data: ERC20TokenPeriodicData = {
+ tokenAddress,
+ periodAmount: uint256HexFromBigInt(periodWei),
+ periodDuration: periodicDurationSeconds,
+ };
+ return data;
+ }
+ return {};
+ }, [
+ erc20AllowanceUnits,
+ erc20PeriodAmountUnits,
+ periodicDurationSeconds,
+ erc20TokenAddress,
+ nativeAllowanceEth,
+ permissionType,
+ ]);
+
+ const buildRequest = useCallback((): PermissionRequest | null => {
+ if (!isAddress(to)) return null;
+ if (isNativeTokenAllowanceLikeType(permissionType) && !nativeAllowanceValid)
+ return null;
+ if (isErc20TokenAllowanceLikeType(permissionType) && !erc20FormValid)
+ return null;
+ if (effectiveExpiryMode === "periods" && !numberOfPeriodsValid) return null;
+
+ const nowSeconds = Math.floor(Date.now() / 1000);
+ const ts =
+ effectiveExpiryMode === "periods"
+ ? nowSeconds + Math.floor(numberOfPeriods) * periodicDurationSeconds
+ : parseDateAndTimeToUnixSeconds(expiryDate, expiryTime);
+ const req: PermissionRequest = {
+ chainId: chainHex,
+ to: getAddress(to),
+ permission: {
+ type: permissionType,
+ isAdjustmentAllowed,
+ data: buildPermissionData(),
+ },
+ rules: [
+ {
+ type: "expiry",
+ data: { timestamp: ts },
+ },
+ ],
+ };
+ if (address) {
+ req.from = getAddress(address);
+ }
+ return req;
+ }, [
+ address,
+ buildPermissionData,
+ chainHex,
+ erc20FormValid,
+ effectiveExpiryMode,
+ expiryDate,
+ expiryTime,
+ isAdjustmentAllowed,
+ nativeAllowanceValid,
+ numberOfPeriods,
+ numberOfPeriodsValid,
+ periodicDurationSeconds,
+ permissionType,
+ to,
+ ]);
+
+ const handleSubmit = (e: FormEvent) => {
+ e.preventDefault();
+ const built = buildRequest();
+ if (!built) return;
+ setLastSubmitted(built);
+ requestMutation.mutate([built]);
+ };
+
+ const freshResponse = requestMutation.data?.[0] ?? null;
+ const hasFreshGrant =
+ freshResponse &&
+ lastSubmitted &&
+ !requestMutation.error &&
+ !requestMutation.isPending;
+
+ const cardResponse = hasFreshGrant
+ ? freshResponse
+ : (storedGrant?.response ?? null);
+ const cardSubmitted = hasFreshGrant
+ ? lastSubmitted
+ : (storedGrant?.submitted ?? null);
+ const isRestoredFromStorage = !hasFreshGrant && !!storedGrant;
+
+ const showRejection =
+ requestMutation.isUserRejected &&
+ requestMutation.error &&
+ !dismissedRejection &&
+ !requestMutation.isPending;
+
+ const livePayloadPreview = useMemo(() => {
+ const permission: Record = {
+ type: permissionType,
+ isAdjustmentAllowed,
+ data: {},
+ };
+
+ let previewError: string | undefined;
+ try {
+ permission.data = buildPermissionData();
+ } catch {
+ previewError =
+ "Permission data is currently invalid; payload preview includes a placeholder.";
+ permission.data = "";
+ }
+
+ const requestPreview: Record = {
+ chainId: chainHex,
+ to: isAddress(to)
+ ? getAddress(to)
+ : to || "",
+ permission,
+ rules: [
+ {
+ type: "expiry",
+ data: {
+ timestamp: computedExpiryUnixSeconds ?? dateModeExpiryUnixSeconds,
+ },
+ },
+ ],
+ };
+
+ if (address && isAddress(address)) {
+ requestPreview.from = getAddress(address);
+ }
+
+ const rpcRequestPreview = {
+ method: "wallet_requestExecutionPermissions",
+ params: [requestPreview],
+ };
+
+ return {
+ error: previewError,
+ json: JSON.stringify(rpcRequestPreview, null, 2),
+ };
+ }, [
+ address,
+ buildPermissionData,
+ chainHex,
+ computedExpiryUnixSeconds,
+ dateModeExpiryUnixSeconds,
+ isAdjustmentAllowed,
+ permissionType,
+ to,
+ ]);
+
+ const periodAmountInput = isNativeAllowanceLike
+ ? nativeAllowanceEth
+ : erc20PeriodAmountUnits;
+ const periodAmountNumber = Number(periodAmountInput);
+ const periodAmountValid = Number.isFinite(periodAmountNumber);
+ const summaryPeriods = !isPeriodicType
+ ? undefined
+ : effectiveExpiryMode === "periods"
+ ? numberOfPeriodsValid
+ ? Math.floor(numberOfPeriods)
+ : undefined
+ : computedExpiryUnixSeconds && periodicDurationSeconds > 0
+ ? Math.max(
+ 0,
+ Math.ceil(
+ (computedExpiryUnixSeconds - Math.floor(Date.now() / 1000)) /
+ periodicDurationSeconds,
+ ),
+ )
+ : undefined;
+ const periodAmountLabel = isNativeAllowanceLike
+ ? "native"
+ : isErc20AllowanceLike
+ ? "tokens"
+ : undefined;
+ const periodAmountDisplay =
+ periodAmountValid && periodAmountLabel
+ ? `${periodAmountInput} ${periodAmountLabel}`
+ : undefined;
+ const totalAmountDisplay =
+ summaryPeriods !== undefined && periodAmountValid
+ ? `${(periodAmountNumber * summaryPeriods).toFixed((periodAmountInput.split(".")[1] ?? "").length)} ${periodAmountLabel ?? ""}`.trim()
+ : undefined;
+ const matchedPeriodPreset = PERIOD_PRESETS.find(
+ (preset) => preset.seconds === periodicDurationSeconds,
+ );
+
+ return (
+
+ {requestMutation.isUnsupported ? (
+
+ ) : null}
+
+ {showRejection ? (
+
+
You rejected the request in your wallet
+
setDismissedRejection(true)}
+ >
+ Dismiss
+
+
+ ) : null}
+
+ {requestMutation.error &&
+ !requestMutation.isUnsupported &&
+ !requestMutation.isUserRejected ? (
+
+ {requestMutation.error.message}
+
+ ) : null}
+
+
+
+ {cardResponse && cardSubmitted ? (
+
+ {isRestoredFromStorage ? (
+
+
+
+ Restored from storage
+
+
+ ) : null}
+
+
{
+ requestMutation.reset();
+ setLastSubmitted(null);
+ setDismissedRejection(false);
+ onGrantChange?.(null);
+ }}
+ >
+ Clear grant
+
+
+ ) : null}
+
+ );
+}
diff --git a/apps/erc7715/components/RequestPermissionsSection.tsx b/apps/erc7715/components/RequestPermissionsSection.tsx
new file mode 100644
index 00000000..a59b3de9
--- /dev/null
+++ b/apps/erc7715/components/RequestPermissionsSection.tsx
@@ -0,0 +1,73 @@
+"use client";
+
+import { PermissionSequenceDiagram } from "@/components/PermissionSequenceDiagram";
+import { RequestPermissionsForm } from "@/components/RequestPermissionsForm";
+import type { StoredGrant } from "@/hooks/useStoredPermission";
+import type { GetSupportedExecutionPermissionsResult } from "@/types/erc7715";
+
+export type RequestPermissionsSectionProps = {
+ supported: GetSupportedExecutionPermissionsResult;
+ storedGrant?: StoredGrant | null;
+ onGrantChange?: (grant: StoredGrant | null) => void;
+};
+
+export function RequestPermissionsSection({
+ supported,
+ storedGrant,
+ onGrantChange,
+}: RequestPermissionsSectionProps) {
+ return (
+
+
+ Request permissions
+
+
+ Build a PermissionRequest and
+ call{" "}
+
+ wallet_requestExecutionPermissions
+ {" "}
+ through your connected wallet.
+
+
+
+
+
+
+
+ When the wallet approves, it returns a context {" "}
+ value: an opaque identifier you will pass back on-chain when
+ redeeming so the delegation system knows which grant to execute
+ against.
+
+
+ delegationManager is the contract you call (per
+ ERC-7710) to redeem—your session account submits the encoded
+ execution together with that context.
+
+
+ dependencies describes accounts that still need
+ deployment: each item includes a{" "}
+ factory and{" "}
+ factoryData you can use
+ to deploy them before redemption succeeds.
+
+
+ Defaults pick a supported permission from Phase 1, pre-fill a
+ one-hour expiry rule,
+ and allow wallet adjustments so you can submit quickly and inspect
+ a real response.
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/erc7715/components/SupportedPermissions.tsx b/apps/erc7715/components/SupportedPermissions.tsx
new file mode 100644
index 00000000..32073f35
--- /dev/null
+++ b/apps/erc7715/components/SupportedPermissions.tsx
@@ -0,0 +1,154 @@
+import type { GetSupportedExecutionPermissionsResult } from "@/types/erc7715";
+import { hexChainIdToNumericString } from "@/lib/executionPermissionsDisplay";
+
+export type SupportedPermissionsProps = {
+ data: GetSupportedExecutionPermissionsResult;
+ rawResponse: unknown;
+};
+
+export function SupportedPermissions({
+ data,
+ rawResponse,
+}: SupportedPermissionsProps) {
+ const rows = Object.entries(data).map(([permissionType, value]) => ({
+ permissionType,
+ chainIds: value.chainIds,
+ ruleTypes: value.ruleTypes,
+ }));
+
+ const rawJson =
+ typeof rawResponse === "string"
+ ? rawResponse
+ : JSON.stringify(rawResponse, null, 2);
+
+ return (
+
+
+
+
+
+
+ Supported permission types
+
+
+ Show
+ Hide
+
+
+
+ Parsed from the wallet response
+
+ {rows.length === 0 ? (
+
+ No supported permission types
+
+ ) : (
+
+ {rows.map((row) => (
+
+
+ {row.permissionType}
+
+
+ {row.chainIds.length} chains
+
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Permission type
+ Chain IDs (hex)
+ Chain IDs (numeric)
+ Rule types
+
+
+
+ {rows.length === 0 ? (
+
+
+ The wallet reported no supported execution permission types
+ (empty object).
+
+
+ ) : (
+ rows.map((row) => (
+
+
+ {row.permissionType}
+
+
+
+ {row.chainIds.map((id) => (
+ {id}
+ ))}
+
+
+
+
+ {row.chainIds.map((id) => (
+
+ {hexChainIdToNumericString(id)}
+
+ ))}
+
+
+
+
+ {row.ruleTypes.map((t) => (
+
+ {t}
+
+ ))}
+
+
+
+ ))
+ )}
+
+
+
+
+
+
+
+ Raw wallet response
+
+
+
+ {rawJson}
+
+
+
+
+ );
+}
diff --git a/apps/erc7715/components/UnsupportedBanner.tsx b/apps/erc7715/components/UnsupportedBanner.tsx
new file mode 100644
index 00000000..aade2381
--- /dev/null
+++ b/apps/erc7715/components/UnsupportedBanner.tsx
@@ -0,0 +1,32 @@
+type UnsupportedBannerProps = {
+ className?: string;
+};
+
+export function UnsupportedBanner({ className }: UnsupportedBannerProps) {
+ return (
+
+
+ Execution permissions API not available
+
+
+ This wallet does not expose{" "}
+
+ wallet_getSupportedExecutionPermissions
+ {" "}
+ (JSON-RPC -32601 method not found). Install{" "}
+
+ MetaMask Flask
+ {" "}
+ or a MetaMask build that supports ERC-7715 execution permissions.
+
+
+ );
+}
diff --git a/apps/erc7715/eslint.config.mjs b/apps/erc7715/eslint.config.mjs
new file mode 100644
index 00000000..d471a3b4
--- /dev/null
+++ b/apps/erc7715/eslint.config.mjs
@@ -0,0 +1,19 @@
+import { FlatCompat } from "@eslint/eslintrc";
+import { dirname } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+const compat = new FlatCompat({
+ baseDirectory: __dirname,
+});
+
+const eslintConfig = [
+ ...compat.extends("next/core-web-vitals", "next/typescript"),
+ {
+ ignores: [".next/**", "out/**", "build/**", "next-env.d.ts"],
+ },
+];
+
+export default eslintConfig;
diff --git a/apps/erc7715/hooks/useRedeemPermission.ts b/apps/erc7715/hooks/useRedeemPermission.ts
new file mode 100644
index 00000000..08f8afdf
--- /dev/null
+++ b/apps/erc7715/hooks/useRedeemPermission.ts
@@ -0,0 +1,110 @@
+"use client";
+
+import { useCallback, useState } from "react";
+import { encodeFunctionData, encodePacked, erc20Abi } from "viem";
+import { useWriteContract } from "wagmi";
+import type { PermissionResponse } from "@/types/erc7715";
+
+const REDEEM_DELEGATIONS_ABI = [
+ {
+ name: "redeemDelegations",
+ type: "function",
+ inputs: [
+ { name: "_permissionContexts", type: "bytes[]" },
+ { name: "_modes", type: "bytes32[]" },
+ { name: "_executionCallData", type: "bytes[]" },
+ ],
+ outputs: [],
+ stateMutability: "nonpayable",
+ },
+] as const;
+
+const EXECUTION_MODE_DEFAULT =
+ "0x0000000000000000000000000000000000000000000000000000000000000000" as `0x${string}`;
+
+export type RedeemParams =
+ | {
+ kind: "native";
+ response: PermissionResponse;
+ recipient: `0x${string}`;
+ value: bigint;
+ }
+ | {
+ kind: "erc20";
+ response: PermissionResponse;
+ tokenAddress: `0x${string}`;
+ recipient: `0x${string}`;
+ amount: bigint;
+ };
+
+export function useRedeemPermission() {
+ const {
+ writeContract,
+ data: hash,
+ isPending,
+ error: writeError,
+ reset: writeReset,
+ } = useWriteContract();
+
+ const [localError, setLocalError] = useState(null);
+
+ const mutate = useCallback(
+ (params: RedeemParams) => {
+ setLocalError(null);
+ writeReset();
+
+ if (params.response.dependencies.length > 0) {
+ setLocalError(
+ new Error(
+ `Cannot redeem: ${params.response.dependencies.length} undeployed dependencies. Deploy them before redeeming.`,
+ ),
+ );
+ return;
+ }
+
+ let executionCallData: `0x${string}`;
+
+ if (params.kind === "erc20") {
+ const transferCalldata = encodeFunctionData({
+ abi: erc20Abi,
+ functionName: "transfer",
+ args: [params.recipient, params.amount],
+ });
+ executionCallData = encodePacked(
+ ["address", "uint256", "bytes"],
+ [params.tokenAddress, BigInt(0), transferCalldata],
+ );
+ } else {
+ executionCallData = encodePacked(
+ ["address", "uint256", "bytes"],
+ [params.recipient, params.value, "0x"],
+ );
+ }
+
+ writeContract({
+ address: params.response.delegationManager,
+ abi: REDEEM_DELEGATIONS_ABI,
+ functionName: "redeemDelegations",
+ args: [
+ [params.response.context],
+ [EXECUTION_MODE_DEFAULT],
+ [executionCallData],
+ ],
+ });
+ },
+ [writeContract, writeReset],
+ );
+
+ const reset = useCallback(() => {
+ setLocalError(null);
+ writeReset();
+ }, [writeReset]);
+
+ return {
+ mutate,
+ isPending,
+ hash,
+ error: localError ?? writeError ?? null,
+ reset,
+ };
+}
diff --git a/apps/erc7715/hooks/useRequestPermissions.ts b/apps/erc7715/hooks/useRequestPermissions.ts
new file mode 100644
index 00000000..8d45b27d
--- /dev/null
+++ b/apps/erc7715/hooks/useRequestPermissions.ts
@@ -0,0 +1,147 @@
+"use client";
+
+import { useMutation } from "@tanstack/react-query";
+import { useCallback, useState } from "react";
+import type { EIP1193Provider } from "viem";
+import type { PermissionRequest, PermissionResponse } from "@/types/erc7715";
+
+export class ExecutionPermissionsUnsupportedError extends Error {
+ readonly code = -32601 as const;
+ override readonly name = "ExecutionPermissionsUnsupportedError";
+ constructor(
+ message = "wallet_requestExecutionPermissions is not supported by this wallet",
+ ) {
+ super(message);
+ Object.setPrototypeOf(this, new.target.prototype);
+ }
+}
+
+export class ExecutionPermissionsUserRejectedError extends Error {
+ readonly code = 4001 as const;
+ override readonly name = "ExecutionPermissionsUserRejectedError";
+ constructor(message = "You rejected the request in your wallet") {
+ super(message);
+ Object.setPrototypeOf(this, new.target.prototype);
+ }
+}
+
+function readRpcCode(error: unknown): number | undefined {
+ if (typeof error !== "object" || error === null) return undefined;
+ const code = (error as { code?: unknown }).code;
+ return typeof code === "number" ? code : undefined;
+}
+
+async function requestWalletMethod(
+ provider: EIP1193Provider,
+ method: string,
+ params: readonly unknown[] = [],
+): Promise {
+ const request = provider.request as (args: {
+ method: string;
+ params?: readonly unknown[];
+ }) => Promise;
+ return request({ method, params });
+}
+
+function isPermissionResponse(value: unknown): value is PermissionResponse {
+ if (typeof value !== "object" || value === null) return false;
+ const v = value as Record;
+ return (
+ typeof v.context === "string" &&
+ typeof v.delegationManager === "string" &&
+ Array.isArray(v.dependencies)
+ );
+}
+
+/**
+ * Calls `wallet_requestExecutionPermissions` with JSON-RPC `params` set to the `PermissionRequest[]` array (ERC-7715).
+ */
+export async function submitExecutionPermissionRequests(
+ requests: PermissionRequest[],
+): Promise {
+ if (typeof window === "undefined") {
+ throw new Error("Wallet is only available in the browser");
+ }
+
+ const provider = window.ethereum as EIP1193Provider | undefined;
+ if (!provider?.request) {
+ throw new Error("No EIP-1193 provider on window.ethereum");
+ }
+
+ try {
+ const params = requests as unknown as readonly unknown[];
+ console.log("ERC-7715 request payload", JSON.stringify(params, null, 2));
+ const raw = await requestWalletMethod(
+ provider,
+ "wallet_requestExecutionPermissions",
+ params,
+ );
+
+ if (!Array.isArray(raw)) {
+ throw new Error("Wallet returned a non-array response");
+ }
+
+ if (!raw.every(isPermissionResponse)) {
+ throw new Error(
+ "Wallet returned an unexpected permission response shape",
+ );
+ }
+
+ return raw;
+ } catch (error: unknown) {
+ const code = readRpcCode(error);
+ if (code === -32601) {
+ throw new ExecutionPermissionsUnsupportedError();
+ }
+ if (code === 4001) {
+ throw new ExecutionPermissionsUserRejectedError();
+ }
+ throw error;
+ }
+}
+
+export type UseRequestPermissionsReturn = {
+ mutate: (requests: PermissionRequest[]) => void;
+ mutateAsync: (requests: PermissionRequest[]) => Promise;
+ isPending: boolean;
+ data: PermissionResponse[] | undefined;
+ error: Error | null;
+ reset: () => void;
+ isUnsupported: boolean;
+ isUserRejected: boolean;
+};
+
+export function useRequestPermissions(): UseRequestPermissionsReturn {
+ const [successData, setSuccessData] = useState<
+ PermissionResponse[] | undefined
+ >();
+
+ const mutation = useMutation<
+ PermissionResponse[],
+ Error,
+ PermissionRequest[]
+ >({
+ mutationFn: submitExecutionPermissionRequests,
+ onSuccess: (data) => {
+ setSuccessData(data);
+ },
+ });
+
+ const reset = useCallback(() => {
+ mutation.reset();
+ setSuccessData(undefined);
+ }, [mutation]);
+
+ const err = mutation.error;
+
+ return {
+ mutate: mutation.mutate,
+ mutateAsync: mutation.mutateAsync,
+ isPending: mutation.isPending,
+ data: successData,
+ error: (err ?? null) as Error | null,
+ reset,
+ isUnsupported: err instanceof ExecutionPermissionsUnsupportedError,
+ isUserRejected: err instanceof ExecutionPermissionsUserRejectedError,
+ };
+}
diff --git a/apps/erc7715/hooks/useStoredPermission.ts b/apps/erc7715/hooks/useStoredPermission.ts
new file mode 100644
index 00000000..77c9f0d2
--- /dev/null
+++ b/apps/erc7715/hooks/useStoredPermission.ts
@@ -0,0 +1,69 @@
+"use client";
+
+import { useCallback, useEffect, useState } from "react";
+import type { PermissionRequest, PermissionResponse } from "@/types/erc7715";
+
+const STORAGE_KEY = "erc7715:grant";
+
+export type StoredGrant = {
+ submitted: PermissionRequest;
+ response: PermissionResponse;
+};
+
+function readFromStorage(): StoredGrant | null {
+ if (typeof window === "undefined") return null;
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (!raw) return null;
+ const parsed = JSON.parse(raw);
+ if (
+ typeof parsed === "object" &&
+ parsed !== null &&
+ typeof parsed.response === "object" &&
+ parsed.response !== null &&
+ typeof parsed.response.context === "string" &&
+ typeof parsed.response.delegationManager === "string" &&
+ Array.isArray(parsed.response.dependencies) &&
+ typeof parsed.submitted === "object" &&
+ parsed.submitted !== null
+ ) {
+ return parsed as StoredGrant;
+ }
+ return null;
+ } catch {
+ return null;
+ }
+}
+
+function writeToStorage(grant: StoredGrant | null) {
+ if (typeof window === "undefined") return;
+ if (grant) {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(grant));
+ } else {
+ localStorage.removeItem(STORAGE_KEY);
+ }
+}
+
+/**
+ * Persists a granted permission (submitted request + wallet response) in
+ * localStorage so both the success card and redeem form survive page reloads.
+ */
+export function useStoredPermission() {
+ const [grant, setGrantState] = useState(null);
+
+ useEffect(() => {
+ setGrantState(readFromStorage());
+ }, []);
+
+ const setGrant = useCallback((next: StoredGrant | null) => {
+ setGrantState(next);
+ writeToStorage(next);
+ }, []);
+
+ const clear = useCallback(() => {
+ setGrantState(null);
+ writeToStorage(null);
+ }, []);
+
+ return { grant, setGrant, clear } as const;
+}
diff --git a/apps/erc7715/hooks/useWalletSupport.ts b/apps/erc7715/hooks/useWalletSupport.ts
new file mode 100644
index 00000000..32b37c72
--- /dev/null
+++ b/apps/erc7715/hooks/useWalletSupport.ts
@@ -0,0 +1,57 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import { useConnection } from "wagmi";
+import { fetchWalletSupportedExecutionPermissions } from "@/lib/walletExecutionPermissions";
+import type { GetSupportedExecutionPermissionsResult } from "@/types/erc7715";
+
+// Composes with `useRequestPermissions` on the page once Phase 1 has completed successfully.
+
+export type UseWalletSupportReturn = {
+ data: GetSupportedExecutionPermissionsResult | undefined;
+ rawResponse: unknown;
+ isLoading: boolean;
+ isError: boolean;
+ isUnsupported: boolean;
+ errorCode: number | undefined;
+ errorMessage: string | undefined;
+ refetch: () => void;
+};
+
+export function useWalletSupport(): UseWalletSupportReturn {
+ const { address, isConnected, status } = useConnection();
+ const enabled = isConnected && !!address && status === "connected";
+
+ const query = useQuery({
+ queryKey: ["wallet_getSupportedExecutionPermissions", address],
+ queryFn: fetchWalletSupportedExecutionPermissions,
+ enabled,
+ });
+
+ const result = query.data;
+
+ const isUnsupported = result?.status === "unsupported";
+ const isError = result?.status === "error" || query.isError;
+
+ return {
+ data: result?.status === "ok" ? result.data : undefined,
+ rawResponse: result?.status === "ok" ? result.raw : undefined,
+ isLoading: enabled && query.isPending,
+ isError,
+ isUnsupported,
+ errorCode: isUnsupported
+ ? -32601
+ : result?.status === "error"
+ ? result.errorCode
+ : undefined,
+ errorMessage:
+ result?.status === "error"
+ ? result.message
+ : query.error instanceof Error
+ ? query.error.message
+ : undefined,
+ refetch: () => {
+ void query.refetch();
+ },
+ };
+}
diff --git a/apps/erc7715/lib/env.ts b/apps/erc7715/lib/env.ts
new file mode 100644
index 00000000..a210b015
--- /dev/null
+++ b/apps/erc7715/lib/env.ts
@@ -0,0 +1,8 @@
+/**
+ * Public build-time defaults from `.env` / `.env.local` (see `next.config.ts` `env` map).
+ * Used for `to` (session account) and ERC-20 `tokenAddress` when the form loads.
+ */
+export const env = {
+ sessionAccountAddress: (process.env.SESSION_ACCOUNT_ADDRESS ?? '').trim(),
+ tokenAddress: (process.env.TOKEN_ADDRESS ?? '').trim(),
+} as const
diff --git a/apps/erc7715/lib/executionPermissionsDisplay.ts b/apps/erc7715/lib/executionPermissionsDisplay.ts
new file mode 100644
index 00000000..0d44af0e
--- /dev/null
+++ b/apps/erc7715/lib/executionPermissionsDisplay.ts
@@ -0,0 +1,5 @@
+import { hexToBigInt } from 'viem'
+
+export function hexChainIdToNumericString(hex: `0x${string}`): string {
+ return hexToBigInt(hex).toString()
+}
diff --git a/apps/erc7715/lib/wagmi.ts b/apps/erc7715/lib/wagmi.ts
new file mode 100644
index 00000000..cce7564c
--- /dev/null
+++ b/apps/erc7715/lib/wagmi.ts
@@ -0,0 +1,18 @@
+import { cookieStorage, createConfig, createStorage, http } from 'wagmi'
+import { berachain, berachainTestnetbArtio } from 'viem/chains'
+import { metaMask } from 'wagmi/connectors'
+
+// TODO: Phase 2 — add transports or wallet-only flows if execution permission requests need dedicated RPCs per chain.
+
+export const config = createConfig({
+ chains: [berachain, berachainTestnetbArtio],
+ connectors: [metaMask()],
+ transports: {
+ [berachain.id]: http(),
+ [berachainTestnetbArtio.id]: http(),
+ },
+ ssr: true,
+ storage: createStorage({
+ storage: cookieStorage,
+ }),
+})
diff --git a/apps/erc7715/lib/walletExecutionPermissions.ts b/apps/erc7715/lib/walletExecutionPermissions.ts
new file mode 100644
index 00000000..bf179fa7
--- /dev/null
+++ b/apps/erc7715/lib/walletExecutionPermissions.ts
@@ -0,0 +1,86 @@
+import type { GetSupportedExecutionPermissionsResult } from '@/types/erc7715'
+import type { EIP1193Provider } from 'viem'
+
+async function requestWalletMethod(
+ provider: EIP1193Provider,
+ method: string,
+ params: readonly unknown[] = [],
+): Promise {
+ const request = provider.request as (args: {
+ method: string
+ params?: readonly unknown[]
+ }) => Promise
+ return request({ method, params })
+}
+
+// TODO: Phase 3 — optional shared helpers for revoke / redeem RPCs (request flow lives in hooks/useRequestPermissions.ts).
+
+export type FetchSupportedExecutionPermissionsResult =
+ | { status: 'ok'; data: GetSupportedExecutionPermissionsResult; raw: unknown }
+ | { status: 'unsupported' }
+ | { status: 'error'; errorCode?: number; message: string }
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
+}
+
+function isSupportedShape(
+ value: unknown,
+): value is GetSupportedExecutionPermissionsResult {
+ if (!isRecord(value)) return false
+ for (const entry of Object.values(value)) {
+ if (!isRecord(entry)) return false
+ const { chainIds, ruleTypes } = entry
+ if (!Array.isArray(chainIds) || !Array.isArray(ruleTypes)) return false
+ if (!chainIds.every((id) => typeof id === 'string')) return false
+ if (!ruleTypes.every((t) => typeof t === 'string')) return false
+ }
+ return true
+}
+
+function readRpcCode(error: unknown): number | undefined {
+ if (!isRecord(error)) return undefined
+ const { code } = error
+ return typeof code === 'number' ? code : undefined
+}
+
+/**
+ * Calls `wallet_getSupportedExecutionPermissions` on `window.ethereum` (MetaMask / Flask).
+ */
+export async function fetchWalletSupportedExecutionPermissions(): Promise {
+ if (typeof window === 'undefined') {
+ return { status: 'error', message: 'Wallet is only available in the browser' }
+ }
+
+ const provider = window.ethereum
+ if (!provider?.request) {
+ return { status: 'error', message: 'No EIP-1193 provider on window.ethereum' }
+ }
+
+ try {
+ const raw = await requestWalletMethod(provider, 'wallet_getSupportedExecutionPermissions', [])
+
+ if (!isSupportedShape(raw)) {
+ return {
+ status: 'error',
+ message: 'Wallet returned an unexpected shape for supported execution permissions',
+ }
+ }
+
+ return { status: 'ok', data: raw, raw }
+ } catch (error: unknown) {
+ const code = readRpcCode(error)
+ if (code === -32601) {
+ return { status: 'unsupported' }
+ }
+
+ const message =
+ error instanceof Error
+ ? error.message
+ : isRecord(error) && typeof error.message === 'string'
+ ? error.message
+ : 'Unknown wallet error'
+
+ return { status: 'error', errorCode: code, message }
+ }
+}
diff --git a/apps/erc7715/next.config.ts b/apps/erc7715/next.config.ts
new file mode 100644
index 00000000..8bded3e2
--- /dev/null
+++ b/apps/erc7715/next.config.ts
@@ -0,0 +1,10 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+ env: {
+ SESSION_ACCOUNT_ADDRESS: process.env.SESSION_ACCOUNT_ADDRESS ?? "",
+ TOKEN_ADDRESS: process.env.TOKEN_ADDRESS ?? "",
+ },
+};
+
+export default nextConfig;
diff --git a/apps/erc7715/package.json b/apps/erc7715/package.json
new file mode 100644
index 00000000..5b1c5fbd
--- /dev/null
+++ b/apps/erc7715/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "erc7715-v2",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev -p 3002",
+ "build": "next build",
+ "start": "next start",
+ "lint": "eslint"
+ },
+ "dependencies": {
+ "@base-org/account": "^2.5.1",
+ "@coinbase/wallet-sdk": "^4.3.6",
+ "@metamask/connect-evm": "^0.9.1",
+ "@safe-global/safe-apps-provider": "~0.18.6",
+ "@safe-global/safe-apps-sdk": "^9.1.0",
+ "@tanstack/react-query": "^5.99.0",
+ "@walletconnect/ethereum-provider": "^2.21.1",
+ "next": "15",
+ "porto": "~0.2.35",
+ "react": "19",
+ "react-dom": "19",
+ "viem": "^2.47.16",
+ "wagmi": "^3.6.1"
+ },
+ "devDependencies": {
+ "@eslint/eslintrc": "^3.3.5",
+ "@tailwindcss/postcss": "^4",
+ "@types/node": "^20",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "eslint": "^9",
+ "eslint-config-next": "15",
+ "tailwindcss": "^4",
+ "typescript": "^5"
+ },
+ "ignoreScripts": [
+ "sharp",
+ "unrs-resolver"
+ ],
+ "trustedDependencies": [
+ "sharp",
+ "unrs-resolver"
+ ]
+}
diff --git a/apps/erc7715/postcss.config.mjs b/apps/erc7715/postcss.config.mjs
new file mode 100644
index 00000000..61e36849
--- /dev/null
+++ b/apps/erc7715/postcss.config.mjs
@@ -0,0 +1,7 @@
+const config = {
+ plugins: {
+ "@tailwindcss/postcss": {},
+ },
+};
+
+export default config;
diff --git a/apps/erc7715/public/file.svg b/apps/erc7715/public/file.svg
new file mode 100644
index 00000000..004145cd
--- /dev/null
+++ b/apps/erc7715/public/file.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/erc7715/public/globe.svg b/apps/erc7715/public/globe.svg
new file mode 100644
index 00000000..567f17b0
--- /dev/null
+++ b/apps/erc7715/public/globe.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/erc7715/public/next.svg b/apps/erc7715/public/next.svg
new file mode 100644
index 00000000..5174b28c
--- /dev/null
+++ b/apps/erc7715/public/next.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/erc7715/public/vercel.svg b/apps/erc7715/public/vercel.svg
new file mode 100644
index 00000000..77053960
--- /dev/null
+++ b/apps/erc7715/public/vercel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/erc7715/public/window.svg b/apps/erc7715/public/window.svg
new file mode 100644
index 00000000..b2b2a44f
--- /dev/null
+++ b/apps/erc7715/public/window.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/erc7715/tsconfig.json b/apps/erc7715/tsconfig.json
new file mode 100644
index 00000000..c3cb3e47
--- /dev/null
+++ b/apps/erc7715/tsconfig.json
@@ -0,0 +1,43 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": [
+ "./*"
+ ]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "types/env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts",
+ "**/*.mts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/apps/erc7715/types/env.d.ts b/apps/erc7715/types/env.d.ts
new file mode 100644
index 00000000..1bfb3ae3
--- /dev/null
+++ b/apps/erc7715/types/env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/apps/erc7715/types/erc7715.ts b/apps/erc7715/types/erc7715.ts
new file mode 100644
index 00000000..44aa458c
--- /dev/null
+++ b/apps/erc7715/types/erc7715.ts
@@ -0,0 +1,80 @@
+import type { Address, Hex } from "viem";
+
+/**
+ * Result of `wallet_getSupportedExecutionPermissions` (ERC-7715 execution permissions probe).
+ * Keys are permission type identifiers; values list supported chains and rule types.
+ */
+export type GetSupportedExecutionPermissionsResult = Record<
+ string,
+ {
+ chainIds: `0x${string}`[];
+ ruleTypes: string[];
+ }
+>;
+
+/** `permission.data` for `native-token-allowance` (hex-encoded uint256). */
+export type NativeTokenAllowanceData = {
+ allowance: `0x${string}`;
+};
+
+/** `permission.data` for `erc20-token-allowance`. */
+export type ERC20TokenAllowanceData = {
+ tokenAddress: `0x${string}`;
+ allowance: `0x${string}`;
+ periodAmount: `0x${string}`;
+ periodDuration: number;
+};
+
+/** `permission.data` for `erc20-token-periodic` (no top-level allowance). */
+export type ERC20TokenPeriodicData = {
+ tokenAddress: `0x${string}`;
+ periodAmount: `0x${string}`;
+ periodDuration: number;
+};
+
+/** Base permission shape per ERC-7715. */
+export type ExecutionPermissionPayload = {
+ type: string;
+ isAdjustmentAllowed: boolean;
+ data: Record;
+};
+
+/** Base rule shape per ERC-7715 (e.g. expiry). */
+export type ExecutionPermissionRule = {
+ type: string;
+ data: Record;
+};
+
+/** Expiry rule from ERC-7715 — constrains validity until a unix timestamp (seconds). */
+export type ExpiryRule = ExecutionPermissionRule & {
+ type: "expiry";
+ data: {
+ timestamp: number;
+ };
+};
+
+/**
+ * Single permission request per ERC-7715 `wallet_requestExecutionPermissions`.
+ */
+export type PermissionRequest = {
+ chainId: Hex;
+ from?: Address;
+ to: Address;
+ permission: ExecutionPermissionPayload;
+ rules?: ExecutionPermissionRule[];
+};
+
+/** Counterparty / 4337 dependency entry returned by the wallet. */
+export type PermissionDependency = {
+ factory: `0x${string}`;
+ factoryData: `0x${string}`;
+};
+
+/**
+ * Wallet response for a granted permission (echoes the request plus ERC-7715 response fields).
+ */
+export type PermissionResponse = PermissionRequest & {
+ context: Hex;
+ dependencies: PermissionDependency[];
+ delegationManager: `0x${string}`;
+};
diff --git a/bun.lockb b/bun.lockb
new file mode 100755
index 00000000..bf235734
Binary files /dev/null and b/bun.lockb differ