Skip to content

Commit d467b26

Browse files
Improve proxy resolver and rpc bacthing (#193)
* Detect proxy during getting abi and add proxy results caching * Add in memory cache layer and requets debuggign in web app * Fix custom error decoding * Update tests * Add fixes and changeset
1 parent ab40931 commit d467b26

25 files changed

+179
-192
lines changed

.changeset/fifty-months-cover.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@3loop/transaction-decoder': patch
3+
---
4+
5+
Improved perfomance of loading proxies by adding batching and cahching of request

apps/web/src/app/data.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,6 @@ export const supportedChains: {
130130
chainID: number
131131
rpcUrl: string
132132
traceAPI?: 'parity' | 'geth' | 'none'
133-
batchMaxCount?: number
134133
}[] = [
135134
{
136135
name: 'Ethereum Mainnet',
@@ -146,8 +145,7 @@ export const supportedChains: {
146145
name: 'Base mainnet',
147146
chainID: 8453,
148147
rpcUrl: process.env.BASE_RPC_URL as string,
149-
traceAPI: 'parity',
150-
batchMaxCount: 1,
148+
traceAPI: 'geth',
151149
},
152150
{
153151
name: 'Polygon Mainnet',

apps/web/src/lib/decode.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getProvider, RPCProviderLive } from './rpc-provider'
2-
import { Config, Effect, Layer, ManagedRuntime } from 'effect'
2+
import { Config, Effect, Layer, ManagedRuntime, Request } from 'effect'
33
import {
44
DecodedTransaction,
55
DecodeResult,
@@ -56,8 +56,10 @@ const MetaStoreLive = Layer.unwrapEffect(
5656
})
5757
}),
5858
)
59+
const CacheLayer = Layer.setRequestCache(Request.makeCache({ capacity: 100, timeToLive: '60 minutes' }))
5960
const DataLayer = Layer.mergeAll(RPCProviderLive, DatabaseLive)
6061
const LoadersLayer = Layer.mergeAll(AbiStoreLive, MetaStoreLive)
62+
6163
const MainLayer = Layer.provideMerge(LoadersLayer, DataLayer) as Layer.Layer<
6264
| AbiStore<AbiParams, ContractAbiResult>
6365
| ContractMetaStore<ContractMetaParams, ContractMetaResult>
@@ -68,7 +70,7 @@ const MainLayer = Layer.provideMerge(LoadersLayer, DataLayer) as Layer.Layer<
6870
never
6971
>
7072

71-
const runtime = ManagedRuntime.make(MainLayer)
73+
const runtime = ManagedRuntime.make(Layer.provide(MainLayer, CacheLayer))
7274

7375
export async function decodeTransaction({
7476
chainID,
@@ -80,10 +82,19 @@ export async function decodeTransaction({
8082
// NOTE: For unknonw reason the context of main layer is still missing the SqlClient in the type
8183
const runnable = decodeTransactionByHash(hash as Hex, chainID)
8284

83-
return runtime.runPromise(runnable).catch((error: unknown) => {
85+
const startTime = performance.now()
86+
87+
try {
88+
const result = await runtime.runPromise(runnable)
89+
const endTime = performance.now()
90+
console.log(`Decode transaction took ${endTime - startTime}ms`)
91+
return result
92+
} catch (error: unknown) {
93+
const endTime = performance.now()
8494
console.error('Decode error', JSON.stringify(error, null, 2))
95+
console.log(`Failed decode transaction took ${endTime - startTime}ms`)
8596
return undefined
86-
})
97+
}
8798
}
8899

89100
export async function decodeCalldata({

apps/web/src/lib/rpc-provider.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,39 @@ export function getProvider(chainID: number): PublicClientObject | null {
1616
if (url != null) {
1717
return {
1818
client: createPublicClient({
19-
transport: http(url),
19+
transport: http(url, {
20+
// Requests logging
21+
// onFetchRequest(request) {
22+
// const reader = request.body?.getReader()
23+
// if (!reader) {
24+
// return
25+
// }
26+
// let body = ''
27+
// reader
28+
// .read()
29+
// .then(function processText({ done, value }) {
30+
// if (done) {
31+
// return
32+
// }
33+
// // value for fetch streams is a Uint8Array
34+
// body += value
35+
// reader.read().then(processText)
36+
// })
37+
// .then(() => {
38+
// const json = JSON.parse(
39+
// body
40+
// .split(',')
41+
// .map((code) => String.fromCharCode(parseInt(code, 10)))
42+
// .join(''),
43+
// )
44+
// try {
45+
// console.log(JSON.stringify(json, null, 2))
46+
// } catch (e) {
47+
// console.log(json['id'], json['method'], body.length)
48+
// }
49+
// })
50+
// },
51+
}),
2052
}),
2153
config: {
2254
traceAPI: providerConfigs[chainID]?.traceAPI,

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

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

1718
export interface AbiParams {
1819
chainID: number
@@ -39,6 +40,7 @@ export interface ContractAbiEmpty {
3940
export type ContractAbiResult = ContractAbiSuccess | ContractAbiNotFound | ContractAbiEmpty
4041

4142
type ChainOrDefault = number | 'default'
43+
4244
export interface AbiStore<Key = AbiParams, Value = ContractAbiResult> {
4345
readonly strategies: Record<ChainOrDefault, readonly ContractAbiResolverStrategy[]>
4446
readonly set: (key: Key, value: Value) => Effect.Effect<void, never>

packages/transaction-decoder/src/contract-meta-loader.ts

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Context, Effect, RequestResolver, Request, Array, Either, pipe, Schema,
22
import { ContractData } from './types.js'
33
import { ContractMetaResolverStrategy, GetContractMetaStrategy } from './meta-strategy/request-model.js'
44
import { Address } from 'viem'
5+
import { ZERO_ADDRESS } from './decoding/constants.js'
56

67
export interface ContractMetaParams {
78
address: string
@@ -155,24 +156,28 @@ const ContractMetaLoaderRequestResolver = RequestResolver.makeBatched((requests:
155156
)
156157

157158
// Fetch ContractMeta from the strategies
158-
const strategyResults = yield* Effect.forEach(remaining, ({ chainID, address }) => {
159-
const allAvailableStrategies = Array.prependAll(strategies.default, strategies[chainID] ?? [])
160-
161-
// TODO: Distinct the errors and missing data, so we can retry on errors
162-
return Effect.validateFirst(allAvailableStrategies, (strategy) =>
163-
pipe(
164-
Effect.request(
165-
new GetContractMetaStrategy({
166-
address,
167-
chainId: chainID,
168-
strategyId: strategy.id,
169-
}),
170-
strategy.resolver,
159+
const strategyResults = yield* Effect.forEach(
160+
remaining,
161+
({ chainID, address }) => {
162+
const allAvailableStrategies = Array.prependAll(strategies.default, strategies[chainID] ?? [])
163+
164+
// TODO: Distinct the errors and missing data, so we can retry on errors
165+
return Effect.validateFirst(allAvailableStrategies, (strategy) =>
166+
pipe(
167+
Effect.request(
168+
new GetContractMetaStrategy({
169+
address,
170+
chainId: chainID,
171+
strategyId: strategy.id,
172+
}),
173+
strategy.resolver,
174+
),
175+
Effect.withRequestCaching(true),
171176
),
172-
Effect.withRequestCaching(true),
173-
),
174-
).pipe(Effect.orElseSucceed(() => null))
175-
})
177+
).pipe(Effect.orElseSucceed(() => null))
178+
},
179+
{ concurrency: 'unbounded', batching: true },
180+
)
176181

177182
// Store results and resolve pending requests
178183
yield* Effect.forEach(
@@ -197,6 +202,8 @@ export const getAndCacheContractMeta = ({
197202
readonly chainID: number
198203
readonly address: Address
199204
}) => {
205+
if (address === ZERO_ADDRESS) return Effect.succeed(null)
206+
200207
return Effect.withSpan(
201208
Effect.request(new ContractMetaLoader({ chainID, address }), ContractMetaLoaderRequestResolver),
202209
'GetAndCacheContractMeta',

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { Effect } from 'effect'
2-
import { isAddress, Hex, getAddress, encodeFunctionData, Address } from 'viem'
3-
import { getProxyStorageSlot } from './proxies.js'
2+
import { Hex, Address, encodeFunctionData, isAddress, getAddress } from 'viem'
43
import { AbiParams, AbiStore, ContractAbiResult, getAndCacheAbi, MissingABIError } from '../abi-loader.js'
54
import * as AbiDecoder from './abi-decode.js'
65
import { TreeNode } from '../types.js'
76
import { PublicClient, RPCFetchError, UnknownNetwork } from '../public-client.js'
87
import { SAFE_MULTISEND_ABI, SAFE_MULTISEND_SIGNATURE } from './constants.js'
8+
import { getProxyImplementation } from './proxies.js'
99

1010
const callDataKeys = ['callData', 'data', '_data']
1111
const addressKeys = ['to', 'target', '_target']
@@ -147,11 +147,12 @@ export const decodeMethod = ({
147147
}) =>
148148
Effect.gen(function* () {
149149
const signature = data.slice(0, 10)
150+
150151
let implementationAddress: Address | undefined
151152

152153
if (isAddress(contractAddress)) {
153154
//if contract is a proxy, get the implementation address
154-
const implementation = yield* getProxyStorageSlot({ address: getAddress(contractAddress), chainID })
155+
const implementation = yield* getProxyImplementation({ address: getAddress(contractAddress), chainID })
155156

156157
if (implementation) {
157158
implementationAddress = implementation.address

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@ export const SAFE_MULTISEND_ABI: Abi = [
3838
outputs: [],
3939
},
4040
]
41+
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'

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

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { type GetTransactionReturnType, type Log, decodeEventLog, getAbiItem, getAddress } from 'viem'
1+
import { Address, type GetTransactionReturnType, type Log, decodeEventLog, getAbiItem, getAddress } from 'viem'
22
import { Effect } from 'effect'
33
import type { DecodedLogEvent, Interaction, RawDecodedLog } from '../types.js'
4-
import { getProxyStorageSlot } from './proxies.js'
4+
import { getProxyImplementation } from './proxies.js'
55
import { getAndCacheAbi } from '../abi-loader.js'
66
import { getAndCacheContractMeta } from '../contract-meta-loader.js'
77
import * as AbiDecoder from './abi-decode.js'
@@ -22,14 +22,8 @@ const decodedLog = (transaction: GetTransactionReturnType, logItem: Log) =>
2222
const chainID = Number(transaction.chainId)
2323

2424
const address = getAddress(logItem.address)
25-
let abiAddress = address
26-
27-
const implementation = yield* getProxyStorageSlot({ address: getAddress(abiAddress), chainID })
28-
29-
if (implementation) {
30-
yield* Effect.logDebug(`Proxy implementation found for ${abiAddress} at ${implementation}`)
31-
abiAddress = implementation.address
32-
}
25+
const implementation = yield* getProxyImplementation({ address, chainID })
26+
const abiAddress = implementation?.address ?? address
3327

3428
const [abiItem, contractData] = yield* Effect.all(
3529
[

0 commit comments

Comments
 (0)