Skip to content

Commit

Permalink
Add erc20 and nfts interpreters (#82)
Browse files Browse the repository at this point in the history
* Add erc20 and nfts interpreters

* Add changeset
  • Loading branch information
anastasiarods authored Jul 27, 2024
1 parent 5696972 commit d1579f1
Show file tree
Hide file tree
Showing 12 changed files with 304 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .changeset/silent-flowers-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@3loop/transaction-interpreter': minor
---

Add default interpreters for erc20, erc721 and erc1155 contracts
9 changes: 3 additions & 6 deletions apps/web/src/app/tx/[chainID]/[hash]/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { DecodedTx } from '@3loop/transaction-decoder'
import { Interpretation, applyInterpreter } from '@/lib/interpreter'
import CodeBlock from '@/components/ui/code-block'
import { NetworkSelect } from '@/components/ui/network-select'
import { fallbackInterpreter, getInterpreterForContract } from '@3loop/transaction-interpreter'
import { fallbackInterpreter, getInterpreter } from '@3loop/transaction-interpreter'

interface FormProps {
currentChainID: number
Expand All @@ -29,15 +29,12 @@ export default function DecodingForm({ decoded, currentHash, currentChainID }: F

if (decoded?.toAddress == null) return null

const schema = getInterpreterForContract({
address: decoded.toAddress,
chain: decoded.chainID,
})
const schema = getInterpreter(decoded)

if (schema != null) return schema

return fallbackInterpreter
}, [decoded?.chainID, decoded?.toAddress, persistedSchema])
}, [decoded, persistedSchema])

const router = useRouter()

Expand Down
7 changes: 2 additions & 5 deletions apps/web/src/lib/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '@3loop/transaction-interpreter'
import { Effect, Layer } from 'effect'
import variant from '@jitl/quickjs-singlefile-browser-release-sync'
import { getInterpreterForContract } from '@3loop/transaction-interpreter'
import { getInterpreter } from '@3loop/transaction-interpreter'

const config = Layer.succeed(QuickjsConfig, {
variant: variant,
Expand Down Expand Up @@ -49,10 +49,7 @@ export async function applyInterpreter(decodedTx: DecodedTx, interpreter: Interp
}

export async function findAndRunInterpreter(decodedTx: DecodedTx): Promise<Interpretation> {
let interpreter = getInterpreterForContract({
address: decodedTx.toAddress ?? '',
chain: decodedTx.chainID,
})
let interpreter = getInterpreter(decodedTx)

if (!interpreter) {
interpreter = fallbackInterpreter
Expand Down
74 changes: 74 additions & 0 deletions packages/transaction-interpreter/interpreters/erc1155.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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<InterpretedTransaction, 'action' | 'type'> = {
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 'setApprovalForAll': {
const nftName = event?.contractName ? event?.contractName + ' ' : ''
const approvalValue = event.methodCall?.arguments?.[1]?.value

if (approvalValue === 'true') {
return {
type: 'approve-nft',
action: `Approved all NFTs ${nftName}to be spent`,
...newEvent,
}
} else {
return {
type: 'approve-nft',
action: `Revoked approval for all NFTs ${nftName}to be spent`,
...newEvent,
}
}
}
case 'safeTransferFrom': {
const from = (event.methodCall?.arguments?.[0]?.value as string) || ''
const name = event.contractName
const tokenId = event.methodCall?.arguments?.[2]?.value

if (!name || !tokenId) break

return {
type: 'transfer-nft',
action: `Sent ${name} #${tokenId}`,
...newEvent,
assetsSent: assetsSent(event.transfers, from),
assetsReceived: assetsReceived(event.transfers, from),
}
}
case 'safeBatchTransferFrom': {
const from = (event.methodCall?.arguments?.[0]?.value as string) || ''
const name = event.contractName
const tokenIds = event.methodCall?.arguments?.[2]?.value as string[]

if (!name || !tokenIds) break

return {
type: 'transfer-nft',
action: 'Sent ' + name + ' ' + tokenIds.map((id) => `#${id}`).join(', '),
...newEvent,
assetsSent: assetsSent(event.transfers, from),
assetsReceived: assetsReceived(event.transfers, from),
}
}
}

return {
type: 'unknown',
action: `Called method '${methodName}'`,
...newEvent,
}
}

export const contractType = 'erc1155'
79 changes: 79 additions & 0 deletions packages/transaction-interpreter/interpreters/erc20.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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<InterpretedTransaction, 'action' | 'type'> = {
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 'approve': {
const approval = event.interactions.find((i) => i.event.eventName === 'Approval')
const approvalValue = (event.methodCall?.arguments?.[1]?.value || '') as string
const name = approval?.contractSymbol || event.contractName || 'unknown'
let action = ''

const isUnlimitedApproval = (value: string) => value.startsWith('11579208923731619542357098')
const isRevokedApproval = (value: string) => value === '0'
const formatAmount = (value: string, decimals: number) =>
(BigInt(value) / BigInt(10 ** decimals)).toString() + ' '

if (approvalValue) {
if (isUnlimitedApproval(approvalValue)) {
action = `Approved unlimited amount of ${name} to be spent`
} else if (isRevokedApproval(approvalValue)) {
action = `Revoked approval for ${name} to be spent`
} else {
const amount = formatAmount(approvalValue, approval?.decimals || 18)
action = `Approved ${amount}${name} to be spent`
}
}

return {
type: 'approve-token',
action,
...newEvent,
}
}
case 'transfer': {
const amount = newEvent.assetsSent?.[0]?.amount || event.methodCall?.arguments?.[1]?.value
const symbol = newEvent.assetsSent?.[0]?.asset?.symbol || event.contractName || 'unknown'

return {
type: 'transfer-token',
action: `Sent ${amount} ${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 symbol = event.transfers[0]?.symbol || event.contractName || 'unknown'

if (!from) break

return {
type: 'transfer-token',
action: `Sent ${amount} ${symbol}`,
...newEvent,
assetsSent: assetsSent(event.transfers, from),
assetsReceived: assetsReceived(event.transfers, from),
}
}
}

return {
type: 'unknown',
action: `Called method '${methodName}'`,
...newEvent,
}
}

export const contractType = 'erc20'
85 changes: 85 additions & 0 deletions packages/transaction-interpreter/interpreters/erc721.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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<InterpretedTransaction, 'action' | 'type'> = {
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 'approve': {
const nftName = event.contractName || ''
const tokenId = event.methodCall?.arguments?.[1]?.value || ''

return {
type: 'approve-nft',
action: `Approved NFT ${nftName ? `${nftName} ` : ''}${tokenId ? `#${tokenId} ` : ''}to be spent`,
...newEvent,
}
}

case 'setApprovalForAll': {
const nftName = event?.contractName ? event?.contractName + ' ' : ''
const approvalValue = event.methodCall?.arguments?.[1]?.value

if (approvalValue === 'true') {
return {
type: 'approve-nft',
action: `Approved all NFTs ${nftName}to be spent`,
...newEvent,
}
} else {
return {
type: 'approve-nft',
action: `Revoked approval for all NFTs ${nftName}to be spent`,
...newEvent,
}
}
}
case 'safeTransferFrom': {
const from = (event.methodCall?.arguments?.[0]?.value as string) || ''
const name = event.contractName
const tokenId = event.methodCall?.arguments?.[2]?.value

if (!name || !tokenId) break

return {
type: 'transfer-nft',
action: `Sent ${name} #${tokenId}`,
...newEvent,
assetsSent: assetsSent(event.transfers, from),
assetsReceived: assetsReceived(event.transfers, from),
}
}
case 'transferFrom': {
const from = (event.methodCall?.arguments?.[0]?.value as string) || ''
const name = event.contractName
const tokenId = event.methodCall?.arguments?.[2]?.value

if (!name || !tokenId) break

return {
type: 'transfer-nft',
action: `Sent ${name} #${tokenId}`,
...newEvent,
assetsSent: assetsSent(event.transfers, from),
assetsReceived: assetsReceived(event.transfers, from),
}
}
}

return {
type: 'unknown',
action: `Called method '${methodName}'`,
...newEvent,
}
}

export const contractType = 'erc721'
27 changes: 21 additions & 6 deletions packages/transaction-interpreter/interpreters/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
import { DecodedTx } from '@3loop/transaction-decoder'

const interpretations: Record<string, string> = {
/**PLACE_INTEPRETATIONS**/
}
const contractToName: Record<string, string> = {
/**PLACE_CONTRACT_MAPPING**/
}

const contractTypeToName: Record<string, string> = {
/**PLACE_CONTRACT_TYPE_MAPPING**/
}

const standardLibrary = '/**PLACE_STD_CONTENT**/'
const fallbackInterpreter = standardLibrary + '\n' + '/**PLACE_FALLBACK_CONTENT**/'

// TODO: Add a default interpreter as a fallback
function getInterpreterForContract({ address, chain }: { address: string; chain: number }): string | undefined {
const key = `${chain}:${address}`.toLowerCase()
function getInterpreter(tx: DecodedTx): string | undefined {
const { chainID, toAddress, contractType } = tx
const key = `${chainID}:${toAddress}`.toLowerCase()
const id = contractToName[key]
if (!id) {
return undefined
if (id) {
return `${standardLibrary} \n ${interpretations[id]}`
}
return `${standardLibrary} \n ${interpretations[id]}`

const contractTypes = ['ERC20', 'ERC721', 'ERC1155']

if (contractTypes.includes(contractType)) {
const typeId = contractTypeToName[contractType.toLowerCase()]
return `${standardLibrary} \n ${interpretations[typeId]}`
}

return undefined
}

export { getInterpreterForContract, fallbackInterpreter }
export { getInterpreter, fallbackInterpreter }
4 changes: 2 additions & 2 deletions packages/transaction-interpreter/interpreters/std.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function assetsSent(transfers: Asset[], fromAddress: string): AssetTransf
from: { address: t.from, name: null },
to: { address: t.to, name: null },
amount: t.amount ?? '0',
asset: { address: t.address, name: t.name, symbol: t.symbol, type: t.type },
asset: { address: t.address, name: t.name, symbol: t.symbol, type: t.type, tokenId: t.tokenId },
}
})
}
Expand All @@ -22,7 +22,7 @@ export function assetsReceived(transfers: Asset[], fromAddress: string): AssetTr
from: { address: t.from, name: null },
to: { address: t.to, name: null },
amount: t.amount ?? '0',
asset: { address: t.address, name: t.name, symbol: t.symbol, type: t.type },
asset: { address: t.address, name: t.name, symbol: t.symbol, type: t.type, tokenId: t.tokenId },
}
})
}
2 changes: 1 addition & 1 deletion packages/transaction-interpreter/interpreters/weth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function transformEvent(event: DecodedTx): InterpretedTransaction {
case 'transfer':
return {
type: 'transfer',
action: 'Transferred ' + newEvent.assetsSent[0]?.amount + ' ' + newEvent.assetsSent[0]?.asset.symbol,
action: 'Sent ' + newEvent.assetsSent[0]?.amount + ' ' + newEvent.assetsSent[0]?.asset.symbol,
...newEvent,
}
}
Expand Down
Loading

0 comments on commit d1579f1

Please sign in to comment.