Ethernauta

Permit signing — USDC

End-to-end EIP-2612 flow against USDC on mainnet. Reads DOMAIN_SEPARATOR + nonces(owner) via multicall, builds the typed Permit struct, and asks the wallet to sign it via eth_signTypedData_v4. The returned 65-byte signature is what a third party (a relayer, a router, a contract's permitAndCall) needs to spend tokens on your behalf — no prior approve() transaction.

TokenUSDC (mainnet)
Spender0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
Value100 USDC

Sign in via Connect wallet at the top right to run this demo.

Needs an EIP-6963 wallet — Ethernauta, MetaMask, or any compliant wallet — connected via the header's Connect wallet button.

View on GitHub

The code

import "./demo.css"
import { eip155_1 } from "@ethernauta/chain/eip155-1"
import {
  AddressSchema,
  Bytes32Schema,
  BytesSchema,
} from "@ethernauta/core"
import { eth_signTypedData_v4 } from "@ethernauta/eip/712"
import {
  DOMAIN_SEPARATOR,
  nonces,
} from "@ethernauta/erc/2612"
import { useProviderDetail } from "@ethernauta/react"
import {
  contract,
  create_multicall,
  create_provider,
  encode_chain_id,
  http,
} from "@ethernauta/transport"
import {
  deadline_in,
  format_unit,
  parse_unit,
} from "@ethernauta/utils"
import { useState } from "react"
import {
  bigint,
  type InferOutput,
  object,
  parse,
} from "valibot"
import { Button } from "../../components/button"
import { SignInHint } from "../../components/sign-in-hint"
import { use_session } from "../../lib/auth/use-session"
import { PROVIDER_STORE_KEY } from "../../lib/provider-store"

const MAINNET_CHAIN_ID = encode_chain_id({
  namespace: "eip155",
  reference: eip155_1.chainId,
})

// USDC on mainnet — EIP-2612 permit support since v2.2.
const USDC = parse(
  AddressSchema,
  "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
)

// Demo spender — Uniswap V2 Router. Picked because it's the
// recognisable spender devs reach for first when explaining
// "approve once, swap later" flows.
const SPENDER = parse(
  AddressSchema,
  "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D",
)

const VALUE = parse_unit("100", 6)

const CHAINS = [
  {
    chainId: MAINNET_CHAIN_ID,
    transports: [
      http("https://ethereum-rpc.publicnode.com"),
    ],
  },
]
const multicall = create_multicall(CHAINS)

const SignedPermitSchema = object({
  owner: AddressSchema,
  spender: AddressSchema,
  value: bigint(),
  nonce: bigint(),
  deadline: bigint(),
  domain_separator: Bytes32Schema,
  signature: BytesSchema,
})
type SignedPermit = InferOutput<typeof SignedPermitSchema>

export function PermitDemo() {
  const session = use_session()
  const owner = session?.address ?? null
  const provider_detail = useProviderDetail({
    key: PROVIDER_STORE_KEY,
  })
  const [signed, set_signed] =
    useState<SignedPermit | null>(null)
  const [loading, set_loading] = useState(false)
  const [error, set_error] = useState<string | null>(null)

  async function sign_permit() {
    if (!owner) return
    if (!provider_detail) {
      set_error(
        "No connected wallet. Reconnect via the header's Connect wallet button.",
      )
      return
    }
    set_loading(true)
    set_error(null)
    try {
      const provider = create_provider(
        provider_detail.provider,
      )
      const ctx = contract({
        chain_id: MAINNET_CHAIN_ID,
        to: USDC,
      })
      const owner_address = parse(AddressSchema, owner)
      const [domain_separator_hex, nonce_hex] =
        await multicall([
          DOMAIN_SEPARATOR()(ctx),
          nonces({ owner: owner_address })(ctx),
        ] as const)
      const nonce = BigInt(nonce_hex)
      const deadline = deadline_in(3600)
      const signature = await eth_signTypedData_v4([
        owner_address,
        {
          domain: {
            name: "USD Coin",
            version: "2",
            chainId: 1n,
            verifyingContract: USDC,
          },
          types: {
            Permit: [
              { name: "owner", type: "address" },
              { name: "spender", type: "address" },
              { name: "value", type: "uint256" },
              { name: "nonce", type: "uint256" },
              { name: "deadline", type: "uint256" },
            ],
          },
          primaryType: "Permit",
          message: {
            owner: owner_address,
            spender: SPENDER,
            value: VALUE,
            nonce,
            deadline,
          },
        },
      ])(provider.signer({ chain_id: MAINNET_CHAIN_ID }))
      set_signed({
        owner: owner_address,
        spender: SPENDER,
        value: VALUE,
        nonce,
        deadline,
        domain_separator: domain_separator_hex,
        signature,
      })
    } catch (e) {
      set_error(
        e instanceof Error ? e.message : "Unknown error",
      )
    } finally {
      set_loading(false)
    }
  }

  return (
    <div className="permit-root">
      <div className="permit-card">
        <Row label="Token" value="USDC (mainnet)" />
        <Row label="Spender" value={SPENDER} mono />
        <Row
          label="Value"
          value={`${format_unit(VALUE, 6)} USDC`}
        />
        {owner && <Row label="Owner" value={owner} mono />}
        {signed && (
          <>
            <Row
              label="Nonce"
              value={signed.nonce.toString()}
            />
            <Row
              label="Deadline"
              value={new Date(
                Number(signed.deadline) * 1000,
              ).toLocaleString()}
            />
            <Row
              label="Domain separator"
              value={signed.domain_separator}
              mono
            />
            <Row
              label="Signature"
              value={signed.signature}
              mono
            />
          </>
        )}
      </div>
      {error && <p className="permit-error">{error}</p>}
      <div className="permit-button-row">
        {!owner && <SignInHint />}
        {owner && (
          <Button onClick={sign_permit} disabled={loading}>
            {loading
              ? "Signing…"
              : signed
                ? "Re-sign permit"
                : "Sign permit"}
          </Button>
        )}
      </div>
      {!owner && (
        <p className="permit-hint">
          Needs an EIP-6963 wallet — Ethernauta, MetaMask,
          or any compliant wallet — connected via the
          header's Connect wallet button.
        </p>
      )}
    </div>
  )
}

function Row({
  label,
  value,
  mono,
}: {
  label: string
  value: string
  mono?: boolean
}) {
  return (
    <div className="permit-row">
      <span className="permit-row-label">{label}</span>
      <span
        className={
          mono
            ? "permit-row-value is-mono"
            : "permit-row-value"
        }
      >
        {value}
      </span>
    </div>
  )
}