diff --git a/.changeset/young-ants-whisper.md b/.changeset/young-ants-whisper.md new file mode 100644 index 0000000..958cdf4 --- /dev/null +++ b/.changeset/young-ants-whisper.md @@ -0,0 +1,5 @@ +--- +'@3loop/transaction-decoder': patch +--- + +Errors logging improvements diff --git a/apps/web/.cursor/rules/web.mdc b/apps/web/.cursor/rules/web.mdc new file mode 100644 index 0000000..915ef0a --- /dev/null +++ b/apps/web/.cursor/rules/web.mdc @@ -0,0 +1,38 @@ +--- +description: +globs: +alwaysApply: true +--- +You are a Senior Front-End Developer and an Expert in ReactJS, NextJS, JavaScript, TypeScript, HTML, CSS and modern UI/UX frameworks (e.g., TailwindCSS, Shadcn, Radix). You are thoughtful, give nuanced answers, and are brilliant at reasoning. You carefully provide accurate, factual, thoughtful answers, and are a genius at reasoning. + +- Follow the user’s requirements carefully & to the letter. +- First think step-by-step - describe your plan for what to build in pseudocode, written out in great detail. +- Confirm, then write code! +- Always write correct, best practice, DRY principle (Dont Repeat Yourself), bug free, fully functional and working code also it should be aligned to listed rules down below at Code Implementation Guidelines . +- Focus on easy and readability code, over being performant. +- Fully implement all requested functionality. +- Leave NO todo’s, placeholders or missing pieces. +- Ensure code is complete! Verify thoroughly finalised. +- Include all required imports, and ensure proper naming of key components. +- Be concise Minimize any other prose. +- If you think there might not be a correct answer, you say so. +- If you do not know the answer, say so, instead of guessing. + +### Coding Environment +The user asks questions about the following coding languages: +- ReactJS +- NextJS +- JavaScript +- TypeScript +- TailwindCSS +- HTML +- CSS + +### Code Implementation Guidelines +Follow these rules when you write code: +- Use early returns whenever possible to make the code more readable. +- Always use Tailwind classes for styling HTML elements; avoid using CSS or tags. +- Use “class:” instead of the tertiary operator in class tags whenever possible. +- Use descriptive variable and function/const names. Also, event functions should be named with a “handle” prefix, like “handleClick” for onClick and “handleKeyDown” for onKeyDown. +- Implement accessibility features on elements. For example, a tag should have a tabindex=“0”, aria-label, on:click, and on:keydown, and similar attributes. +- Use consts instead of functions, for example, “const toggle = () =>”. Also, define a type if possible. diff --git a/packages/transaction-decoder/src/decoding/proxies.ts b/packages/transaction-decoder/src/decoding/proxies.ts index c1ced4f..a8412a1 100644 --- a/packages/transaction-decoder/src/decoding/proxies.ts +++ b/packages/transaction-decoder/src/decoding/proxies.ts @@ -167,7 +167,7 @@ export const GetProxyResolver = RequestResolver.fromEffect( const code = codeResult.right //If code is empty and it is EOA, return empty result - if (!code) return undefined + if (!code || code === '0x') return undefined let proxySlots: StorageSlot[] | undefined @@ -213,8 +213,8 @@ export const GetProxyResolver = RequestResolver.fromEffect( ) const res = yield* Effect.all(effects, { - concurrency: 'inherit', - batching: 'inherit', + concurrency: 'unbounded', + batching: true, mode: 'either', }) diff --git a/packages/transaction-decoder/src/transaction-decoder.ts b/packages/transaction-decoder/src/transaction-decoder.ts index 1bcb528..6326649 100644 --- a/packages/transaction-decoder/src/transaction-decoder.ts +++ b/packages/transaction-decoder/src/transaction-decoder.ts @@ -8,7 +8,7 @@ import * as CalldataDecode from './decoding/calldata-decode.js' import type { DecodedTransaction, Interaction, ContractData, DecodeResult } from './types.js' import { TxType } from './types.js' import type { TraceLog } from './schema/trace.js' -import { getAssetsTransfers } from './transformers/tokens.js' +import { getAssetTransfers } from './transformers/transfers.js' import { getAndCacheContractMeta } from './contract-meta-loader.js' import { chainIdToNetwork } from './helpers/networks.js' import { stringify } from './helpers/stringify.js' @@ -112,24 +112,31 @@ export const decodeTransaction = ({ const decodedDataRight = Either.isRight(decodedData) ? decodedData.right : undefined const decodedErrorTraceRight = decodedErrorTrace.filter(Either.isRight).map((r) => r.right) - const logsErrors = decodedLogs.filter(Either.isLeft).map((r) => r.left) - - if (logsErrors.length > 0) { - yield* Effect.logError(`Logs decode errors: ${stringify(logsErrors)}`) + const logsDecodeErrors = decodedLogs.filter(Either.isLeft).map((r) => r.left) + if (logsDecodeErrors.length > 0) { + yield* Effect.logError(`Logs decode errors: ${stringify(logsDecodeErrors)}`) } if (!nativeTransfer && Either.isLeft(decodedData)) { yield* Effect.logError(`Data decode error: ${decodedData.left}`) } - const traceErrors = decodedTrace.filter(Either.isLeft).map((r) => r.left) - if (traceErrors.length > 0) { - yield* Effect.logError(`Trace decode errors: ${stringify(traceErrors)}`) + const traceDecodeErrors = decodedTrace + .filter(Either.isLeft) + .map((r) => r.left) + .filter((error, index, self) => self.findIndex((e) => e.message === error.message) === index) + + if (traceDecodeErrors.length > 0) { + yield* Effect.logError(`Trace decode errors: ${stringify(traceDecodeErrors)}`) } - const errorTraceErrors = decodedErrorTrace.filter(Either.isLeft).map((r) => r.left) - if (errorTraceErrors.length > 0) { - yield* Effect.logError(`ErrorTrace decode errors: ${stringify(errorTraceErrors)}`) + const errorTraceDecodeErrors = decodedErrorTrace + .filter(Either.isLeft) + .map((r) => r.left) + .filter((error, index, self) => self.findIndex((e) => e.message === error.message) === index) + + if (errorTraceDecodeErrors.length > 0) { + yield* Effect.logError(`ErrorTrace decode errors: ${stringify(errorTraceDecodeErrors)}`) } const interactions: Interaction[] = TraceDecoder.augmentTraceLogs(transaction, decodedLogsRight, trace) @@ -163,6 +170,12 @@ export const decodeTransaction = ({ const contractMeta = contractsMeta[contractAddress] + const transfersResult = yield* getAssetTransfers(interactions, value, receipt.from, receipt.to!) + + if (transfersResult.errors.length > 0) { + yield* Effect.logError(`Transfers decode errors: ${stringify(transfersResult.errors)}`) + } + const decodedTx: DecodedTransaction = { txHash: transaction.hash, txType: nativeTransfer ? TxType.TRANSFER : TxType.CONTRACT_INTERACTION, @@ -187,7 +200,7 @@ export const decodeTransaction = ({ timestamp, txIndex: receipt.transactionIndex, reverted: receipt.status === 'reverted', // will return true if status==undefined - transfers: getAssetsTransfers(interactions, value, receipt.from, receipt.to!), + transfers: transfersResult.transfers, interactedAddresses, addressesMeta: contractsMeta, errors: decodedErrorTraceRight.length > 0 ? decodedErrorTraceRight : null, diff --git a/packages/transaction-decoder/src/transformers/tokens.ts b/packages/transaction-decoder/src/transformers/tokens.ts deleted file mode 100644 index 4bf87d4..0000000 --- a/packages/transaction-decoder/src/transformers/tokens.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { formatEther, formatUnits } from 'viem' -import { Asset, AssetType, EventParams, Interaction } from '../types.js' -import { sameAddress } from '../helpers/address.js' - -const toKeys = ['to', '_to', 'dst'] -const fromKeys = ['from', '_from', 'src'] -const valueKeys = ['value', 'amount', 'wad', '_amount'] -export const ethAddress = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' -const transferEvents = ['Transfer', 'TransferBatch', 'TransferSingle'] - -function findValue(params: EventParams): EventParams[keyof EventParams] { - // Find first value that is not null - const match = valueKeys.find((key) => params[key] != null) - if (match != null) { - return params[match] - } else { - return '' - } -} - -function getTokenType(interaction: Interaction): AssetType { - let tokenType = AssetType.DEFAULT - - if (interaction.contractType === 'ERC1155') { - tokenType = AssetType.ERC1155 - // ERC-721 - } else if (interaction.contractType === 'ERC721') { - tokenType = AssetType.ERC721 - // ERC-20 - } else if (interaction.contractType === 'ERC20' || interaction.contractType === 'WETH') { - tokenType = AssetType.ERC20 - } - - return tokenType -} - -function getTokens(interactions: Interaction[]): Asset[] { - const filteredInteractions = interactions.filter((d) => transferEvents.includes(d.event.eventName || '')) - - return filteredInteractions - .map((interaction) => { - // NOTE: Already filtered by eventName, but we do not have yet configured - // a more robust typing for events for TS to auotmatically handle it - if ('nativeTransfer' in interaction.event) return [] - - const event = interaction.event - const tokenType = getTokenType(interaction) - - const value = findValue(event.params) - const fromKey = fromKeys.find((key) => key in event.params && event.params[key] != null) - const toKey = toKeys.find((key) => key in event.params && event.params[key] != null) - const tokenId = (event.params.id ?? event.params.tokenId)?.toString() - - if (!fromKey || !toKey) { - console.error('Invalid event:', event) - return [] - } - - const to = event.params[toKey] as string - const from = event.params[fromKey] as string - - if (tokenType === AssetType.ERC20) { - const decimals = interaction.decimals ?? 18 - const amountNumber = formatUnits(BigInt(value as string), decimals) - - return [ - { - type: tokenType, - name: interaction.contractName, - symbol: interaction.contractSymbol, - address: interaction.contractAddress, - amount: amountNumber, - to, - from, - }, - ] - } else if (tokenType === AssetType.ERC721) { - return [ - { - type: tokenType, - name: interaction.contractName, - symbol: interaction.contractSymbol, - address: interaction.contractAddress, - amount: '1', - tokenId, - to, - from, - }, - ] - } else if (tokenType === AssetType.ERC1155) { - return [ - { - type: tokenType, - name: interaction.contractName, - symbol: interaction.contractSymbol, - address: interaction.contractAddress, - tokenId, - amount: value as string, - to, - from, - }, - ] - } - - console.warn('Unsupported type:', interaction) - // TODO: Batch transfers are not supported yet - return [] - }) - .flat() -} - -function getNativeTokenValueEvents(interactions: Interaction[], from: string): Asset[] { - return interactions.reduce((acc, interaction) => { - if ( - 'nativeTransfer' in interaction.event && - interaction.event.nativeTransfer && - // NOTE: We already have native transfer from receipt, thus we ignore the one from trace - !sameAddress(interaction.event.params.from, from) - ) { - const eventParams = interaction.event.params - return [ - ...acc, - { - from: eventParams.from, - to: eventParams.to, - type: AssetType.native, - amount: getNativeTokenValueSent(eventParams.value), - name: 'Ethereum', // TODO: Make chain agnostic - symbol: 'ETH', - address: ethAddress, - }, - ] - } else { - return acc - } - }, [] as Asset[]) -} - -function getNativeTokenValueSent(nativeValueSent: string | undefined): string { - return Number(formatEther(BigInt(nativeValueSent || 0))) - .toString() - .replace(/^(\d+\.\d*?[0-9])0+$/g, '$1') -} - -export function getAssetsTransfers(interactions: Interaction[], value: string, from: string, to: string): Asset[] { - const assets = getTokens(interactions) - const ethValueSent = getNativeTokenValueSent(value) - const nativeTransfer = getNativeTokenValueEvents(interactions, from) - - if (Number(ethValueSent)) { - assets.push({ - from, - to, - type: AssetType.native, - amount: ethValueSent, - name: 'Ethereum', // TODO: Make chain agnostic - symbol: 'ETH', - address: ethAddress, - }) - } - - return [...assets, ...nativeTransfer] -} diff --git a/packages/transaction-decoder/src/transformers/transfers.ts b/packages/transaction-decoder/src/transformers/transfers.ts new file mode 100644 index 0000000..80a5d81 --- /dev/null +++ b/packages/transaction-decoder/src/transformers/transfers.ts @@ -0,0 +1,181 @@ +import { formatEther, formatUnits } from 'viem' +import { Asset, AssetType, EventParams, Interaction } from '../types.js' +import { sameAddress } from '../helpers/address.js' +import { Data, Effect, Either } from 'effect' + +class TransferDecodeError extends Data.TaggedError('TransferDecodeError')<{ message: string }> { + constructor(message: string) { + super({ message }) + } +} + +const toKeys = ['to', '_to', 'dst'] +const fromKeys = ['from', '_from', 'src'] +const valueKeys = ['value', 'amount', 'wad', '_amount'] +export const ethAddress = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' +const transferEvents = ['Transfer', 'TransferBatch', 'TransferSingle'] + +function findValue(params: EventParams): EventParams[keyof EventParams] { + // Find first value that is not null + const match = valueKeys.find((key) => params[key] != null) + if (match != null) { + return params[match] + } else { + return '' + } +} + +function getTokenType(interaction: Interaction): AssetType { + let tokenType = AssetType.DEFAULT + + if (interaction.contractType === 'ERC1155') { + tokenType = AssetType.ERC1155 + // ERC-721 + } else if (interaction.contractType === 'ERC721') { + tokenType = AssetType.ERC721 + // ERC-20 + } else if (interaction.contractType === 'ERC20' || interaction.contractType === 'WETH') { + tokenType = AssetType.ERC20 + } + + return tokenType +} + +function decodeTransfer(interaction: Interaction): Effect.Effect { + return Effect.gen(function* () { + const event = interaction.event + const tokenType = getTokenType(interaction) + + if ('nativeTransfer' in event) { + return yield* new TransferDecodeError(`Native transfer at index ${event.logIndex}`) + } + + const value = findValue(event.params) + const fromKey = fromKeys.find((key) => key in event.params && event.params[key] != null) + const toKey = toKeys.find((key) => key in event.params && event.params[key] != null) + const tokenId = (event.params.id ?? event.params.tokenId)?.toString() + + if (!fromKey || !toKey) { + yield* new TransferDecodeError(`Invalid event at index ${event.logIndex}`) + } + + // Type guard: fromKey and toKey are defined here + const to = event.params[toKey!] as string + const from = event.params[fromKey!] as string + + if (tokenType === AssetType.ERC20) { + const decimals = interaction.decimals ?? 18 + const amountNumber = formatUnits(BigInt(value as string), decimals) + return { + type: tokenType, + name: interaction.contractName, + symbol: interaction.contractSymbol, + address: interaction.contractAddress, + amount: amountNumber, + to, + from, + } + } else if (tokenType === AssetType.ERC721) { + return { + type: tokenType, + name: interaction.contractName, + symbol: interaction.contractSymbol, + address: interaction.contractAddress, + amount: '1', + tokenId, + to, + from, + } + } else if (tokenType === AssetType.ERC1155) { + return { + type: tokenType, + name: interaction.contractName, + symbol: interaction.contractSymbol, + address: interaction.contractAddress, + tokenId, + amount: value as string, + to, + from, + } + } else { + // TODO: Batch transfers are not supported yet + return yield* new TransferDecodeError(`Unsupported token type: ${tokenType}`) + } + }) +} + +function getTokenTransfers(interactions: Interaction[]) { + return Effect.gen(function* () { + const filteredInteractions = interactions + .filter((d) => transferEvents.includes(d.event.eventName || '')) + .filter((d) => !('nativeTransfer' in d.event)) + + const effects = filteredInteractions.map(decodeTransfer) + const eithers = effects.map((e) => Effect.either(e)) + + const result = yield* Effect.all(eithers, { + concurrency: 'inherit', + batching: 'inherit', + }) + + return result + }) +} + +function getNativeTokenValueEvents(interactions: Interaction[], from: string): Asset[] { + return interactions.reduce((acc, interaction) => { + if ( + 'nativeTransfer' in interaction.event && + interaction.event.nativeTransfer && + // NOTE: We already have native transfer from receipt, thus we ignore the one from trace + !sameAddress(interaction.event.params.from, from) + ) { + const eventParams = interaction.event.params + return [ + ...acc, + { + from: eventParams.from, + to: eventParams.to, + type: AssetType.native, + amount: getNativeTokenValueSent(eventParams.value), + name: 'Ethereum', // TODO: Make chain agnostic + symbol: 'ETH', + address: ethAddress, + }, + ] + } else { + return acc + } + }, [] as Asset[]) +} + +function getNativeTokenValueSent(nativeValueSent: string | undefined): string { + return Number(formatEther(BigInt(nativeValueSent || 0))) + .toString() + .replace(/^(\d+\.\d*?[0-9])0+$/g, '$1') +} + +export function getAssetTransfers(interactions: Interaction[], value: string, from: string, to: string) { + return Effect.gen(function* () { + const tokenTransfers = yield* getTokenTransfers(interactions) + const tokenTransfersRight = tokenTransfers.filter(Either.isRight).map((r) => r.right) + const tokenTransfersErrors = tokenTransfers.filter(Either.isLeft).map((r) => r.left) + + const ethValueSent = getNativeTokenValueSent(value) + const nativeTransfer = getNativeTokenValueEvents(interactions, from) + + if (Number(ethValueSent)) { + tokenTransfersRight.push({ + from, + to, + type: AssetType.native, + amount: ethValueSent, + name: 'Ethereum', // TODO: Make chain agnostic + symbol: 'ETH', + address: ethAddress, + }) + } + + return { errors: tokenTransfersErrors, transfers: [...tokenTransfersRight, ...nativeTransfer] } + }) +}