From 815c07dab9e58616fb425c04afa426968eb3d848 Mon Sep 17 00:00:00 2001 From: Anastasia Rodionova Date: Mon, 6 Jan 2025 16:46:19 +0800 Subject: [PATCH 1/5] Detect proxy during getting abi and add proxy results caching --- .../transaction-decoder/src/abi-loader.ts | 17 ++- .../src/contract-meta-loader.ts | 41 ++++--- .../src/decoding/calldata-decode.ts | 15 +-- .../src/decoding/constants.ts | 1 + .../src/decoding/log-decode.ts | 11 +- .../src/decoding/proxies.ts | 114 +++++++++++------- .../src/meta-strategy/proxy-rpc-strategy.ts | 4 +- .../src/meta-strategy/request-model.ts | 8 +- 8 files changed, 121 insertions(+), 90 deletions(-) diff --git a/packages/transaction-decoder/src/abi-loader.ts b/packages/transaction-decoder/src/abi-loader.ts index fd17c6a6..4a66bcb5 100644 --- a/packages/transaction-decoder/src/abi-loader.ts +++ b/packages/transaction-decoder/src/abi-loader.ts @@ -12,7 +12,8 @@ import { SchemaAST, } from 'effect' import { ContractABI, ContractAbiResolverStrategy, GetContractABIStrategy } from './abi-strategy/request-model.js' -import { Abi } from 'viem' +import { Abi, getAddress } from 'viem' +import { getProxyImplementation } from './decoding/proxies.js' export interface AbiParams { chainID: number @@ -39,6 +40,7 @@ export interface ContractAbiEmpty { export type ContractAbiResult = ContractAbiSuccess | ContractAbiNotFound | ContractAbiEmpty type ChainOrDefault = number | 'default' + export interface AbiStore { readonly strategies: Record readonly set: (key: Key, value: Value) => Effect.Effect @@ -312,7 +314,18 @@ export const getAndCacheAbi = (params: AbiParams) => return yield* Effect.fail(new EmptyCalldataError(params)) } - return yield* Effect.request(new AbiLoader(params), AbiLoaderRequestResolver) + const implementation = yield* getProxyImplementation({ + address: getAddress(params.address), + chainID: params.chainID, + }) + + return yield* Effect.request( + new AbiLoader({ + ...params, + address: implementation?.address ?? params.address, + }), + AbiLoaderRequestResolver, + ) }).pipe( Effect.withSpan('AbiLoader.GetAndCacheAbi', { attributes: { diff --git a/packages/transaction-decoder/src/contract-meta-loader.ts b/packages/transaction-decoder/src/contract-meta-loader.ts index 5c118acb..f463dfc4 100644 --- a/packages/transaction-decoder/src/contract-meta-loader.ts +++ b/packages/transaction-decoder/src/contract-meta-loader.ts @@ -2,6 +2,7 @@ import { Context, Effect, RequestResolver, Request, Array, Either, pipe, Schema, import { ContractData } from './types.js' import { ContractMetaResolverStrategy, GetContractMetaStrategy } from './meta-strategy/request-model.js' import { Address } from 'viem' +import { ZERO_ADDRESS } from './decoding/constants.js' export interface ContractMetaParams { address: string @@ -155,24 +156,28 @@ const ContractMetaLoaderRequestResolver = RequestResolver.makeBatched((requests: ) // Fetch ContractMeta from the strategies - const strategyResults = yield* Effect.forEach(remaining, ({ chainID, address }) => { - 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) => - pipe( - Effect.request( - new GetContractMetaStrategy({ - address, - chainId: chainID, - strategyId: strategy.id, - }), - strategy.resolver, + const strategyResults = yield* Effect.forEach( + remaining, + ({ chainID, address }) => { + 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) => + pipe( + Effect.request( + new GetContractMetaStrategy({ + address, + chainId: chainID, + strategyId: strategy.id, + }), + strategy.resolver, + ), + Effect.withRequestCaching(true), ), - Effect.withRequestCaching(true), - ), - ).pipe(Effect.orElseSucceed(() => null)) - }) + ).pipe(Effect.orElseSucceed(() => null)) + }, + { concurrency: 'unbounded', batching: true }, + ) // Store results and resolve pending requests yield* Effect.forEach( @@ -197,6 +202,8 @@ export const getAndCacheContractMeta = ({ readonly chainID: number readonly address: Address }) => { + if (address === ZERO_ADDRESS) return Effect.succeed(null) + return Effect.withSpan( Effect.request(new ContractMetaLoader({ chainID, address }), ContractMetaLoaderRequestResolver), 'GetAndCacheContractMeta', diff --git a/packages/transaction-decoder/src/decoding/calldata-decode.ts b/packages/transaction-decoder/src/decoding/calldata-decode.ts index 98700eda..8b721caf 100644 --- a/packages/transaction-decoder/src/decoding/calldata-decode.ts +++ b/packages/transaction-decoder/src/decoding/calldata-decode.ts @@ -1,6 +1,5 @@ import { Effect } from 'effect' -import { isAddress, Hex, getAddress, encodeFunctionData, Address } from 'viem' -import { getProxyStorageSlot } from './proxies.js' +import { Hex, Address, encodeFunctionData } from 'viem' import { AbiParams, AbiStore, ContractAbiResult, getAndCacheAbi, MissingABIError } from '../abi-loader.js' import * as AbiDecoder from './abi-decode.js' import { TreeNode } from '../types.js' @@ -147,19 +146,9 @@ export const decodeMethod = ({ }) => Effect.gen(function* () { const signature = data.slice(0, 10) - let implementationAddress: Address | undefined - - if (isAddress(contractAddress)) { - //if contract is a proxy, get the implementation address - const implementation = yield* getProxyStorageSlot({ address: getAddress(contractAddress), chainID }) - - if (implementation) { - implementationAddress = implementation.address - } - } const abi = yield* getAndCacheAbi({ - address: implementationAddress ?? contractAddress, + address: contractAddress, signature, chainID, }) diff --git a/packages/transaction-decoder/src/decoding/constants.ts b/packages/transaction-decoder/src/decoding/constants.ts index 33d7aec4..4aa5604b 100644 --- a/packages/transaction-decoder/src/decoding/constants.ts +++ b/packages/transaction-decoder/src/decoding/constants.ts @@ -38,3 +38,4 @@ export const SAFE_MULTISEND_ABI: Abi = [ outputs: [], }, ] +export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' diff --git a/packages/transaction-decoder/src/decoding/log-decode.ts b/packages/transaction-decoder/src/decoding/log-decode.ts index c81a674c..18835cdd 100644 --- a/packages/transaction-decoder/src/decoding/log-decode.ts +++ b/packages/transaction-decoder/src/decoding/log-decode.ts @@ -1,7 +1,7 @@ import { type GetTransactionReturnType, type Log, decodeEventLog, getAbiItem, getAddress } from 'viem' import { Effect } from 'effect' import type { DecodedLogEvent, Interaction, RawDecodedLog } from '../types.js' -import { getProxyStorageSlot } from './proxies.js' +import { getProxyImplementation } from './proxies.js' import { getAndCacheAbi } from '../abi-loader.js' import { getAndCacheContractMeta } from '../contract-meta-loader.js' import * as AbiDecoder from './abi-decode.js' @@ -22,14 +22,7 @@ const decodedLog = (transaction: GetTransactionReturnType, logItem: Log) => const chainID = Number(transaction.chainId) const address = getAddress(logItem.address) - let abiAddress = address - - const implementation = yield* getProxyStorageSlot({ address: getAddress(abiAddress), chainID }) - - if (implementation) { - yield* Effect.logDebug(`Proxy implementation found for ${abiAddress} at ${implementation}`) - abiAddress = implementation.address - } + const abiAddress = address const [abiItem, contractData] = yield* Effect.all( [ diff --git a/packages/transaction-decoder/src/decoding/proxies.ts b/packages/transaction-decoder/src/decoding/proxies.ts index ceacf865..3e82010e 100644 --- a/packages/transaction-decoder/src/decoding/proxies.ts +++ b/packages/transaction-decoder/src/decoding/proxies.ts @@ -1,13 +1,20 @@ -import { Effect, Request, RequestResolver, Schedule } from 'effect' -import { PublicClient, RPCCallError, RPCFetchError, UnknownNetwork } from '../public-client.js' +import { Effect, PrimaryKey, Request, RequestResolver, Schedule, Schema, SchemaAST } from 'effect' + +import { PublicClient, RPCCallError, RPCFetchError } from '../public-client.js' import { Address, Hex } from 'viem' import { ProxyType } from '../types.js' +import { ZERO_ADDRESS } from './constants.js' interface StorageSlot { type: ProxyType slot: Hex } +interface ProxyResult { + type: ProxyType + address: Address +} + const storageSlots: StorageSlot[] = [ { type: 'eip1967', slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' }, //EIP1967 { type: 'zeppelin', slot: '0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3' }, //zeppelin @@ -16,14 +23,30 @@ const storageSlots: StorageSlot[] = [ const zeroSlot = '0x0000000000000000000000000000000000000000000000000000000000000000' -export interface GetStorageSlot - extends Request.Request<{ type: ProxyType; address: Address } | undefined, UnknownNetwork> { - readonly _tag: 'GetStorageSlot' +export interface GetProxy extends Request.Request { + readonly _tag: 'GetProxy' readonly address: Address readonly chainID: number } -const getStorageSlot = (request: GetStorageSlot, slot: StorageSlot) => +export const GetProxy = Request.tagged('GetProxy') +class SchemaAddress extends Schema.make
(SchemaAST.stringKeyword) {} +class SchemaProxy extends Schema.make(SchemaAST.objectKeyword) {} + +class ProxyLoader extends Schema.TaggedRequest()('ProxyLoader', { + failure: Schema.instanceOf(RPCFetchError), + success: Schema.NullOr(SchemaProxy), + payload: { + address: SchemaAddress, + chainID: Schema.Number, + }, +}) { + [PrimaryKey.symbol]() { + return `proxy::${this.chainID}:${this.address}` + } +} + +const getStorageSlot = (request: ProxyLoader, slot: StorageSlot) => Effect.gen(function* () { const service = yield* PublicClient const { client: publicClient } = yield* service.getPublicClient(request.chainID) @@ -37,7 +60,7 @@ const getStorageSlot = (request: GetStorageSlot, slot: StorageSlot) => }) }) -const ethCall = (request: GetStorageSlot, slot: StorageSlot) => +const ethCall = (request: ProxyLoader, slot: StorageSlot) => Effect.gen(function* () { const service = yield* PublicClient const { client: publicClient } = yield* service.getPublicClient(request.chainID) @@ -49,52 +72,53 @@ const ethCall = (request: GetStorageSlot, slot: StorageSlot) => data: slot.slot, }) )?.data, - catch: () => new RPCCallError('Get storage'), + catch: () => new RPCCallError('Eth call'), }) }) -export const GetStorageSlot = Request.tagged('GetStorageSlot') +export const GetProxyResolver = RequestResolver.fromEffect( + (request: ProxyLoader): Effect.Effect => + Effect.gen(function* () { + // NOTE: Should we make this recursive when we have a Proxy of a Proxy? -export const GetStorageSlotResolver = RequestResolver.fromEffect((request: GetStorageSlot) => - Effect.gen(function* () { - // NOTE: Should we make this recursive when we have a Proxy of a Proxy? - const effects = storageSlots.map((slot) => - Effect.gen(function* () { - const res: { type: ProxyType; address: Hex } | undefined = { type: slot.type, address: '0x' } - - let addressString: Address | undefined - switch (slot.type) { - case 'eip1967': - case 'zeppelin': { - addressString = yield* getStorageSlot(request, slot) - break - } - case 'safe': { - addressString = yield* ethCall(request, slot).pipe(Effect.orElseSucceed(() => undefined)) - break - } - } + if (request.address === ZERO_ADDRESS) return undefined - if (addressString == null || addressString === zeroSlot) return undefined + const effects = storageSlots.map((slot) => + Effect.gen(function* () { + const res: ProxyResult | undefined = { type: slot.type, address: '0x' } - res.address = ('0x' + addressString.slice(addressString.length - 40)) as Address + let address: Hex | undefined + switch (slot.type) { + case 'eip1967': + case 'zeppelin': { + address = yield* getStorageSlot(request, slot) + break + } + case 'safe': { + address = yield* ethCall(request, slot).pipe(Effect.orElseSucceed(() => undefined)) + break + } + } + + if (!address || address === zeroSlot) return undefined - return res - }), - ) + res.address = ('0x' + address.slice(address.length - 40)) as Address + return res + }), + ) - const policy = Schedule.addDelay( - Schedule.recurs(2), // Retry for a maximum of 2 times - () => '100 millis', // Add a delay of 100 milliseconds between retries - ) - const res = yield* Effect.all(effects, { - concurrency: 'inherit', - batching: 'inherit', - }).pipe(Effect.retryOrElse(policy, () => Effect.succeed(undefined))) + const policy = Schedule.addDelay( + Schedule.recurs(2), // Retry for a maximum of 2 times + () => '100 millis', // Add a delay of 100 milliseconds between retries + ) + const res = yield* Effect.all(effects, { + concurrency: 'inherit', + batching: 'inherit', + }).pipe(Effect.retryOrElse(policy, () => Effect.succeed(undefined))) - return res?.find((x) => x != null) - }), + return res?.find((x) => x != null) + }), ).pipe(RequestResolver.contextFromEffect) -export const getProxyStorageSlot = ({ address, chainID }: { address: Address; chainID: number }) => - Effect.request(GetStorageSlot({ address, chainID }), GetStorageSlotResolver).pipe(Effect.withRequestCaching(true)) +export const getProxyImplementation = ({ address, chainID }: { address: Address; chainID: number }) => + Effect.request(new ProxyLoader({ address, chainID }), GetProxyResolver).pipe(Effect.withRequestCaching(true)) diff --git a/packages/transaction-decoder/src/meta-strategy/proxy-rpc-strategy.ts b/packages/transaction-decoder/src/meta-strategy/proxy-rpc-strategy.ts index cdddf945..4f5a193b 100644 --- a/packages/transaction-decoder/src/meta-strategy/proxy-rpc-strategy.ts +++ b/packages/transaction-decoder/src/meta-strategy/proxy-rpc-strategy.ts @@ -1,14 +1,14 @@ import { ContractData, ContractType } from '../types.js' import * as RequestModel from './request-model.js' import { Effect, RequestResolver } from 'effect' -import { getProxyStorageSlot } from '../decoding/proxies.js' +import { getProxyImplementation } from '../decoding/proxies.js' import { PublicClient } from '../public-client.js' export const ProxyRPCStrategyResolver = (publicClientLive: PublicClient) => ({ id: 'proxy-rpc-strategy', resolver: RequestResolver.fromEffect(({ chainId, address }: RequestModel.GetContractMetaStrategy) => Effect.gen(function* () { - const proxyResult = yield* getProxyStorageSlot({ address, chainID: chainId }) + const proxyResult = yield* getProxyImplementation({ address, chainID: chainId }) const { address: implementationAddress, type: proxyType } = proxyResult ?? {} const fail = new RequestModel.ResolveStrategyMetaError('ProxyRPCStrategy', address, chainId) diff --git a/packages/transaction-decoder/src/meta-strategy/request-model.ts b/packages/transaction-decoder/src/meta-strategy/request-model.ts index 0e8478f6..e2c2795a 100644 --- a/packages/transaction-decoder/src/meta-strategy/request-model.ts +++ b/packages/transaction-decoder/src/meta-strategy/request-model.ts @@ -1,4 +1,4 @@ -import { UnknownNetwork } from '../public-client.js' +import { RPCFetchError, UnknownNetwork } from '../public-client.js' import { ContractData } from '../types.js' import { PrimaryKey, RequestResolver, Schema, SchemaAST } from 'effect' import { Address } from 'viem' @@ -28,7 +28,11 @@ class SchemaContractData extends Schema.make(SchemaAST.objectKeywo export class GetContractMetaStrategy extends Schema.TaggedRequest()( 'GetContractMetaStrategy', { - failure: Schema.Union(Schema.instanceOf(ResolveStrategyMetaError), Schema.instanceOf(UnknownNetwork)), + failure: Schema.Union( + Schema.instanceOf(ResolveStrategyMetaError), + Schema.instanceOf(UnknownNetwork), + Schema.instanceOf(RPCFetchError), + ), success: SchemaContractData, payload: { chainId: Schema.Number, From e189efdcda640d71972ff54fdd26e69a8ada1d40 Mon Sep 17 00:00:00 2001 From: Anastasia Rodionova Date: Mon, 6 Jan 2025 16:50:26 +0800 Subject: [PATCH 2/5] Add in memory cache layer and requets debuggign in web app --- apps/web/src/app/data.ts | 4 +--- apps/web/src/lib/decode.ts | 19 ++++++++++++++---- apps/web/src/lib/rpc-provider.ts | 34 +++++++++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/apps/web/src/app/data.ts b/apps/web/src/app/data.ts index 9f5b8a66..3089e6ba 100644 --- a/apps/web/src/app/data.ts +++ b/apps/web/src/app/data.ts @@ -130,7 +130,6 @@ export const supportedChains: { chainID: number rpcUrl: string traceAPI?: 'parity' | 'geth' | 'none' - batchMaxCount?: number }[] = [ { name: 'Ethereum Mainnet', @@ -146,8 +145,7 @@ export const supportedChains: { name: 'Base mainnet', chainID: 8453, rpcUrl: process.env.BASE_RPC_URL as string, - traceAPI: 'parity', - batchMaxCount: 1, + traceAPI: 'geth', }, { name: 'Polygon Mainnet', diff --git a/apps/web/src/lib/decode.ts b/apps/web/src/lib/decode.ts index e52ae9d8..fe8c9d64 100644 --- a/apps/web/src/lib/decode.ts +++ b/apps/web/src/lib/decode.ts @@ -1,5 +1,5 @@ import { getProvider, RPCProviderLive } from './rpc-provider' -import { Config, Effect, Layer, ManagedRuntime } from 'effect' +import { Config, Effect, Layer, ManagedRuntime, Request } from 'effect' import { DecodedTransaction, DecodeResult, @@ -56,8 +56,10 @@ const MetaStoreLive = Layer.unwrapEffect( }) }), ) +const CacheLayer = Layer.setRequestCache(Request.makeCache({ capacity: 100, timeToLive: '60 minutes' })) const DataLayer = Layer.mergeAll(RPCProviderLive, DatabaseLive) const LoadersLayer = Layer.mergeAll(AbiStoreLive, MetaStoreLive) + const MainLayer = Layer.provideMerge(LoadersLayer, DataLayer) as Layer.Layer< | AbiStore | ContractMetaStore @@ -68,7 +70,7 @@ const MainLayer = Layer.provideMerge(LoadersLayer, DataLayer) as Layer.Layer< never > -const runtime = ManagedRuntime.make(MainLayer) +const runtime = ManagedRuntime.make(Layer.provide(MainLayer, CacheLayer)) export async function decodeTransaction({ chainID, @@ -80,10 +82,19 @@ export async function decodeTransaction({ // NOTE: For unknonw reason the context of main layer is still missing the SqlClient in the type const runnable = decodeTransactionByHash(hash as Hex, chainID) - return runtime.runPromise(runnable).catch((error: unknown) => { + const startTime = performance.now() + + try { + const result = await runtime.runPromise(runnable) + const endTime = performance.now() + console.log(`Decode transaction took ${endTime - startTime}ms`) + return result + } catch (error: unknown) { + const endTime = performance.now() console.error('Decode error', JSON.stringify(error, null, 2)) + console.log(`Failed decode transaction took ${endTime - startTime}ms`) return undefined - }) + } } export async function decodeCalldata({ diff --git a/apps/web/src/lib/rpc-provider.ts b/apps/web/src/lib/rpc-provider.ts index b72c60a0..3a3bbc60 100644 --- a/apps/web/src/lib/rpc-provider.ts +++ b/apps/web/src/lib/rpc-provider.ts @@ -16,7 +16,39 @@ export function getProvider(chainID: number): PublicClientObject | null { if (url != null) { return { client: createPublicClient({ - transport: http(url), + transport: http(url, { + // Requests logging + // onFetchRequest(request) { + // const reader = request.body?.getReader() + // if (!reader) { + // return + // } + // let body = '' + // reader + // .read() + // .then(function processText({ done, value }) { + // if (done) { + // return + // } + // // value for fetch streams is a Uint8Array + // body += value + // reader.read().then(processText) + // }) + // .then(() => { + // const json = JSON.parse( + // body + // .split(',') + // .map((code) => String.fromCharCode(parseInt(code, 10))) + // .join(''), + // ) + // try { + // console.log(JSON.stringify(json, null, 2)) + // } catch (e) { + // console.log(json['id'], json['method'], body.length) + // } + // }) + // }, + }), }), config: { traceAPI: providerConfigs[chainID]?.traceAPI, From bf3604f523b93638a100a7102ff4b31f48ed03b9 Mon Sep 17 00:00:00 2001 From: Anastasia Rodionova Date: Mon, 6 Jan 2025 17:31:02 +0800 Subject: [PATCH 3/5] Fix custom error decoding --- packages/transaction-decoder/src/abi-loader.ts | 12 ++++++++---- .../src/decoding/trace-decode.ts | 14 ++++++++++---- .../transaction-decoder/src/transaction-decoder.ts | 2 +- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/transaction-decoder/src/abi-loader.ts b/packages/transaction-decoder/src/abi-loader.ts index 4a66bcb5..ce25aac2 100644 --- a/packages/transaction-decoder/src/abi-loader.ts +++ b/packages/transaction-decoder/src/abi-loader.ts @@ -314,10 +314,14 @@ export const getAndCacheAbi = (params: AbiParams) => return yield* Effect.fail(new EmptyCalldataError(params)) } - const implementation = yield* getProxyImplementation({ - address: getAddress(params.address), - chainID: params.chainID, - }) + let implementation = null + + if (params.address !== '') { + implementation = yield* getProxyImplementation({ + address: getAddress(params.address), + chainID: params.chainID, + }) + } return yield* Effect.request( new AbiLoader({ diff --git a/packages/transaction-decoder/src/decoding/trace-decode.ts b/packages/transaction-decoder/src/decoding/trace-decode.ts index 0d7f7d93..906cab47 100644 --- a/packages/transaction-decoder/src/decoding/trace-decode.ts +++ b/packages/transaction-decoder/src/decoding/trace-decode.ts @@ -47,7 +47,7 @@ const decodeTraceLog = (call: TraceLog, transaction: GetTransactionReturnType) = return yield* new DecodeError(`Could not decode trace log ${stringify(call)}`) }) -const decodeTraceLogOutput = (call: TraceLog) => +const decodeTraceLogOutput = (call: TraceLog, chainID: number) => Effect.gen(function* () { if (call.result && 'output' in call.result && call.result.output !== '0x') { const data = call.result.output as Hex @@ -61,7 +61,7 @@ const decodeTraceLogOutput = (call: TraceLog) => const abi_ = yield* getAndCacheAbi({ address: '', signature, - chainID: 0, + chainID, }) abi = [...abi, ...abi_] @@ -184,7 +184,13 @@ export function augmentTraceLogs( return [...interactionsWithoutNativeTransfers, ...nativeTransfers] } -export const decodeErrorTrace = ({ trace }: { trace: TraceLog[] }) => +export const decodeErrorTrace = ({ + trace, + transaction, +}: { + trace: TraceLog[] + transaction: GetTransactionReturnType +}) => Effect.gen(function* () { const errorCalls = trace?.filter((call) => call.error != null) if (errorCalls.length === 0) { @@ -212,7 +218,7 @@ export const decodeErrorTrace = ({ trace }: { trace: TraceLog[] }) => message: null, } } - const decodedOutput = yield* decodeTraceLogOutput(call) + const decodedOutput = yield* decodeTraceLogOutput(call, Number(transaction.chainId)) const value = decodedOutput?.params?.[0]?.value?.toString() let message: string | null = null diff --git a/packages/transaction-decoder/src/transaction-decoder.ts b/packages/transaction-decoder/src/transaction-decoder.ts index b5977173..1bcb528f 100644 --- a/packages/transaction-decoder/src/transaction-decoder.ts +++ b/packages/transaction-decoder/src/transaction-decoder.ts @@ -99,7 +99,7 @@ export const decodeTransaction = ({ ), decodedTrace: decodeTrace({ trace, transaction }), decodedLogs: decodeLogs({ receipt, transaction }), - decodedErrorTrace: decodeErrorTrace({ trace }), + decodedErrorTrace: decodeErrorTrace({ trace, transaction }), }, { batching: true, From 5afb00c97ef1e0053ac125cd520fcf501d2641b9 Mon Sep 17 00:00:00 2001 From: Anastasia Rodionova Date: Mon, 6 Jan 2025 17:31:10 +0800 Subject: [PATCH 4/5] Update tests --- ...aa080d65ed7c34785b9d0f4c20aa6c91c51a63ee1fa4.snapshot | 9 --------- ...4b5794f0f3eccef6a75cec1f1680b9295187effa3788.snapshot | 9 --------- ...acfaa124f39c64405f6692069ea003ce100f08192bcd.snapshot | 9 --------- ...088f2eaa38ddad3cc2a96ab47c6b6a167548cf2da175.snapshot | 9 --------- ...68768b784473de034156a81f12f3485bc157e4ac60c9.snapshot | 9 --------- ...c2211b122ee952c358ee1d509c45b1157c2b3be8313c.snapshot | 9 --------- ...618498b08dddf466513705d6402413016311844d3189.snapshot | 9 --------- ...d0f9e2f7c6d41fd8f6eb5a5697832c45a738518cd284.snapshot | 9 --------- ...71661ead5e16da228e168b0572b1ddc30a967968f8f6.snapshot | 9 --------- ...7dab0052a9363cc9b6a33d91449ccc523bdd5d99861c.snapshot | 9 --------- ...0f3a012b7e60a48055a6f2cd99ae7efce205c390f710.snapshot | 9 --------- 11 files changed, 99 deletions(-) diff --git a/packages/transaction-decoder/test/snapshots/decoder/0x02a4dda78f1452772d87aa080d65ed7c34785b9d0f4c20aa6c91c51a63ee1fa4.snapshot b/packages/transaction-decoder/test/snapshots/decoder/0x02a4dda78f1452772d87aa080d65ed7c34785b9d0f4c20aa6c91c51a63ee1fa4.snapshot index a36668fb..3a8d83ca 100644 --- a/packages/transaction-decoder/test/snapshots/decoder/0x02a4dda78f1452772d87aa080d65ed7c34785b9d0f4c20aa6c91c51a63ee1fa4.snapshot +++ b/packages/transaction-decoder/test/snapshots/decoder/0x02a4dda78f1452772d87aa080d65ed7c34785b9d0f4c20aa6c91c51a63ee1fa4.snapshot @@ -1,14 +1,5 @@ { "addressesMeta": { - "0x0000000000000000000000000000000000000000": { - "address": "0x0000000000000000000000000000000000000000", - "chainID": 1, - "contractAddress": "0x0000000000000000000000000000000000000000", - "contractName": "Mock ERC20 Contract", - "decimals": 18, - "tokenSymbol": "ERC20", - "type": "ERC20", - }, "0x0000000000A39bb272e79075ade125fd351887Ac": { "address": "0x0000000000A39bb272e79075ade125fd351887Ac", "chainID": 1, diff --git a/packages/transaction-decoder/test/snapshots/decoder/0x1cf9beb600427c0fef7b4b5794f0f3eccef6a75cec1f1680b9295187effa3788.snapshot b/packages/transaction-decoder/test/snapshots/decoder/0x1cf9beb600427c0fef7b4b5794f0f3eccef6a75cec1f1680b9295187effa3788.snapshot index 5247de38..fe788290 100644 --- a/packages/transaction-decoder/test/snapshots/decoder/0x1cf9beb600427c0fef7b4b5794f0f3eccef6a75cec1f1680b9295187effa3788.snapshot +++ b/packages/transaction-decoder/test/snapshots/decoder/0x1cf9beb600427c0fef7b4b5794f0f3eccef6a75cec1f1680b9295187effa3788.snapshot @@ -1,14 +1,5 @@ { "addressesMeta": { - "0x0000000000000000000000000000000000000000": { - "address": "0x0000000000000000000000000000000000000000", - "chainID": 5, - "contractAddress": "0x0000000000000000000000000000000000000000", - "contractName": "Mock ERC20 Contract", - "decimals": 18, - "tokenSymbol": "ERC20", - "type": "ERC20", - }, "0x58c3c2547084CC1C94130D6fd750A3877c7Ca5D2": { "address": "0x58c3c2547084CC1C94130D6fd750A3877c7Ca5D2", "chainID": 5, diff --git a/packages/transaction-decoder/test/snapshots/decoder/0x65324c58b7c555d7fccfacfaa124f39c64405f6692069ea003ce100f08192bcd.snapshot b/packages/transaction-decoder/test/snapshots/decoder/0x65324c58b7c555d7fccfacfaa124f39c64405f6692069ea003ce100f08192bcd.snapshot index 698b8d50..29ac9b6c 100644 --- a/packages/transaction-decoder/test/snapshots/decoder/0x65324c58b7c555d7fccfacfaa124f39c64405f6692069ea003ce100f08192bcd.snapshot +++ b/packages/transaction-decoder/test/snapshots/decoder/0x65324c58b7c555d7fccfacfaa124f39c64405f6692069ea003ce100f08192bcd.snapshot @@ -1,14 +1,5 @@ { "addressesMeta": { - "0x0000000000000000000000000000000000000000": { - "address": "0x0000000000000000000000000000000000000000", - "chainID": 5, - "contractAddress": "0x0000000000000000000000000000000000000000", - "contractName": "Mock ERC20 Contract", - "decimals": 18, - "tokenSymbol": "ERC20", - "type": "ERC20", - }, "0x891D3e3f3C7eB90506D6705E643C53a05D390A3E": { "address": "0x891D3e3f3C7eB90506D6705E643C53a05D390A3E", "chainID": 5, diff --git a/packages/transaction-decoder/test/snapshots/decoder/0x65954673ecca8c3c8939088f2eaa38ddad3cc2a96ab47c6b6a167548cf2da175.snapshot b/packages/transaction-decoder/test/snapshots/decoder/0x65954673ecca8c3c8939088f2eaa38ddad3cc2a96ab47c6b6a167548cf2da175.snapshot index b7d77aac..68efc7b9 100644 --- a/packages/transaction-decoder/test/snapshots/decoder/0x65954673ecca8c3c8939088f2eaa38ddad3cc2a96ab47c6b6a167548cf2da175.snapshot +++ b/packages/transaction-decoder/test/snapshots/decoder/0x65954673ecca8c3c8939088f2eaa38ddad3cc2a96ab47c6b6a167548cf2da175.snapshot @@ -1,14 +1,5 @@ { "addressesMeta": { - "0x0000000000000000000000000000000000000000": { - "address": "0x0000000000000000000000000000000000000000", - "chainID": 5, - "contractAddress": "0x0000000000000000000000000000000000000000", - "contractName": "Mock ERC20 Contract", - "decimals": 18, - "tokenSymbol": "ERC20", - "type": "ERC20", - }, "0x2bF4761d2f9002c0a24fBAC3A65B794f0561BB7F": { "address": "0x2bF4761d2f9002c0a24fBAC3A65B794f0561BB7F", "chainID": 5, diff --git a/packages/transaction-decoder/test/snapshots/decoder/0x695ed05f74461801e15d68768b784473de034156a81f12f3485bc157e4ac60c9.snapshot b/packages/transaction-decoder/test/snapshots/decoder/0x695ed05f74461801e15d68768b784473de034156a81f12f3485bc157e4ac60c9.snapshot index 8418ef3e..6884cae2 100644 --- a/packages/transaction-decoder/test/snapshots/decoder/0x695ed05f74461801e15d68768b784473de034156a81f12f3485bc157e4ac60c9.snapshot +++ b/packages/transaction-decoder/test/snapshots/decoder/0x695ed05f74461801e15d68768b784473de034156a81f12f3485bc157e4ac60c9.snapshot @@ -1,14 +1,5 @@ { "addressesMeta": { - "0x0000000000000000000000000000000000000000": { - "address": "0x0000000000000000000000000000000000000000", - "chainID": 5, - "contractAddress": "0x0000000000000000000000000000000000000000", - "contractName": "Mock ERC20 Contract", - "decimals": 18, - "tokenSymbol": "ERC20", - "type": "ERC20", - }, "0x4BED3aE022FD201AB7185a9BC80cB8Bf9819bb80": { "address": "0x4BED3aE022FD201AB7185a9BC80cB8Bf9819bb80", "chainID": 5, diff --git a/packages/transaction-decoder/test/snapshots/decoder/0x867ee0665a0e37bb52e5c2211b122ee952c358ee1d509c45b1157c2b3be8313c.snapshot b/packages/transaction-decoder/test/snapshots/decoder/0x867ee0665a0e37bb52e5c2211b122ee952c358ee1d509c45b1157c2b3be8313c.snapshot index 9c7cd529..969b146c 100644 --- a/packages/transaction-decoder/test/snapshots/decoder/0x867ee0665a0e37bb52e5c2211b122ee952c358ee1d509c45b1157c2b3be8313c.snapshot +++ b/packages/transaction-decoder/test/snapshots/decoder/0x867ee0665a0e37bb52e5c2211b122ee952c358ee1d509c45b1157c2b3be8313c.snapshot @@ -1,14 +1,5 @@ { "addressesMeta": { - "0x0000000000000000000000000000000000000000": { - "address": "0x0000000000000000000000000000000000000000", - "chainID": 5, - "contractAddress": "0x0000000000000000000000000000000000000000", - "contractName": "Mock ERC20 Contract", - "decimals": 18, - "tokenSymbol": "ERC20", - "type": "ERC20", - }, "0x63c640dE6f9963A90AFdFa64fD44Ee97e2169a18": { "address": "0x63c640dE6f9963A90AFdFa64fD44Ee97e2169a18", "chainID": 5, diff --git a/packages/transaction-decoder/test/snapshots/decoder/0xabe682c315f7eb233c02618498b08dddf466513705d6402413016311844d3189.snapshot b/packages/transaction-decoder/test/snapshots/decoder/0xabe682c315f7eb233c02618498b08dddf466513705d6402413016311844d3189.snapshot index 842a4cd1..ef2cdb9f 100644 --- a/packages/transaction-decoder/test/snapshots/decoder/0xabe682c315f7eb233c02618498b08dddf466513705d6402413016311844d3189.snapshot +++ b/packages/transaction-decoder/test/snapshots/decoder/0xabe682c315f7eb233c02618498b08dddf466513705d6402413016311844d3189.snapshot @@ -1,14 +1,5 @@ { "addressesMeta": { - "0x0000000000000000000000000000000000000000": { - "address": "0x0000000000000000000000000000000000000000", - "chainID": 1, - "contractAddress": "0x0000000000000000000000000000000000000000", - "contractName": "Mock ERC20 Contract", - "decimals": 18, - "tokenSymbol": "ERC20", - "type": "ERC20", - }, "0x2aB047c293fC5b431d2e17A5A0a33467D7Ce3440": { "address": "0x2aB047c293fC5b431d2e17A5A0a33467D7Ce3440", "chainID": 1, diff --git a/packages/transaction-decoder/test/snapshots/decoder/0xbb07f5dbff6c4b7bdd26d0f9e2f7c6d41fd8f6eb5a5697832c45a738518cd284.snapshot b/packages/transaction-decoder/test/snapshots/decoder/0xbb07f5dbff6c4b7bdd26d0f9e2f7c6d41fd8f6eb5a5697832c45a738518cd284.snapshot index 95a1d562..428cb475 100644 --- a/packages/transaction-decoder/test/snapshots/decoder/0xbb07f5dbff6c4b7bdd26d0f9e2f7c6d41fd8f6eb5a5697832c45a738518cd284.snapshot +++ b/packages/transaction-decoder/test/snapshots/decoder/0xbb07f5dbff6c4b7bdd26d0f9e2f7c6d41fd8f6eb5a5697832c45a738518cd284.snapshot @@ -1,14 +1,5 @@ { "addressesMeta": { - "0x0000000000000000000000000000000000000000": { - "address": "0x0000000000000000000000000000000000000000", - "chainID": 1, - "contractAddress": "0x0000000000000000000000000000000000000000", - "contractName": "Mock ERC20 Contract", - "decimals": 18, - "tokenSymbol": "ERC20", - "type": "ERC20", - }, "0x5b87C8323352C57Dac33884154aACE8b3D593A07": { "address": "0x5b87C8323352C57Dac33884154aACE8b3D593A07", "chainID": 1, diff --git a/packages/transaction-decoder/test/snapshots/decoder/0xde9f6210899218e17a3e71661ead5e16da228e168b0572b1ddc30a967968f8f6.snapshot b/packages/transaction-decoder/test/snapshots/decoder/0xde9f6210899218e17a3e71661ead5e16da228e168b0572b1ddc30a967968f8f6.snapshot index 6a846b8c..72bb77bf 100644 --- a/packages/transaction-decoder/test/snapshots/decoder/0xde9f6210899218e17a3e71661ead5e16da228e168b0572b1ddc30a967968f8f6.snapshot +++ b/packages/transaction-decoder/test/snapshots/decoder/0xde9f6210899218e17a3e71661ead5e16da228e168b0572b1ddc30a967968f8f6.snapshot @@ -1,14 +1,5 @@ { "addressesMeta": { - "0x0000000000000000000000000000000000000000": { - "address": "0x0000000000000000000000000000000000000000", - "chainID": 1, - "contractAddress": "0x0000000000000000000000000000000000000000", - "contractName": "Mock ERC20 Contract", - "decimals": 18, - "tokenSymbol": "ERC20", - "type": "ERC20", - }, "0x018008bfb33d285247A21d44E50697654f754e63": { "address": "0x018008bfb33d285247A21d44E50697654f754e63", "chainID": 1, diff --git a/packages/transaction-decoder/test/snapshots/decoder/0xe6acfd7099db3c9bf94e7dab0052a9363cc9b6a33d91449ccc523bdd5d99861c.snapshot b/packages/transaction-decoder/test/snapshots/decoder/0xe6acfd7099db3c9bf94e7dab0052a9363cc9b6a33d91449ccc523bdd5d99861c.snapshot index 7fd9ec8d..36ae9728 100644 --- a/packages/transaction-decoder/test/snapshots/decoder/0xe6acfd7099db3c9bf94e7dab0052a9363cc9b6a33d91449ccc523bdd5d99861c.snapshot +++ b/packages/transaction-decoder/test/snapshots/decoder/0xe6acfd7099db3c9bf94e7dab0052a9363cc9b6a33d91449ccc523bdd5d99861c.snapshot @@ -1,14 +1,5 @@ { "addressesMeta": { - "0x0000000000000000000000000000000000000000": { - "address": "0x0000000000000000000000000000000000000000", - "chainID": 1, - "contractAddress": "0x0000000000000000000000000000000000000000", - "contractName": "Mock ERC20 Contract", - "decimals": 18, - "tokenSymbol": "ERC20", - "type": "ERC20", - }, "0x5b87C8323352C57Dac33884154aACE8b3D593A07": { "address": "0x5b87C8323352C57Dac33884154aACE8b3D593A07", "chainID": 1, diff --git a/packages/transaction-decoder/test/snapshots/decoder/0xf852866a9d10ab4713b00f3a012b7e60a48055a6f2cd99ae7efce205c390f710.snapshot b/packages/transaction-decoder/test/snapshots/decoder/0xf852866a9d10ab4713b00f3a012b7e60a48055a6f2cd99ae7efce205c390f710.snapshot index 700b7558..af0c2425 100644 --- a/packages/transaction-decoder/test/snapshots/decoder/0xf852866a9d10ab4713b00f3a012b7e60a48055a6f2cd99ae7efce205c390f710.snapshot +++ b/packages/transaction-decoder/test/snapshots/decoder/0xf852866a9d10ab4713b00f3a012b7e60a48055a6f2cd99ae7efce205c390f710.snapshot @@ -1,14 +1,5 @@ { "addressesMeta": { - "0x0000000000000000000000000000000000000000": { - "address": "0x0000000000000000000000000000000000000000", - "chainID": 1, - "contractAddress": "0x0000000000000000000000000000000000000000", - "contractName": "Mock ERC20 Contract", - "decimals": 18, - "tokenSymbol": "ERC20", - "type": "ERC20", - }, "0x0000000000A39bb272e79075ade125fd351887Ac": { "address": "0x0000000000A39bb272e79075ade125fd351887Ac", "chainID": 1, From 1e5319603bb7955097032ec8130036c767c12a38 Mon Sep 17 00:00:00 2001 From: Anastasia Rodionova Date: Mon, 6 Jan 2025 18:33:01 +0800 Subject: [PATCH 5/5] Add fixes and changeset --- .changeset/fifty-months-cover.md | 5 +++++ packages/transaction-decoder/src/abi-loader.ts | 17 +---------------- .../src/decoding/calldata-decode.ts | 16 ++++++++++++++-- .../src/decoding/log-decode.ts | 5 +++-- .../transaction-decoder/src/decoding/proxies.ts | 9 +++++---- 5 files changed, 28 insertions(+), 24 deletions(-) create mode 100644 .changeset/fifty-months-cover.md diff --git a/.changeset/fifty-months-cover.md b/.changeset/fifty-months-cover.md new file mode 100644 index 00000000..6a09d647 --- /dev/null +++ b/.changeset/fifty-months-cover.md @@ -0,0 +1,5 @@ +--- +'@3loop/transaction-decoder': patch +--- + +Improved perfomance of loading proxies by adding batching and cahching of request diff --git a/packages/transaction-decoder/src/abi-loader.ts b/packages/transaction-decoder/src/abi-loader.ts index ce25aac2..8832a1fc 100644 --- a/packages/transaction-decoder/src/abi-loader.ts +++ b/packages/transaction-decoder/src/abi-loader.ts @@ -314,22 +314,7 @@ export const getAndCacheAbi = (params: AbiParams) => return yield* Effect.fail(new EmptyCalldataError(params)) } - let implementation = null - - if (params.address !== '') { - implementation = yield* getProxyImplementation({ - address: getAddress(params.address), - chainID: params.chainID, - }) - } - - return yield* Effect.request( - new AbiLoader({ - ...params, - address: implementation?.address ?? params.address, - }), - AbiLoaderRequestResolver, - ) + return yield* Effect.request(new AbiLoader(params), AbiLoaderRequestResolver) }).pipe( Effect.withSpan('AbiLoader.GetAndCacheAbi', { attributes: { diff --git a/packages/transaction-decoder/src/decoding/calldata-decode.ts b/packages/transaction-decoder/src/decoding/calldata-decode.ts index 8b721caf..3ca79b41 100644 --- a/packages/transaction-decoder/src/decoding/calldata-decode.ts +++ b/packages/transaction-decoder/src/decoding/calldata-decode.ts @@ -1,10 +1,11 @@ import { Effect } from 'effect' -import { Hex, Address, encodeFunctionData } from 'viem' +import { Hex, Address, encodeFunctionData, isAddress, getAddress } from 'viem' import { AbiParams, AbiStore, ContractAbiResult, getAndCacheAbi, MissingABIError } from '../abi-loader.js' import * as AbiDecoder from './abi-decode.js' import { TreeNode } from '../types.js' import { PublicClient, RPCFetchError, UnknownNetwork } from '../public-client.js' import { SAFE_MULTISEND_ABI, SAFE_MULTISEND_SIGNATURE } from './constants.js' +import { getProxyImplementation } from './proxies.js' const callDataKeys = ['callData', 'data', '_data'] const addressKeys = ['to', 'target', '_target'] @@ -147,8 +148,19 @@ export const decodeMethod = ({ Effect.gen(function* () { const signature = data.slice(0, 10) + let implementationAddress: Address | undefined + + if (isAddress(contractAddress)) { + //if contract is a proxy, get the implementation address + const implementation = yield* getProxyImplementation({ address: getAddress(contractAddress), chainID }) + + if (implementation) { + implementationAddress = implementation.address + } + } + const abi = yield* getAndCacheAbi({ - address: contractAddress, + address: implementationAddress ?? contractAddress, signature, chainID, }) diff --git a/packages/transaction-decoder/src/decoding/log-decode.ts b/packages/transaction-decoder/src/decoding/log-decode.ts index 18835cdd..03c893cf 100644 --- a/packages/transaction-decoder/src/decoding/log-decode.ts +++ b/packages/transaction-decoder/src/decoding/log-decode.ts @@ -1,4 +1,4 @@ -import { type GetTransactionReturnType, type Log, decodeEventLog, getAbiItem, getAddress } from 'viem' +import { Address, type GetTransactionReturnType, type Log, decodeEventLog, getAbiItem, getAddress } from 'viem' import { Effect } from 'effect' import type { DecodedLogEvent, Interaction, RawDecodedLog } from '../types.js' import { getProxyImplementation } from './proxies.js' @@ -22,7 +22,8 @@ const decodedLog = (transaction: GetTransactionReturnType, logItem: Log) => const chainID = Number(transaction.chainId) const address = getAddress(logItem.address) - const abiAddress = address + const implementation = yield* getProxyImplementation({ address, chainID }) + const abiAddress = implementation?.address ?? address const [abiItem, contractData] = yield* Effect.all( [ diff --git a/packages/transaction-decoder/src/decoding/proxies.ts b/packages/transaction-decoder/src/decoding/proxies.ts index 3e82010e..39b2b83d 100644 --- a/packages/transaction-decoder/src/decoding/proxies.ts +++ b/packages/transaction-decoder/src/decoding/proxies.ts @@ -81,8 +81,6 @@ export const GetProxyResolver = RequestResolver.fromEffect( Effect.gen(function* () { // NOTE: Should we make this recursive when we have a Proxy of a Proxy? - if (request.address === ZERO_ADDRESS) return undefined - const effects = storageSlots.map((slot) => Effect.gen(function* () { const res: ProxyResult | undefined = { type: slot.type, address: '0x' } @@ -120,5 +118,8 @@ export const GetProxyResolver = RequestResolver.fromEffect( }), ).pipe(RequestResolver.contextFromEffect) -export const getProxyImplementation = ({ address, chainID }: { address: Address; chainID: number }) => - Effect.request(new ProxyLoader({ address, chainID }), GetProxyResolver).pipe(Effect.withRequestCaching(true)) +export const getProxyImplementation = ({ address, chainID }: { address: Address; chainID: number }) => { + if (address === ZERO_ADDRESS) return Effect.succeed(null) + + return Effect.request(new ProxyLoader({ address, chainID }), GetProxyResolver).pipe(Effect.withRequestCaching(true)) +}