diff --git a/.changeset/early-carrots-smell.md b/.changeset/early-carrots-smell.md new file mode 100644 index 00000000..f8166572 --- /dev/null +++ b/.changeset/early-carrots-smell.md @@ -0,0 +1,5 @@ +--- +'@3loop/transaction-interpreter': minor +--- + +Add default categorization of swaps into fallback interpreter diff --git a/.changeset/lovely-trainers-destroy.md b/.changeset/lovely-trainers-destroy.md new file mode 100644 index 00000000..60cd2a0c --- /dev/null +++ b/.changeset/lovely-trainers-destroy.md @@ -0,0 +1,5 @@ +--- +'@3loop/transaction-decoder': patch +--- + +Improve decoder error displaying diff --git a/packages/transaction-decoder/src/decoding/abi-decode.ts b/packages/transaction-decoder/src/decoding/abi-decode.ts index 7526edc0..781ff20f 100644 --- a/packages/transaction-decoder/src/decoding/abi-decode.ts +++ b/packages/transaction-decoder/src/decoding/abi-decode.ts @@ -5,8 +5,8 @@ import { Data, Effect } from 'effect' import { messageFromUnknown } from '../helpers/error.js' export class DecodeError extends Data.TaggedError('DecodeError')<{ message: string }> { - constructor(error: unknown) { - super({ message: `Failed to decode ${messageFromUnknown(error)}` }) + constructor(message: string, error?: unknown) { + super({ message: `${message} ${messageFromUnknown(error)}` }) } } @@ -72,7 +72,7 @@ export const decodeMethod = (data: Hex, abi: Abi): Effect.Effect decodeFunctionData({ abi, data }), - catch: (error) => new DecodeError(error), + catch: (error) => new DecodeError(`Could not decode function data`, error), }) const method = getAbiItem({ abi, name: functionName, args }) as AbiFunction | undefined @@ -80,7 +80,7 @@ export const decodeMethod = (data: Hex, abi: Abi): Effect.Effect formatAbiItem(method), - catch: (error) => new DecodeError(error), + catch: (error) => new DecodeError(`Could not format function data`, error), }) const paramsTree = attachValues(method.inputs, args) diff --git a/packages/transaction-decoder/src/decoding/calldata-decode.ts b/packages/transaction-decoder/src/decoding/calldata-decode.ts index 36151d39..4399d629 100644 --- a/packages/transaction-decoder/src/decoding/calldata-decode.ts +++ b/packages/transaction-decoder/src/decoding/calldata-decode.ts @@ -107,7 +107,7 @@ const decodeGnosisMultisendParams = ( abi: SAFE_MULTISEND_ABI, args: [txs], }), - catch: (error) => new AbiDecoder.DecodeError(error), + catch: (error) => new AbiDecoder.DecodeError(`Could not encode multisend transactions`, error), }) const txsDecoded = yield* AbiDecoder.decodeMethod(txsEncoded, SAFE_MULTISEND_ABI) diff --git a/packages/transaction-decoder/src/decoding/log-decode.ts b/packages/transaction-decoder/src/decoding/log-decode.ts index dec43091..cff2132b 100644 --- a/packages/transaction-decoder/src/decoding/log-decode.ts +++ b/packages/transaction-decoder/src/decoding/log-decode.ts @@ -45,11 +45,7 @@ const decodedLog = (transaction: GetTransactionReturnType, logItem: Log) => data: logItem.data, strict: false, }), - catch: (err) => - Effect.gen(function* () { - yield* Effect.logError(`Could not decode log ${abiAddress} `, err) - return new AbiDecoder.DecodeError(`Could not decode log ${abiAddress}`) - }), + catch: (err) => new AbiDecoder.DecodeError(`Could not decode log ${abiAddress}`, err), }) if (eventName == null) { @@ -58,7 +54,12 @@ const decodedLog = (transaction: GetTransactionReturnType, logItem: Log) => const args = args_ as any - const fragment = getAbiItem({ abi: abiItem, name: eventName }) + const fragment = yield* Effect.try({ + try: () => getAbiItem({ abi: abiItem, name: eventName }), + catch: () => { + Effect.logError(`Could not find fragment in ABI ${abiAddress} ${eventName}`) + }, + }) if (fragment == null) { return yield* new AbiDecoder.DecodeError(`Could not find fragment in ABI ${abiAddress} ${eventName}`) @@ -136,8 +137,10 @@ export const decodeLogs = ({ logs, transaction }: { logs: readonly Log[]; transa const eithers = effects.map((e) => Effect.either(e)) - return yield* Effect.all(eithers, { + const resp = yield* Effect.all(eithers, { concurrency: 'inherit', batching: 'inherit', }) + + return resp }) diff --git a/packages/transaction-interpreter/interpreters/1inch.ts b/packages/transaction-interpreter/interpreters/1inch.ts index 2b7d281c..f274e3a5 100644 --- a/packages/transaction-interpreter/interpreters/1inch.ts +++ b/packages/transaction-interpreter/interpreters/1inch.ts @@ -1,16 +1,16 @@ -import { displayAsset, getPayments, isSwap, defaultEvent } from './std.js' +import { displayAsset, getNetTransfers, isSwap, defaultEvent } from './std.js' import type { InterpretedTransaction } from '@/types.js' import type { DecodedTransaction } from '@3loop/transaction-decoder' export function transformEvent(event: DecodedTransaction): InterpretedTransaction { const newEvent = defaultEvent(event) - const netSent = getPayments({ + const netSent = getNetTransfers({ transfers: event.transfers, fromAddresses: [event.fromAddress], }) - const netReceived = getPayments({ + const netReceived = getNetTransfers({ transfers: event.transfers, toAddresses: [event.fromAddress], }) diff --git a/packages/transaction-interpreter/interpreters/aerodrom.ts b/packages/transaction-interpreter/interpreters/aerodrom.ts index 7f6cf482..82bbaac0 100644 --- a/packages/transaction-interpreter/interpreters/aerodrom.ts +++ b/packages/transaction-interpreter/interpreters/aerodrom.ts @@ -1,16 +1,16 @@ -import { displayAsset, getPayments, isSwap, defaultEvent } from './std.js' +import { displayAsset, getNetTransfers, isSwap, defaultEvent } from './std.js' import type { InterpretedTransaction } from '@/types.js' import type { DecodedTransaction } from '@3loop/transaction-decoder' export function transformEvent(event: DecodedTransaction): InterpretedTransaction { const newEvent = defaultEvent(event) - const netSent = getPayments({ + const netSent = getNetTransfers({ transfers: event.transfers, fromAddresses: [event.fromAddress], }) - const netReceived = getPayments({ + const netReceived = getNetTransfers({ transfers: event.transfers, toAddresses: [event.fromAddress], }) diff --git a/packages/transaction-interpreter/interpreters/banana-gun.ts b/packages/transaction-interpreter/interpreters/banana-gun.ts index 93857b88..54e33af9 100644 --- a/packages/transaction-interpreter/interpreters/banana-gun.ts +++ b/packages/transaction-interpreter/interpreters/banana-gun.ts @@ -1,16 +1,16 @@ -import { displayAsset, getPayments, isSwap, defaultEvent } from './std.js' +import { displayAsset, getNetTransfers, isSwap, defaultEvent } from './std.js' import type { InterpretedTransaction } from '@/types.js' import type { DecodedTransaction } from '@3loop/transaction-decoder' export function transformEvent(event: DecodedTransaction): InterpretedTransaction { const newEvent = defaultEvent(event) - const netSent = getPayments({ + const netSent = getNetTransfers({ transfers: event.transfers, fromAddresses: [event.fromAddress], }) - const netReceived = getPayments({ + const netReceived = getNetTransfers({ transfers: event.transfers, toAddresses: [event.fromAddress], }) diff --git a/packages/transaction-interpreter/interpreters/blur.ts b/packages/transaction-interpreter/interpreters/blur.ts index 2df5c386..eb0d2f9a 100644 --- a/packages/transaction-interpreter/interpreters/blur.ts +++ b/packages/transaction-interpreter/interpreters/blur.ts @@ -1,4 +1,4 @@ -import { displayPayments, processNftTransfers, defaultEvent } from './std.js' +import { displayAssets, processNftTransfers, defaultEvent } from './std.js' import type { InterpretedTransaction } from '@/types.js' import type { DecodedTransaction } from '@3loop/transaction-decoder' @@ -16,7 +16,7 @@ export function transformEvent(event: DecodedTransaction): InterpretedTransactio const collection = nftTransfers[0].name ?? '' const numberOfNfts = nftTransfers.length > 1 ? ` ${nftTransfers.length} ${collection} NFTS` : ` 1 ${collection} NFT` - const payment = displayPayments(erc20Payments, nativePayments) + const payment = displayAssets([...erc20Payments, ...nativePayments]) const sell = ['takeBidSingle', 'takeBid'] const buy = ['takeAskSinglePool', 'takeAskSingle', 'takeAsk', 'takeAskPool', 'batchBuyWithETH', 'batchBuyWithERC20s'] diff --git a/packages/transaction-interpreter/interpreters/friend-tech.ts b/packages/transaction-interpreter/interpreters/friend-tech.ts index 17cc5799..d2d614b4 100644 --- a/packages/transaction-interpreter/interpreters/friend-tech.ts +++ b/packages/transaction-interpreter/interpreters/friend-tech.ts @@ -1,16 +1,16 @@ -import { displayAddress, displayAsset, getPayments, defaultEvent } from './std.js' +import { displayAddress, displayAsset, getNetTransfers, defaultEvent } from './std.js' import type { InterpretedTransaction } from '@/types.js' import type { DecodedTransaction } from '@3loop/transaction-decoder' export function transformEvent(event: DecodedTransaction): InterpretedTransaction { const newEvent = defaultEvent(event) - const netSent = getPayments({ + const netSent = getNetTransfers({ transfers: event.transfers, fromAddresses: [event.fromAddress], }) - const netReceived = getPayments({ + const netReceived = getNetTransfers({ transfers: event.transfers, toAddresses: [event.fromAddress], }) diff --git a/packages/transaction-interpreter/interpreters/kyberswap.ts b/packages/transaction-interpreter/interpreters/kyberswap.ts index 652bb421..c24ff12d 100644 --- a/packages/transaction-interpreter/interpreters/kyberswap.ts +++ b/packages/transaction-interpreter/interpreters/kyberswap.ts @@ -1,16 +1,16 @@ -import { displayAsset, getPayments, isSwap, defaultEvent } from './std.js' +import { displayAsset, getNetTransfers, isSwap, defaultEvent } from './std.js' import type { InterpretedTransaction } from '@/types.js' import type { DecodedTransaction } from '@3loop/transaction-decoder' export function transformEvent(event: DecodedTransaction): InterpretedTransaction { const newEvent = defaultEvent(event) - const netSent = getPayments({ + const netSent = getNetTransfers({ transfers: event.transfers, fromAddresses: [event.fromAddress], }) - const netReceived = getPayments({ + const netReceived = getNetTransfers({ transfers: event.transfers, toAddresses: [event.fromAddress], }) diff --git a/packages/transaction-interpreter/interpreters/metamaskRouter.ts b/packages/transaction-interpreter/interpreters/metamaskRouter.ts index 449623ff..4c228ce9 100644 --- a/packages/transaction-interpreter/interpreters/metamaskRouter.ts +++ b/packages/transaction-interpreter/interpreters/metamaskRouter.ts @@ -1,16 +1,16 @@ -import { displayAsset, getPayments, isSwap, defaultEvent } from './std.js' +import { displayAsset, getNetTransfers, isSwap, defaultEvent } from './std.js' import type { InterpretedTransaction } from '@/types.js' import type { DecodedTransaction } from '@3loop/transaction-decoder' export function transformEvent(event: DecodedTransaction): InterpretedTransaction { const newEvent = defaultEvent(event) - const netSent = getPayments({ + const netSent = getNetTransfers({ transfers: event.transfers, fromAddresses: [event.fromAddress], }) - const netReceived = getPayments({ + const netReceived = getNetTransfers({ transfers: event.transfers, toAddresses: [event.fromAddress], }) diff --git a/packages/transaction-interpreter/interpreters/okx.ts b/packages/transaction-interpreter/interpreters/okx.ts index b7691160..fa88bf67 100644 --- a/packages/transaction-interpreter/interpreters/okx.ts +++ b/packages/transaction-interpreter/interpreters/okx.ts @@ -1,16 +1,16 @@ -import { displayAsset, getPayments, isSwap, defaultEvent } from './std.js' +import { displayAsset, getNetTransfers, isSwap, defaultEvent } from './std.js' import type { InterpretedTransaction } from '@/types.js' import type { DecodedTransaction } from '@3loop/transaction-decoder' export function transformEvent(event: DecodedTransaction): InterpretedTransaction { const newEvent = defaultEvent(event) - const netSent = getPayments({ + const netSent = getNetTransfers({ transfers: event.transfers, fromAddresses: [event.fromAddress], }) - const netReceived = getPayments({ + const netReceived = getNetTransfers({ transfers: event.transfers, toAddresses: [event.fromAddress], }) diff --git a/packages/transaction-interpreter/interpreters/opensea.ts b/packages/transaction-interpreter/interpreters/opensea.ts index 3eafbf6d..b2e52f90 100644 --- a/packages/transaction-interpreter/interpreters/opensea.ts +++ b/packages/transaction-interpreter/interpreters/opensea.ts @@ -1,4 +1,4 @@ -import { processNftTransfers, displayPayments, defaultEvent } from './std.js' +import { processNftTransfers, displayAssets, defaultEvent } from './std.js' import type { InterpretedTransaction } from '@/types.js' import type { DecodedTransaction } from '@3loop/transaction-decoder' @@ -15,7 +15,7 @@ export function transformEvent(event: DecodedTransaction): InterpretedTransactio const collection = nftTransfers[0].name ?? '' const numberOfNfts = nftTransfers.length > 1 ? ` ${nftTransfers.length} ${collection} NFTS` : ` 1 ${collection} NFT` - const payment = displayPayments(erc20Payments, nativePayments) + const payment = displayAssets([...erc20Payments, ...nativePayments]) if (sendingAddresses.includes(event.fromAddress.toLowerCase())) { const from = receivingAddresses.length > 1 ? ` to ${receivingAddresses.length} users` : '' diff --git a/packages/transaction-interpreter/interpreters/pendle.ts b/packages/transaction-interpreter/interpreters/pendle.ts index 3f689d7e..c20787ab 100644 --- a/packages/transaction-interpreter/interpreters/pendle.ts +++ b/packages/transaction-interpreter/interpreters/pendle.ts @@ -1,16 +1,16 @@ -import { displayAsset, getPayments, isSwap, defaultEvent } from './std.js' +import { displayAsset, getNetTransfers, isSwap, defaultEvent } from './std.js' import type { InterpretedTransaction } from '@/types.js' import type { DecodedTransaction } from '@3loop/transaction-decoder' export function transformEvent(event: DecodedTransaction): InterpretedTransaction { const newEvent = defaultEvent(event) - const netSent = getPayments({ + const netSent = getNetTransfers({ transfers: event.transfers, fromAddresses: [event.fromAddress], }) - const netReceived = getPayments({ + const netReceived = getNetTransfers({ transfers: event.transfers, toAddresses: [event.fromAddress], }) diff --git a/packages/transaction-interpreter/interpreters/std.ts b/packages/transaction-interpreter/interpreters/std.ts index 68433e06..a58afea6 100644 --- a/packages/transaction-interpreter/interpreters/std.ts +++ b/packages/transaction-interpreter/interpreters/std.ts @@ -3,18 +3,33 @@ import { Asset, DecodedTransaction } from '@3loop/transaction-decoder' export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000' -export function filterZeroTransfers(transfers: Asset[]): Asset[] { - return transfers.filter((t) => (t.amount && t.amount !== '0') || !t.amount) -} +//------------------------------------------------------------------------------ +//Core helper functions -export function filterNullTransfers(transfers: Asset[]): Asset[] { - return transfers.filter((t) => t.from !== NULL_ADDRESS && t.to !== NULL_ADDRESS) +interface FilterOptions { + excludeZero?: boolean + excludeNull?: boolean + excludeDuplicates?: boolean } -export function filterDuplicateTransfers(transfers: Asset[]): Asset[] { - return transfers.filter( - (t, i, self) => self.findIndex((t2) => t2.address === t.address && t2.amount === t.amount) === i, - ) +export const filterTransfers = (transfers: Asset[], filters: FilterOptions = {}): Asset[] => { + let filtered = [...transfers] + + if (filters.excludeZero) { + filtered = filtered.filter((t) => (t.amount && t.amount !== '0') || !t.amount) + } + + if (filters.excludeNull) { + filtered = filtered.filter((t) => t.from !== NULL_ADDRESS && t.to !== NULL_ADDRESS) + } + + if (filters.excludeDuplicates) { + filtered = filtered.filter( + (t, i, self) => self.findIndex((t2) => t2.address === t.address && t2.amount === t.amount) === i, + ) + } + + return filtered } export function toAssetTransfer(transfer: Asset): AssetTransfer { @@ -33,91 +48,33 @@ export function toAssetTransfer(transfer: Asset): AssetTransfer { } export function assetsSent(transfers: Asset[], address: string): AssetTransfer[] { - let filteredTransfers = filterZeroTransfers(transfers) - - if (address !== NULL_ADDRESS) { - filteredTransfers = filterNullTransfers(filteredTransfers) - } + const filtered = filterTransfers(transfers, { + excludeZero: true, + excludeNull: address !== NULL_ADDRESS, + }) - return filteredTransfers.filter((t) => t.from.toLowerCase() === address.toLowerCase()).map(toAssetTransfer) + return filtered.filter((t) => t.from.toLowerCase() === address.toLowerCase()).map(toAssetTransfer) } export function assetsReceived(transfers: Asset[], address: string): AssetTransfer[] { - let filteredTransfers = filterZeroTransfers(transfers) - - if (address !== NULL_ADDRESS) { - filteredTransfers = filterNullTransfers(filteredTransfers) - } - - return filteredTransfers.filter((t) => t.to.toLowerCase() === address.toLowerCase()).map(toAssetTransfer) -} - -export function displayAddress(address: string): string { - return address.slice(0, 6) + '...' + address.slice(-4) -} - -export function isSwap(event: DecodedTransaction): boolean { - if (event.transfers.some((t) => t.type !== 'ERC20' && t.type !== 'native')) return false - - const transfers = event.transfers.filter((t) => t.from !== NULL_ADDRESS && t.to !== NULL_ADDRESS) - - const sent = new Set( - transfers.filter((t) => t.from.toLowerCase() === event.fromAddress.toLowerCase()).map((t) => t.address), - ) - const received = new Set( - transfers.filter((t) => t.to.toLowerCase() === event.fromAddress.toLowerCase()).map((t) => t.address), - ) - - if (sent.size !== 1 || received.size !== 1 || sent.values() === received.values()) return false - - return true -} - -export function formatNumber(numberString: string, precision?: number): string { - const [integerPart, decimalPart] = numberString.split('.') - const bigIntPart = BigInt(integerPart) - - if ((integerPart && integerPart.length < 3 && !decimalPart) || (decimalPart && decimalPart.startsWith('000'))) - return numberString - - // Format the integer part manually - let formattedIntegerPart = '' - const integerStr = bigIntPart.toString() - for (let i = 0; i < integerStr.length; i++) { - if (i > 0 && (integerStr.length - i) % 3 === 0) { - formattedIntegerPart += ',' - } - formattedIntegerPart += integerStr[i] - } - - // Format the decimal part - const formattedDecimalPart = decimalPart - ? parseFloat('0.' + decimalPart) - .toFixed(precision ?? 3) - .split('.')[1] - : '00' - - return formattedIntegerPart + '.' + formattedDecimalPart -} - -export function displayAsset(asset: Payment | undefined): string { - if (!asset || !asset.asset) return 'unknown asset' - - const symbol = asset.asset.type === 'ERC20' ? asset.asset.symbol : asset.asset.name - - if (symbol) return formatNumber(asset.amount) + ' ' + symbol + const filtered = filterTransfers(transfers, { + excludeZero: true, + excludeNull: address !== NULL_ADDRESS, + }) - return formatNumber(asset.amount) + ' ' + displayAddress(asset.asset.address) + return filtered.filter((t) => t.to.toLowerCase() === address.toLowerCase()).map(toAssetTransfer) } -export function getPayments({ +export function getNetTransfers({ transfers, fromAddresses, toAddresses, + type, }: { transfers: Asset[] fromAddresses?: string[] toAddresses?: string[] + type?: string | string[] }): Payment[] { const fromAddressFilter = fromAddresses?.map((a) => a.toLowerCase()) const toAddressFilter = toAddresses?.map((a) => a.toLowerCase()) @@ -131,6 +88,14 @@ export function getPayments({ filteredTransfers = filteredTransfers.filter((t) => toAddressFilter.includes(t.to.toLowerCase())) } + if (type) { + if (Array.isArray(type)) { + filteredTransfers = filteredTransfers.filter((t) => type.includes(t.type)) + } else { + filteredTransfers = filteredTransfers.filter((t) => t.type === type) + } + } + return Object.values( filteredTransfers.reduce>((acc, t) => { const address = t.address @@ -184,12 +149,12 @@ export function processNftTransfers(transfers: Asset[]) { } }) - const erc20Payments = getPayments({ + const erc20Payments = getNetTransfers({ transfers: erc20Transfers, fromAddresses: Array.from(receivingAddresses), }) - const nativePayments = getPayments({ + const nativePayments = getNetTransfers({ transfers: nativeTransfers, fromAddresses: Array.from(receivingAddresses), }) @@ -203,27 +168,91 @@ export function processNftTransfers(transfers: Asset[]) { } } -export function displayPayments(erc20Payments: Payment[], nativePayments: Payment[]) { - if (erc20Payments.length > 0 && nativePayments.length > 0) { - const amount = (erc20Payments.length + 1).toString() - return amount + ' assets' - } else if (erc20Payments.length > 0) { - return ( - erc20Payments[0].amount + - ' ' + - (erc20Payments[0].asset?.symbol || erc20Payments[0].asset?.name + ' tokens' || 'ERCC20 tokens') - ) - } else if (nativePayments.length > 0) { - return ( - nativePayments[0].amount + - ' ' + - (nativePayments[0].asset?.symbol || nativePayments[0].asset?.name + ' tokens' || 'native tokens') - ) +//------------------------------------------------------------------------------ +// Formatting Functions + +export function displayAddress(address: string): string { + return address.slice(0, 6) + '...' + address.slice(-4) +} + +export const formatNumber = (numberString: string, precision = 3): string => { + const [integerPart, decimalPart] = numberString.split('.') + const bigIntPart = BigInt(integerPart) + + if ((integerPart && integerPart.length < 3 && !decimalPart) || (decimalPart && decimalPart.startsWith('000'))) + return numberString + + // Format the integer part manually + let formattedIntegerPart = '' + const integerStr = bigIntPart.toString() + for (let i = 0; i < integerStr.length; i++) { + if (i > 0 && (integerStr.length - i) % 3 === 0) { + formattedIntegerPart += ',' + } + formattedIntegerPart += integerStr[i] + } + + // Format the decimal part + const formattedDecimalPart = decimalPart + ? parseFloat('0.' + decimalPart) + .toFixed(precision) + .split('.')[1] + : '00' + + return formattedIntegerPart + '.' + formattedDecimalPart +} + +export const displayAsset = (asset?: Payment): string => { + if (!asset?.asset) return 'unknown asset' + + const symbol = asset.asset.type === 'ERC20' ? asset.asset.symbol : asset.asset.name + + if (symbol) return formatNumber(asset.amount) + ' ' + symbol + + return formatNumber(asset.amount) + ' ' + displayAddress(asset.asset.address) +} + +export function displayAssets(assets: Payment[]) { + const erc20 = assets.filter((a) => a.asset.type === 'ERC20') + const native = assets.filter((a) => a.asset.type === 'native') + + if (assets.length === 1) { + return displayAsset(assets[0]) + } else if (erc20.length > 0 && native.length > 0) { + return (erc20.length + 1).toString() + ' assets' + } else if (erc20.length > 0) { + return erc20[0].amount + ' ' + (erc20[0].asset?.symbol || erc20[0].asset?.name + ' tokens' || 'ERCC20 tokens') + } else if (native.length > 0) { + return native[0].amount + ' ' + (native[0].asset?.symbol || native[0].asset?.name + ' tokens' || 'native tokens') } else { return '' } } +//------------------------------------------------------------------------------ +// Categorization Functions + +export function isSwap(event: DecodedTransaction): boolean { + if (event.transfers.some((t) => t.type !== 'ERC20' && t.type !== 'native')) return false + + const transfers = filterTransfers(event.transfers, { + excludeZero: true, + excludeNull: true, + }) + + const uniqueSent = new Set( + transfers.filter((t) => t.from.toLowerCase() === event.fromAddress.toLowerCase()).map((t) => t.address), + ) + const uniqueReceived = new Set( + transfers.filter((t) => t.to.toLowerCase() === event.fromAddress.toLowerCase()).map((t) => t.address), + ) + + if (uniqueSent.size !== 1 || uniqueReceived.size !== 1 || uniqueSent.values() === uniqueReceived.values()) + return false + + return true +} + export function defaultEvent(event: DecodedTransaction): InterpretedTransaction { const burned = assetsReceived(event.transfers, NULL_ADDRESS) const minted = assetsSent(event.transfers, NULL_ADDRESS) @@ -246,13 +275,17 @@ export function defaultEvent(event: DecodedTransaction): InterpretedTransaction export function categorizedDefaultEvent(event: DecodedTransaction): InterpretedTransaction { const newEvent = defaultEvent(event) - const nonDuplicateTransfers = filterDuplicateTransfers(event.transfers) - const nonZeroTransfers = filterZeroTransfers(nonDuplicateTransfers) + const transfers = filterTransfers(event.transfers, { + excludeDuplicates: true, + }) + const nonZeroTransfers = filterTransfers(transfers, { + excludeZero: true, + }) const minted = newEvent.assetsMinted || [] const burned = newEvent.assetsBurned || [] // single burn - if (burned.length === 1 && nonDuplicateTransfers.length <= 2) { + if (burned.length === 1 && nonZeroTransfers.length <= 2) { return { ...newEvent, type: 'burn', @@ -261,7 +294,7 @@ export function categorizedDefaultEvent(event: DecodedTransaction): InterpretedT } // single mint - if (minted.length === 1 && nonDuplicateTransfers.length <= 2) { + if (minted.length === 1 && transfers.length <= 2) { const price = newEvent.assetsSent.length === 1 ? newEvent.assetsSent[0] : undefined return { ...newEvent, @@ -308,5 +341,23 @@ export function categorizedDefaultEvent(event: DecodedTransaction): InterpretedT } } + if (isSwap(event)) { + const netSent = getNetTransfers({ + transfers: event.transfers, + fromAddresses: [event.fromAddress], + }) + + const netReceived = getNetTransfers({ + transfers: event.transfers, + toAddresses: [event.fromAddress], + }) + + return { + ...newEvent, + type: 'swap', + action: 'Swapped ' + displayAsset(netSent[0]) + ' for ' + displayAsset(netReceived[0]), + } + } + return newEvent } diff --git a/packages/transaction-interpreter/interpreters/uniswapv3.ts b/packages/transaction-interpreter/interpreters/uniswapv3.ts index 8f221e37..e69d40ff 100644 --- a/packages/transaction-interpreter/interpreters/uniswapv3.ts +++ b/packages/transaction-interpreter/interpreters/uniswapv3.ts @@ -1,4 +1,4 @@ -import { displayAsset, defaultEvent } from './std.js' +import { displayAsset, defaultEvent, getNetTransfers, isSwap } from './std.js' import type { InterpretedTransaction } from '@/types.js' import type { DecodedTransaction } from '@3loop/transaction-decoder' @@ -6,14 +6,21 @@ export function transformEvent(event: DecodedTransaction): InterpretedTransactio const newEvent = defaultEvent(event) const hasSwap = event.traceCalls.some((call) => call.name === 'swap') - if (hasSwap && newEvent.assetsSent.length === 1 && newEvent.assetsReceived.length === 1) { - const from = displayAsset(newEvent.assetsSent[0]) - const to = displayAsset(newEvent.assetsReceived[0]) + if (hasSwap && isSwap(event)) { + const netSent = getNetTransfers({ + transfers: event.transfers, + fromAddresses: [event.fromAddress], + }) + + const netReceived = getNetTransfers({ + transfers: event.transfers, + toAddresses: [event.fromAddress], + }) return { ...newEvent, type: 'swap', - action: 'Swapped ' + from + ' for ' + to, + action: 'Swapped ' + displayAsset(netSent[0]) + ' for ' + displayAsset(netReceived[0]), } } diff --git a/packages/transaction-interpreter/interpreters/zeroEx.ts b/packages/transaction-interpreter/interpreters/zeroEx.ts new file mode 100644 index 00000000..1621d0b3 --- /dev/null +++ b/packages/transaction-interpreter/interpreters/zeroEx.ts @@ -0,0 +1,42 @@ +import { categorizedDefaultEvent, displayAsset, getNetTransfers } from './std.js' +import type { InterpretedTransaction } from '@/types.js' +import type { DecodedTransaction } from '@3loop/transaction-decoder' + +export function transformEvent(event: DecodedTransaction): InterpretedTransaction { + const newEvent = categorizedDefaultEvent(event) + + if (newEvent.type !== 'unknown') return newEvent + + const netSent = getNetTransfers({ + transfers: event.transfers, + fromAddresses: [event.fromAddress], + type: ['ERC20', 'native'], + }) + + const netReceived = getNetTransfers({ + transfers: event.transfers, + toAddresses: [event.fromAddress], + type: ['ERC20', 'native'], + }) + + if (netSent.length === 1 && netReceived.length === 1) { + return { + ...newEvent, + type: 'swap', + action: 'Swapped ' + displayAsset(netSent[0]) + ' for ' + displayAsset(netReceived[0]), + } + } + + return newEvent +} + +export const contracts = [ + //Exchange Proxy + '1:0xDef1C0ded9bec7F1a1670819833240f027b25EfF', + '10:0xDef1C0ded9bec7F1a1670819833240f027b25EfF', + '42161:0xDef1C0ded9bec7F1a1670819833240f027b25EfF', + '8453:0xDef1C0ded9bec7F1a1670819833240f027b25EfF', + + //Settlers + '8453:0xbc3c5ca50b6a215edf00815965485527f26f5da8', +]