@1001-digital/proxies 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/decode.ts ADDED
@@ -0,0 +1,103 @@
1
+ import { ProxiesDecodeError } from './errors'
2
+
3
+ // Sanity limits to reject malformed data that accidentally decodes.
4
+ // Real diamonds have <100 facets and <200 selectors/facet; bound malformed-input cost.
5
+ const MAX_FACETS = 200
6
+ const MAX_SELECTORS_PER_FACET = 1000
7
+
8
+ /**
9
+ * Literal on-chain facet tuple (`(address, bytes4[])`) — the shape returned by
10
+ * the ERC-2535 loupe `facets()` call. Use {@link decodeFacets} to parse it.
11
+ */
12
+ export interface DecodedFacet {
13
+ facetAddress: string
14
+ functionSelectors: string[]
15
+ }
16
+
17
+ /**
18
+ * Decode the return value of `facets()` — type `(address, bytes4[])[]`.
19
+ * Throws {@link ProxiesDecodeError} on malformed input.
20
+ */
21
+ export function decodeFacets(hex: string): DecodedFacet[] {
22
+ const h = hex.startsWith('0x') ? hex.slice(2) : hex
23
+ const W = 64 // one 32-byte word in hex chars
24
+
25
+ const readWord = (pos: number): string => {
26
+ if (pos < 0 || pos + W > h.length) {
27
+ throw new ProxiesDecodeError('malformed facets() return: out of bounds')
28
+ }
29
+ return h.slice(pos, pos + W)
30
+ }
31
+
32
+ const readUint = (pos: number): number => {
33
+ const word = readWord(pos)
34
+ // Upper 24 bytes must be zero for the value to fit safely in a JS number
35
+ if (!/^0{48}/.test(word)) {
36
+ throw new ProxiesDecodeError('malformed facets() return: value too large')
37
+ }
38
+ return parseInt(word, 16)
39
+ }
40
+
41
+ const outerOff = readUint(0) * 2
42
+ const n = readUint(outerOff)
43
+ if (n > MAX_FACETS) {
44
+ throw new ProxiesDecodeError(`malformed facets() return: ${n} facets exceeds limit`)
45
+ }
46
+
47
+ const head = outerOff + W
48
+ const facets: DecodedFacet[] = []
49
+
50
+ for (let i = 0; i < n; i++) {
51
+ const tupleOff = readUint(head + i * W) * 2
52
+ const tx = head + tupleOff
53
+
54
+ const addrWord = readWord(tx)
55
+ if (!/^0{24}/.test(addrWord)) {
56
+ throw new ProxiesDecodeError('malformed facets() return: invalid address')
57
+ }
58
+ const facetAddress = '0x' + addrWord.slice(24).toLowerCase()
59
+
60
+ const selOff = readUint(tx + W) * 2
61
+ const selStart = tx + selOff
62
+
63
+ const m = readUint(selStart)
64
+ if (m > MAX_SELECTORS_PER_FACET) {
65
+ throw new ProxiesDecodeError(
66
+ `malformed facets() return: ${m} selectors exceeds limit`,
67
+ )
68
+ }
69
+
70
+ const selectors: string[] = []
71
+ for (let j = 0; j < m; j++) {
72
+ const slot = readWord(selStart + W + j * W)
73
+ selectors.push('0x' + slot.slice(0, 8).toLowerCase())
74
+ }
75
+
76
+ facets.push({ facetAddress, functionSelectors: selectors })
77
+ }
78
+
79
+ return facets
80
+ }
81
+
82
+ /**
83
+ * Parse a 32-byte ABI-encoded bool. Returns `null` if the response is not a
84
+ * well-formed bool (wrong length, or non-zero high bits).
85
+ */
86
+ export function parseBool(hex: string): boolean | null {
87
+ if (hex.length !== 66) return null
88
+ const body = hex.slice(2).toLowerCase()
89
+ if (!/^0{63}[01]$/.test(body)) return null
90
+ return body.slice(-1) === '1'
91
+ }
92
+
93
+ /**
94
+ * Parse a 32-byte ABI-encoded address (right-padded). Returns the lowercase
95
+ * `0x` address, or `null` if the payload is not a well-formed address (wrong
96
+ * length, or non-zero high bytes).
97
+ */
98
+ export function parseAddress(hex: string): string | null {
99
+ if (hex.length !== 66) return null
100
+ const body = hex.slice(2).toLowerCase()
101
+ if (!/^0{24}[0-9a-f]{40}$/.test(body)) return null
102
+ return '0x' + body.slice(24)
103
+ }
package/src/detect.ts ADDED
@@ -0,0 +1,60 @@
1
+ import { detectDiamond } from './patterns/diamond'
2
+ import { detectEip1167 } from './patterns/eip1167'
3
+ import { detectEip1822 } from './patterns/eip1822'
4
+ import { detectEip1967 } from './patterns/eip1967'
5
+ import { detectEip1967Beacon } from './patterns/eip1967-beacon'
6
+ import { detectEip897 } from './patterns/eip897'
7
+ import { detectGnosisSafe } from './patterns/safe'
8
+ import type { RawProxy } from './types'
9
+
10
+ type Detector = (
11
+ rpc: string,
12
+ address: string,
13
+ fetchFn: typeof globalThis.fetch,
14
+ ) => Promise<RawProxy | null>
15
+
16
+ /**
17
+ * Priority order used by {@link detectProxy}. First detector that returns
18
+ * non-null wins. Diamonds come first because the loupe probe is a specific,
19
+ * low-false-positive signal; standard storage slots follow; Safe's slot-0
20
+ * convention and EIP-897's `implementation()` view function come last.
21
+ */
22
+ const DETECTORS: Detector[] = [
23
+ detectDiamond,
24
+ detectEip1967,
25
+ detectEip1967Beacon,
26
+ detectEip1822,
27
+ detectEip1167,
28
+ detectGnosisSafe,
29
+ detectEip897,
30
+ ]
31
+
32
+ /**
33
+ * Detect any supported proxy pattern at `address`.
34
+ *
35
+ * Tries the built-in detectors in priority order (`eip-2535-diamond → eip-1967
36
+ * → eip-1967-beacon → eip-1822 → eip-1167 → gnosis-safe → eip-897`). Returns
37
+ * the first match, or `null` when no pattern matches.
38
+ *
39
+ * Each detector runs with error isolation — an RPC failure in one pattern does
40
+ * not poison the probe for subsequent ones.
41
+ *
42
+ * Single-hop only: if the resolved implementation is itself a proxy, the
43
+ * result describes the direct pattern only. Re-run detection on the resolved
44
+ * implementation to chain manually.
45
+ */
46
+ export async function detectProxy(
47
+ rpc: string,
48
+ address: string,
49
+ fetchFn: typeof globalThis.fetch,
50
+ ): Promise<RawProxy | null> {
51
+ for (const detect of DETECTORS) {
52
+ try {
53
+ const match = await detect(rpc, address, fetchFn)
54
+ if (match) return match
55
+ } catch {
56
+ // Detector-level errors shouldn't block later detectors
57
+ }
58
+ }
59
+ return null
60
+ }
package/src/enrich.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { filterAbiBySelectors } from './abi'
2
+ import type { EnrichedTarget, ResolvedTarget, TargetEnricher } from './types'
3
+
4
+ /**
5
+ * Apply an enricher to each resolved target, producing an `EnrichedTarget[]`.
6
+ *
7
+ * Enricher errors are swallowed per-target — one bad fetch does not fail the
8
+ * whole resolution. Pass `null` to skip enrichment entirely (targets will
9
+ * carry only `address` + `selectors`).
10
+ *
11
+ * When a target has `selectors` defined (diamond facets), the returned ABI is
12
+ * filtered to those selectors. When `selectors` is undefined (any single-impl
13
+ * proxy pattern), the full implementation ABI is passed through untouched.
14
+ */
15
+ export async function enrichTargets(
16
+ targets: ResolvedTarget[],
17
+ enrich: TargetEnricher | null,
18
+ ): Promise<EnrichedTarget[]> {
19
+ const enrichments = enrich
20
+ ? await Promise.all(
21
+ targets.map(t => enrich(t.address).catch(() => null)),
22
+ )
23
+ : targets.map(() => null)
24
+
25
+ return targets.map((t, i) => {
26
+ const src = enrichments[i]
27
+ const info: EnrichedTarget = { address: t.address }
28
+ if (t.selectors !== undefined) info.selectors = t.selectors
29
+ if (src?.abi) {
30
+ info.abi = t.selectors !== undefined
31
+ ? filterAbiBySelectors(src.abi, t.selectors)
32
+ : src.abi
33
+ }
34
+ return info
35
+ })
36
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,33 @@
1
+ export function errorMessage(error: unknown): string {
2
+ return error instanceof Error ? error.message : String(error)
3
+ }
4
+
5
+ export class ProxiesError extends Error {
6
+ constructor(message: string, options?: ErrorOptions) {
7
+ super(message, options)
8
+ this.name = 'ProxiesError'
9
+ }
10
+ }
11
+
12
+ /** Raised when the on-chain `facets()` return value cannot be decoded. */
13
+ export class ProxiesDecodeError extends ProxiesError {
14
+ constructor(message: string, options?: ErrorOptions) {
15
+ super(message, options)
16
+ this.name = 'ProxiesDecodeError'
17
+ }
18
+ }
19
+
20
+ /** Raised when a JSON-RPC request fails at the transport level. */
21
+ export class ProxiesFetchError extends ProxiesError {
22
+ public readonly status: number
23
+
24
+ constructor(
25
+ message: string,
26
+ details: { status: number },
27
+ options?: ErrorOptions,
28
+ ) {
29
+ super(message, options)
30
+ this.name = 'ProxiesFetchError'
31
+ this.status = details.status
32
+ }
33
+ }
package/src/index.ts ADDED
@@ -0,0 +1,122 @@
1
+ import { buildCompositeAbi } from './abi'
2
+ import { detectProxy } from './detect'
3
+ import { enrichTargets } from './enrich'
4
+ import type {
5
+ FetchProxyOptions,
6
+ Proxy,
7
+ ProxiesClient,
8
+ ProxiesConfig,
9
+ TargetEnricher,
10
+ } from './types'
11
+
12
+ /**
13
+ * Create a proxy inspection client.
14
+ *
15
+ * ```ts
16
+ * const proxies = createProxies()
17
+ * const result = await proxies.fetch('https://rpc…', '0x…')
18
+ * if (result) console.log(result.pattern, result.targets)
19
+ * ```
20
+ *
21
+ * Pass `config.enrich` to populate each target's ABI from any source
22
+ * (Sourcify, Etherscan, a local cache). Omit it to get raw targets with
23
+ * address + (optional) selectors only.
24
+ */
25
+ export function createProxies(config: ProxiesConfig = {}): ProxiesClient {
26
+ const fetchFn = config.fetch ?? globalThis.fetch
27
+ const defaultEnrich = config.enrich ?? null
28
+
29
+ return {
30
+ detect(rpc, address) {
31
+ return detectProxy(rpc, address, fetchFn)
32
+ },
33
+
34
+ async fetch(rpc, address, options): Promise<Proxy | null> {
35
+ const raw = await detectProxy(rpc, address, fetchFn)
36
+ if (!raw) return null
37
+
38
+ const enricher: TargetEnricher | null = options?.enrich === false
39
+ ? null
40
+ : (options?.enrich ?? defaultEnrich)
41
+
42
+ const targets = await enrichTargets(raw.targets, enricher)
43
+
44
+ const abiLayers = targets
45
+ .map(t => t.abi)
46
+ .filter((a): a is unknown[] => a !== undefined)
47
+ const compositeAbi = abiLayers.length > 0
48
+ ? buildCompositeAbi(abiLayers)
49
+ : undefined
50
+
51
+ const proxy: Proxy = { pattern: raw.pattern, targets }
52
+ if (raw.beacon) proxy.beacon = raw.beacon
53
+ if (raw.admin) proxy.admin = raw.admin
54
+ if (compositeAbi) proxy.compositeAbi = compositeAbi
55
+ return proxy
56
+ },
57
+ }
58
+ }
59
+
60
+ // ── Detection ──
61
+
62
+ export { detectProxy } from './detect'
63
+ export { detectDiamond } from './patterns/diamond'
64
+ export { detectEip1967 } from './patterns/eip1967'
65
+ export { detectEip1967Beacon } from './patterns/eip1967-beacon'
66
+ export { detectEip1822 } from './patterns/eip1822'
67
+ export { detectEip1167 } from './patterns/eip1167'
68
+ export { detectGnosisSafe } from './patterns/safe'
69
+ export { detectEip897 } from './patterns/eip897'
70
+
71
+ // ── Composition ──
72
+
73
+ export { enrichTargets } from './enrich'
74
+ export { filterAbiBySelectors, buildCompositeAbi } from './abi'
75
+
76
+ // ── Utilities ──
77
+
78
+ export { decodeFacets, parseAddress } from './decode'
79
+ export { computeSelector, canonicalSignature } from './selector'
80
+
81
+ // ── RPC ──
82
+
83
+ export { ethCall, ethGetStorageAt, ethGetCode } from './rpc'
84
+
85
+ // ── Constants ──
86
+
87
+ export {
88
+ SUPPORTS_INTERFACE_SELECTOR,
89
+ DIAMOND_LOUPE_INTERFACE_ID,
90
+ FACETS_SELECTOR,
91
+ IMPLEMENTATION_SELECTOR,
92
+ EIP1967_IMPL_SLOT,
93
+ EIP1967_BEACON_SLOT,
94
+ EIP1967_ADMIN_SLOT,
95
+ EIP1822_PROXIABLE_SLOT,
96
+ EIP1167_BYTECODE_PREFIX,
97
+ EIP1167_BYTECODE_SUFFIX,
98
+ ZERO_ADDRESS,
99
+ } from './constants'
100
+
101
+ // ── Errors ──
102
+
103
+ export { ProxiesError, ProxiesDecodeError, ProxiesFetchError } from './errors'
104
+
105
+ // ── Types ──
106
+
107
+ export type {
108
+ ProxiesConfig,
109
+ ProxiesClient,
110
+ FetchProxyOptions,
111
+ ProxyPattern,
112
+ ResolvedTarget,
113
+ RawProxy,
114
+ EnrichedTarget,
115
+ Proxy,
116
+ TargetEnrichment,
117
+ TargetEnricher,
118
+ AbiParam,
119
+ AbiFunctionLike,
120
+ } from './types'
121
+
122
+ export type { DecodedFacet } from './decode'
@@ -0,0 +1,77 @@
1
+ import {
2
+ DIAMOND_LOUPE_INTERFACE_ID,
3
+ FACETS_SELECTOR,
4
+ SUPPORTS_INTERFACE_SELECTOR,
5
+ ZERO_ADDRESS,
6
+ } from '../constants'
7
+ import { decodeFacets, parseBool } from '../decode'
8
+ import { ethCall } from '../rpc'
9
+ import type { DecodedFacet } from '../decode'
10
+ import type { RawProxy, ResolvedTarget } from '../types'
11
+
12
+ /**
13
+ * Detect whether a contract implements ERC-2535 (Diamond) and return its
14
+ * facets as a {@link RawProxy}. Returns `null` if not a diamond.
15
+ *
16
+ * Strategy:
17
+ * 1. Try ERC-165 `supportsInterface(0x48e2b093)`.
18
+ * - Valid bool `true` → fetch and return facets
19
+ * - Valid bool `false` → definitively not a diamond (null)
20
+ * - Malformed / error → fall through to step 2
21
+ * 2. Probe `facets()` directly. If it returns a non-empty decoded array,
22
+ * treat the contract as a diamond.
23
+ *
24
+ * Zero-address facets (deleted selectors) are filtered out of the result.
25
+ */
26
+ export async function detectDiamond(
27
+ rpc: string,
28
+ address: string,
29
+ fetchFn: typeof globalThis.fetch,
30
+ ): Promise<RawProxy | null> {
31
+ const calldata = SUPPORTS_INTERFACE_SELECTOR
32
+ + DIAMOND_LOUPE_INTERFACE_ID.slice(2).padEnd(64, '0')
33
+
34
+ try {
35
+ const res = await ethCall(rpc, address, calldata, fetchFn)
36
+ const bool = parseBool(res)
37
+ if (bool === true) return tryFacets(rpc, address, fetchFn)
38
+ if (bool === false) return null
39
+ // Malformed response — fall through to facets() probe
40
+ } catch {
41
+ // RPC/revert error — fall through to facets() probe
42
+ }
43
+
44
+ return tryFacets(rpc, address, fetchFn)
45
+ }
46
+
47
+ // Minimum valid `facets()` payload: outer offset word + length word = 2 × 32
48
+ // bytes → 128 hex chars + '0x' prefix.
49
+ const MIN_FACETS_PAYLOAD_LEN = 130
50
+
51
+ async function tryFacets(
52
+ rpc: string,
53
+ address: string,
54
+ fetchFn: typeof globalThis.fetch,
55
+ ): Promise<RawProxy | null> {
56
+ let res: string
57
+ try {
58
+ res = await ethCall(rpc, address, FACETS_SELECTOR, fetchFn)
59
+ } catch {
60
+ return null
61
+ }
62
+
63
+ if (res === '0x' || res.length < MIN_FACETS_PAYLOAD_LEN) return null
64
+
65
+ let facets: DecodedFacet[]
66
+ try {
67
+ facets = decodeFacets(res)
68
+ } catch {
69
+ return null
70
+ }
71
+
72
+ const targets: ResolvedTarget[] = facets
73
+ .filter(f => f.facetAddress !== ZERO_ADDRESS)
74
+ .map(f => ({ address: f.facetAddress, selectors: f.functionSelectors }))
75
+
76
+ return targets.length > 0 ? { pattern: 'eip-2535-diamond', targets } : null
77
+ }
@@ -0,0 +1,45 @@
1
+ import {
2
+ EIP1167_BYTECODE_PREFIX,
3
+ EIP1167_BYTECODE_SUFFIX,
4
+ ZERO_ADDRESS,
5
+ } from '../constants'
6
+ import { ethGetCode } from '../rpc'
7
+ import type { RawProxy } from '../types'
8
+
9
+ /**
10
+ * Detect an EIP-1167 minimal proxy (clone) from runtime bytecode.
11
+ *
12
+ * Matches the canonical 45-byte runtime:
13
+ * `363d3d373d3d3d363d73<20-byte impl>5af43d82803e903d91602b57fd5bf3`
14
+ *
15
+ * Does NOT match the "optimized" variants (e.g. push-based relays) that some
16
+ * tooling emits — this is the standards-compliant shape per EIP-1167.
17
+ *
18
+ * Returns `null` when the bytecode does not match.
19
+ */
20
+ export async function detectEip1167(
21
+ rpc: string,
22
+ address: string,
23
+ fetchFn: typeof globalThis.fetch,
24
+ ): Promise<RawProxy | null> {
25
+ let code: string
26
+ try {
27
+ code = await ethGetCode(rpc, address, fetchFn)
28
+ } catch {
29
+ return null
30
+ }
31
+
32
+ const body = code.toLowerCase().replace(/^0x/, '')
33
+ // 20 prefix + 40 address + 30 suffix = 90 hex chars
34
+ if (body.length !== 90) return null
35
+ if (!body.startsWith(EIP1167_BYTECODE_PREFIX)) return null
36
+ if (!body.endsWith(EIP1167_BYTECODE_SUFFIX)) return null
37
+
38
+ const impl = '0x' + body.slice(EIP1167_BYTECODE_PREFIX.length, EIP1167_BYTECODE_PREFIX.length + 40)
39
+ if (impl === ZERO_ADDRESS) return null
40
+
41
+ return {
42
+ pattern: 'eip-1167',
43
+ targets: [{ address: impl }],
44
+ }
45
+ }
@@ -0,0 +1,34 @@
1
+ import { EIP1822_PROXIABLE_SLOT, ZERO_ADDRESS } from '../constants'
2
+ import { parseAddress } from '../decode'
3
+ import { ethGetStorageAt } from '../rpc'
4
+ import type { RawProxy } from '../types'
5
+
6
+ /**
7
+ * Detect an EIP-1822 UUPS proxy.
8
+ *
9
+ * Reads the PROXIABLE slot (`keccak256('PROXIABLE')`). Returns `null` when the
10
+ * slot is empty or malformed.
11
+ *
12
+ * Note: most modern UUPS proxies use the EIP-1967 slot instead — this detector
13
+ * only catches the older EIP-1822 convention.
14
+ */
15
+ export async function detectEip1822(
16
+ rpc: string,
17
+ address: string,
18
+ fetchFn: typeof globalThis.fetch,
19
+ ): Promise<RawProxy | null> {
20
+ let raw: string
21
+ try {
22
+ raw = await ethGetStorageAt(rpc, address, EIP1822_PROXIABLE_SLOT, fetchFn)
23
+ } catch {
24
+ return null
25
+ }
26
+
27
+ const impl = parseAddress(raw)
28
+ if (!impl || impl === ZERO_ADDRESS) return null
29
+
30
+ return {
31
+ pattern: 'eip-1822',
32
+ targets: [{ address: impl }],
33
+ }
34
+ }
@@ -0,0 +1,43 @@
1
+ import { EIP1967_BEACON_SLOT, IMPLEMENTATION_SELECTOR, ZERO_ADDRESS } from '../constants'
2
+ import { parseAddress } from '../decode'
3
+ import { ethCall, ethGetStorageAt } from '../rpc'
4
+ import type { RawProxy } from '../types'
5
+
6
+ /**
7
+ * Detect an EIP-1967 beacon proxy.
8
+ *
9
+ * Reads the beacon slot (`0xa3f0…750d`); if non-zero, calls `implementation()`
10
+ * on the beacon contract. Returns `null` when the slot is empty or the beacon
11
+ * call fails.
12
+ */
13
+ export async function detectEip1967Beacon(
14
+ rpc: string,
15
+ address: string,
16
+ fetchFn: typeof globalThis.fetch,
17
+ ): Promise<RawProxy | null> {
18
+ let beaconRaw: string
19
+ try {
20
+ beaconRaw = await ethGetStorageAt(rpc, address, EIP1967_BEACON_SLOT, fetchFn)
21
+ } catch {
22
+ return null
23
+ }
24
+
25
+ const beacon = parseAddress(beaconRaw)
26
+ if (!beacon || beacon === ZERO_ADDRESS) return null
27
+
28
+ let implRaw: string
29
+ try {
30
+ implRaw = await ethCall(rpc, beacon, IMPLEMENTATION_SELECTOR, fetchFn)
31
+ } catch {
32
+ return null
33
+ }
34
+
35
+ const impl = parseAddress(implRaw)
36
+ if (!impl || impl === ZERO_ADDRESS) return null
37
+
38
+ return {
39
+ pattern: 'eip-1967-beacon',
40
+ targets: [{ address: impl }],
41
+ beacon,
42
+ }
43
+ }
@@ -0,0 +1,44 @@
1
+ import { EIP1967_ADMIN_SLOT, EIP1967_IMPL_SLOT } from '../constants'
2
+ import { parseAddress } from '../decode'
3
+ import { ethGetStorageAt } from '../rpc'
4
+ import type { RawProxy } from '../types'
5
+
6
+ /**
7
+ * Detect an EIP-1967 transparent or UUPS proxy.
8
+ *
9
+ * Reads the implementation slot (`0x3608…3bc3`). If non-zero, reads the admin
10
+ * slot in parallel and attaches it when present.
11
+ *
12
+ * Returns `null` when the implementation slot is empty or malformed.
13
+ */
14
+ export async function detectEip1967(
15
+ rpc: string,
16
+ address: string,
17
+ fetchFn: typeof globalThis.fetch,
18
+ ): Promise<RawProxy | null> {
19
+ let implRaw: string
20
+ let adminRaw: string
21
+ try {
22
+ [implRaw, adminRaw] = await Promise.all([
23
+ ethGetStorageAt(rpc, address, EIP1967_IMPL_SLOT, fetchFn),
24
+ ethGetStorageAt(rpc, address, EIP1967_ADMIN_SLOT, fetchFn).catch(() => '0x'),
25
+ ])
26
+ } catch {
27
+ return null
28
+ }
29
+
30
+ const impl = parseAddress(implRaw)
31
+ if (!impl || impl === '0x0000000000000000000000000000000000000000') return null
32
+
33
+ const proxy: RawProxy = {
34
+ pattern: 'eip-1967',
35
+ targets: [{ address: impl }],
36
+ }
37
+
38
+ const admin = parseAddress(adminRaw)
39
+ if (admin && admin !== '0x0000000000000000000000000000000000000000') {
40
+ proxy.admin = admin
41
+ }
42
+
43
+ return proxy
44
+ }
@@ -0,0 +1,35 @@
1
+ import { IMPLEMENTATION_SELECTOR, ZERO_ADDRESS } from '../constants'
2
+ import { parseAddress } from '../decode'
3
+ import { ethCall } from '../rpc'
4
+ import type { RawProxy } from '../types'
5
+
6
+ /**
7
+ * Detect an EIP-897 delegate proxy by calling `implementation()` as a view
8
+ * function.
9
+ *
10
+ * Last-resort detector — any contract with a public `implementation()` view
11
+ * returning a non-zero address will match. Only reached when all other
12
+ * pattern detectors have returned `null`.
13
+ *
14
+ * Returns `null` when the call reverts or returns zero.
15
+ */
16
+ export async function detectEip897(
17
+ rpc: string,
18
+ address: string,
19
+ fetchFn: typeof globalThis.fetch,
20
+ ): Promise<RawProxy | null> {
21
+ let raw: string
22
+ try {
23
+ raw = await ethCall(rpc, address, IMPLEMENTATION_SELECTOR, fetchFn)
24
+ } catch {
25
+ return null
26
+ }
27
+
28
+ const impl = parseAddress(raw)
29
+ if (!impl || impl === ZERO_ADDRESS) return null
30
+
31
+ return {
32
+ pattern: 'eip-897',
33
+ targets: [{ address: impl }],
34
+ }
35
+ }
@@ -0,0 +1,37 @@
1
+ import { ZERO_ADDRESS } from '../constants'
2
+ import { parseAddress } from '../decode'
3
+ import { ethGetStorageAt } from '../rpc'
4
+ import type { RawProxy } from '../types'
5
+
6
+ const SAFE_SINGLETON_SLOT = '0x0000000000000000000000000000000000000000000000000000000000000000'
7
+
8
+ /**
9
+ * Detect a Gnosis Safe proxy — the singleton (implementation) address is
10
+ * stored at storage slot 0.
11
+ *
12
+ * Returns `null` when slot 0 is empty or not address-shaped.
13
+ *
14
+ * Cheap, but has a higher false-positive surface than the standard slots
15
+ * because any contract may use storage slot 0. Detector priority puts this
16
+ * after all EIP-standard patterns to keep the match rate clean.
17
+ */
18
+ export async function detectGnosisSafe(
19
+ rpc: string,
20
+ address: string,
21
+ fetchFn: typeof globalThis.fetch,
22
+ ): Promise<RawProxy | null> {
23
+ let raw: string
24
+ try {
25
+ raw = await ethGetStorageAt(rpc, address, SAFE_SINGLETON_SLOT, fetchFn)
26
+ } catch {
27
+ return null
28
+ }
29
+
30
+ const impl = parseAddress(raw)
31
+ if (!impl || impl === ZERO_ADDRESS) return null
32
+
33
+ return {
34
+ pattern: 'gnosis-safe',
35
+ targets: [{ address: impl }],
36
+ }
37
+ }