Skip to content

Commit 32af229

Browse files
authored
Separate the Abi Resolvers by fragment and full address abi (#80)
1 parent 215e045 commit 32af229

File tree

9 files changed

+143
-76
lines changed

9 files changed

+143
-76
lines changed

.changeset/proud-ghosts-behave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@3loop/transaction-decoder': minor
3+
---
4+
5+
Separate ABI resolvers into fragment and full address abi

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

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Context, Effect, Either, RequestResolver, Request, Array, pipe } from 'effect'
2-
import { ContractABI, GetContractABIStrategy } from './abi-strategy/request-model.js'
2+
import { ContractABI, ContractAbiResolverStrategy, GetContractABIStrategy } from './abi-strategy/request-model.js'
33

44
const STRATEGY_TIMEOUT = 5000
55
export interface AbiParams {
@@ -28,7 +28,7 @@ export type ContractAbiResult = ContractAbiSuccess | ContractAbiNotFound | Contr
2828

2929
type ChainOrDefault = number | 'default'
3030
export interface AbiStore<Key = AbiParams, Value = ContractAbiResult> {
31-
readonly strategies: Record<ChainOrDefault, readonly RequestResolver.RequestResolver<GetContractABIStrategy>[]>
31+
readonly strategies: Record<ChainOrDefault, readonly ContractAbiResolverStrategy[]>
3232
readonly set: (key: Key, value: Value) => Effect.Effect<void, never>
3333
readonly get: (arg: Key) => Effect.Effect<Value, never>
3434
readonly getMany?: (arg: Array<Key>) => Effect.Effect<Array<Value>, never>
@@ -46,7 +46,7 @@ export interface AbiLoader extends Request.Request<string | null, unknown> {
4646

4747
const AbiLoader = Request.tagged<AbiLoader>('AbiLoader')
4848

49-
function makeKey(key: AbiLoader) {
49+
function makeRequestKey(key: AbiLoader) {
5050
return `abi::${key.chainID}:${key.address}:${key.event}:${key.signature}`
5151
}
5252

@@ -121,7 +121,7 @@ const getBestMatch = (abi: ContractABI | null) => {
121121
* To optimize concurrent requests, the AbiLoader uses the RequestResolver
122122
* to batch and cache requests. However, out-of-the-box, the RequestResolver does not
123123
* perform request deduplication. To address this, we implement request deduplication
124-
* inside the resolver's body. We use the `makeKey` function to generate a unique key
124+
* inside the resolver's body. We use the `makeRequestKey` function to generate a unique key
125125
* for each request and group them by that key. We then load the ABI for the unique
126126
* requests and resolve the pending requests in a group with the same result.
127127
*
@@ -133,12 +133,11 @@ const AbiLoaderRequestResolver = RequestResolver.makeBatched((requests: Array<Ab
133133
if (requests.length === 0) return
134134

135135
const { strategies } = yield* AbiStore
136-
// NOTE: We can further optimize if we have match by Address by avoid extra requests for each signature
137-
// but might need to update the Loader public API
138-
const groups = Array.groupBy(requests, makeKey)
139-
const uniqueRequests = Object.values(groups).map((group) => group[0])
140136

141-
const [remaining, results] = yield* pipe(
137+
const requestGroups = Array.groupBy(requests, makeRequestKey)
138+
const uniqueRequests = Object.values(requestGroups).map((group) => group[0])
139+
140+
const [remaining, cachedResults] = yield* pipe(
142141
getMany(uniqueRequests),
143142
Effect.map(
144143
Array.partitionMap((resp, i) => {
@@ -152,9 +151,9 @@ const AbiLoaderRequestResolver = RequestResolver.makeBatched((requests: Array<Ab
152151

153152
// Resolve ABI from the store
154153
yield* Effect.forEach(
155-
results,
154+
cachedResults,
156155
([request, result]) => {
157-
const group = groups[makeKey(request)]
156+
const group = requestGroups[makeRequestKey(request)]
158157
const abi = result?.abi ?? null
159158
return Effect.forEach(group, (req) => Request.succeed(req, abi), { discard: true })
160159
},
@@ -163,32 +162,64 @@ const AbiLoaderRequestResolver = RequestResolver.makeBatched((requests: Array<Ab
163162
},
164163
)
165164

166-
// Load the ABI from the strategies
167-
const strategyResults = yield* Effect.forEach(remaining, ({ chainID, address, event, signature }) => {
165+
// NOTE: Firstly we batch strategies by address because in a transaction most of events and traces are from the same abi
166+
const response = yield* Effect.forEach(remaining, (req) => {
167+
const strategyRequest = GetContractABIStrategy({
168+
address: req.address,
169+
chainID: req.chainID,
170+
})
171+
172+
const allAvailableStrategies = Array.prependAll(strategies.default, strategies[req.chainID] ?? []).filter(
173+
(strategy) => strategy.type === 'address',
174+
)
175+
176+
return Effect.validateFirst(allAvailableStrategies, (strategy) =>
177+
pipe(
178+
Effect.request(strategyRequest, strategy.resolver),
179+
Effect.withRequestCaching(true),
180+
Effect.timeout(STRATEGY_TIMEOUT),
181+
),
182+
).pipe(
183+
Effect.map(Either.left),
184+
Effect.orElseSucceed(() => Either.right(req)),
185+
)
186+
})
187+
188+
const [addressStrategyResults, notFound] = Array.partitionMap(response, (res) => res)
189+
190+
// NOTE: Secondly we request strategies to fetch fragments
191+
const fragmentStrategyResults = yield* Effect.forEach(notFound, ({ chainID, address, event, signature }) => {
168192
const strategyRequest = GetContractABIStrategy({
169193
address,
194+
chainID,
170195
event,
171196
signature,
172-
chainID,
173197
})
174198

175-
const allAvailableStrategies = Array.prependAll(strategies.default, strategies[chainID] ?? [])
199+
const allAvailableStrategies = Array.prependAll(strategies.default, strategies[chainID] ?? []).filter(
200+
(strategy) => strategy.type === 'fragment',
201+
)
176202

177203
// TODO: Distinct the errors and missing data, so we can retry on errors
178-
return Effect.validateFirst(allAvailableStrategies, (strategy) => Effect.request(strategyRequest, strategy)).pipe(
179-
Effect.timeout(STRATEGY_TIMEOUT),
180-
Effect.orElseSucceed(() => null),
181-
)
204+
return Effect.validateFirst(allAvailableStrategies, (strategy) =>
205+
pipe(
206+
Effect.request(strategyRequest, strategy.resolver),
207+
Effect.withRequestCaching(true),
208+
Effect.timeout(STRATEGY_TIMEOUT),
209+
),
210+
).pipe(Effect.orElseSucceed(() => null))
182211
})
183212

213+
const strategyResults = Array.appendAll(addressStrategyResults, fragmentStrategyResults)
214+
184215
// Store results and resolve pending requests
185216
yield* Effect.forEach(
186217
strategyResults,
187218
(abi, i) => {
188219
const request = remaining[i]
189220
const result = getBestMatch(abi)
190221

191-
const group = groups[makeKey(request)]
222+
const group = requestGroups[makeRequestKey(request)]
192223

193224
return Effect.zipRight(
194225
setValue(request, abi),

packages/transaction-decoder/src/abi-strategy/blockscout-abi.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,21 @@ async function fetchContractABI(
3434
throw new Error(`Failed to fetch ABI for ${address} on chain ${chainID}`)
3535
}
3636

37-
export const BlockscoutStrategyResolver = (config: { apikey?: string; endpoint: string }) =>
38-
RequestResolver.fromEffect((req: RequestModel.GetContractABIStrategy) =>
39-
Effect.withSpan(
40-
Effect.tryPromise({
41-
try: () => fetchContractABI(req, config),
42-
catch: () => new RequestModel.ResolveStrategyABIError('Blockscout', req.address, req.chainID),
43-
}),
44-
'AbiStrategy.BlockscoutStrategyResolver',
45-
{ attributes: { chainID: req.chainID, address: req.address } },
37+
export const BlockscoutStrategyResolver = (config: {
38+
apikey?: string
39+
endpoint: string
40+
}): RequestModel.ContractAbiResolverStrategy => {
41+
return {
42+
type: 'address',
43+
resolver: RequestResolver.fromEffect((req: RequestModel.GetContractABIStrategy) =>
44+
Effect.withSpan(
45+
Effect.tryPromise({
46+
try: () => fetchContractABI(req, config),
47+
catch: () => new RequestModel.ResolveStrategyABIError('Blockscout', req.address, req.chainID),
48+
}),
49+
'AbiStrategy.BlockscoutStrategyResolver',
50+
{ attributes: { chainID: req.chainID, address: req.address } },
51+
),
4652
),
47-
)
53+
}
54+
}

packages/transaction-decoder/src/abi-strategy/etherscan-abi.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ async function fetchContractABI(
5252
config?: { apikey?: string; endpoint?: string },
5353
): Promise<RequestModel.ContractABI> {
5454
const endpoint = config?.endpoint ?? endpoints[chainID]
55-
55+
console.log('fetchContractABI', address)
5656
const params: Record<string, string> = {
5757
module: 'contract',
5858
action: 'getabi',
@@ -80,14 +80,21 @@ async function fetchContractABI(
8080
throw new Error(`Failed to fetch ABI for ${address} on chain ${chainID}`)
8181
}
8282

83-
export const EtherscanStrategyResolver = (config?: { apikey?: string; endpoint?: string }) =>
84-
RequestResolver.fromEffect((req: RequestModel.GetContractABIStrategy) =>
85-
Effect.withSpan(
86-
Effect.tryPromise({
87-
try: () => fetchContractABI(req, config),
88-
catch: () => new RequestModel.ResolveStrategyABIError('etherscan', req.address, req.chainID),
89-
}),
90-
'AbiStrategy.EtherscanStrategyResolver',
91-
{ attributes: { chainID: req.chainID, address: req.address } },
83+
export const EtherscanStrategyResolver = (config?: {
84+
apikey?: string
85+
endpoint?: string
86+
}): RequestModel.ContractAbiResolverStrategy => {
87+
return {
88+
type: 'address',
89+
resolver: RequestResolver.fromEffect((req: RequestModel.GetContractABIStrategy) =>
90+
Effect.withSpan(
91+
Effect.tryPromise({
92+
try: () => fetchContractABI(req, config),
93+
catch: () => new RequestModel.ResolveStrategyABIError('etherscan', req.address, req.chainID),
94+
}),
95+
'AbiStrategy.EtherscanStrategyResolver',
96+
{ attributes: { chainID: req.chainID, address: req.address } },
97+
),
9298
),
93-
)
99+
}
100+
}

packages/transaction-decoder/src/abi-strategy/fourbyte-abi.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,18 @@ async function fetchABI({
5858
throw new Error(`Failed to fetch ABI for ${address} on chain ${chainID}`)
5959
}
6060

61-
export const FourByteStrategyResolver = () =>
62-
RequestResolver.fromEffect((req: RequestModel.GetContractABIStrategy) =>
63-
Effect.withSpan(
64-
Effect.tryPromise({
65-
try: () => fetchABI(req),
66-
catch: () => new RequestModel.ResolveStrategyABIError('4byte.directory', req.address, req.chainID),
67-
}),
68-
'AbiStrategy.FourByteStrategyResolver',
69-
{ attributes: { chainID: req.chainID, address: req.address } },
61+
export const FourByteStrategyResolver = (): RequestModel.ContractAbiResolverStrategy => {
62+
return {
63+
type: 'fragment',
64+
resolver: RequestResolver.fromEffect((req: RequestModel.GetContractABIStrategy) =>
65+
Effect.withSpan(
66+
Effect.tryPromise({
67+
try: () => fetchABI(req),
68+
catch: () => new RequestModel.ResolveStrategyABIError('4byte.directory', req.address, req.chainID),
69+
}),
70+
'AbiStrategy.FourByteStrategyResolver',
71+
{ attributes: { chainID: req.chainID, address: req.address } },
72+
),
7073
),
71-
)
74+
}
75+
}

packages/transaction-decoder/src/abi-strategy/openchain-abi.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,18 @@ async function fetchABI({
7676
throw new Error(`Failed to fetch ABI for ${address} on chain ${chainID}`)
7777
}
7878

79-
export const OpenchainStrategyResolver = () =>
80-
RequestResolver.fromEffect((req: RequestModel.GetContractABIStrategy) =>
81-
Effect.withSpan(
82-
Effect.tryPromise({
83-
try: () => fetchABI(req),
84-
catch: () => new RequestModel.ResolveStrategyABIError('openchain', req.address, req.chainID),
85-
}),
86-
'AbiStrategy.OpenchainStrategyResolver',
87-
{ attributes: { chainID: req.chainID, address: req.address } },
79+
export const OpenchainStrategyResolver = (): RequestModel.ContractAbiResolverStrategy => {
80+
return {
81+
type: 'fragment',
82+
resolver: RequestResolver.fromEffect((req: RequestModel.GetContractABIStrategy) =>
83+
Effect.withSpan(
84+
Effect.tryPromise({
85+
try: () => fetchABI(req),
86+
catch: () => new RequestModel.ResolveStrategyABIError('openchain', req.address, req.chainID),
87+
}),
88+
'AbiStrategy.OpenchainStrategyResolver',
89+
{ attributes: { chainID: req.chainID, address: req.address } },
90+
),
8891
),
89-
)
92+
}
93+
}

packages/transaction-decoder/src/abi-strategy/request-model.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Request } from 'effect'
1+
import { Request, RequestResolver } from 'effect'
22

33
export interface FetchABIParams {
44
readonly chainID: number
@@ -47,3 +47,8 @@ export interface GetContractABIStrategy extends Request.Request<ContractABI, Res
4747
}
4848

4949
export const GetContractABIStrategy = Request.tagged<GetContractABIStrategy>('GetContractABIStrategy')
50+
51+
export interface ContractAbiResolverStrategy {
52+
type: 'address' | 'fragment'
53+
resolver: RequestResolver.RequestResolver<GetContractABIStrategy, never>
54+
}

packages/transaction-decoder/src/abi-strategy/sourcify-abi.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,18 @@ async function fetchContractABI({
4343
throw new Error(`Failed to fetch ABI for ${address} on chain ${chainID}`)
4444
}
4545

46-
export const SourcifyStrategyResolver = () =>
47-
RequestResolver.fromEffect((req: RequestModel.GetContractABIStrategy) =>
48-
Effect.withSpan(
49-
Effect.tryPromise({
50-
try: () => fetchContractABI(req),
51-
catch: () => new RequestModel.ResolveStrategyABIError('sourcify', req.address, req.chainID),
52-
}),
53-
'AbiStrategy.SourcifyStrategyResolver',
54-
{ attributes: { chainID: req.chainID, address: req.address } },
46+
export const SourcifyStrategyResolver = (): RequestModel.ContractAbiResolverStrategy => {
47+
return {
48+
type: 'address',
49+
resolver: RequestResolver.fromEffect((req: RequestModel.GetContractABIStrategy) =>
50+
Effect.withSpan(
51+
Effect.tryPromise({
52+
try: () => fetchContractABI(req),
53+
catch: () => new RequestModel.ResolveStrategyABIError('sourcify', req.address, req.chainID),
54+
}),
55+
'AbiStrategy.SourcifyStrategyResolver',
56+
{ attributes: { chainID: req.chainID, address: req.address } },
57+
),
5558
),
56-
)
59+
}
60+
}

packages/transaction-decoder/src/vanilla.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import {
77
ContractMetaResult,
88
ContractMetaStore as EffectContractMetaStore,
99
} from './contract-meta-loader.js'
10-
import { GetContractABIStrategy } from './abi-strategy/index.js'
11-
import { Hex } from 'viem'
12-
import { GetContractMetaStrategy } from './meta-strategy/request-model.js'
10+
import type { ContractAbiResolverStrategy } from './abi-strategy/index.js'
11+
import type { Hex } from 'viem'
12+
import type { GetContractMetaStrategy } from './meta-strategy/request-model.js'
1313

1414
export interface TransactionDecoderOptions {
1515
getPublicClient: (chainID: number) => PublicClientObject | undefined
@@ -19,7 +19,7 @@ export interface TransactionDecoderOptions {
1919
}
2020

2121
export interface VanillaAbiStore {
22-
strategies?: readonly RequestResolver.RequestResolver<GetContractABIStrategy>[]
22+
strategies?: readonly ContractAbiResolverStrategy[]
2323
get: (key: AbiParams) => Promise<ContractAbiResult>
2424
set: (key: AbiParams, val: ContractAbiResult) => Promise<void>
2525
}

0 commit comments

Comments
 (0)