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

Use tagged error and minor refactors #41

Merged
merged 2 commits into from
Apr 28, 2024
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
79 changes: 40 additions & 39 deletions packages/transaction-decoder/src/abi-loader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Context, Effect, Either, RequestResolver, Request, Array } from 'effect'
import { Context, Effect, Either, RequestResolver, Request, Array, Console } from 'effect'

Check warning on line 1 in packages/transaction-decoder/src/abi-loader.ts

View workflow job for this annotation

GitHub Actions / pr

'Console' is defined but never used
import { ContractABI, GetContractABIStrategy } from './abi-strategy/request-model.js'

export interface GetAbiParams {
Expand Down Expand Up @@ -38,7 +38,6 @@
if (requests.length === 0) return

const abiStore = yield* AbiStore
const strategies = abiStore.strategies
// NOTE: We can further optimize if we have match by Address by avoid extra requests for each signature
// but might need to update the Loader public API
const groups = Array.groupBy(requests, makeKey)
Expand All @@ -61,6 +60,7 @@
}

const set = (abi: ContractABI | null) => {
// NOTE: Now we ignore the null value, but we might want to store it to avoid pinging the same strategy again?
return abi ? abiStore.set(abi) : Effect.succeed(null)
}

Expand All @@ -85,55 +85,56 @@
},
)

const strategies = abiStore.strategies

// Load the ABI from the strategies
yield* Effect.forEach(remaining, ({ chainID, address, event, signature }) => {
const strategyResults = yield* Effect.forEach(remaining, ({ chainID, address, event, signature }) => {
const strategyRequest = GetContractABIStrategy({
address,
event,
signature,
chainID,
})

const allAvailableStrategies = [...(strategies[chainID] ?? []), ...strategies.default]
const allAvailableStrategies = Array.prependAll(strategies.default, strategies[chainID] ?? [])

return Effect.validateFirst(allAvailableStrategies, (strategy) => Effect.request(strategyRequest, strategy)).pipe(
Effect.orElseSucceed(() => null),
)
}).pipe(
Effect.flatMap((results) =>
Effect.forEach(
results,
(abi, i) => {
const request = remaining[i]
const { address, event, signature } = request

let result: string | null = null

const addressmatch = abi?.address?.[address]
if (addressmatch != null) {
result = addressmatch
}

const funcmatch = signature ? abi?.func?.[signature] : null
if (result == null && funcmatch != null) {
result = `[${funcmatch}]`
}

const eventmatch = event ? abi?.event?.[event] : null
if (result == null && eventmatch != null) {
result = `[${eventmatch}]`
}

const group = groups[makeKey(request)]

return Effect.zipRight(
set(abi),
Effect.forEach(group, (req) => Request.succeed(req, result), { discard: true }),
)
},
{ discard: true },
),
),
})

// Store results and resolve pending requests
yield* Effect.forEach(
strategyResults,
(abi, i) => {
const request = remaining[i]
const { address, event, signature } = request

let result: string | null = null

const addressmatch = abi?.address?.[address]
if (addressmatch != null) {
result = addressmatch
}

const funcmatch = signature ? abi?.func?.[signature] : null
if (result == null && funcmatch != null) {
result = `[${funcmatch}]`
}

const eventmatch = event ? abi?.event?.[event] : null
if (result == null && eventmatch != null) {
result = `[${eventmatch}]`
}

const group = groups[makeKey(request)]

return Effect.zipRight(
set(abi),
Effect.forEach(group, (req) => Request.succeed(req, result), { discard: true }),
)
},
{ discard: true },
)
}),
).pipe(RequestResolver.contextFromServices(AbiStore), Effect.withRequestCaching(true))
Expand Down
37 changes: 19 additions & 18 deletions packages/transaction-decoder/src/contract-meta-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ function makeKey(key: ContractMetaLoader) {
const ContractMetaLoaderRequestResolver = RequestResolver.makeBatched((requests: Array<ContractMetaLoader>) =>
Effect.gen(function* () {
const contractMetaStore = yield* ContractMetaStore
const strategies = contractMetaStore.strategies

const groups = Array.groupBy(requests, makeKey)
const uniqueRequests = Object.values(groups).map((group) => group[0])
Expand Down Expand Up @@ -79,32 +78,34 @@ const ContractMetaLoaderRequestResolver = RequestResolver.makeBatched((requests:
},
)

const strategies = contractMetaStore.strategies

// Resolve ContractMeta from the strategies
yield* Effect.forEach(remaining, ({ chainID, address }) => {
const strategyResults = yield* Effect.forEach(remaining, ({ chainID, address }) => {
const strategyRequest = GetContractMetaStrategy({
address,
chainID,
})
const allAvailableStrategies = [...(strategies[chainID] ?? []), ...strategies.default]

const allAvailableStrategies = Array.prependAll(strategies.default, strategies[chainID] ?? [])

return Effect.validateFirst(allAvailableStrategies, (strategy) => Effect.request(strategyRequest, strategy)).pipe(
Effect.orElseSucceed(() => null),
)
}).pipe(
Effect.flatMap((results) =>
Effect.forEach(
results,
(result, i) => {
const group = groups[makeKey(remaining[i])]

return Effect.zipRight(
set(remaining[i], result),
Effect.forEach(group, (req) => Request.succeed(req, result), { discard: true }),
)
},
{ discard: true },
),
),
})

// Store results and resolve pending requests
yield* Effect.forEach(
strategyResults,
(result, i) => {
const group = groups[makeKey(remaining[i])]

return Effect.zipRight(
set(remaining[i], result),
Effect.forEach(group, (req) => Request.succeed(req, result), { discard: true }),
)
},
{ discard: true },
)
}),
).pipe(RequestResolver.contextFromServices(ContractMetaStore), Effect.withRequestCaching(true))
Expand Down
49 changes: 30 additions & 19 deletions packages/transaction-decoder/src/decoding/abi-decode.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { formatAbiItem } from 'viem/utils'
import type { DecodeResult, MostTypes, TreeNode } from '../types.js'
import { Hex, Abi, decodeFunctionData, AbiParameter, AbiFunction, getAbiItem } from 'viem'
import { Data, Effect } from 'effect'
import { messageFromUnknown } from '../helpers/error.js'

export class DecodeError {
readonly _tag = 'DecodeError'
constructor(readonly error: unknown) {}
export class DecodeError extends Data.TaggedError('DecodeError')<{ message: string }> {
constructor(error: unknown) {
super({ message: `Failed to decode ${messageFromUnknown(error)}` })
}
}

export class MissingABIError {
readonly _tag = 'MissingABIError'
export class MissingABIError extends Data.TaggedError('DecodeError')<{ message: string }> {
constructor(
readonly address: string,
readonly signature: string,
readonly chainID: number,
) {}
) {
super({ message: `Missing ABI for ${address} with signature ${signature} on chain ${chainID}` })
}
}

function stringifyValue(value: MostTypes): string | string[] {
Expand Down Expand Up @@ -79,21 +83,28 @@ BigInt.prototype.toJSON = function () {
return this.toString()
}

export function decodeMethod(data: Hex, abi: Abi): DecodeResult | undefined {
const { functionName, args = [] } = decodeFunctionData({ abi, data })
export const decodeMethod = (data: Hex, abi: Abi): Effect.Effect<DecodeResult | undefined, DecodeError> =>
Effect.gen(function* () {
const { functionName, args = [] } = yield* Effect.try({
try: () => decodeFunctionData({ abi, data }),
catch: (error) => new DecodeError(error),
})

const method = getAbiItem({ abi, name: functionName, args }) as AbiFunction | undefined
const method = getAbiItem({ abi, name: functionName, args }) as AbiFunction | undefined

if (method != null) {
const signature = formatAbiItem(method)
if (method != null) {
const signature = yield* Effect.try({
try: () => formatAbiItem(method),
catch: (error) => new DecodeError(error),
})

const paramsTree = attachValues(method.inputs, args)
const paramsTree = attachValues(method.inputs, args)

return {
name: functionName,
signature,
type: 'function',
params: paramsTree,
return {
name: functionName,
signature,
type: 'function',
params: paramsTree,
}
}
}
}
})
6 changes: 3 additions & 3 deletions packages/transaction-decoder/src/decoding/log-decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
})

if (abiItem_ == null) {
return yield* Effect.fail(new AbiDecoder.MissingABIError(abiAddress, logItem.topics[0]!, chainID))
return yield* new AbiDecoder.MissingABIError(abiAddress, logItem.topics[0]!, chainID)

Check warning on line 30 in packages/transaction-decoder/src/decoding/log-decode.ts

View workflow job for this annotation

GitHub Actions / pr

Forbidden non-null assertion
}

const abiItem = JSON.parse(abiItem_) as Abi[]
Expand All @@ -46,15 +46,15 @@
})

if (args_ == null || eventName == null) {
return yield* Effect.fail(new AbiDecoder.DecodeError(`Could not decode log ${abiAddress}`))
return yield* new AbiDecoder.DecodeError(`Could not decode log ${abiAddress}`)
}

const args = args_ as any

const fragment = getAbiItem({ abi: abiItem, name: eventName })

if (fragment == null) {
return yield* Effect.fail(new AbiDecoder.DecodeError(`Could not find fragment in ABI ${abiAddress} ${eventName}`))
return yield* new AbiDecoder.DecodeError(`Could not find fragment in ABI ${abiAddress} ${eventName}`)
}

const decodedParams = yield* Effect.try({
Expand Down
24 changes: 9 additions & 15 deletions packages/transaction-decoder/src/decoding/trace-decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,27 +43,21 @@ const decodeTraceLog = (call: TraceLog, transaction: GetTransactionReturnType) =
})

if (abi_ == null) {
return yield* Effect.fail(new MissingABIError(contractAddress, signature, chainID))
return yield* new MissingABIError(contractAddress, signature, chainID)
}

const abi = JSON.parse(abi_) as Abi

return yield* Effect.try({
try: () => {
const method = decodeMethod(input as Hex, abi)
return {
...method,
from,
to,
} as DecodeTraceResult
},
catch: (e) => {
return new DecodeError(e)
},
})
const method = yield* decodeMethod(input as Hex, abi)

return {
...method,
from,
to,
} as DecodeTraceResult
}

return yield* Effect.fail(new DecodeError(`Could not decode trace log ${JSON.stringify(call, replacer)}`))
return yield* new DecodeError(`Could not decode trace log ${JSON.stringify(call, replacer)}`)
})

export const decodeTransactionTrace = ({
Expand Down
12 changes: 12 additions & 0 deletions packages/transaction-decoder/src/helpers/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function messageFromUnknown(cause: unknown, fallback?: string) {
if (typeof cause === 'string') {
return cause
}
if (cause instanceof Error) {
return cause.message
}
if (cause && typeof cause === 'object' && 'message' in cause && typeof cause.message === 'string') {
return cause.message
}
return fallback ?? 'An unknown error occurred'
}
Loading
Loading