Skip to content

Commit

Permalink
Improve proxy resolver and rpc bacthing (#193)
Browse files Browse the repository at this point in the history
* Detect proxy during getting abi and add proxy results caching

* Add in memory cache layer and requets debuggign in web app

* Fix custom error decoding

* Update tests

* Add fixes and changeset
  • Loading branch information
anastasiarods authored Jan 6, 2025
1 parent ab40931 commit d467b26
Show file tree
Hide file tree
Showing 25 changed files with 179 additions and 192 deletions.
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

0 comments on commit d467b26

Please sign in to comment.