diff --git a/.changeset/hot-gorillas-fly.md b/.changeset/hot-gorillas-fly.md new file mode 100644 index 00000000..2b19321e --- /dev/null +++ b/.changeset/hot-gorillas-fly.md @@ -0,0 +1,16 @@ +--- +'@3loop/transaction-decoder': minor +--- + +Refactor stores to call `set` when the data is not found in strategies. This introduces new types for store values, to +be able to differentiate between a missing value, and one that was never requested. + +- 1. `Success` - The data is found successfully in the store +- 2. `NotFound` - The data is found in the store, but is missing the value +- 3. `MetaEmpty` - The contract metadata is not found in the store + +This change requires the users of the library to persist the NotFound state. Having the NotFound state allows us +to skip the strategy lookup, which is one of the most expensive operations when decoding transactions. + +We suggest to keep a timestamp for the NotFound state, and invalidate it after a certain period of time. This will +ensure that the strategy lookup is not skipped indefinitely. Separately, users can upload their own data to the store. diff --git a/apps/web/src/lib/contract-loader.ts b/apps/web/src/lib/contract-loader.ts index bad7940f..78cd7f92 100644 --- a/apps/web/src/lib/contract-loader.ts +++ b/apps/web/src/lib/contract-loader.ts @@ -32,34 +32,28 @@ export const AbiStoreLive = Layer.succeed( }), ], }, - set: ({ address = {} }) => + set: (value) => Effect.gen(function* () { - const addressMatches = Object.entries(address) + if (value.status !== 'success' || value.result.type !== 'address') { + // TODO: Store it to avoid fetching again + return + } - // Cache all addresses - yield* Effect.all( - addressMatches.map(([key, value]) => - Effect.promise(() => - prisma.contractAbi - .create({ - data: { - address: key, - abi: value, - }, - }) - .catch((e) => { - console.error('Failed to cache abi', e) - return null - }), - ), - ), - { - concurrency: 'inherit', - batching: 'inherit', - }, + yield* Effect.promise(() => + prisma.contractAbi + .create({ + data: { + address: value.result.address.toLowerCase(), + abi: value.result.abi, + }, + }) + .catch((e) => { + console.error('Failed to cache abi', e) + return null + }), ) }), - get: ({ address }) => + get: ({ address, chainID }) => Effect.gen(function* () { const normAddress = address.toLowerCase() const cached = yield* Effect.promise(() => @@ -71,9 +65,21 @@ export const AbiStoreLive = Layer.succeed( ) if (cached != null) { - return cached.abi + return { + status: 'success', + result: { + type: 'address', + address: address, + abi: cached.abi, + chainID: chainID, + }, + } + } + + return { + status: 'empty', + result: null, } - return null }), }), ) @@ -103,17 +109,32 @@ export const ContractMetaStoreLive = Layer.effect( }) as Promise, ).pipe(Effect.catchAll((_) => Effect.succeed(null))) - return data + if (data == null) { + return { + status: 'empty', + result: null, + } + } else { + return { + status: 'success', + result: data, + } + } }), set: ({ address, chainID }, contractMeta) => Effect.gen(function* () { const normAddress = address.toLowerCase() + if (contractMeta.result == null) { + // TODO: Store it to avoid fetching again + return + } + yield* Effect.tryPromise(() => prisma.contractMeta.create({ data: { - ...contractMeta, - decimals: contractMeta.decimals ?? 0, + ...contractMeta.result, + decimals: contractMeta.result.decimals ?? 0, address: normAddress, chainID: chainID, }, diff --git a/packages/transaction-decoder/README.md b/packages/transaction-decoder/README.md index 8c7e123d..0788cb75 100644 --- a/packages/transaction-decoder/README.md +++ b/packages/transaction-decoder/README.md @@ -33,33 +33,19 @@ const decoded = new TransactionDecoder({ } }, abiStore: { - get: async (req: { - chainID: number - address: string - event?: string | undefined - signature?: string | undefined - }) => { + get: async (req: GetAbiParams) => { return db.getContractAbi(req) }, - set: async (req: { - address?: Record - signature?: Record - }) => { + set: async (req: ContractAbiResult) => { await db.setContractAbi(req) }, }, contractMetaStore: { - get: async (req: { - address: string - chainID: number - }) => { + get: async (req: ContractMetaParams) => { return db.getContractMeta(req) }, - set: async (req: { - address: string - chainID: number - }) { - // NOTE: not yet called as we do not have any automatic resolve strategy + set: async (req: ContractMetaParams, val: ContractMetaResult) { + await db.setContractMeta(req, val) }, }, }) @@ -113,11 +99,11 @@ const AbiStoreLive = Layer.succeed( AbiStore, AbiStore.of({ strategies: { default: [] }, - set: ({ address = {}, func = {}, event = {} }) => + set: (result: ContractAbiResult) => Effect.sync(() => { // NOTE: Ignore caching as we relay only on local abis }), - get: ({ address, signature, event }) => + get: ({ address, signature, event, chainID }) => Effect.sync(() => { const signatureAbiMap = { '0x3593564c': 'execute(bytes,bytes[],uint256)', @@ -131,10 +117,19 @@ const AbiStoreLive = Layer.succeed( const abi = signatureAbiMap[signature] if (abi) { - return abi + return { + type: 'func', + abi: `[${abi}]`, + address, + chainID: chainID, + signature, + } } - return null + return { + status: 'empty', + result: null, + } }), }), ) @@ -150,6 +145,8 @@ export const MetaStoreLive = Layer.succeed( ContractMetaStore.of({ get: ({ address, chainID }) => Effect.sync(() => { return { + status: 'success', + result: { address: request.address, chainID: request.chainID, contractName: 'Mock Contract', @@ -157,9 +154,10 @@ export const MetaStoreLive = Layer.succeed( tokenSymbol: 'MOCK', decimals: 18, type: ContractType.ERC20, + }, } }), - set: ({ address, chainID }) => Effect.sync(() => { + set: () => Effect.sync(() => { // NOTE: Ignore for now }), }) diff --git a/packages/transaction-decoder/src/abi-loader.ts b/packages/transaction-decoder/src/abi-loader.ts index 6a25f0a9..fe1caefe 100644 --- a/packages/transaction-decoder/src/abi-loader.ts +++ b/packages/transaction-decoder/src/abi-loader.ts @@ -1,6 +1,7 @@ import { Context, Effect, Either, RequestResolver, Request, Array, pipe } from 'effect' import { ContractABI, GetContractABIStrategy } from './abi-strategy/request-model.js' +const STRATEGY_TIMEOUT = 5000 export interface GetAbiParams { chainID: number address: string @@ -8,11 +9,27 @@ export interface GetAbiParams { signature?: string | undefined } -type ChainOrDefault = number | 'default' +export interface ContractAbiSuccess { + status: 'success' + result: ContractABI +} + +export interface ContractAbiNotFound { + status: 'not-found' + result: null +} + +export interface ContractAbiEmpty { + status: 'empty' + result: null +} + +export type ContractAbiResult = ContractAbiSuccess | ContractAbiNotFound | ContractAbiEmpty -export interface AbiStore { +type ChainOrDefault = number | 'default' +export interface AbiStore { readonly strategies: Record[]> - readonly set: (value: SetValue) => Effect.Effect + readonly set: (value: Value) => Effect.Effect readonly get: (arg: Key) => Effect.Effect readonly getMany?: (arg: Array) => Effect.Effect, never> } @@ -50,36 +67,20 @@ const getMany = (requests: Array) => } }) -const setOnValue = (abi: ContractABI | null) => +const setValue = (abi: ContractABI | null) => Effect.gen(function* () { const { set } = yield* AbiStore - // NOTE: Now we ignore the null value, but we might want to store it to avoid pinging the same strategy again? - if (abi) yield* set(abi) + yield* set(abi == null ? { status: 'not-found', result: null } : { status: 'success', result: abi }) }) -const getBestMatch = (abi: ContractABI | null, request: AbiLoader) => { +const getBestMatch = (abi: ContractABI | null) => { if (abi == null) return null - 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}]` + if (abi.type === 'address') { + return abi.abi } - return result + return `[${abi.abi}]` } /** @@ -133,7 +134,9 @@ const AbiLoaderRequestResolver = RequestResolver.makeBatched((requests: Array { - return resp == null ? Either.left(uniqueRequests[i]) : Either.right([uniqueRequests[i], resp] as const) + return resp.status === 'empty' + ? Either.left(uniqueRequests[i]) + : Either.right([uniqueRequests[i], resp.result] as const) }), ), Effect.orElseSucceed(() => [uniqueRequests, []] as const), @@ -144,7 +147,8 @@ const AbiLoaderRequestResolver = RequestResolver.makeBatched((requests: Array { const group = groups[makeKey(request)] - return Effect.forEach(group, (req) => Request.succeed(req, result), { discard: true }) + const abi = result?.abi ?? null + return Effect.forEach(group, (req) => Request.succeed(req, abi), { discard: true }) }, { discard: true, @@ -162,7 +166,9 @@ const AbiLoaderRequestResolver = RequestResolver.makeBatched((requests: Array Effect.request(strategyRequest, strategy)).pipe( + Effect.timeout(STRATEGY_TIMEOUT), Effect.orElseSucceed(() => null), ) }) @@ -172,12 +178,12 @@ const AbiLoaderRequestResolver = RequestResolver.makeBatched((requests: Array { const request = remaining[i] - const result = getBestMatch(abi, request) + const result = getBestMatch(abi) const group = groups[makeKey(request)] return Effect.zipRight( - setOnValue(abi), + setValue(abi), Effect.forEach(group, (req) => Request.succeed(req, result), { discard: true }), ) }, @@ -186,11 +192,12 @@ const AbiLoaderRequestResolver = RequestResolver.makeBatched((requests: Array { if (params.event === '0x' || params.signature === '0x') { return Effect.succeed(null) } - return Effect.withSpan(Effect.request(AbiLoader(params), AbiLoaderRequestResolver), 'GetAndCacheAbi', { + return Effect.withSpan(Effect.request(AbiLoader(params), AbiLoaderRequestResolver), 'AbiLoader.GetAndCacheAbi', { attributes: { chainID: params.chainID, address: params.address, diff --git a/packages/transaction-decoder/src/abi-strategy/blockscout-abi.ts b/packages/transaction-decoder/src/abi-strategy/blockscout-abi.ts index 31e84b81..04b728f0 100644 --- a/packages/transaction-decoder/src/abi-strategy/blockscout-abi.ts +++ b/packages/transaction-decoder/src/abi-strategy/blockscout-abi.ts @@ -4,7 +4,7 @@ import * as RequestModel from './request-model.js' async function fetchContractABI( { address, chainID }: RequestModel.GetContractABIStrategy, config: { apikey?: string; endpoint: string }, -) { +): Promise { const endpoint = config.endpoint const params: Record = { @@ -24,9 +24,10 @@ async function fetchContractABI( if (json.status === '1') { return { - address: { - [address]: json.result, - }, + chainID, + address, + abi: json.result, + type: 'address', } } diff --git a/packages/transaction-decoder/src/abi-strategy/etherscan-abi.ts b/packages/transaction-decoder/src/abi-strategy/etherscan-abi.ts index a79e80aa..bb82ccad 100644 --- a/packages/transaction-decoder/src/abi-strategy/etherscan-abi.ts +++ b/packages/transaction-decoder/src/abi-strategy/etherscan-abi.ts @@ -1,4 +1,4 @@ -import { Effect, RequestResolver, pipe } from 'effect' +import { Effect, RequestResolver } from 'effect' import * as RequestModel from './request-model.js' const endpoints: { [k: number]: string } = { @@ -50,7 +50,7 @@ const endpoints: { [k: number]: string } = { async function fetchContractABI( { address, chainID }: RequestModel.GetContractABIStrategy, config?: { apikey?: string; endpoint?: string }, -) { +): Promise { const endpoint = config?.endpoint ?? endpoints[chainID] const params: Record = { @@ -70,9 +70,10 @@ async function fetchContractABI( if (json.status === '1') { return { - address: { - [address]: json.result, - }, + type: 'address', + address, + chainID, + abi: json.result, } } diff --git a/packages/transaction-decoder/src/abi-strategy/fourbyte-abi.ts b/packages/transaction-decoder/src/abi-strategy/fourbyte-abi.ts index 37cdf5cf..0feb1b09 100644 --- a/packages/transaction-decoder/src/abi-strategy/fourbyte-abi.ts +++ b/packages/transaction-decoder/src/abi-strategy/fourbyte-abi.ts @@ -32,21 +32,25 @@ async function fetchABI({ const json = (await full_match.json()) as FourBytesResponse return { - func: { - [signature]: parseFunctionSignature(json.results[0]?.text_signature), - }, + type: 'func', + address, + chainID, + abi: parseFunctionSignature(json.results[0]?.text_signature), + signature, } } } if (event != null) { - const partial_match = await fetch(`${endpoint}/event-signatures/?hex_signature=${signature}`) + const partial_match = await fetch(`${endpoint}/event-signatures/?hex_signature=${event}`) if (partial_match.status === 200) { const json = (await partial_match.json()) as FourBytesResponse return { - event: { - [event]: parseEventSignature(json.results[0]?.text_signature), - }, + type: 'event', + address, + chainID, + abi: parseEventSignature(json.results[0]?.text_signature), + event, } } } diff --git a/packages/transaction-decoder/src/abi-strategy/openchain-abi.ts b/packages/transaction-decoder/src/abi-strategy/openchain-abi.ts index c396111e..224e6983 100644 --- a/packages/transaction-decoder/src/abi-strategy/openchain-abi.ts +++ b/packages/transaction-decoder/src/abi-strategy/openchain-abi.ts @@ -50,21 +50,25 @@ async function fetchABI({ const json = (await response.json()) as OpenchainResponse return { - func: { - [signature]: parseFunctionSignature(json.result.function[signature][0].name), - }, + type: 'func', + address, + chainID, + abi: parseFunctionSignature(json.result.function[signature][0].name), + signature, } } } if (event != null) { - const response = await fetch(`${endpoint}?event=${signature}`, options) + const response = await fetch(`${endpoint}?event=${event}`, options) if (response.status === 200) { const json = (await response.json()) as OpenchainResponse return { - event: { - [event]: parseEventSignature(json.result.event[event][0].name), - }, + type: 'event', + address, + chainID, + abi: parseEventSignature(json.result.event[event][0].name), + event, } } } diff --git a/packages/transaction-decoder/src/abi-strategy/request-model.ts b/packages/transaction-decoder/src/abi-strategy/request-model.ts index bc9e19a9..158a1e30 100644 --- a/packages/transaction-decoder/src/abi-strategy/request-model.ts +++ b/packages/transaction-decoder/src/abi-strategy/request-model.ts @@ -16,13 +16,32 @@ export class ResolveStrategyABIError { ) {} } -//NOTE: we store address as key to be able to know adddress to abi mapping for caching -export interface ContractABI { - address?: Record - func?: Record - event?: Record +interface FunctionFragmentABI { + type: 'func' + abi: string + address: string + chainID: number + signature: string } +interface EventFragmentABI { + type: 'event' + abi: string + address: string + chainID: number + event: string +} + +interface AddressABI { + type: 'address' + abi: string + address: string + chainID: number +} + +export type ContractABI = FunctionFragmentABI | EventFragmentABI | AddressABI + +// NOTE: We might want to return a list of ABIs, this might be helpful when fetching for signature export interface GetContractABIStrategy extends Request.Request, FetchABIParams { readonly _tag: 'GetContractABIStrategy' } diff --git a/packages/transaction-decoder/src/abi-strategy/sourcify-abi.ts b/packages/transaction-decoder/src/abi-strategy/sourcify-abi.ts index 2806d248..5a0da4ca 100644 --- a/packages/transaction-decoder/src/abi-strategy/sourcify-abi.ts +++ b/packages/transaction-decoder/src/abi-strategy/sourcify-abi.ts @@ -10,7 +10,10 @@ interface SourcifyResponse { const endpoint = 'https://repo.sourcify.dev/contracts/' -async function fetchContractABI({ address, chainID }: RequestModel.GetContractABIStrategy) { +async function fetchContractABI({ + address, + chainID, +}: RequestModel.GetContractABIStrategy): Promise { const normalisedAddress = getAddress(address) const full_match = await fetch(`${endpoint}/full_match/${chainID}/${normalisedAddress}/metadata.json`) @@ -19,9 +22,10 @@ async function fetchContractABI({ address, chainID }: RequestModel.GetContractAB const json = (await full_match.json()) as SourcifyResponse return { - address: { - [address]: JSON.stringify(json.output.abi), - }, + type: 'address', + address, + chainID, + abi: JSON.stringify(json.output.abi), } } @@ -29,9 +33,10 @@ async function fetchContractABI({ address, chainID }: RequestModel.GetContractAB if (partial_match.status === 200) { const json = (await partial_match.json()) as SourcifyResponse return { - address: { - [address]: JSON.stringify(json.output.abi), - }, + type: 'address', + address, + chainID, + abi: JSON.stringify(json.output.abi), } } diff --git a/packages/transaction-decoder/src/contract-meta-loader.ts b/packages/transaction-decoder/src/contract-meta-loader.ts index c2f70741..71ec86eb 100644 --- a/packages/transaction-decoder/src/contract-meta-loader.ts +++ b/packages/transaction-decoder/src/contract-meta-loader.ts @@ -3,19 +3,49 @@ import { ContractData } from './types.js' import { GetContractMetaStrategy } from './meta-strategy/request-model.js' import { Address } from 'viem' +const STRATEGY_TIMEOUT = 5000 + export interface ContractMetaParams { address: string chainID: number } +interface ContractMetaSuccess { + status: 'success' + result: ContractData +} + +interface ContractMetaNotFound { + status: 'not-found' + result: null +} + +interface ContractMetaEmpty { + status: 'empty' + result: null +} + +export type ContractMetaResult = ContractMetaSuccess | ContractMetaNotFound | ContractMetaEmpty + type ChainOrDefault = number | 'default' -// NOTE: Maybe we can avoid passing RPCProvider and let the user provide it? -export interface ContractMetaStore { +export interface ContractMetaStore { readonly strategies: Record[]> readonly set: (arg: Key, value: Value) => Effect.Effect - readonly get: (arg: Key) => Effect.Effect - readonly getMany?: (arg: Array) => Effect.Effect, never> + /** + * The `get` function might return 3 states: + * 1. `ContractMetaSuccess` - The contract metadata is found in the store + * 2. `ContractMetaNotFound` - The contract metadata is found in the store, but is missing value + * 3. `ContractMetaEmpty` - The contract metadata is not found in the store + * + * We have state 2 to be able to skip the meta strategy in case we know that it's not available + * this can significantly reduce the number of requests to the strategies, and improve performance. + * + * Some strategies might be able to add the data later, because of that we encurage to store a timestamp + * and remove the NotFound state to be able to check again. + */ + readonly get: (arg: Key) => Effect.Effect + readonly getMany?: (arg: Array) => Effect.Effect, never> } export const ContractMetaStore = Context.GenericTag('@3loop-decoder/ContractMetaStore') @@ -49,10 +79,13 @@ const getMany = (requests: Array) => } }) -const setOnValue = ({ chainID, address }: ContractMetaLoader, result: ContractData | null) => +const setValue = ({ chainID, address }: ContractMetaLoader, result: ContractData | null) => Effect.gen(function* () { const { set } = yield* ContractMetaStore - if (result) yield* set({ chainID, address }, result) + yield* set( + { chainID, address }, + result == null ? { status: 'not-found', result: null } : { status: 'success', result }, + ) }) /** @@ -93,7 +126,9 @@ const ContractMetaLoaderRequestResolver = RequestResolver.makeBatched((requests: getMany(uniqueRequests), Effect.map( Array.partitionMap((resp, i) => { - return resp == null ? Either.left(uniqueRequests[i]) : Either.right([uniqueRequests[i], resp] as const) + return resp.status === 'empty' + ? Either.left(uniqueRequests[i]) + : Either.right([uniqueRequests[i], resp.result] as const) }), ), Effect.orElseSucceed(() => [uniqueRequests, []] as const), @@ -120,7 +155,9 @@ const ContractMetaLoaderRequestResolver = RequestResolver.makeBatched((requests: 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) => Effect.request(strategyRequest, strategy)).pipe( + Effect.timeout(STRATEGY_TIMEOUT), Effect.orElseSucceed(() => null), ) }) @@ -132,7 +169,7 @@ const ContractMetaLoaderRequestResolver = RequestResolver.makeBatched((requests: const group = groups[makeKey(remaining[i])] return Effect.zipRight( - setOnValue(remaining[i], result), + setValue(remaining[i], result), Effect.forEach(group, (req) => Request.succeed(req, result), { discard: true }), ) }, diff --git a/packages/transaction-decoder/src/decoding/log-decode.ts b/packages/transaction-decoder/src/decoding/log-decode.ts index cc34c9d9..23df935a 100644 --- a/packages/transaction-decoder/src/decoding/log-decode.ts +++ b/packages/transaction-decoder/src/decoding/log-decode.ts @@ -41,8 +41,8 @@ const decodedLog = (transaction: GetTransactionReturnType, logItem: Log) => data: logItem.data, strict: false, }), - catch: () => { - Effect.logWarning(`Could not decode log ${abiAddress} ${stringify(logItem)}`) + catch: (err) => { + console.error(`Could not decode log ${abiAddress} ${stringify(logItem)}`, err) }, }) diff --git a/packages/transaction-decoder/src/transaction-loader.ts b/packages/transaction-decoder/src/transaction-loader.ts index af739d61..01bc2fbe 100644 --- a/packages/transaction-decoder/src/transaction-loader.ts +++ b/packages/transaction-decoder/src/transaction-loader.ts @@ -1,4 +1,4 @@ -import { Effect, pipe } from 'effect' +import { Effect } from 'effect' import * as Schema from '@effect/schema/Schema' import { RPCFetchError, PublicClient } from './public-client.js' import type { TraceLog, TraceLogTree } from './schema/trace.js' diff --git a/packages/transaction-decoder/src/vanilla.ts b/packages/transaction-decoder/src/vanilla.ts index ed6cb688..d543c595 100644 --- a/packages/transaction-decoder/src/vanilla.ts +++ b/packages/transaction-decoder/src/vanilla.ts @@ -1,10 +1,13 @@ import { Effect, Context, Logger, LogLevel, RequestResolver } from 'effect' import { PublicClient, PublicClientObject, UnknownNetwork } from './public-client.js' -import { ContractData } from './types.js' import { decodeTransactionByHash, decodeCalldata } from './transaction-decoder.js' -import { AbiStore as EffectAbiStore, GetAbiParams } from './abi-loader.js' -import { ContractMetaParams, ContractMetaStore as EffectContractMetaStore } from './contract-meta-loader.js' -import { ContractABI, GetContractABIStrategy } from './abi-strategy/index.js' +import { ContractAbiResult, AbiStore as EffectAbiStore, GetAbiParams } from './abi-loader.js' +import { + ContractMetaParams, + ContractMetaResult, + ContractMetaStore as EffectContractMetaStore, +} from './contract-meta-loader.js' +import { GetContractABIStrategy } from './abi-strategy/index.js' import { Hex } from 'viem' import { GetContractMetaStrategy } from './meta-strategy/request-model.js' @@ -17,16 +20,16 @@ export interface TransactionDecoderOptions { export interface VanillaAbiStore { strategies?: readonly RequestResolver.RequestResolver[] - get: (key: GetAbiParams) => Promise - set: (val: ContractABI) => Promise + get: (key: GetAbiParams) => Promise + set: (val: ContractAbiResult) => Promise } type VanillaContractMetaStategy = (client: PublicClient) => RequestResolver.RequestResolver export interface VanillaContractMetaStore { strategies?: readonly VanillaContractMetaStategy[] - get: (key: ContractMetaParams) => Promise - set: (key: ContractMetaParams, val: ContractData) => Promise + get: (key: ContractMetaParams) => Promise + set: (key: ContractMetaParams, val: ContractMetaResult) => Promise } // TODO: allow adding custom strategies to vanilla API diff --git a/packages/transaction-decoder/test/mocks/abi-loader-mock.ts b/packages/transaction-decoder/test/mocks/abi-loader-mock.ts index 3a79fca0..baecfa2d 100644 --- a/packages/transaction-decoder/test/mocks/abi-loader-mock.ts +++ b/packages/transaction-decoder/test/mocks/abi-loader-mock.ts @@ -1,4 +1,4 @@ -import { Effect, Layer } from 'effect' +import { Effect, Layer, Match } from 'effect' import fs from 'node:fs' import { AbiStore } from '@/abi-loader.js' // import { FourByteStrategyResolver } from '@/effect.js' @@ -14,32 +14,48 @@ export const MockedAbiStoreLive = Layer.succeed( // FourByteStrategyResolver(), ], }, - set: ({ address = {}, func = {}, event = {} }) => + set: (response) => Effect.gen(function* () { - const addressMatches = Object.entries(address) - const sigMatches = Object.entries(func).concat(Object.entries(event)) - - // Cache all addresses - yield* Effect.all( - addressMatches.map(([key, value]) => - Effect.sync(() => fs.writeFileSync(`./test/mocks/abi/${key.toLowerCase()}.json`, value)), - ), - ) + if (response.status !== 'success') return - // Cache all signatures - yield* Effect.all( - sigMatches.map(([key, value]) => - Effect.sync(() => fs.writeFileSync(`./test/mocks/abi/${key.toLowerCase()}.json`, value)), - ), + const { key, value } = Match.value(response.result).pipe( + Match.when({ type: 'address' } as const, (value) => ({ + key: value.address.toLowerCase(), + value: value.abi, + })), + Match.when({ type: 'func' } as const, (value) => ({ + key: value.signature.toLowerCase(), + value: value.abi, + })), + Match.when({ type: 'event' } as const, (value) => ({ + key: value.event.toLowerCase(), + value: value.abi, + })), + Match.exhaustive, ) + + yield* Effect.sync(() => fs.writeFileSync(`./test/mocks/abi/${key}.json`, JSON.stringify(value))) }), get: ({ address, signature, event }) => Effect.gen(function* () { const addressExists = yield* Effect.sync(() => fs.existsSync(`./test/mocks/abi/${address.toLowerCase()}.json`)) if (addressExists) { - return yield* Effect.sync(() => fs.readFileSync(`./test/mocks/abi/${address.toLowerCase()}.json`)?.toString()) + const abi = yield* Effect.sync( + () => fs.readFileSync(`./test/mocks/abi/${address.toLowerCase()}.json`)?.toString(), + ) + + return { + status: 'success', + result: { + type: 'address', + abi, + address, + chainID: 1, + }, + } } + const sig = signature ?? event if (sig != null) { @@ -50,11 +66,36 @@ export const MockedAbiStoreLive = Layer.succeed( () => fs.readFileSync(`./test/mocks/abi/${sig.toLowerCase()}.json`)?.toString(), ) - return `[${signatureAbi}]` + if (signature) { + return { + status: 'success', + result: { + type: 'func', + abi: `[${signatureAbi}]`, + address, + chainID: 1, + signature, + }, + } + } else if (event) { + return { + status: 'success', + result: { + type: 'event', + abi: `[${signatureAbi}]`, + address, + chainID: 1, + event, + }, + } + } } } - return null + return { + status: 'empty', + result: null, + } }), }), ) diff --git a/packages/transaction-decoder/test/mocks/meta-loader-mock.ts b/packages/transaction-decoder/test/mocks/meta-loader-mock.ts index 483e6e1b..08dfbb31 100644 --- a/packages/transaction-decoder/test/mocks/meta-loader-mock.ts +++ b/packages/transaction-decoder/test/mocks/meta-loader-mock.ts @@ -14,51 +14,63 @@ export const MockedMetaStoreLive = Layer.succeed( Effect.sync(() => { if ('0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6' === address.toLowerCase()) { return { - address, - chainID, - contractName: 'Wrapped Ether', - contractAddress: address, - tokenSymbol: 'WETH', - decimals: 18, - type: 'WETH', + status: 'success', + result: { + address, + chainID, + contractName: 'Wrapped Ether', + contractAddress: address, + tokenSymbol: 'WETH', + decimals: 18, + type: 'WETH', + }, } } if (ERC1155_CONTRACTS.includes(address.toLowerCase())) { return { - address, - chainID, - contractName: 'Mock ERC1155 Contract', - contractAddress: address, - tokenSymbol: 'ERC1155', - type: 'ERC1155', + status: 'success', + result: { + address, + chainID, + contractName: 'Mock ERC1155 Contract', + contractAddress: address, + tokenSymbol: 'ERC1155', + type: 'ERC1155', + }, } } if (ERC721_CONTRACTS.includes(address.toLowerCase())) { return { - address, - chainID, - contractName: 'Mock ERC721 Contract', - contractAddress: address, - tokenSymbol: 'ERC721', - type: 'ERC721', + status: 'success', + result: { + address, + chainID, + contractName: 'Mock ERC721 Contract', + contractAddress: address, + tokenSymbol: 'ERC721', + type: 'ERC721', + }, } } return { - address, - chainID, - contractName: 'Mock ERC20 Contract', - contractAddress: address, - tokenSymbol: 'ERC20', - decimals: 18, - type: 'ERC20', + status: 'success', + result: { + address, + chainID, + contractName: 'Mock ERC20 Contract', + contractAddress: address, + tokenSymbol: 'ERC20', + decimals: 18, + type: 'ERC20', + }, } }), set: () => Effect.sync(() => { - console.error('MockedMetaStoreLive.set not implemented in tests') + console.debug('MockedMetaStoreLive.set not implemented in tests') }), }), ) diff --git a/packages/transaction-decoder/test/transaction-decoder.test.ts b/packages/transaction-decoder/test/transaction-decoder.test.ts index b94fc026..e5f5f0fe 100644 --- a/packages/transaction-decoder/test/transaction-decoder.test.ts +++ b/packages/transaction-decoder/test/transaction-decoder.test.ts @@ -9,7 +9,7 @@ import { MockedMetaStoreLive } from './mocks/meta-loader-mock.js' import fs from 'fs' describe('Transaction Decoder', () => { - test.each(TEST_TRANSACTIONS)('Resolve and decode transaction', async ({ hash, chainID }) => { + test.each(TEST_TRANSACTIONS)('Resolve and decode transaction %', async ({ hash, chainID }) => { const program = Effect.gen(function* () { return yield* decodeTransactionByHash(hash, chainID) }) diff --git a/packages/transaction-decoder/test/vanilla.test.ts b/packages/transaction-decoder/test/vanilla.test.ts index a9c3231f..0ee7f408 100644 --- a/packages/transaction-decoder/test/vanilla.test.ts +++ b/packages/transaction-decoder/test/vanilla.test.ts @@ -26,23 +26,53 @@ describe('Transaction Decoder', () => { const addressExists = fs.existsSync(`./test/mocks/abi/${address.toLowerCase()}.json`) if (addressExists) { - return fs.readFileSync(`./test/mocks/abi/${address.toLowerCase()}.json`)?.toString() + return { + status: 'success', + result: { + type: 'address', + abi: fs.readFileSync(`./test/mocks/abi/${address.toLowerCase()}.json`)?.toString(), + address, + chainID: 5, + }, + } } const sig = signature ?? event if (sig != null) { - const signatureExists = fs.existsSync(`./test/mocks/abi/${sig.toLowerCase()}.json`) + const signatureAbi = fs.readFileSync(`./test/mocks/abi/${sig.toLowerCase()}.json`)?.toString() - if (signatureExists) { - const signatureAbi = fs.readFileSync(`./test/mocks/abi/${sig.toLowerCase()}.json`)?.toString() - return `[${signatureAbi}]` + if (signature) { + return { + status: 'success', + result: { + type: 'func', + abi: `[${signatureAbi}]`, + address, + chainID: 1, + signature, + }, + } + } else if (event) { + return { + status: 'success', + result: { + type: 'event', + abi: `[${signatureAbi}]`, + address, + chainID: 1, + event, + }, + } } } - return null + return { + status: 'empty', + result: null, + } }, set: async () => { - console.error('Not implemented') + console.debug('Not implemented') }, }, contractMetaStore: { @@ -50,27 +80,33 @@ describe('Transaction Decoder', () => { get: async (request) => { if ('0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6' === request.address.toLowerCase()) { return { + status: 'success', + result: { + address: request.address, + chainID: request.chainID, + contractName: 'Wrapped Ether', + contractAddress: request.address, + tokenSymbol: 'WETH', + decimals: 18, + type: 'WETH', + }, + } + } + return { + status: 'success', + result: { address: request.address, chainID: request.chainID, - contractName: 'Wrapped Ether', + contractName: 'Mock ERC20 Contract', contractAddress: request.address, - tokenSymbol: 'WETH', + tokenSymbol: 'ERC20', decimals: 18, - type: 'WETH', - } - } - return { - address: request.address, - chainID: request.chainID, - contractName: 'Mock ERC20 Contract', - contractAddress: request.address, - tokenSymbol: 'ERC20', - decimals: 18, - type: 'ERC20', + type: 'ERC20', + }, } }, set: async () => { - console.error('Not implemented') + console.debug('Not implemented') }, }, })