Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new get trace method #19

Merged
merged 4 commits into from
Dec 22, 2023
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
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
Loading