Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lucky-crabs-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@3loop/transaction-decoder': minor
---

Add expermiental erc20 abi strategy and change how sql strategy is defined
5 changes: 5 additions & 0 deletions .changeset/thick-bottles-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@3loop/transaction-decoder': patch
---

Fix timeout on strategy would stop fetching all strategies
46 changes: 30 additions & 16 deletions apps/web/src/lib/decode.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { getProvider, RPCProviderLive } from './rpc-provider'
import { Effect, Layer, ManagedRuntime } from 'effect'
import { Config, Effect, Layer, ManagedRuntime } from 'effect'
import {
DecodedTransaction,
DecodeResult,
decodeCalldata as calldataDecoder,
decodeTransactionByHash,
EtherscanV2StrategyResolver,
FetchTransactionError,
ExperimentalErc20AbiStrategyResolver,
FourByteStrategyResolver,
OpenchainStrategyResolver,
RPCFetchError,
SourcifyStrategyResolver,
UnknownNetwork,
UnsupportedEvent,
AbiStore,
AbiParams,
ContractAbiResult,
ContractMetaStore,
ContractMetaParams,
ContractMetaResult,
PublicClient,
ERC20RPCStrategyResolver,
NFTRPCStrategyResolver,
ProxyRPCStrategyResolver,
} from '@3loop/transaction-decoder'
import { SqlAbiStore, SqlContractMetaStore } from '@3loop/transaction-decoder/sql'
import { Hex } from 'viem'
Expand All @@ -29,19 +29,33 @@ import { SqlClient } from '@effect/sql/SqlClient'
import { ConfigError } from 'effect/ConfigError'
import { SqlError } from '@effect/sql/SqlError'

const AbiStoreLive = SqlAbiStore.make({
default: [
EtherscanV2StrategyResolver({
apikey: process.env.ETHERSCAN_API_KEY,
}),
SourcifyStrategyResolver(),
OpenchainStrategyResolver(),
FourByteStrategyResolver(),
],
})
const AbiStoreLive = Layer.unwrapEffect(
Effect.gen(function* () {
const service = yield* PublicClient
const apikey = yield* Config.withDefault(Config.string('ETHERSCAN_API_KEY'), undefined)
return SqlAbiStore.make({
default: [
EtherscanV2StrategyResolver({
apikey: apikey,
}),
ExperimentalErc20AbiStrategyResolver(service),
OpenchainStrategyResolver(),
SourcifyStrategyResolver(),
FourByteStrategyResolver(),
],
})
}),
)

const MetaStoreLive = SqlContractMetaStore.make()
const MetaStoreLive = Layer.unwrapEffect(
Effect.gen(function* () {
const service = yield* PublicClient

return SqlContractMetaStore.make({
default: [ERC20RPCStrategyResolver(service), NFTRPCStrategyResolver(service), ProxyRPCStrategyResolver(service)],
})
}),
)
const DataLayer = Layer.mergeAll(RPCProviderLive, DatabaseLive)
const LoadersLayer = Layer.mergeAll(AbiStoreLive, MetaStoreLive)
const MainLayer = Layer.provideMerge(LoadersLayer, DataLayer) as Layer.Layer<
Expand Down
56 changes: 29 additions & 27 deletions packages/transaction-decoder/src/abi-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
import { ContractABI, ContractAbiResolverStrategy, GetContractABIStrategy } from './abi-strategy/request-model.js'
import { Abi } from 'viem'

const STRATEGY_TIMEOUT = 5000
export interface AbiParams {
chainID: number
address: string
Expand Down Expand Up @@ -212,27 +211,30 @@ const AbiLoaderRequestResolver: Effect.Effect<
)

// NOTE: Firstly we batch strategies by address because in a transaction most of events and traces are from the same abi
const response = yield* Effect.forEach(remaining, (req) => {
const strategyRequest = new GetContractABIStrategy({
address: req.address,
chainID: req.chainID,
})

const allAvailableStrategies = Array.prependAll(strategies.default, strategies[req.chainID] ?? []).filter(
(strategy) => strategy.type === 'address',
)
const response = yield* Effect.forEach(
remaining,
(req) => {
const strategyRequest = new GetContractABIStrategy({
address: req.address,
chainID: req.chainID,
})

const allAvailableStrategies = Array.prependAll(strategies.default, strategies[req.chainID] ?? []).filter(
(strategy) => strategy.type === 'address',
)

return Effect.validateFirst(allAvailableStrategies, (strategy) =>
pipe(
Effect.request(strategyRequest, strategy.resolver),
Effect.withRequestCaching(true),
Effect.timeout(STRATEGY_TIMEOUT),
),
).pipe(
Effect.map(Either.left),
Effect.orElseSucceed(() => Either.right(req)),
)
})
return Effect.validateFirst(allAvailableStrategies, (strategy) => {
return pipe(Effect.request(strategyRequest, strategy.resolver), Effect.withRequestCaching(true))
}).pipe(
Effect.map(Either.left),
Effect.orElseSucceed(() => Either.right(req)),
)
},
{
concurrency: 'unbounded',
batching: true,
},
)

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

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

// TODO: Distinct the errors and missing data, so we can retry on errors
return Effect.validateFirst(allAvailableStrategies, (strategy) =>
pipe(
Effect.request(strategyRequest, strategy.resolver),
Effect.withRequestCaching(true),
Effect.timeout(STRATEGY_TIMEOUT),
),
pipe(Effect.request(strategyRequest, strategy.resolver), Effect.withRequestCaching(true)),
).pipe(Effect.orElseSucceed(() => null))
})

Expand All @@ -276,7 +274,11 @@ const AbiLoaderRequestResolver: Effect.Effect<
Effect.forEach(group, (req) => Request.completeEffect(req, result), { discard: true }),
)
},
{ discard: true },
{
discard: true,
concurrency: 'unbounded',
batching: true,
},
)
}),
).pipe(RequestResolver.contextFromServices(AbiStore), Effect.withRequestCaching(true))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as RequestModel from './request-model.js'
import { Effect, RequestResolver } from 'effect'
import { PublicClient } from '../public-client.js'
import { erc20Abi, getAddress, getContract } from 'viem'

const getLocalFragments = (service: PublicClient, { address, chainID }: RequestModel.GetContractABIStrategy) =>
Effect.gen(function* () {
const client = yield* service
.getPublicClient(chainID)
.pipe(
Effect.catchAll(() =>
Effect.fail(new RequestModel.ResolveStrategyABIError('local-strategy', address, chainID)),
),
)

const inst = getContract({
abi: erc20Abi,
address: getAddress(address),
client: client.client,
})

const decimals = yield* Effect.tryPromise({
try: () => inst.read.decimals(),
catch: () => new RequestModel.ResolveStrategyABIError('local-strategy', address, chainID),
})

if (decimals != null) {
return [
{
type: 'address',
address,
chainID,
abi: JSON.stringify(erc20Abi),
},
] as RequestModel.ContractABI[]
}

return yield* Effect.fail(new RequestModel.ResolveStrategyABIError('local-strategy', address, chainID))
})

export const ExperimentalErc20AbiStrategyResolver = (
service: PublicClient,
): RequestModel.ContractAbiResolverStrategy => {
return {
type: 'address',
resolver: RequestResolver.fromEffect((req: RequestModel.GetContractABIStrategy) =>
Effect.withSpan(getLocalFragments(service, req), 'AbiStrategy.ExperimentalErc20AbiStrategyResolver', {
attributes: { chainID: req.chainID, address: req.address },
}),
),
}
}
1 change: 1 addition & 0 deletions packages/transaction-decoder/src/abi-strategy/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './blockscout-abi.js'
export * from './etherscan-abi.js'
export * from './etherscanv2-abi.js'
export * from './experimental-erc20.js'
export * from './fourbyte-abi.js'
export * from './openchain-abi.js'
export * from './request-model.js'
Expand Down
9 changes: 3 additions & 6 deletions packages/transaction-decoder/src/contract-meta-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import { ContractData } from './types.js'
import { GetContractMetaStrategy } from './meta-strategy/request-model.js'
import { Address } from 'viem'

const STRATEGY_TIMEOUT = 5000

export interface ContractMetaParams {
address: string
chainID: number
Expand Down Expand Up @@ -166,10 +164,9 @@ const ContractMetaLoaderRequestResolver = RequestResolver.makeBatched((requests:
const allAvailableStrategies = Array.prependAll(strategies.default, strategies[chainID] ?? [])

// TODO: Distinct the errors and missing data, so we can retry on errors
return Effect.validateFirst(allAvailableStrategies, (strategy) => Effect.request(strategyRequest, strategy)).pipe(
Effect.timeout(STRATEGY_TIMEOUT),
Effect.orElseSucceed(() => null),
)
return Effect.validateFirst(allAvailableStrategies, (strategy) =>
pipe(Effect.request(strategyRequest, strategy), Effect.withRequestCaching(true)),
).pipe(Effect.orElseSucceed(() => null))
})

// Store results and resolve pending requests
Expand Down
22 changes: 3 additions & 19 deletions packages/transaction-decoder/src/sql/contract-meta-store.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,14 @@
import { SqlClient } from '@effect/sql'
import { Effect, Layer } from 'effect'
import {
ContractData,
ContractMetaStore,
ERC20RPCStrategyResolver,
NFTRPCStrategyResolver,
ProxyRPCStrategyResolver,
PublicClient,
} from '../effect.js'
import { ContractData, ContractMetaStore } from '../effect.js'

export const make = () =>
export const make = (strategies: ContractMetaStore['strategies']) =>
Layer.effect(
ContractMetaStore,
Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient
const publicClient = yield* PublicClient

const table = sql('_loop_decoder_contract_meta_')

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

return ContractMetaStore.of({
strategies: {
default: [
ERC20RPCStrategyResolver(publicClient),
NFTRPCStrategyResolver(publicClient),
ProxyRPCStrategyResolver(publicClient),
],
},
strategies,
set: (key, value) =>
Effect.gen(function* () {
if (value.status === 'success') {
Expand Down
Loading