From 797da0fc4c6d5511168d23067464d0274f82a1b5 Mon Sep 17 00:00:00 2001 From: anastasiarods Date: Fri, 23 Aug 2024 19:21:13 +0200 Subject: [PATCH] Add more interpreters (#86) * Add more interpreters * Add changeset --- .changeset/angry-adults-fly.md | 5 + .../interpreters/1inch.ts | 46 +++++ .../interpreters/aa.ts | 33 ++++ .../interpreters/aave.ts | 84 +++++++++ .../interpreters/aave2.ts | 54 ------ .../{1inchv5.ts => banana-gun.ts} | 35 ++-- .../interpreters/blur.ts | 66 +++++++ .../interpreters/erc20.ts | 12 +- .../interpreters/hop-protocol.ts | 7 +- .../interpreters/kyberswap.ts | 44 +++++ .../interpreters/metamaskRouter.ts | 42 +++++ .../interpreters/okx.ts | 45 +++++ .../interpreters/opensea.ts | 63 +++++++ .../interpreters/pendle.ts | 44 +++++ .../interpreters/std.ts | 175 +++++++++++++++++- .../interpreters/uniswapv3.ts | 8 +- packages/transaction-interpreter/src/types.ts | 5 + 17 files changed, 675 insertions(+), 93 deletions(-) create mode 100644 .changeset/angry-adults-fly.md create mode 100644 packages/transaction-interpreter/interpreters/1inch.ts create mode 100644 packages/transaction-interpreter/interpreters/aa.ts create mode 100644 packages/transaction-interpreter/interpreters/aave.ts delete mode 100644 packages/transaction-interpreter/interpreters/aave2.ts rename packages/transaction-interpreter/interpreters/{1inchv5.ts => banana-gun.ts} (50%) create mode 100644 packages/transaction-interpreter/interpreters/blur.ts create mode 100644 packages/transaction-interpreter/interpreters/kyberswap.ts create mode 100644 packages/transaction-interpreter/interpreters/metamaskRouter.ts create mode 100644 packages/transaction-interpreter/interpreters/okx.ts create mode 100644 packages/transaction-interpreter/interpreters/opensea.ts create mode 100644 packages/transaction-interpreter/interpreters/pendle.ts diff --git a/.changeset/angry-adults-fly.md b/.changeset/angry-adults-fly.md new file mode 100644 index 0000000..2de6f25 --- /dev/null +++ b/.changeset/angry-adults-fly.md @@ -0,0 +1,5 @@ +--- +'@3loop/transaction-interpreter': minor +--- + +Add more default interpreters and std functions diff --git a/packages/transaction-interpreter/interpreters/1inch.ts b/packages/transaction-interpreter/interpreters/1inch.ts new file mode 100644 index 0000000..90325eb --- /dev/null +++ b/packages/transaction-interpreter/interpreters/1inch.ts @@ -0,0 +1,46 @@ +import { assetsReceived, assetsSent, displayAsset, getPayments, isSwap } from './std.js' +import type { InterpretedTransaction } from '@/types.js' +import type { DecodedTx } from '@3loop/transaction-decoder' + +export function transformEvent(event: DecodedTx): InterpretedTransaction { + const methodName = event.methodCall.name + + const newEvent: Omit = { + chain: event.chainID, + txHash: event.txHash, + user: { address: event.fromAddress, name: null }, + method: methodName, + assetsSent: assetsSent(event.transfers, event.fromAddress), + assetsReceived: assetsReceived(event.transfers, event.fromAddress), + } + + const netSent = getPayments({ + transfers: event.transfers, + fromAddresses: [event.fromAddress], + }) + + const netReceived = getPayments({ + transfers: event.transfers, + toAddresses: [event.fromAddress], + }) + + if (isSwap(event)) { + return { + type: 'swap', + action: 'Swapped ' + displayAsset(netSent[0]) + ' for ' + displayAsset(netReceived[0]), + ...newEvent, + } + } + + return { + type: 'unknown', + action: `Called method '${methodName}'`, + ...newEvent, + } +} + +export const contracts = [ + '1:0x111111125421cA6dc452d289314280a0f8842A65', // 1inchv6 + '1:0x1111111254EEB25477B68fb85Ed929f73A960582', // 1inchv5 + '1:0x1111111254fb6c44bAC0beD2854e76F90643097d', // 1inchv4 +] diff --git a/packages/transaction-interpreter/interpreters/aa.ts b/packages/transaction-interpreter/interpreters/aa.ts new file mode 100644 index 0000000..4c916c8 --- /dev/null +++ b/packages/transaction-interpreter/interpreters/aa.ts @@ -0,0 +1,33 @@ +import { InterpretedTransaction } from '@/types.js' +import { DecodedTx } from '@3loop/transaction-decoder' +import { assetsReceived, assetsSent, displayAddress, formatNumber } from './std.js' + +export function transformEvent(event: DecodedTx): InterpretedTransaction { + const methodName = event.methodCall.name + + const newEvent: Omit = { + chain: event.chainID, + txHash: event.txHash, + user: { address: event.fromAddress, name: null }, + method: methodName, + assetsSent: assetsSent(event.transfers, event.fromAddress), + assetsReceived: assetsReceived(event.transfers, event.fromAddress), + } + + if (event.methodCall.name !== 'handleOps') return { type: 'unknown', action: 'Unknown action', ...newEvent } + + const userOpEvents = event.interactions.filter((e) => e.event.eventName === 'UserOperationEvent') + const isBatch = userOpEvents.length > 1 + const fee = newEvent.assetsReceived[0]?.amount + const sender = (userOpEvents[0].event.params as { sender: string }).sender + + return { + type: 'account-abstraction', + action: `Account Abstraction transaction by ${ + isBatch ? userOpEvents.length + ' adresses' : displayAddress(sender) + } with fee ${formatNumber(fee, 4)}`, + ...newEvent, + } +} + +export const contracts = ['1:0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789'] diff --git a/packages/transaction-interpreter/interpreters/aave.ts b/packages/transaction-interpreter/interpreters/aave.ts new file mode 100644 index 0000000..58f1e2b --- /dev/null +++ b/packages/transaction-interpreter/interpreters/aave.ts @@ -0,0 +1,84 @@ +import { assetsReceived, assetsSent, displayAsset, NULL_ADDRESS } from './std.js' +import type { InterpretedTransaction } from '@/types.js' +import type { DecodedTx } from '@3loop/transaction-decoder' + +export function transformEvent(event: DecodedTx): InterpretedTransaction { + const methodName = event.methodCall.name + + const newEvent: Omit = { + chain: event.chainID, + txHash: event.txHash, + user: { address: event.fromAddress, name: null }, + method: methodName, + assetsSent: assetsSent(event.transfers, event.fromAddress), + assetsReceived: assetsReceived(event.transfers, event.fromAddress), + } + + const directSent = newEvent.assetsSent.filter((a) => a.from.address !== NULL_ADDRESS && a.to.address !== NULL_ADDRESS) + const directReceived = newEvent.assetsReceived.filter( + (a) => a.from.address !== NULL_ADDRESS && a.to.address !== NULL_ADDRESS, + ) + + switch (methodName) { + case 'repay': + case 'repayWithPermit': + case 'repayWithATokens': + return { + type: 'repay-loan', + action: 'User repaid ' + displayAsset(directSent[0]), + ...newEvent, + } + + case 'deposit': + case 'supplyWithPermit': + case 'supply': + return { + type: 'deposit-collateral', + action: 'User deposited ' + displayAsset(directSent[0]), + ...newEvent, + } + + case 'borrow': + return { + type: 'borrow', + action: 'User borrowed ' + displayAsset(directReceived[0]), + ...newEvent, + } + + case 'withdraw': + return { + type: 'withdraw-collateral', + action: 'User withdrew ' + displayAsset(directReceived[0]), + ...newEvent, + } + + case 'flashLoanSimple': { + return { + type: 'unknown', + action: 'Executed flash loan with ' + displayAsset(directSent[0]), + ...newEvent, + } + } + + case 'setUserUseReserveAsCollateral': { + const assetAddress = event.methodCall.arguments[0].value as string + const enabled = event.methodCall.arguments[1].value === 'true' + return { + type: 'set-user-use-reserve-as-collateral', + action: `User ${enabled ? 'enabled' : 'disabled'} ${assetAddress} as collateral`, + ...newEvent, + } + } + } + + return { + ...newEvent, + type: 'unknown', + action: 'Unknown action', + } +} + +export const contracts = [ + '1:0x7d2768de32b0b80b7a3454c06bdac94a69ddc7a9', // Aave v2 + '1:0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2', // Aave v3 +] diff --git a/packages/transaction-interpreter/interpreters/aave2.ts b/packages/transaction-interpreter/interpreters/aave2.ts deleted file mode 100644 index 631e847..0000000 --- a/packages/transaction-interpreter/interpreters/aave2.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { assetsReceived, assetsSent } from './std.js' -import type { InterpretedTransaction } from '@/types.js' -import type { DecodedTx } from '@3loop/transaction-decoder' - -export function transformEvent(event: DecodedTx): InterpretedTransaction { - const methodName = event.methodCall.name - - const newEvent: Omit = { - chain: event.chainID, - txHash: event.txHash, - user: { address: event.fromAddress, name: null }, - method: methodName, - assetsSent: assetsSent(event.transfers, event.fromAddress), - assetsReceived: assetsReceived(event.transfers, event.fromAddress), - } - - switch (methodName) { - case 'repay': - return { - type: 'repay-loan', - action: 'User repaid ' + newEvent.assetsSent[1]?.amount + ' ' + newEvent.assetsSent[1]?.asset.symbol, - ...newEvent, - } - - case 'deposit': - return { - type: 'deposit-collateral', - action: 'User deposited ' + newEvent.assetsReceived[0]?.amount + ' ' + newEvent.assetsReceived[0]?.asset.symbol, - ...newEvent, - } - - case 'borrow': - return { - type: 'borrow', - action: 'User borrowed ' + newEvent.assetsReceived[0]?.amount + ' ' + newEvent.assetsReceived[0]?.asset.symbol, - ...newEvent, - } - - case 'withdraw': - return { - type: 'withdraw-collateral', - action: 'User withdrew ' + newEvent.assetsSent[0]?.amount + ' ' + newEvent.assetsSent[0]?.asset.symbol, - ...newEvent, - } - } - - return { - ...newEvent, - type: 'unknown', - action: 'Unknown action', - } -} - -export const contracts = ['1:0x7d2768de32b0b80b7a3454c06bdac94a69ddc7a9'] diff --git a/packages/transaction-interpreter/interpreters/1inchv5.ts b/packages/transaction-interpreter/interpreters/banana-gun.ts similarity index 50% rename from packages/transaction-interpreter/interpreters/1inchv5.ts rename to packages/transaction-interpreter/interpreters/banana-gun.ts index c70f9e9..26f0904 100644 --- a/packages/transaction-interpreter/interpreters/1inchv5.ts +++ b/packages/transaction-interpreter/interpreters/banana-gun.ts @@ -1,11 +1,7 @@ -import { assetsReceived, assetsSent } from './std.js' -import type { AssetTransfer, InterpretedTransaction } from '@/types.js' +import { assetsReceived, assetsSent, displayAsset, getPayments, isSwap } from './std.js' +import type { InterpretedTransaction } from '@/types.js' import type { DecodedTx } from '@3loop/transaction-decoder' -function displayAsset(asset: AssetTransfer): string { - return asset.amount + ' ' + asset.asset.symbol -} - export function transformEvent(event: DecodedTx): InterpretedTransaction { const methodName = event.methodCall.name @@ -18,15 +14,22 @@ export function transformEvent(event: DecodedTx): InterpretedTransaction { assetsReceived: assetsReceived(event.transfers, event.fromAddress), } - switch (methodName) { - case 'uniswapV3Swap': - case 'swap': - case 'unoswap': - return { - type: 'swap', - action: 'Swapped ' + displayAsset(newEvent.assetsSent[0]) + ' for ' + displayAsset(newEvent.assetsReceived[0]), - ...newEvent, - } + const netSent = getPayments({ + transfers: event.transfers, + fromAddresses: [event.fromAddress], + }) + + const netReceived = getPayments({ + transfers: event.transfers, + toAddresses: [event.fromAddress], + }) + + if (isSwap(event)) { + return { + type: 'swap', + action: 'Swapped ' + displayAsset(netSent[0]) + ' for ' + displayAsset(netReceived[0]), + ...newEvent, + } } return { @@ -36,4 +39,4 @@ export function transformEvent(event: DecodedTx): InterpretedTransaction { } } -export const contracts = ['1:0x1111111254EEB25477B68fb85Ed929f73A960582'] +export const contracts = ['1:0x3328f7f4a1d1c57c35df56bbf0c9dcafca309c49'] diff --git a/packages/transaction-interpreter/interpreters/blur.ts b/packages/transaction-interpreter/interpreters/blur.ts new file mode 100644 index 0000000..0257afc --- /dev/null +++ b/packages/transaction-interpreter/interpreters/blur.ts @@ -0,0 +1,66 @@ +import { assetsReceived, assetsSent, displayPayments, processNftTransfers } from './std.js' +import type { InterpretedTransaction } from '@/types.js' +import type { DecodedTx } from '@3loop/transaction-decoder' + +export function transformEvent(event: DecodedTx): InterpretedTransaction { + const methodName = event.methodCall.name ?? 'unknown' + + const newEvent: Omit = { + chain: event.chainID, + txHash: event.txHash, + user: { address: event.fromAddress, name: null }, + method: methodName, + assetsSent: assetsSent(event.transfers, event.fromAddress), + assetsReceived: assetsReceived(event.transfers, event.fromAddress), + } + + const { sendingAddresses, receivingAddresses, nftTransfers, erc20Payments, nativePayments } = processNftTransfers( + event.transfers, + ) + + if (nftTransfers.length === 0) { + return { + type: 'unknown', + action: 'Unknown action', + ...newEvent, + } + } + + const collection = nftTransfers[0].name ?? '' + const numberOfNfts = nftTransfers.length > 1 ? ` ${nftTransfers.length} ${collection} NFTS` : ` 1 ${collection} NFT` + const payment = displayPayments(erc20Payments, nativePayments) + + const sell = ['takeBidSingle', 'takeBid'] + const buy = ['takeAskSinglePool', 'takeAskSingle', 'takeAsk', 'takeAskPool', 'batchBuyWithETH', 'batchBuyWithERC20s'] + + if (sell.includes(methodName)) { + const from = receivingAddresses.length > 1 ? ` to ${receivingAddresses.length} users` : '' + return { + type: 'transfer-nft', + action: `Sold${numberOfNfts} for ${payment}${from}`, + ...newEvent, + } + } + + if (buy.includes(methodName)) { + const from = sendingAddresses.length > 1 ? ` from ${sendingAddresses.length} users` : '' + return { + type: 'transfer-nft', + action: `Bought${numberOfNfts} for ${payment}${from}`, + ...newEvent, + } + } + + return { + type: 'unknown', + action: 'Unknown action', + ...newEvent, + } +} + +export const contracts = [ + //Blur v3 + '1:0xb2ecfE4E4D61f8790bbb9DE2D1259B9e2410CEA5', + //Blur v2 + '1:0x39da41747a83aeE658334415666f3EF92DD0D541', +] diff --git a/packages/transaction-interpreter/interpreters/erc20.ts b/packages/transaction-interpreter/interpreters/erc20.ts index 8451464..9f073ee 100644 --- a/packages/transaction-interpreter/interpreters/erc20.ts +++ b/packages/transaction-interpreter/interpreters/erc20.ts @@ -1,4 +1,4 @@ -import { assetsReceived, assetsSent } from './std.js' +import { assetsReceived, assetsSent, formatNumber } from './std.js' import type { InterpretedTransaction } from '@/types.js' import type { DecodedTx } from '@3loop/transaction-decoder' @@ -32,7 +32,7 @@ export function transformEvent(event: DecodedTx): InterpretedTransaction { action = `Revoked approval for ${name} to be spent` } else { const amount = formatAmount(approvalValue, approval?.decimals || 18) - action = `Approved ${amount}${name} to be spent` + action = `Approved ${formatNumber(amount)}${name} to be spent` } } @@ -43,25 +43,25 @@ export function transformEvent(event: DecodedTx): InterpretedTransaction { } } case 'transfer': { - const amount = newEvent.assetsSent?.[0]?.amount || event.methodCall?.arguments?.[1]?.value + const amount = newEvent.assetsSent?.[0]?.amount || event.methodCall?.arguments?.[1]?.value || '0' const symbol = newEvent.assetsSent?.[0]?.asset?.symbol || event.contractName || 'unknown' return { type: 'transfer-token', - action: `Sent ${amount} ${symbol}`, + action: `Sent ${formatNumber(amount.toString())} ${symbol}`, ...newEvent, } } case 'transferFrom': { const from = event.methodCall?.arguments?.[0]?.value as string - const amount = event.transfers[0]?.amount || event.methodCall?.arguments?.[2]?.value + const amount = event.transfers[0]?.amount || event.methodCall?.arguments?.[2]?.value || '0' const symbol = event.transfers[0]?.symbol || event.contractName || 'unknown' if (!from) break return { type: 'transfer-token', - action: `Sent ${amount} ${symbol}`, + action: `Sent ${formatNumber(amount.toString())} ${symbol}`, ...newEvent, assetsSent: assetsSent(event.transfers, from), assetsReceived: assetsReceived(event.transfers, from), diff --git a/packages/transaction-interpreter/interpreters/hop-protocol.ts b/packages/transaction-interpreter/interpreters/hop-protocol.ts index 315760d..8a8c75b 100644 --- a/packages/transaction-interpreter/interpreters/hop-protocol.ts +++ b/packages/transaction-interpreter/interpreters/hop-protocol.ts @@ -1,10 +1,7 @@ -import type { AssetTransfer, InterpretedTransaction } from '@/types.js' +import type { InterpretedTransaction } from '@/types.js' import type { DecodedTx } from '@3loop/transaction-decoder' -import { assetsReceived, assetsSent } from './std.js' +import { assetsReceived, assetsSent, displayAsset } from './std.js' -function displayAsset(asset: AssetTransfer): string { - return asset.amount + ' ' + asset.asset.symbol -} export function transformEvent(tx: DecodedTx): InterpretedTransaction { const methodName = tx.methodCall.name diff --git a/packages/transaction-interpreter/interpreters/kyberswap.ts b/packages/transaction-interpreter/interpreters/kyberswap.ts new file mode 100644 index 0000000..7c731b7 --- /dev/null +++ b/packages/transaction-interpreter/interpreters/kyberswap.ts @@ -0,0 +1,44 @@ +import { assetsReceived, assetsSent, displayAsset, getPayments, isSwap } from './std.js' +import type { InterpretedTransaction } from '@/types.js' +import type { DecodedTx } from '@3loop/transaction-decoder' + +export function transformEvent(event: DecodedTx): InterpretedTransaction { + const methodName = event.methodCall.name + + const newEvent: Omit = { + chain: event.chainID, + txHash: event.txHash, + user: { address: event.fromAddress, name: null }, + method: methodName, + assetsSent: assetsSent(event.transfers, event.fromAddress), + assetsReceived: assetsReceived(event.transfers, event.fromAddress), + } + + const netSent = getPayments({ + transfers: event.transfers, + fromAddresses: [event.fromAddress], + }) + + const netReceived = getPayments({ + transfers: event.transfers, + toAddresses: [event.fromAddress], + }) + + if (isSwap(event)) { + return { + type: 'swap', + action: 'Swapped ' + displayAsset(netSent[0]) + ' for ' + displayAsset(netReceived[0]), + ...newEvent, + } + } + + return { + type: 'unknown', + action: `Called method '${methodName}'`, + ...newEvent, + } +} + +export const contracts = [ + '1:0x6131b5fae19ea4f9d964eac0408e4408b66337b5', // KyberSwap v2 +] diff --git a/packages/transaction-interpreter/interpreters/metamaskRouter.ts b/packages/transaction-interpreter/interpreters/metamaskRouter.ts new file mode 100644 index 0000000..c6736d5 --- /dev/null +++ b/packages/transaction-interpreter/interpreters/metamaskRouter.ts @@ -0,0 +1,42 @@ +import { assetsReceived, assetsSent, displayAsset, getPayments, isSwap } from './std.js' +import type { InterpretedTransaction } from '@/types.js' +import type { DecodedTx } from '@3loop/transaction-decoder' + +export function transformEvent(event: DecodedTx): InterpretedTransaction { + const methodName = event.methodCall.name + + const newEvent: Omit = { + chain: event.chainID, + txHash: event.txHash, + user: { address: event.fromAddress, name: null }, + method: methodName, + assetsSent: assetsSent(event.transfers, event.fromAddress), + assetsReceived: assetsReceived(event.transfers, event.fromAddress), + } + + const netSent = getPayments({ + transfers: event.transfers, + fromAddresses: [event.fromAddress], + }) + + const netReceived = getPayments({ + transfers: event.transfers, + toAddresses: [event.fromAddress], + }) + + if (isSwap(event)) { + return { + type: 'swap', + action: 'Swapped ' + displayAsset(netSent[0]) + ' for ' + displayAsset(netReceived[0]), + ...newEvent, + } + } + + return { + type: 'unknown', + action: `Called method '${methodName}'`, + ...newEvent, + } +} + +export const contracts = ['1:0x881D40237659C251811CEC9c364ef91dC08D300C'] diff --git a/packages/transaction-interpreter/interpreters/okx.ts b/packages/transaction-interpreter/interpreters/okx.ts new file mode 100644 index 0000000..a94aaa5 --- /dev/null +++ b/packages/transaction-interpreter/interpreters/okx.ts @@ -0,0 +1,45 @@ +import { assetsReceived, assetsSent, displayAsset, getPayments, isSwap } from './std.js' +import type { InterpretedTransaction } from '@/types.js' +import type { DecodedTx } from '@3loop/transaction-decoder' + +export function transformEvent(event: DecodedTx): InterpretedTransaction { + const methodName = event.methodCall.name + + const newEvent: Omit = { + chain: event.chainID, + txHash: event.txHash, + user: { address: event.fromAddress, name: null }, + method: methodName, + assetsSent: assetsSent(event.transfers, event.fromAddress), + assetsReceived: assetsReceived(event.transfers, event.fromAddress), + } + + const netSent = getPayments({ + transfers: event.transfers, + fromAddresses: [event.fromAddress], + }) + + const netReceived = getPayments({ + transfers: event.transfers, + toAddresses: [event.fromAddress], + }) + + if (isSwap(event)) { + return { + type: 'swap', + action: 'Swapped ' + displayAsset(netSent[0]) + ' for ' + displayAsset(netReceived[0]), + ...newEvent, + } + } + + return { + type: 'unknown', + action: `Called method '${methodName}'`, + ...newEvent, + } +} + +export const contracts = [ + '1:0xF3dE3C0d654FDa23daD170f0f320a92172509127', // OKX Aggregation Router + '1:0x7d0ccaa3fac1e5a943c5168b6ced828691b46b36', // OKX Router +] diff --git a/packages/transaction-interpreter/interpreters/opensea.ts b/packages/transaction-interpreter/interpreters/opensea.ts new file mode 100644 index 0000000..abe212c --- /dev/null +++ b/packages/transaction-interpreter/interpreters/opensea.ts @@ -0,0 +1,63 @@ +import { assetsReceived, assetsSent, processNftTransfers, displayPayments } from './std.js' +import type { InterpretedTransaction } from '@/types.js' +import type { DecodedTx } from '@3loop/transaction-decoder' + +export function transformEvent(event: DecodedTx): InterpretedTransaction { + const methodName = event.methodCall.name ?? 'unknown' + + const newEvent: Omit = { + chain: event.chainID, + txHash: event.txHash, + user: { address: event.fromAddress, name: null }, + method: methodName, + assetsSent: assetsSent(event.transfers, event.fromAddress), + assetsReceived: assetsReceived(event.transfers, event.fromAddress), + } + + const { sendingAddresses, receivingAddresses, nftTransfers, erc20Payments, nativePayments } = processNftTransfers( + event.transfers, + ) + + if (nftTransfers.length === 0) { + return { + type: 'unknown', + action: 'Unknown action', + ...newEvent, + } + } + + const collection = nftTransfers[0].name ?? '' + const numberOfNfts = nftTransfers.length > 1 ? ` ${nftTransfers.length} ${collection} NFTS` : ` 1 ${collection} NFT` + const payment = displayPayments(erc20Payments, nativePayments) + + if (sendingAddresses.includes(event.fromAddress.toLowerCase())) { + const from = receivingAddresses.length > 1 ? ` to ${receivingAddresses.length} users` : '' + return { + type: 'transfer-nft', + action: `Sold${numberOfNfts} for ${payment}${from}`, + ...newEvent, + } + } + + if (receivingAddresses.includes(event.fromAddress.toLowerCase())) { + const from = sendingAddresses.length > 1 ? ` from ${sendingAddresses.length} users` : '' + return { + type: 'transfer-nft', + action: `Bought${numberOfNfts} for ${payment}${from}`, + ...newEvent, + } + } + + return { + type: 'unknown', + action: 'Unknown action', + ...newEvent, + } +} + +export const contracts = [ + //Seaport 1.6 + '1:0x0000000000000068F116a894984e2DB1123eB395', + //Seaport 1.5 + '1:0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC', +] diff --git a/packages/transaction-interpreter/interpreters/pendle.ts b/packages/transaction-interpreter/interpreters/pendle.ts new file mode 100644 index 0000000..62b150f --- /dev/null +++ b/packages/transaction-interpreter/interpreters/pendle.ts @@ -0,0 +1,44 @@ +import { assetsReceived, assetsSent, displayAsset, getPayments, isSwap } from './std.js' +import type { InterpretedTransaction } from '@/types.js' +import type { DecodedTx } from '@3loop/transaction-decoder' + +export function transformEvent(event: DecodedTx): InterpretedTransaction { + const methodName = event.methodCall.name || 'unknown' + + const newEvent: Omit = { + chain: event.chainID, + txHash: event.txHash, + user: { address: event.fromAddress, name: null }, + method: methodName, + assetsSent: assetsSent(event.transfers, event.fromAddress), + assetsReceived: assetsReceived(event.transfers, event.fromAddress), + } + + const netSent = getPayments({ + transfers: event.transfers, + fromAddresses: [event.fromAddress], + }) + + const netReceived = getPayments({ + transfers: event.transfers, + toAddresses: [event.fromAddress], + }) + + if (isSwap(event)) { + return { + type: 'swap', + action: 'Swapped ' + displayAsset(netSent[0]) + ' for ' + displayAsset(netReceived[0]), + ...newEvent, + } + } + + return { + type: 'unknown', + action: `Called method '${methodName}'`, + ...newEvent, + } +} + +export const contracts = [ + '1:0x888888888889758F76e7103c6CbF23ABbF58F946', // Pendle Router v4 +] diff --git a/packages/transaction-interpreter/interpreters/std.ts b/packages/transaction-interpreter/interpreters/std.ts index e25ce7b..4d5fc0f 100644 --- a/packages/transaction-interpreter/interpreters/std.ts +++ b/packages/transaction-interpreter/interpreters/std.ts @@ -1,9 +1,11 @@ -import type { AssetTransfer } from '@/types.js' -import type { Asset } from '@3loop/transaction-decoder' +import type { AssetTransfer, Payment } from '@/types.js' +import { Asset, DecodedTx } from '@3loop/transaction-decoder' -export function assetsSent(transfers: Asset[], fromAddress: string): AssetTransfer[] { +export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000' + +export function assetsSent(transfers: Asset[], address: string): AssetTransfer[] { return transfers - .filter((t) => t.from.toLowerCase() === fromAddress.toLowerCase()) + .filter((t) => t.from.toLowerCase() === address.toLowerCase()) .map((t) => { return { from: { address: t.from, name: null }, @@ -14,9 +16,9 @@ export function assetsSent(transfers: Asset[], fromAddress: string): AssetTransf }) } -export function assetsReceived(transfers: Asset[], fromAddress: string): AssetTransfer[] { +export function assetsReceived(transfers: Asset[], address: string): AssetTransfer[] { return transfers - .filter((t) => t.to.toLowerCase() === fromAddress.toLowerCase()) + .filter((t) => t.to.toLowerCase() === address.toLowerCase()) .map((t) => { return { from: { address: t.from, name: null }, @@ -26,3 +28,164 @@ export function assetsReceived(transfers: Asset[], fromAddress: string): AssetTr } }) } + +export function displayAddress(address: string): string { + return address.slice(0, 6) + '...' + address.slice(-4) +} + +export function isSwap(event: DecodedTx): 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) return false + + if (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) + + // 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' + + if (asset.asset.symbol) return formatNumber(asset.amount) + ' ' + asset.asset.symbol + + return formatNumber(asset.amount) + ' ' + displayAddress(asset.asset.address) +} + +export function getPayments({ + transfers, + fromAddresses, + toAddresses, +}: { + transfers: Asset[] + fromAddresses?: string[] + toAddresses?: string[] +}): Payment[] { + const fromAddressFilter = fromAddresses?.map((a) => a.toLowerCase()) + const toAddressFilter = toAddresses?.map((a) => a.toLowerCase()) + let filteredTransfers = transfers + + if (fromAddressFilter && fromAddressFilter.length > 0) { + filteredTransfers = filteredTransfers.filter((t) => fromAddressFilter.includes(t.from.toLowerCase())) + } + + if (toAddressFilter && toAddressFilter.length > 0) { + filteredTransfers = filteredTransfers.filter((t) => toAddressFilter.includes(t.to.toLowerCase())) + } + + return Object.values( + filteredTransfers.reduce>((acc, t) => { + const address = t.address + const amount = Number(t.amount ?? '0') + + if (acc[address]) { + acc[address].amount = (Number(acc[address].amount) + amount).toString() + } else { + acc[address] = { + amount: amount.toString(), + asset: { + address: t.address, + name: t.name, + symbol: t.symbol, + type: t.type, + }, + } + } + + return acc + }, {}), + ) +} + +export function processNftTransfers(transfers: Asset[]) { + const nftTransfers: Asset[] = [] + const sendingAddresses: Set = new Set() + const receivingAddresses: Set = new Set() + const erc20Transfers: Asset[] = [] + const nativeTransfers: Asset[] = [] + + transfers.forEach((t) => { + switch (t.type) { + case 'ERC721': + case 'ERC1155': + nftTransfers.push(t) + break + case 'ERC20': + erc20Transfers.push(t) + break + case 'native': + nativeTransfers.push(t) + break + } + }) + + nftTransfers.forEach((t) => { + if (t.from.toLowerCase() !== t.to.toLowerCase()) { + receivingAddresses.add(t.to.toLowerCase()) + sendingAddresses.add(t.from.toLowerCase()) + } + }) + + const erc20Payments = getPayments({ + transfers: erc20Transfers, + fromAddresses: Array.from(receivingAddresses), + }) + + const nativePayments = getPayments({ + transfers: nativeTransfers, + fromAddresses: Array.from(receivingAddresses), + }) + + return { + nftTransfers, + erc20Payments, + nativePayments, + sendingAddresses: Array.from(sendingAddresses), + receivingAddresses: Array.from(receivingAddresses), + } +} + +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 + } else if (nativePayments.length > 0) { + return nativePayments[0].amount + ' ' + nativePayments[0].asset.symbol + } else { + return '' + } +} diff --git a/packages/transaction-interpreter/interpreters/uniswapv3.ts b/packages/transaction-interpreter/interpreters/uniswapv3.ts index 2c4eba0..cf9f5e7 100644 --- a/packages/transaction-interpreter/interpreters/uniswapv3.ts +++ b/packages/transaction-interpreter/interpreters/uniswapv3.ts @@ -1,11 +1,7 @@ -import { assetsReceived, assetsSent } from './std.js' -import type { AssetTransfer, InterpretedTransaction } from '@/types.js' +import { assetsReceived, assetsSent, displayAsset } from './std.js' +import type { InterpretedTransaction } from '@/types.js' import type { DecodedTx } from '@3loop/transaction-decoder' -function displayAsset(asset: AssetTransfer): string { - return asset.amount + ' ' + asset.asset.symbol -} - export function transformEvent(event: DecodedTx): InterpretedTransaction { const methodName = event.methodCall.name diff --git a/packages/transaction-interpreter/src/types.ts b/packages/transaction-interpreter/src/types.ts index 7e7879a..e480121 100644 --- a/packages/transaction-interpreter/src/types.ts +++ b/packages/transaction-interpreter/src/types.ts @@ -24,6 +24,9 @@ type TransactionType = | 'transfer-nft' | 'send-to-bridge' | 'receive-from-bridge' + | 'account-abstraction' + | 'stake-token' + | 'unstake-token' | 'unknown' | string @@ -42,6 +45,8 @@ export type AssetTransfer = { asset: Asset } +export type Payment = Omit + export type InterpretedTransaction = { chain: number action: string