Skip to content

Commit

Permalink
Add an unique id for each strategy and use it in request param (#169)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ferossgp authored Dec 6, 2024
1 parent 2d17903 commit 611d1c4
Show file tree
Hide file tree
Showing 16 changed files with 156 additions and 105 deletions.
5 changes: 5 additions & 0 deletions .changeset/swift-taxis-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@3loop/transaction-decoder': minor
---

Add an id to each strategy to make each request to a strategy unique. Effect will cache the request in a single global cache, thus to avoid the same request of being cached across different strategies we added an unique id that will identify each request.
63 changes: 40 additions & 23 deletions packages/transaction-decoder/src/abi-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,17 +214,22 @@ const AbiLoaderRequestResolver: Effect.Effect<
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) => {
return pipe(Effect.request(strategyRequest, strategy.resolver), Effect.withRequestCaching(false))
return pipe(
Effect.request(
new GetContractABIStrategy({
address: req.address,
chainId: req.chainID,
strategyId: strategy.id,
}),
strategy.resolver,
),
Effect.withRequestCaching(true),
)
}).pipe(
Effect.map(Either.left),
Effect.orElseSucceed(() => Either.right(req)),
Expand All @@ -239,23 +244,35 @@ const AbiLoaderRequestResolver: Effect.Effect<
const [addressStrategyResults, notFound] = Array.partitionMap(response, (res) => res)

// NOTE: Secondly we request strategies to fetch fragments
const fragmentStrategyResults = yield* Effect.forEach(notFound, ({ chainID, address, event, signature }) => {
const strategyRequest = new GetContractABIStrategy({
address,
chainID,
event,
signature,
})

const allAvailableStrategies = Array.prependAll(strategies.default, strategies[chainID] ?? []).filter(
(strategy) => strategy.type === 'fragment',
)
const fragmentStrategyResults = yield* Effect.forEach(
notFound,
({ chainID, address, event, signature }) => {
const allAvailableStrategies = Array.prependAll(strategies.default, strategies[chainID] ?? []).filter(
(strategy) => strategy.type === 'fragment',
)

// 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(false)),
).pipe(Effect.orElseSucceed(() => null))
})
// TODO: Distinct the errors and missing data, so we can retry on errors
return Effect.validateFirst(allAvailableStrategies, (strategy) =>
pipe(
Effect.request(
new GetContractABIStrategy({
address,
chainId: chainID,
event,
signature,
strategyId: strategy.id,
}),
strategy.resolver,
),
Effect.withRequestCaching(true),
),
).pipe(Effect.orElseSucceed(() => null))
},
{
concurrency: 'unbounded',
batching: true,
},
)

const strategyResults = Array.appendAll(addressStrategyResults, fragmentStrategyResults)

Expand Down Expand Up @@ -299,7 +316,7 @@ export const getAndCacheAbi = (params: AbiParams) =>
}).pipe(
Effect.withSpan('AbiLoader.GetAndCacheAbi', {
attributes: {
chainID: params.chainID,
chainId: params.chainID,
address: params.address,
event: params.event,
signature: params.signature,
Expand Down
11 changes: 6 additions & 5 deletions packages/transaction-decoder/src/abi-strategy/blockscout-abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Effect, RequestResolver } from 'effect'
import * as RequestModel from './request-model.js'

async function fetchContractABI(
{ address, chainID }: RequestModel.GetContractABIStrategy,
{ address, chainId }: RequestModel.GetContractABIStrategy,
config: { apikey?: string; endpoint: string },
): Promise<RequestModel.ContractABI[]> {
const endpoint = config.endpoint
Expand All @@ -25,31 +25,32 @@ async function fetchContractABI(
if (json.status === '1') {
return [
{
chainID,
chainID: chainId,
address,
abi: json.result,
type: 'address',
},
]
}

throw new Error(`Failed to fetch ABI for ${address} on chain ${chainID}`)
throw new Error(`Failed to fetch ABI for ${address} on chain ${chainId}`)
}

export const BlockscoutStrategyResolver = (config: {
apikey?: string
endpoint: string
}): RequestModel.ContractAbiResolverStrategy => {
return {
id: 'blockscout-strategy',
type: 'address',
resolver: RequestResolver.fromEffect((req: RequestModel.GetContractABIStrategy) =>
Effect.withSpan(
Effect.tryPromise({
try: () => fetchContractABI(req, config),
catch: () => new RequestModel.ResolveStrategyABIError('Blockscout', req.address, req.chainID),
catch: () => new RequestModel.ResolveStrategyABIError('Blockscout', req.address, req.chainId),
}),
'AbiStrategy.BlockscoutStrategyResolver',
{ attributes: { chainID: req.chainID, address: req.address } },
{ attributes: { chainId: req.chainId, address: req.address } },
),
),
}
Expand Down
13 changes: 7 additions & 6 deletions packages/transaction-decoder/src/abi-strategy/etherscan-abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ const endpoints: { [k: number]: string } = {
}

async function fetchContractABI(
{ address, chainID }: RequestModel.GetContractABIStrategy,
{ address, chainId }: RequestModel.GetContractABIStrategy,
config?: { apikey?: string; endpoint?: string },
): Promise<RequestModel.ContractABI[]> {
const endpoint = config?.endpoint ?? endpoints[chainID]
const endpoint = config?.endpoint ?? endpoints[chainId]
const params: Record<string, string> = {
module: 'contract',
action: 'getabi',
Expand All @@ -72,29 +72,30 @@ async function fetchContractABI(
{
type: 'address',
address,
chainID,
chainID: chainId,
abi: json.result,
},
]
}

throw new Error(`Failed to fetch ABI for ${address} on chain ${chainID}`)
throw new Error(`Failed to fetch ABI for ${address} on chain ${chainId}`)
}

export const EtherscanStrategyResolver = (config?: {
apikey?: string
endpoint?: string
}): RequestModel.ContractAbiResolverStrategy => {
return {
id: 'etherscan-strategy',
type: 'address',
resolver: RequestResolver.fromEffect((req: RequestModel.GetContractABIStrategy) =>
Effect.withSpan(
Effect.tryPromise({
try: () => fetchContractABI(req, config),
catch: () => new RequestModel.ResolveStrategyABIError('etherscan', req.address, req.chainID),
catch: () => new RequestModel.ResolveStrategyABIError('etherscan', req.address, req.chainId),
}),
'AbiStrategy.EtherscanStrategyResolver',
{ attributes: { chainID: req.chainID, address: req.address } },
{ attributes: { chainId: req.chainId, address: req.address } },
),
),
}
Expand Down
13 changes: 7 additions & 6 deletions packages/transaction-decoder/src/abi-strategy/etherscanv2-abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import * as RequestModel from './request-model.js'
const endpoint = 'https://api.etherscan.io/v2/api'

async function fetchContractABI(
{ address, chainID }: RequestModel.GetContractABIStrategy,
{ address, chainId }: RequestModel.GetContractABIStrategy,
config?: { apikey?: string },
): Promise<RequestModel.ContractABI[]> {
const params: Record<string, string> = {
module: 'contract',
action: 'getabi',
chainId: chainID.toString(),
chainId: chainId.toString(),
address,
}

Expand All @@ -28,26 +28,27 @@ async function fetchContractABI(
{
type: 'address',
address,
chainID,
chainID: chainId,
abi: json.result,
},
]
}

throw new Error(`Failed to fetch ABI for ${address} on chain ${chainID}`)
throw new Error(`Failed to fetch ABI for ${address} on chain ${chainId}`)
}

export const EtherscanV2StrategyResolver = (config?: { apikey?: string }): RequestModel.ContractAbiResolverStrategy => {
return {
id: 'etherscanV2-strategy',
type: 'address',
resolver: RequestResolver.fromEffect((req: RequestModel.GetContractABIStrategy) =>
Effect.withSpan(
Effect.tryPromise({
try: () => fetchContractABI(req, config),
catch: () => new RequestModel.ResolveStrategyABIError('etherscanV2', req.address, req.chainID),
catch: () => new RequestModel.ResolveStrategyABIError('etherscanV2', req.address, req.chainId),
}),
'AbiStrategy.EtherscanV2StrategyResolver',
{ attributes: { chainID: req.chainID, address: req.address } },
{ attributes: { chainId: req.chainId, address: req.address } },
),
),
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ 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) =>
const getLocalFragments = (service: PublicClient, { address, chainId }: RequestModel.GetContractABIStrategy) =>
Effect.gen(function* () {
const client = yield* service
.getPublicClient(chainID)
.getPublicClient(chainId)
.pipe(
Effect.catchAll(() =>
Effect.fail(new RequestModel.ResolveStrategyABIError('local-strategy', address, chainID)),
Effect.fail(new RequestModel.ResolveStrategyABIError('local-strategy', address, chainId)),
),
)

Expand All @@ -21,31 +21,32 @@ const getLocalFragments = (service: PublicClient, { address, chainID }: RequestM

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

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

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

export const ExperimentalErc20AbiStrategyResolver = (
service: PublicClient,
): RequestModel.ContractAbiResolverStrategy => {
return {
id: 'experimental-erc20-strategy',
type: 'address',
resolver: RequestResolver.fromEffect((req: RequestModel.GetContractABIStrategy) =>
Effect.withSpan(getLocalFragments(service, req), 'AbiStrategy.ExperimentalErc20AbiStrategyResolver', {
attributes: { chainID: req.chainID, address: req.address },
attributes: { chainId: req.chainId, address: req.address },
}),
),
}
Expand Down
13 changes: 7 additions & 6 deletions packages/transaction-decoder/src/abi-strategy/fourbyte-abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ async function fetchABI({
address,
event,
signature,
chainID,
chainId,
}: RequestModel.GetContractABIStrategy): Promise<RequestModel.ContractABI[]> {
if (signature != null) {
const full_match = await fetch(`${endpoint}/signatures/?hex_signature=${signature}`)
Expand All @@ -34,7 +34,7 @@ async function fetchABI({
return json.results.map((result) => ({
type: 'func',
address,
chainID,
chainID: chainId,
abi: parseFunctionSignature(result.text_signature),
signature,
}))
Expand All @@ -48,27 +48,28 @@ async function fetchABI({
return json.results.map((result) => ({
type: 'event',
address,
chainID,
chainID: chainId,
abi: parseEventSignature(result.text_signature),
event,
}))
}
}

throw new Error(`Failed to fetch ABI for ${address} on chain ${chainID}`)
throw new Error(`Failed to fetch ABI for ${address} on chain ${chainId}`)
}

export const FourByteStrategyResolver = (): RequestModel.ContractAbiResolverStrategy => {
return {
id: 'fourbyte-strategy',
type: 'fragment',
resolver: RequestResolver.fromEffect((req: RequestModel.GetContractABIStrategy) =>
Effect.withSpan(
Effect.tryPromise({
try: () => fetchABI(req),
catch: () => new RequestModel.ResolveStrategyABIError('4byte.directory', req.address, req.chainID),
catch: () => new RequestModel.ResolveStrategyABIError('4byte.directory', req.address, req.chainId),
}),
'AbiStrategy.FourByteStrategyResolver',
{ attributes: { chainID: req.chainID, address: req.address } },
{ attributes: { chainId: req.chainId, address: req.address } },
),
),
}
Expand Down
13 changes: 7 additions & 6 deletions packages/transaction-decoder/src/abi-strategy/openchain-abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function parseEventSignature(signature: string): string {

async function fetchABI({
address,
chainID,
chainId,
signature,
event,
}: RequestModel.GetContractABIStrategy): Promise<RequestModel.ContractABI[]> {
Expand All @@ -52,7 +52,7 @@ async function fetchABI({
return json.result.function[signature].map((f) => ({
type: 'func',
address,
chainID,
chainID: chainId,
abi: parseFunctionSignature(f.name),
signature,
}))
Expand All @@ -66,27 +66,28 @@ async function fetchABI({
return json.result.event[event].map((e) => ({
type: 'event',
address,
chainID,
chainID: chainId,
abi: parseEventSignature(e.name),
event,
}))
}
}

throw new Error(`Failed to fetch ABI for ${address} on chain ${chainID}`)
throw new Error(`Failed to fetch ABI for ${address} on chain ${chainId}`)
}

export const OpenchainStrategyResolver = (): RequestModel.ContractAbiResolverStrategy => {
return {
id: 'openchain-strategy',
type: 'fragment',
resolver: RequestResolver.fromEffect((req: RequestModel.GetContractABIStrategy) =>
Effect.withSpan(
Effect.tryPromise({
try: () => fetchABI(req),
catch: () => new RequestModel.ResolveStrategyABIError('openchain', req.address, req.chainID),
catch: () => new RequestModel.ResolveStrategyABIError('openchain', req.address, req.chainId),
}),
'AbiStrategy.OpenchainStrategyResolver',
{ attributes: { chainID: req.chainID, address: req.address } },
{ attributes: { chainId: req.chainId, address: req.address } },
),
),
}
Expand Down
Loading

0 comments on commit 611d1c4

Please sign in to comment.