@1001-digital/proxies
v0.1.1
Published
Ethereum proxy pattern detection primitives — ERC-2535 diamonds, EIP-1967, EIP-1167, beacon, Safe, EIP-1822, EIP-897. Detect, enrich, compose.
Downloads
233
Maintainers
Readme
@1001-digital/proxies
Ethereum proxy pattern detection primitives for TypeScript — ERC-2535 diamonds, EIP-1967, EIP-1167, beacon, Safe, EIP-1822, EIP-897.
Given a contract address, resolve where the real code lives: one implementation (plain proxy) or N facets (diamond). Uniform detect → enrich → compose pipeline across all patterns.
Narrow on purpose: this package knows proxy conventions, selectors, and ABIs. Anything richer — Sourcify, NatSpec, repository metadata — is the consumer's concern. Bring your own enricher.
Who this is for
Use this package when you need a small, composable proxy-resolution layer in:
- explorer and indexer backends
- contract introspection CLIs
- ABI fetchers and decoding pipelines
- developer tooling that needs implementation or facet discovery without owning proxy-specific logic
What this package is not
This package intentionally does not try to be:
- a contract metadata SDK
- a verification or source-code retrieval client
- a recursive proxy resolver
- an upgrade history tracker
- an opinionated registry or indexing layer
If you need sources, NatSpec, repository metadata, verification status, or custom per-target fields, use detect() and own enrichment yourself.
Install
pnpm add @1001-digital/proxiesUsage
Detect + fetch a proxy
import { createProxies } from '@1001-digital/proxies'
const proxies = createProxies()
const result = await proxies.fetch(
'https://eth.llamarpc.com',
'0xProxyAddress…',
)
if (result) {
console.log(result.pattern) // 'eip-1967' | 'eip-2535-diamond' | …
console.log(result.targets) // [{ address, selectors?, abi? }, …]
console.log(result.compositeAbi) // undefined — no enricher configured
}Enrich with Sourcify (or anything else)
The enricher returns each target's ABI. Anything richer (sources, bytecode, documentation, …) is the consumer's concern — see Detect and do your own enrichment below.
import { createProxies } from '@1001-digital/proxies'
async function sourcifyAbi(address: string) {
const res = await fetch(
`https://sourcify.dev/server/v2/contract/1/${address}?fields=abi`,
)
if (!res.ok) return null
const { abi } = await res.json()
return { abi }
}
const proxies = createProxies({ enrich: sourcifyAbi })
const result = await proxies.fetch(
'https://eth.llamarpc.com',
'0xProxyAddress…',
)
if (result) {
console.log(result.targets[0].abi) // ABI (filtered to selectors for diamonds, full for plain proxies)
console.log(result.compositeAbi) // all target ABIs deduped by selector
}Detect and do your own enrichment
For richer per-target metadata, use detect and own the enrichment step
end-to-end. filterAbiBySelectors and buildCompositeAbi remain useful
primitives.
import {
createProxies,
filterAbiBySelectors,
buildCompositeAbi,
} from '@1001-digital/proxies'
const proxies = createProxies()
const raw = await proxies.detect(rpc, address)
if (raw) {
const enriched = await Promise.all(raw.targets.map(async t => {
const src = await mySource(t.address)
return {
...t,
abi: src?.abi && t.selectors
? filterAbiBySelectors(src.abi, t.selectors)
: src?.abi,
metadata: src?.metadata,
}
}))
const compositeAbi = buildCompositeAbi(
enriched.map(t => t.abi).filter((a): a is unknown[] => !!a),
)
}Standalone primitives
All low-level utilities are exported directly — use them without the factory:
import {
detectProxy,
detectDiamond,
detectEip1967,
decodeFacets,
computeSelector,
canonicalSignature,
filterAbiBySelectors,
buildCompositeAbi,
enrichTargets,
} from '@1001-digital/proxies'
decodeFacets('0x…') // parse a facets() return value
computeSelector('transfer(address,uint256)') // '0xa9059cbb'
canonicalSignature({ type: 'function', name: 'transfer', inputs: [/*…*/] })API
createProxies(config?)
Creates a proxies client.
Config options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| enrich | TargetEnricher | — | Default per-target enricher. Called with each target address; return { abi? } or null. Errors are swallowed per-target. |
| fetch | typeof fetch | globalThis.fetch | Custom fetch function. |
Returns a ProxiesClient with:
detect(rpc, address)—Promise<RawProxy | null>. On-chain probe only.fetch(rpc, address, options?)—Promise<Proxy | null>. Detect, enrich, and compose.options.enrich— per-call enricher (overrides config-level)options.enrich = false— skip enrichment for this call
Detection
detectProxy(rpc, address, fetchFn)— tries all patterns in priority order (diamond → 1967 → 1967-beacon → 1822 → 1167 → safe → 897), returns the first match.detectDiamond— ERC-165 probe, thenfacets()fallback.detectEip1967— reads impl slot0x3608…3bc3; optionally admin slot.detectEip1967Beacon— reads beacon slot0xa3f0…750d, thenimplementation()on the beacon.detectEip1822— reads PROXIABLE slot0xc5f1…f8e2.detectEip1167—eth_getCode, matches minimal proxy bytecode (363d3d…bf3).detectGnosisSafe— reads storage slot 0.detectEip897— callsimplementation()as a last resort.
Each detector returns null if the pattern doesn't match; otherwise a RawProxy.
Detection tradeoffs
- First match wins — detector order is intentional. If a contract could satisfy multiple heuristics,
detectProxyreturns the first supported pattern in priority order. - Single-hop resolution — if a resolved implementation is itself a proxy, detection stops there. Beacon remains supported as a defined two-step pattern.
- Error isolation — one failed RPC probe does not poison the full detection pipeline.
Composition
enrichTargets(targets, enricher | null)— applies the enricher to each target; ABIs are filtered to live selectors for diamonds, passed through for plain proxies.buildCompositeAbi(abis)— pure; dedupes functions/events/errors across ABIs (first-wins).
Utilities
decodeFacets(hex)— pure; decodes(address, bytes4[])[]loupe return.computeSelector(signature)— pure; keccak256-based 4-byte selector.canonicalSignature(abiEntry)— pure; normalizes tuples/arrays into a signature string.filterAbiBySelectors(abi, selectors)— pure; keeps non-functions, filters functions by selector.ethCall,ethGetStorageAt,ethGetCode— minimal JSON-RPC helpers.
Constants
SUPPORTS_INTERFACE_SELECTOR // '0x01ffc9a7'
DIAMOND_LOUPE_INTERFACE_ID // '0x48e2b093'
FACETS_SELECTOR // '0x7a0ed627'
IMPLEMENTATION_SELECTOR // '0x5c60da1b'
EIP1967_IMPL_SLOT
EIP1967_BEACON_SLOT
EIP1967_ADMIN_SLOT
EIP1822_PROXIABLE_SLOT
EIP1167_BYTECODE_PREFIX
EIP1167_BYTECODE_SUFFIX
ZERO_ADDRESSErrors
ProxiesError— base class.ProxiesDecodeError— malformedfacets()return.ProxiesFetchError— JSON-RPC transport error.
The client's detect and fetch methods swallow RPC-layer errors and return
null; only decodeFacets (called directly) can throw ProxiesDecodeError
on malformed input.
Shapes
ProxyPattern
type ProxyPattern =
| 'eip-2535-diamond'
| 'eip-1967'
| 'eip-1967-beacon'
| 'eip-1822'
| 'eip-1167'
| 'gnosis-safe'
| 'eip-897'ResolvedTarget
{
address: string
// undefined = all selectors route here (plain proxy)
// defined = diamond facet selector scope
selectors?: string[]
}RawProxy
{
pattern: ProxyPattern
targets: ResolvedTarget[] // 1 entry except for diamonds
beacon?: string // only for eip-1967-beacon
admin?: string // only for eip-1967 when admin slot is set
}EnrichedTarget
{
address: string
selectors?: string[]
abi?: unknown[]
}Proxy
{
pattern: ProxyPattern
targets: EnrichedTarget[]
beacon?: string
admin?: string
compositeAbi?: unknown[] // deduped by selector
}TargetEnrichment
{ abi?: unknown[] }TargetEnricher
type TargetEnricher = (address: string) => Promise<TargetEnrichment | null>Design notes
- Narrow scope — detects patterns and composes ABIs; no opinion on documentation formats or richer per-target metadata.
- Dependency-injected enrichment — the factory wires I/O when you ask; the primitives work offline.
- First-wins ABI dedup — pass the most authoritative ABI first (e.g. main contract → impl, or main diamond → facets).
- Single-hop resolution — if a resolved implementation is itself a proxy,
detectProxydoes not recurse. Beacon stays supported as a defined two-step pattern. - Minimal runtime deps — only
@noble/hashesfor keccak256.
Example consumers
- An explorer backend can call
detect()to resolve a proxy and then enrich targets from Sourcify or an internal cache. - A CLI can call
fetch()to print the detected pattern, target addresses, and a composite ABI for downstream decoding. - An indexing pipeline can treat
detectProxy → enrichTargets → buildCompositeAbias a reusable normalization step before storage.
License
MIT
