Multicall — ERC-20 snapshot
Four ERC-20 reads (name, symbol, decimals, totalSupply)
against WETH on Sepolia, batched into a single eth_call
via Multicall3.
View on GitHub
The code
import "./demo.css"
import { eip155_11155111 } from "@ethernauta/chain/eip155-11155111"
import { AddressSchema } from "@ethernauta/core"
import {
decimals,
name,
symbol,
totalSupply,
} from "@ethernauta/erc/20"
import {
contract,
create_multicall,
encode_chain_id,
http,
} from "@ethernauta/transport"
import { hex_to_number } from "@ethernauta/utils"
import { useCallback, useEffect, useState } from "react"
import {
bigint,
type InferOutput,
number,
object,
parse,
string,
} from "valibot"
import { Button } from "../../components/button"
const SEPOLIA_CHAIN_ID = encode_chain_id({
namespace: "eip155",
reference: eip155_11155111.chainId,
})
// WETH on Sepolia — canonical wrapped-ether deployment.
const WETH_SEPOLIA =
"0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14"
const multicall = create_multicall([
{
chainId: SEPOLIA_CHAIN_ID,
transports: [
http("https://ethereum-sepolia-rpc.publicnode.com"),
],
},
])
const SnapshotSchema = object({
name: string(),
symbol: string(),
decimals: number(),
totalSupply: bigint(),
elapsed_ms: number(),
})
type Snapshot = InferOutput<typeof SnapshotSchema>
export function MulticallDemo() {
const [snapshot, set_snapshot] =
useState<Snapshot | null>(null)
const [loading, set_loading] = useState(false)
const [error, set_error] = useState<string | null>(null)
const run = useCallback(async () => {
set_loading(true)
set_error(null)
try {
const ctx = contract({
chain_id: SEPOLIA_CHAIN_ID,
to: parse(AddressSchema, WETH_SEPOLIA),
})
const start = performance.now()
const [name_, symbol_, decimals_, supply_] =
await multicall([
name()(ctx),
symbol()(ctx),
decimals()(ctx),
totalSupply()(ctx),
] as const)
const elapsed_ms = Math.round(
performance.now() - start,
)
set_snapshot({
name: name_,
symbol: symbol_,
decimals: hex_to_number(decimals_),
totalSupply: BigInt(supply_),
elapsed_ms,
})
} catch (e) {
set_error(
e instanceof Error ? e.message : "Unknown error",
)
} finally {
set_loading(false)
}
}, [])
useEffect(() => {
run()
}, [run])
return (
<div className="multicall-root">
<div className="multicall-card">
{loading && (
<p className="multicall-loading">Loading…</p>
)}
{error && (
<p className="multicall-error">{error}</p>
)}
{snapshot && (
<>
<Row label="Name" value={snapshot.name} />
<Row label="Symbol" value={snapshot.symbol} />
<Row
label="Decimals"
value={String(snapshot.decimals)}
/>
<Row
label="Total supply"
value={`${format_supply(snapshot.totalSupply, snapshot.decimals)} ${snapshot.symbol}`}
/>
<Row
label="Roundtrip"
value={`${snapshot.elapsed_ms} ms · 1 RPC call`}
/>
</>
)}
</div>
<Button onClick={run} disabled={loading}>
{loading ? "Running…" : "Re-run multicall"}
</Button>
</div>
)
}
function Row({
label,
value,
}: {
label: string
value: string
}) {
return (
<div className="multicall-row">
<span className="multicall-row-label">{label}</span>
<span className="multicall-row-value">{value}</span>
</div>
)
}
function format_supply(
raw: bigint,
decimals: number,
): string {
const base = 10n ** BigInt(decimals)
const whole = raw / base
const fraction = raw % base
const fraction_str = fraction
.toString()
.padStart(decimals, "0")
.slice(0, 4)
return `${whole.toLocaleString()}.${fraction_str}`
}