diff --git a/.changeset/orange-wolves-watch.md b/.changeset/orange-wolves-watch.md new file mode 100644 index 00000000..3532258b --- /dev/null +++ b/.changeset/orange-wolves-watch.md @@ -0,0 +1,5 @@ +--- +'@3loop/transaction-interpreter': patch +--- + +Fix interpreter crash during number formatting diff --git a/packages/transaction-interpreter/interpreters/std.ts b/packages/transaction-interpreter/interpreters/std.ts index a58afea6..f4c06418 100644 --- a/packages/transaction-interpreter/interpreters/std.ts +++ b/packages/transaction-interpreter/interpreters/std.ts @@ -10,6 +10,7 @@ interface FilterOptions { excludeZero?: boolean excludeNull?: boolean excludeDuplicates?: boolean + sumUpRepeated?: boolean } export const filterTransfers = (transfers: Asset[], filters: FilterOptions = {}): Asset[] => { @@ -29,6 +30,25 @@ export const filterTransfers = (transfers: Asset[], filters: FilterOptions = {}) ) } + if (filters.sumUpRepeated) { + const transferMap = new Map() + + filtered.forEach((transfer) => { + const key = transfer.from.toLowerCase() + '-' + transfer.to.toLowerCase() + '-' + transfer.address.toLowerCase() + + if (transferMap.has(key)) { + const existing = transferMap.get(key) || { ...transfer } + const existingAmount = existing.amount ? Number(existing.amount) : 0 + const newAmount = transfer.amount ? Number(transfer.amount) : 0 + existing.amount = (existingAmount + newAmount).toString() + } else { + transferMap.set(key, { ...transfer }) + } + }) + + filtered = Array.from(transferMap.values()) + } + return filtered } @@ -176,20 +196,28 @@ export function displayAddress(address: string): string { } export const formatNumber = (numberString: string, precision = 3): string => { - const [integerPart, decimalPart] = numberString.split('.') - const bigIntPart = BigInt(integerPart) + // Convert scientific notation to regular number + const num = Number(numberString) + + if (isNaN(num)) return numberString + + // For very small numbers (less than 0.000001), return in scientific notation + if (num < 0.000001 && num > 0) { + return num.toExponential(precision) + } + + const [integerPart, decimalPart] = num.toString().split('.') 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) { + for (let i = 0; i < integerPart.length; i++) { + if (i > 0 && (integerPart.length - i) % 3 === 0) { formattedIntegerPart += ',' } - formattedIntegerPart += integerStr[i] + formattedIntegerPart += integerPart[i] } // Format the decimal part @@ -235,6 +263,11 @@ export function displayAssets(assets: Payment[]) { export function isSwap(event: DecodedTransaction): boolean { if (event.transfers.some((t) => t.type !== 'ERC20' && t.type !== 'native')) return false + const minted = assetsSent(event.transfers, NULL_ADDRESS) + const burned = assetsReceived(event.transfers, NULL_ADDRESS) + + if (minted.length > 0 || burned.length > 0) return false + const transfers = filterTransfers(event.transfers, { excludeZero: true, excludeNull: true, diff --git a/packages/transaction-interpreter/interpreters/zeroEx.ts b/packages/transaction-interpreter/interpreters/zeroEx.ts index d22647dc..a70641a5 100644 --- a/packages/transaction-interpreter/interpreters/zeroEx.ts +++ b/packages/transaction-interpreter/interpreters/zeroEx.ts @@ -1,39 +1,55 @@ -import { assetsReceived, categorizedDefaultEvent, displayAsset, getNetTransfers } from './std.js' +import { assetsReceived, categorizedDefaultEvent, displayAsset, getNetTransfers, filterTransfers } 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) - const recipient = event.fromAddress - const contractAddress = event.toAddress - if (!contractAddress) return newEvent + if (!event.toAddress) return newEvent - const netSent = getNetTransfers({ - transfers: event.transfers, - fromAddresses: [event.fromAddress], - type: ['ERC20', 'native'], + const filteredTransfers = filterTransfers(event.transfers, { + excludeZero: true, + excludeNull: true, + sumUpRepeated: true, }) - const netReceived = getNetTransfers({ - transfers: event.transfers, - toAddresses: [recipient], - fromAddresses: [contractAddress], + const netSent = getNetTransfers({ + transfers: filteredTransfers, + fromAddresses: [event.fromAddress], type: ['ERC20', 'native'], }) - //filter the same tokne from netReceived (to filter out received fees) const sentTokens = netSent.map((t) => t.asset.address) - const filteredNetReceived = netReceived.filter((t) => !sentTokens.includes(t.asset.address)) - if (netSent.length === 1 && filteredNetReceived.length === 1) { + let netReceived + + const buyToken = event.methodCall?.params?.[0]?.components?.find((c) => c.name === 'buyToken') as + | { value: string } + | undefined + + if (buyToken) { + netReceived = getNetTransfers({ + transfers: filteredTransfers.filter((t) => t.address === buyToken.value), + type: ['ERC20', 'native'], + }) + } else { + netReceived = getNetTransfers({ + transfers: filteredTransfers, + toAddresses: [event.toAddress], + type: ['ERC20', 'native'], + }).filter((t) => !sentTokens.includes(t.asset.address)) + } + + const receivedTokens = netReceived.map((t) => t.asset.address) + + if (netSent.length === 1 && netReceived.length === 1) { return { ...newEvent, type: 'swap', - action: 'Swapped ' + displayAsset(netSent[0]) + ' for ' + displayAsset(filteredNetReceived[0]), + action: 'Swapped ' + displayAsset(netSent[0]) + ' for ' + displayAsset(netReceived[0]), assetsReceived: assetsReceived( - event.transfers.filter((t) => filteredNetReceived.some((r) => r.asset.address === t.address)), - recipient, + filteredTransfers.filter((t) => receivedTokens.includes(t.address)), + event.fromAddress, ), } }