Skip to content

Commit b9d5c1f

Browse files
committed
Add eip1167 proxy support and get proxies list from bytecode using whatsabi
1 parent d467b26 commit b9d5c1f

File tree

8 files changed

+248
-59
lines changed

8 files changed

+248
-59
lines changed

apps/docs/src/content/docs/index.mdx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ import { Card, CardGrid, LinkCard } from '@astrojs/starlight/components'
2323
Fully written in TypeScript, can be used on both the client side and server side in JS applications.
2424
</Card>
2525
<Card title="Highly customizable" icon="puzzle">
26-
Provides a set of data loaders to simplify resolution of ABIs and other data required for decoding.
26+
Leverage plug-and-play data loaders for ABI and metadata resolution, and connect your own storage for metadata
27+
caching.
2728
</Card>
2829
<Card title="You only need an RPC" icon="rocket">
29-
Optional API providers can be used to fetch contract metadata or you can connect just your storage.
30+
The library uses standart JSON RPC methods to fetch transaction data.
3031
</Card>
3132
<Card title="Flexible Intepreters" icon="setting">
32-
Define any custom interpretation of EVM transactions.
33+
Define custom interpretations for EVM transactions or use the default ones.
3334
</Card>
3435
</CardGrid>
3536

packages/transaction-decoder/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,5 +93,8 @@
9393
"engines": {
9494
"node": ">=18.16"
9595
},
96-
"sideEffects": false
96+
"sideEffects": false,
97+
"dependencies": {
98+
"@shazow/whatsabi": "^0.18.0"
99+
}
97100
}

packages/transaction-decoder/src/abi-loader.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ import {
1212
SchemaAST,
1313
} from 'effect'
1414
import { ContractABI, ContractAbiResolverStrategy, GetContractABIStrategy } from './abi-strategy/request-model.js'
15-
import { Abi, getAddress } from 'viem'
16-
import { getProxyImplementation } from './decoding/proxies.js'
15+
import { Abi } from 'viem'
1716

1817
export interface AbiParams {
1918
chainID: number

packages/transaction-decoder/src/decoding/log-decode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Address, type GetTransactionReturnType, type Log, decodeEventLog, getAbiItem, getAddress } from 'viem'
1+
import { type GetTransactionReturnType, type Log, decodeEventLog, getAbiItem, getAddress } from 'viem'
22
import { Effect } from 'effect'
33
import type { DecodedLogEvent, Interaction, RawDecodedLog } from '../types.js'
44
import { getProxyImplementation } from './proxies.js'

packages/transaction-decoder/src/decoding/proxies.ts

Lines changed: 134 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
1-
import { Effect, PrimaryKey, Request, RequestResolver, Schedule, Schema, SchemaAST } from 'effect'
2-
3-
import { PublicClient, RPCCallError, RPCFetchError } from '../public-client.js'
4-
import { Address, Hex } from 'viem'
1+
import { Effect, Either, PrimaryKey, Request, RequestResolver, Schema, SchemaAST } from 'effect'
2+
import { PublicClient, RPCFetchError, UnknownNetwork } from '../public-client.js'
3+
import { Address, getAddress, Hex } from 'viem'
54
import { ProxyType } from '../types.js'
65
import { ZERO_ADDRESS } from './constants.js'
6+
import { whatsabi } from '@shazow/whatsabi'
77

88
interface StorageSlot {
99
type: ProxyType
1010
slot: Hex
1111
}
1212

13-
interface ProxyResult {
14-
type: ProxyType
13+
interface ProxyResult extends StorageSlot {
1514
address: Address
1615
}
1716

18-
const storageSlots: StorageSlot[] = [
17+
const knownStorageSlots: StorageSlot[] = [
18+
{ type: 'eip1167', slot: '0x' }, //EIP1167 minimal proxy
1919
{ type: 'eip1967', slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' }, //EIP1967
2020
{ type: 'zeppelin', slot: '0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3' }, //zeppelin
2121
{ type: 'safe', slot: '0xa619486e00000000000000000000000000000000000000000000000000000000' }, // gnosis Safe Proxy Factor 1.1.1
2222
]
2323

2424
const zeroSlot = '0x0000000000000000000000000000000000000000000000000000000000000000'
2525

26-
export interface GetProxy extends Request.Request<ProxyResult | undefined, RPCFetchError> {
26+
export interface GetProxy extends Request.Request<ProxyResult | undefined, RPCFetchError | UnknownNetwork> {
2727
readonly _tag: 'GetProxy'
2828
readonly address: Address
2929
readonly chainID: number
@@ -34,7 +34,7 @@ class SchemaAddress extends Schema.make<Address>(SchemaAST.stringKeyword) {}
3434
class SchemaProxy extends Schema.make<ProxyResult | undefined>(SchemaAST.objectKeyword) {}
3535

3636
class ProxyLoader extends Schema.TaggedRequest<ProxyLoader>()('ProxyLoader', {
37-
failure: Schema.instanceOf(RPCFetchError),
37+
failure: Schema.Union(Schema.instanceOf(RPCFetchError), Schema.instanceOf(UnknownNetwork)),
3838
success: Schema.NullOr(SchemaProxy),
3939
payload: {
4040
address: SchemaAddress,
@@ -56,7 +56,9 @@ const getStorageSlot = (request: ProxyLoader, slot: StorageSlot) =>
5656
address: request.address,
5757
slot: slot.slot,
5858
}),
59-
catch: () => new RPCFetchError('Get storage'),
59+
catch: (e) => {
60+
return new RPCFetchError(`Get storage error: ${(e as { details?: string }).details ?? ''}`)
61+
},
6062
})
6163
})
6264

@@ -72,19 +74,117 @@ const ethCall = (request: ProxyLoader, slot: StorageSlot) =>
7274
data: slot.slot,
7375
})
7476
)?.data,
75-
catch: () => new RPCCallError('Eth call'),
77+
catch: (e) => new RPCFetchError(`Eth call error: ${(e as { details?: string }).details ?? ''}`),
78+
})
79+
})
80+
81+
const ethGetCode = (request: ProxyLoader) =>
82+
Effect.gen(function* () {
83+
const service = yield* PublicClient
84+
const { client: publicClient } = yield* service.getPublicClient(request.chainID)
85+
return yield* Effect.tryPromise({
86+
try: () => publicClient.getCode({ address: request.address }),
87+
catch: (e) => new RPCFetchError(`Eth get code error: ${(e as { details?: string }).details ?? ''}`),
7688
})
7789
})
7890

91+
const getProxyTypeFromBytecode = (request: ProxyLoader, code: Hex) =>
92+
Effect.gen(function* () {
93+
const service = yield* PublicClient
94+
const { client: publicClient } = yield* service.getPublicClient(request.chainID)
95+
96+
//use whatsabi to only resolve proxies with a known bytecode
97+
const cachedCodeProvider = whatsabi.providers.WithCachedCode(publicClient, {
98+
[request.address]: code,
99+
})
100+
101+
const result = yield* Effect.tryPromise({
102+
try: () =>
103+
whatsabi.autoload(request.address, {
104+
provider: cachedCodeProvider,
105+
abiLoader: false, // Skip ABI loaders
106+
signatureLookup: false, // Skip looking up selector signatures
107+
}),
108+
catch: () => new RPCFetchError('Get proxy type from bytecode'),
109+
})
110+
111+
//if there are soeme proxies, return the list of them but with udpdated types
112+
if (result && result.proxies.length > 0) {
113+
const proxies: (ProxyResult | StorageSlot)[] = result.proxies
114+
.map((proxy) => {
115+
if (proxy.name === 'EIP1967Proxy') {
116+
return knownStorageSlots.find((slot) => slot.type === 'eip1967')
117+
}
118+
119+
if (proxy.name === 'GnosisSafeProxy') {
120+
return knownStorageSlots.find((slot) => slot.type === 'safe')
121+
}
122+
123+
if (proxy.name === 'ZeppelinOSProxy') {
124+
return knownStorageSlots.find((slot) => slot.type === 'zeppelin')
125+
}
126+
127+
if (proxy.name === 'FixedProxy') {
128+
const implementation = (proxy as any as { resolvedAddress: Address }).resolvedAddress
129+
130+
if (!implementation) return undefined
131+
132+
return {
133+
type: 'eip1167',
134+
address: getAddress(implementation),
135+
slot: '0x',
136+
} as ProxyResult
137+
}
138+
139+
return undefined
140+
})
141+
.filter(Boolean)
142+
.filter((proxy, index, self) => self.findIndex((p) => p?.type === proxy.type) === index)
143+
144+
return proxies
145+
}
146+
147+
return undefined
148+
})
149+
79150
export const GetProxyResolver = RequestResolver.fromEffect(
80-
(request: ProxyLoader): Effect.Effect<ProxyResult | undefined, RPCFetchError, PublicClient> =>
151+
(request: ProxyLoader): Effect.Effect<ProxyResult | undefined, RPCFetchError | UnknownNetwork, PublicClient> =>
81152
Effect.gen(function* () {
82153
// NOTE: Should we make this recursive when we have a Proxy of a Proxy?
83154

84-
const effects = storageSlots.map((slot) =>
85-
Effect.gen(function* () {
86-
const res: ProxyResult | undefined = { type: slot.type, address: '0x' }
155+
//Getting the bytecode of the address first
156+
const codeResult = yield* ethGetCode(request).pipe(Effect.either)
157+
158+
if (Either.isLeft(codeResult)) {
159+
yield* Effect.logError(`ProxyResolver error: ${JSON.stringify(codeResult.left)}`)
160+
return undefined
161+
}
162+
163+
const code = codeResult.right
87164

165+
//If code is empty and it is EOA, return empty result
166+
if (!code) return undefined
167+
168+
let proxySlots: StorageSlot[] | undefined
169+
170+
//Getting the proxies list from the bytecode
171+
const proxies = yield* getProxyTypeFromBytecode(request, code)
172+
if (proxies && proxies.length > 0) {
173+
//If it is EIP1167 proxy, return it becasue it is alredy resolved from the bytecode
174+
if (proxies.some((proxy) => proxy.type === 'eip1167')) {
175+
return proxies.find((proxy) => proxy.type === 'eip1167') as ProxyResult
176+
}
177+
178+
proxySlots = proxies as StorageSlot[]
179+
}
180+
181+
if (!proxySlots) {
182+
return undefined
183+
}
184+
185+
//get the implementation address by requesting the storage slot value of possible proxies
186+
const effects = (proxySlots ?? knownStorageSlots).map((slot) =>
187+
Effect.gen(function* () {
88188
let address: Hex | undefined
89189
switch (slot.type) {
90190
case 'eip1967':
@@ -100,21 +200,32 @@ export const GetProxyResolver = RequestResolver.fromEffect(
100200

101201
if (!address || address === zeroSlot) return undefined
102202

103-
res.address = ('0x' + address.slice(address.length - 40)) as Address
104-
return res
203+
return {
204+
type: slot.type,
205+
address: ('0x' + address.slice(address.length - 40)) as Address,
206+
slot: slot.slot,
207+
}
105208
}),
106209
)
107210

108-
const policy = Schedule.addDelay(
109-
Schedule.recurs(2), // Retry for a maximum of 2 times
110-
() => '100 millis', // Add a delay of 100 milliseconds between retries
111-
)
112211
const res = yield* Effect.all(effects, {
113212
concurrency: 'inherit',
114213
batching: 'inherit',
115-
}).pipe(Effect.retryOrElse(policy, () => Effect.succeed(undefined)))
214+
mode: 'either',
215+
})
216+
217+
const resRight = res
218+
.filter(Either.isRight)
219+
.map((r) => r.right)
220+
.find((x) => x != null)
221+
222+
const resLeft = res.filter(Either.isLeft).map((r) => r.left)
223+
224+
if (resLeft.length > 0) {
225+
yield* Effect.logError(`ProxyResolver error: ${resLeft.map((e) => JSON.stringify(e)).join(', ')}`)
226+
}
116227

117-
return res?.find((x) => x != null)
228+
return resRight
118229
}),
119230
).pipe(RequestResolver.contextFromEffect)
120231

packages/transaction-decoder/src/public-client.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,6 @@ export class RPCFetchError {
1111
constructor(readonly reason: unknown) {}
1212
}
1313

14-
export class RPCCallError {
15-
readonly _tag = 'EPCCallError'
16-
constructor(readonly reason: unknown) {}
17-
}
18-
1914
export interface PublicClientConfig {
2015
readonly traceAPI?: 'parity' | 'geth' | 'none'
2116
}

packages/transaction-decoder/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,4 @@ export interface Asset {
162162
tokenId?: string
163163
}
164164

165-
export type ProxyType = 'eip1967' | 'zeppelin' | 'safe'
165+
export type ProxyType = 'eip1967' | 'zeppelin' | 'safe' | 'eip1167'

0 commit comments

Comments
 (0)