Skip to content

Commit 87fa837

Browse files
authored
Fix timeout on abi strategy stops other strategies fetching (#164)
* Fix timeout on abi strategy stops other strategies fetching * Add experimental strategy to resolve erc20 abis
1 parent 2648619 commit 87fa837

File tree

8 files changed

+128
-68
lines changed

8 files changed

+128
-68
lines changed

.changeset/lucky-crabs-float.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+
Add expermiental erc20 abi strategy and change how sql strategy is defined

.changeset/thick-bottles-cry.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+
Fix timeout on strategy would stop fetching all strategies

apps/web/src/lib/decode.ts

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
import { getProvider, RPCProviderLive } from './rpc-provider'
2-
import { Effect, Layer, ManagedRuntime } from 'effect'
2+
import { Config, Effect, Layer, ManagedRuntime } from 'effect'
33
import {
44
DecodedTransaction,
55
DecodeResult,
66
decodeCalldata as calldataDecoder,
77
decodeTransactionByHash,
88
EtherscanV2StrategyResolver,
9-
FetchTransactionError,
9+
ExperimentalErc20AbiStrategyResolver,
1010
FourByteStrategyResolver,
1111
OpenchainStrategyResolver,
12-
RPCFetchError,
1312
SourcifyStrategyResolver,
14-
UnknownNetwork,
15-
UnsupportedEvent,
1613
AbiStore,
1714
AbiParams,
1815
ContractAbiResult,
1916
ContractMetaStore,
2017
ContractMetaParams,
2118
ContractMetaResult,
2219
PublicClient,
20+
ERC20RPCStrategyResolver,
21+
NFTRPCStrategyResolver,
22+
ProxyRPCStrategyResolver,
2323
} from '@3loop/transaction-decoder'
2424
import { SqlAbiStore, SqlContractMetaStore } from '@3loop/transaction-decoder/sql'
2525
import { Hex } from 'viem'
@@ -29,19 +29,33 @@ import { SqlClient } from '@effect/sql/SqlClient'
2929
import { ConfigError } from 'effect/ConfigError'
3030
import { SqlError } from '@effect/sql/SqlError'
3131

32-
const AbiStoreLive = SqlAbiStore.make({
33-
default: [
34-
EtherscanV2StrategyResolver({
35-
apikey: process.env.ETHERSCAN_API_KEY,
36-
}),
37-
SourcifyStrategyResolver(),
38-
OpenchainStrategyResolver(),
39-
FourByteStrategyResolver(),
40-
],
41-
})
32+
const AbiStoreLive = Layer.unwrapEffect(
33+
Effect.gen(function* () {
34+
const service = yield* PublicClient
35+
const apikey = yield* Config.withDefault(Config.string('ETHERSCAN_API_KEY'), undefined)
36+
return SqlAbiStore.make({
37+
default: [
38+
EtherscanV2StrategyResolver({
39+
apikey: apikey,
40+
}),
41+
ExperimentalErc20AbiStrategyResolver(service),
42+
OpenchainStrategyResolver(),
43+
SourcifyStrategyResolver(),
44+
FourByteStrategyResolver(),
45+
],
46+
})
47+
}),
48+
)
4249

43-
const MetaStoreLive = SqlContractMetaStore.make()
50+
const MetaStoreLive = Layer.unwrapEffect(
51+
Effect.gen(function* () {
52+
const service = yield* PublicClient
4453

54+
return SqlContractMetaStore.make({
55+
default: [ERC20RPCStrategyResolver(service), NFTRPCStrategyResolver(service), ProxyRPCStrategyResolver(service)],
56+
})
57+
}),
58+
)
4559
const DataLayer = Layer.mergeAll(RPCProviderLive, DatabaseLive)
4660
const LoadersLayer = Layer.mergeAll(AbiStoreLive, MetaStoreLive)
4761
const MainLayer = Layer.provideMerge(LoadersLayer, DataLayer) as Layer.Layer<

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

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
import { ContractABI, ContractAbiResolverStrategy, GetContractABIStrategy } from './abi-strategy/request-model.js'
1515
import { Abi } from 'viem'
1616

17-
const STRATEGY_TIMEOUT = 5000
1817
export interface AbiParams {
1918
chainID: number
2019
address: string
@@ -212,27 +211,30 @@ const AbiLoaderRequestResolver: Effect.Effect<
212211
)
213212

214213
// NOTE: Firstly we batch strategies by address because in a transaction most of events and traces are from the same abi
215-
const response = yield* Effect.forEach(remaining, (req) => {
216-
const strategyRequest = new GetContractABIStrategy({
217-
address: req.address,
218-
chainID: req.chainID,
219-
})
220-
221-
const allAvailableStrategies = Array.prependAll(strategies.default, strategies[req.chainID] ?? []).filter(
222-
(strategy) => strategy.type === 'address',
223-
)
214+
const response = yield* Effect.forEach(
215+
remaining,
216+
(req) => {
217+
const strategyRequest = new GetContractABIStrategy({
218+
address: req.address,
219+
chainID: req.chainID,
220+
})
221+
222+
const allAvailableStrategies = Array.prependAll(strategies.default, strategies[req.chainID] ?? []).filter(
223+
(strategy) => strategy.type === 'address',
224+
)
224225

225-
return Effect.validateFirst(allAvailableStrategies, (strategy) =>
226-
pipe(
227-
Effect.request(strategyRequest, strategy.resolver),
228-
Effect.withRequestCaching(true),
229-
Effect.timeout(STRATEGY_TIMEOUT),
230-
),
231-
).pipe(
232-
Effect.map(Either.left),
233-
Effect.orElseSucceed(() => Either.right(req)),
234-
)
235-
})
226+
return Effect.validateFirst(allAvailableStrategies, (strategy) => {
227+
return pipe(Effect.request(strategyRequest, strategy.resolver), Effect.withRequestCaching(true))
228+
}).pipe(
229+
Effect.map(Either.left),
230+
Effect.orElseSucceed(() => Either.right(req)),
231+
)
232+
},
233+
{
234+
concurrency: 'unbounded',
235+
batching: true,
236+
},
237+
)
236238

237239
const [addressStrategyResults, notFound] = Array.partitionMap(response, (res) => res)
238240

@@ -251,11 +253,7 @@ const AbiLoaderRequestResolver: Effect.Effect<
251253

252254
// TODO: Distinct the errors and missing data, so we can retry on errors
253255
return Effect.validateFirst(allAvailableStrategies, (strategy) =>
254-
pipe(
255-
Effect.request(strategyRequest, strategy.resolver),
256-
Effect.withRequestCaching(true),
257-
Effect.timeout(STRATEGY_TIMEOUT),
258-
),
256+
pipe(Effect.request(strategyRequest, strategy.resolver), Effect.withRequestCaching(true)),
259257
).pipe(Effect.orElseSucceed(() => null))
260258
})
261259

@@ -276,7 +274,11 @@ const AbiLoaderRequestResolver: Effect.Effect<
276274
Effect.forEach(group, (req) => Request.completeEffect(req, result), { discard: true }),
277275
)
278276
},
279-
{ discard: true },
277+
{
278+
discard: true,
279+
concurrency: 'unbounded',
280+
batching: true,
281+
},
280282
)
281283
}),
282284
).pipe(RequestResolver.contextFromServices(AbiStore), Effect.withRequestCaching(true))
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import * as RequestModel from './request-model.js'
2+
import { Effect, RequestResolver } from 'effect'
3+
import { PublicClient } from '../public-client.js'
4+
import { erc20Abi, getAddress, getContract } from 'viem'
5+
6+
const getLocalFragments = (service: PublicClient, { address, chainID }: RequestModel.GetContractABIStrategy) =>
7+
Effect.gen(function* () {
8+
const client = yield* service
9+
.getPublicClient(chainID)
10+
.pipe(
11+
Effect.catchAll(() =>
12+
Effect.fail(new RequestModel.ResolveStrategyABIError('local-strategy', address, chainID)),
13+
),
14+
)
15+
16+
const inst = getContract({
17+
abi: erc20Abi,
18+
address: getAddress(address),
19+
client: client.client,
20+
})
21+
22+
const decimals = yield* Effect.tryPromise({
23+
try: () => inst.read.decimals(),
24+
catch: () => new RequestModel.ResolveStrategyABIError('local-strategy', address, chainID),
25+
})
26+
27+
if (decimals != null) {
28+
return [
29+
{
30+
type: 'address',
31+
address,
32+
chainID,
33+
abi: JSON.stringify(erc20Abi),
34+
},
35+
] as RequestModel.ContractABI[]
36+
}
37+
38+
return yield* Effect.fail(new RequestModel.ResolveStrategyABIError('local-strategy', address, chainID))
39+
})
40+
41+
export const ExperimentalErc20AbiStrategyResolver = (
42+
service: PublicClient,
43+
): RequestModel.ContractAbiResolverStrategy => {
44+
return {
45+
type: 'address',
46+
resolver: RequestResolver.fromEffect((req: RequestModel.GetContractABIStrategy) =>
47+
Effect.withSpan(getLocalFragments(service, req), 'AbiStrategy.ExperimentalErc20AbiStrategyResolver', {
48+
attributes: { chainID: req.chainID, address: req.address },
49+
}),
50+
),
51+
}
52+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './blockscout-abi.js'
22
export * from './etherscan-abi.js'
33
export * from './etherscanv2-abi.js'
4+
export * from './experimental-erc20.js'
45
export * from './fourbyte-abi.js'
56
export * from './openchain-abi.js'
67
export * from './request-model.js'

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import { ContractData } from './types.js'
33
import { GetContractMetaStrategy } from './meta-strategy/request-model.js'
44
import { Address } from 'viem'
55

6-
const STRATEGY_TIMEOUT = 5000
7-
86
export interface ContractMetaParams {
97
address: string
108
chainID: number
@@ -166,10 +164,9 @@ const ContractMetaLoaderRequestResolver = RequestResolver.makeBatched((requests:
166164
const allAvailableStrategies = Array.prependAll(strategies.default, strategies[chainID] ?? [])
167165

168166
// TODO: Distinct the errors and missing data, so we can retry on errors
169-
return Effect.validateFirst(allAvailableStrategies, (strategy) => Effect.request(strategyRequest, strategy)).pipe(
170-
Effect.timeout(STRATEGY_TIMEOUT),
171-
Effect.orElseSucceed(() => null),
172-
)
167+
return Effect.validateFirst(allAvailableStrategies, (strategy) =>
168+
pipe(Effect.request(strategyRequest, strategy), Effect.withRequestCaching(true)),
169+
).pipe(Effect.orElseSucceed(() => null))
173170
})
174171

175172
// Store results and resolve pending requests

packages/transaction-decoder/src/sql/contract-meta-store.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,14 @@
11
import { SqlClient } from '@effect/sql'
22
import { Effect, Layer } from 'effect'
3-
import {
4-
ContractData,
5-
ContractMetaStore,
6-
ERC20RPCStrategyResolver,
7-
NFTRPCStrategyResolver,
8-
ProxyRPCStrategyResolver,
9-
PublicClient,
10-
} from '../effect.js'
3+
import { ContractData, ContractMetaStore } from '../effect.js'
114

12-
export const make = () =>
5+
export const make = (strategies: ContractMetaStore['strategies']) =>
136
Layer.effect(
147
ContractMetaStore,
158
Effect.gen(function* () {
169
const sql = yield* SqlClient.SqlClient
17-
const publicClient = yield* PublicClient
18-
1910
const table = sql('_loop_decoder_contract_meta_')
2011

21-
// TODO; add timestamp to the table
2212
yield* sql`
2313
CREATE TABLE IF NOT EXISTS ${table} (
2414
address TEXT NOT NULL,
@@ -37,13 +27,7 @@ export const make = () =>
3727
)
3828

3929
return ContractMetaStore.of({
40-
strategies: {
41-
default: [
42-
ERC20RPCStrategyResolver(publicClient),
43-
NFTRPCStrategyResolver(publicClient),
44-
ProxyRPCStrategyResolver(publicClient),
45-
],
46-
},
30+
strategies,
4731
set: (key, value) =>
4832
Effect.gen(function* () {
4933
if (value.status === 'success') {

0 commit comments

Comments
 (0)