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/fifty-months-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@3loop/transaction-decoder': patch
---

Improved perfomance of loading proxies by adding batching and cahching of request
4 changes: 1 addition & 3 deletions apps/web/src/app/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,6 @@ export const supportedChains: {
chainID: number
rpcUrl: string
traceAPI?: 'parity' | 'geth' | 'none'
batchMaxCount?: number
}[] = [
{
name: 'Ethereum Mainnet',
Expand All @@ -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',
Expand Down
19 changes: 15 additions & 4 deletions apps/web/src/lib/decode.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<AbiParams, ContractAbiResult>
| ContractMetaStore<ContractMetaParams, ContractMetaResult>
Expand All @@ -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,
Expand All @@ -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({
Expand Down
34 changes: 33 additions & 1 deletion apps/web/src/lib/rpc-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion packages/transaction-decoder/src/abi-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,6 +40,7 @@ export interface ContractAbiEmpty {
export type ContractAbiResult = ContractAbiSuccess | ContractAbiNotFound | ContractAbiEmpty

type ChainOrDefault = number | 'default'

export interface AbiStore<Key = AbiParams, Value = ContractAbiResult> {
readonly strategies: Record<ChainOrDefault, readonly ContractAbiResolverStrategy[]>
readonly set: (key: Key, value: Value) => Effect.Effect<void, never>
Expand Down
41 changes: 24 additions & 17 deletions packages/transaction-decoder/src/contract-meta-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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',
Expand Down
7 changes: 4 additions & 3 deletions packages/transaction-decoder/src/decoding/calldata-decode.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Effect } from 'effect'
import { isAddress, Hex, getAddress, encodeFunctionData, Address } from 'viem'
import { getProxyStorageSlot } from './proxies.js'
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']
Expand Down Expand Up @@ -147,11 +147,12 @@ 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 })
const implementation = yield* getProxyImplementation({ address: getAddress(contractAddress), chainID })

if (implementation) {
implementationAddress = implementation.address
Expand Down
1 change: 1 addition & 0 deletions packages/transaction-decoder/src/decoding/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ export const SAFE_MULTISEND_ABI: Abi = [
outputs: [],
},
]
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
14 changes: 4 additions & 10 deletions packages/transaction-decoder/src/decoding/log-decode.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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 { 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'
Expand All @@ -22,14 +22,8 @@ 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 implementation = yield* getProxyImplementation({ address, chainID })
const abiAddress = implementation?.address ?? address

const [abiItem, contractData] = yield* Effect.all(
[
Expand Down
Loading
Loading