Skip to content

Commit

Permalink
Add new get trace method (#19)
Browse files Browse the repository at this point in the history
* Refactor decode traces

* Refactor get second level calls

* Add support of debug_traceTransaction method

* Upd tests and readme
  • Loading branch information
anastasiarods authored Dec 22, 2023
1 parent b06a408 commit 7121c4c
Show file tree
Hide file tree
Showing 12 changed files with 217 additions and 88 deletions.
2 changes: 1 addition & 1 deletion apps/web/src/lib/contract-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const fetchAndCacheErc20Meta = ({
}) =>
Effect.gen(function* (_) {
const rpcService = yield* _(RPCProvider);
const provider = yield* _(rpcService.getProvider(chainID));
const { provider } = yield* _(rpcService.getProvider(chainID));

const inst = yield* _(
Effect.sync(() => new Contract(contractAddress, ERC20, provider)),
Expand Down
35 changes: 21 additions & 14 deletions apps/web/src/lib/rpc-provider.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
import { RPCProvider, UnknownNetwork } from "@3loop/transaction-decoder";
import {
RPCProvider,
UnknownNetwork,
RPCProviderObject,
} from "@3loop/transaction-decoder";
import { JsonRpcProvider } from "ethers";
import { Layer, Effect } from "effect";
import { supportedChains } from "@/app/data";

const urls: Record<number, string> = supportedChains.reduce(
(acc, { chainID, rpcUrl }) => {
return {
...acc,
[chainID]: rpcUrl,
};
},
{},
);
const providerConfigs = supportedChains.reduce((acc, config) => {
return {
...acc,
[config.chainID]: config,
};
}, {});

const providers: Record<number, JsonRpcProvider> = {};
const providers: Record<number, RPCProviderObject> = {};

export function getProvider(chainID: number): JsonRpcProvider | null {
export function getProvider(chainID: number): RPCProviderObject | null {
let provider = providers[chainID];
if (provider != null) {
return provider;
}

const url = urls[chainID];
const url = providerConfigs[chainID]?.rpcUrl;

if (url != null) {
provider = new JsonRpcProvider(url);
provider = {
provider: new JsonRpcProvider(url),
config: {
supportTraceAPI: providerConfigs[chainID]?.supportTraceAPI,
},
};
providers[chainID] = provider;
return provider;
}
Expand Down
11 changes: 6 additions & 5 deletions packages/transaction-decoder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ $ npm i @3loop/transaction-decoder

To begin using the Loop Decoder, you need to create an instance of the LoopDecoder class. At a minimum, you must provide three data loaders:

- `getProvider`: This function returns an ethers JsonRpcProvider based on the chain ID.
- `getProvider`: This function returns an object with ethers JsonRpcProvider based on the chain ID.
- `contractMetaStore`: This object has 2 properties `get` and `set` that returns and caches contract meta-information. See the `ContractData` type for the required properties.
- `abiStore`: Similarly, this object has 2 properties `get` and `set` that returns and cache the contract or fragment ABI based on the chain ID, address, and/or signature.

Expand All @@ -26,7 +26,7 @@ const db = {} // Your data source

const decoded = new TransactionDecoder({
getProvider: (chainId: number) => {
return new JsonRpcProvider(RPC_URL[chainId])
return {provider: new JsonRpcProvider(RPC_URL[chainId])}
},
abiStore: {
get: async (req: {
Expand Down Expand Up @@ -83,12 +83,13 @@ To get started with using the Decoder, first, you have to provide the RPC Provid
1. Create an RPC Provider

```ts
import { JsonRpcProvider } from 'ethers'
import { RPCProviderObject } from 'ethers'
import { RPCProviderObject } from '@3loop/transaction-decoder'
import { Effect } from 'effect'

const getProvider = (chainID: number): Effect.Effect<never, UnknownNetwork, JsonRpcProvider> => {
const getProvider = (chainID: number): Effect.Effect<never, UnknownNetwork, RPCProviderObject> => {
if (chainID === 5) {
return Effect.succeed(new JsonRpcProvider(GOERLI_RPC))
return { provider: Effect.succeed(new JsonRpcProvider(GOERLI_RPC)) }
}
return Effect.fail(new UnknownNetwork(chainID))
}
Expand Down
2 changes: 1 addition & 1 deletion packages/transaction-decoder/src/decoding/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const GetStorageSlot = Request.tagged<GetStorageSlot>('GetStorageSlot')
export const GetStorageSlotResolver = RequestResolver.fromFunctionEffect((request: GetStorageSlot) =>
Effect.gen(function* (_) {
const service = yield* _(RPCProvider)
const provider = yield* _(service.getProvider(request.chainID))
const { provider } = yield* _(service.getProvider(request.chainID))
const effects = storageSlots.map((slot) =>
Effect.tryPromise({
try: async () => {
Expand Down
34 changes: 11 additions & 23 deletions packages/transaction-decoder/src/decoding/trace-decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,16 @@ import type { CallTraceLog, TraceLog } from '../schema/trace.js'
import { DecodeError, decodeMethod } from './abi-decode.js'
import { getAndCacheAbi } from '../abi-loader.js'

const pruneTraceRecursive = (calls: TraceLog[]): TraceLog[] => {
if (calls.length === 0) {
console.warn('ERROR! Faulty structure of multicall subtraces')
return []
}
if (calls[0].subtraces == null) {
return calls
}
const callsToRemove = calls[0].subtraces
let newRestOfCalls = [...calls]
for (let i = 0; i < callsToRemove; i++) {
newRestOfCalls = [newRestOfCalls[0], ...pruneTraceRecursive(newRestOfCalls.slice(1))]
newRestOfCalls.splice(1, 1)
function getSecondLevelCalls(trace: TraceLog[]) {
const secondLevelCalls: TraceLog[] = []

for (let i = 0; i < trace.length; i++) {
if (trace[i].traceAddress.length === 1) {
secondLevelCalls.push(trace[i])
}
}

return newRestOfCalls
return secondLevelCalls
}

const decodeTraceLog = (call: TraceLog, transaction: TransactionResponse) =>
Expand Down Expand Up @@ -75,15 +69,9 @@ export const decodeTransactionTrace = ({
return []
}

const secondLevelCallsCount = trace[0]?.subtraces
const secondLevelCalls: TraceLog[] = []
let callsToPrune = trace.slice(1)
for (let i = 0; i < secondLevelCallsCount; i++) {
callsToPrune = pruneTraceRecursive(callsToPrune)
secondLevelCalls.push(callsToPrune[0])
callsToPrune.shift()
}
if (callsToPrune.length > 0) {
const secondLevelCalls = getSecondLevelCalls(trace)

if (secondLevelCalls.length === 0) {
return []
}

Expand Down
86 changes: 86 additions & 0 deletions packages/transaction-decoder/src/helpers/trace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type { TraceLogTree, TraceLog, CallType } from '../schema/trace.js'

export function nodeToTraceLog(node: TraceLogTree, path: number[]): TraceLog | undefined {
let traceLog: TraceLog | undefined

const traceLogBase = {
subtraces: node.calls?.length ?? 0,
traceAddress: path,
result: {
output: node.output,
gasUsed: node.gasUsed,
},
}

switch (node.type) {
case 'CALL':
case 'DELEGATECALL':
case 'CALLCODE':
case 'STATICCALL':
traceLog = {
...traceLogBase,
type: 'call',
action: {
callType: node.type.toLowerCase() as CallType,
from: node.from,
to: node.to,
gas: node.gas,
input: node.input,
value: node.value ?? BigInt('0x0'),
},
}
break
case 'CREATE':
traceLog = {
...traceLogBase,
type: 'create',
action: {
from: node.from,
gas: node.gas,
value: node.value ?? BigInt('0x0'),
},
}
break
case 'SELFDESTRUCT':
traceLog = {
...traceLogBase,
type: 'suicide',
action: {
refundAddress: node.to,
address: node.from,
balance: node.value ?? BigInt('0x0'),
},
}
break
default:
traceLog = undefined
}

return traceLog
}

function visit(node: TraceLogTree, path: number[]) {
const children = node.calls
const transformedNode = nodeToTraceLog(node, path)
let result: TraceLog[] = []

if (children) {
for (let i = 0; i < children?.length; i++) {
const transformedChildNode = visit(children[i], [...path, i])

if (transformedChildNode) {
result = result.concat(transformedChildNode)
}
}
}

if (transformedNode) {
return result.concat(transformedNode)
} else {
return result
}
}

export function transformTraceTree(trace: TraceLogTree) {
return visit(trace, [])
}
15 changes: 10 additions & 5 deletions packages/transaction-decoder/src/provider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { Networkish, JsonRpcApiProviderOptions } from 'ethers'
import { JsonRpcProvider } from 'ethers'
import { Context, Effect } from 'effect'

Expand All @@ -12,12 +11,18 @@ export class RPCFetchError {
constructor(readonly reason: unknown) {}
}

export interface RPCProviderConfig {
readonly supportTraceAPI?: boolean
}

export interface RPCProviderObject {
provider: JsonRpcProvider
config?: RPCProviderConfig
}

export interface RPCProvider {
readonly _tag: 'RPCProvider'
readonly getProvider: (chainID: number) => Effect.Effect<never, UnknownNetwork, JsonRpcProvider>
readonly getProvider: (chainID: number) => Effect.Effect<never, UnknownNetwork, RPCProviderObject>
}

export const RPCProvider = Context.Tag<RPCProvider>('@3loop-decoder/RPCProvider')

export const RPCProviderLayer = (url: string, network?: Networkish, options?: JsonRpcApiProviderOptions) =>
Effect.sync(() => new JsonRpcProvider(url, network, options))
37 changes: 29 additions & 8 deletions packages/transaction-decoder/src/schema/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ const EthTraceActionCall = Schema.struct({
const EthTraceActionCreate = Schema.struct({
from: Address,
gas: bigintFromString,
init: Schema.string,
value: bigintFromString,
})

Expand All @@ -39,21 +38,15 @@ const EthTraceActionReward = Schema.struct({
})

const EthTraceResult = Schema.struct({
address: Schema.optional(Schema.string),
gasUsed: bigintFromString,
output: Schema.string,
code: Schema.optional(Schema.string),
})

const EthTraceBase = Schema.struct({
result: Schema.optional(EthTraceResult),
blockHash: Schema.string,
blockNumber: Schema.number,
error: Schema.optional(Schema.string),
subtraces: Schema.number,
traceAddress: Schema.array(Schema.number),
transactionHash: Schema.string,
transactionPosition: Schema.number,
error: Schema.optional(Schema.string),
})

const CallTrace = Schema.extend(
Expand Down Expand Up @@ -88,7 +81,35 @@ const SuicideTrace = Schema.extend(
}),
)

const DebugCallType = Schema.literal(
'CALL',
'DELEGATECALL',
'CALLCODE',
'STATICCALL',
'CREATE',
'SELFDESTRUCT',
'REWARD',
)

const EthDebugTraceBase = Schema.struct({
gas: bigintFromString,
to: Address,
from: Address,
gasUsed: bigintFromString,
input: Schema.string,
type: DebugCallType,
value: Schema.optional(bigintFromString),
output: Schema.string,
})

type DebugTraceLog = Schema.To<typeof EthDebugTraceBase>

export type TraceLogTree = {
calls?: Array<TraceLogTree>
} & DebugTraceLog

export const EthTrace = Schema.union(CallTrace, CreateTrace, RewardTrace, SuicideTrace)

export type TraceLog = Schema.To<typeof EthTrace>
export type CallTraceLog = Schema.To<typeof CallTrace>
export type CallType = Schema.To<typeof CallType>
Loading

0 comments on commit 7121c4c

Please sign in to comment.