diff --git a/apps/web/package.json b/apps/web/package.json
index f21148e8..ccbab877 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -11,7 +11,7 @@
"dependencies": {
"@3loop/transaction-decoder": "workspace:*",
"@3loop/transaction-interpreter": "workspace:*",
- "@jitl/quickjs-singlefile-browser-release-sync": "^0.31.0",
+ "@jitl/quickjs-singlefile-cjs-release-sync": "^0.31.0",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.4",
diff --git a/apps/web/src/app/api/interpret/route.ts b/apps/web/src/app/api/interpret/route.ts
new file mode 100644
index 00000000..a3d17cb0
--- /dev/null
+++ b/apps/web/src/app/api/interpret/route.ts
@@ -0,0 +1,42 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { applyInterpreterServer } from '@/lib/interpreter-server'
+import type { DecodedTransaction } from '@3loop/transaction-decoder'
+
+export const runtime = 'nodejs'
+export const dynamic = 'force-dynamic'
+
+interface InterpretRequest {
+ decodedTx: DecodedTransaction
+ interpreter: {
+ id: string
+ schema: string
+ }
+ interpretAsUserAddress?: string
+}
+
+export const POST = async (request: NextRequest) => {
+ try {
+ const body: InterpretRequest = await request.json()
+
+ const { decodedTx, interpreter, interpretAsUserAddress } = body
+
+ if (!decodedTx || !interpreter) {
+ return NextResponse.json(
+ { error: 'Missing required fields: decodedTx and interpreter are required' },
+ { status: 400 },
+ )
+ }
+
+ const result = await applyInterpreterServer(decodedTx, interpreter, interpretAsUserAddress)
+
+ return NextResponse.json(result)
+ } catch (error) {
+ console.error('Interpreter API error:', error)
+ return NextResponse.json(
+ {
+ error: error instanceof Error ? error.message : 'Failed to interpret transaction',
+ },
+ { status: 500 },
+ )
+ }
+}
diff --git a/apps/web/src/app/contract/[contract]/loading.tsx b/apps/web/src/app/contract/[contract]/loading.tsx
deleted file mode 100644
index 90d87e04..00000000
--- a/apps/web/src/app/contract/[contract]/loading.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { aaveV2 } from '@/app/data'
-import { Input } from '@/components/ui/input'
-import { Label } from '@/components/ui/label'
-import { Separator } from '@/components/ui/separator'
-import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
-
-const LOADING_ROWS = Array.from(Array(5).keys())
-
-export default function Loading() {
- return (
-
-
-
-
-
-
-
- A list of recent transactions.
-
-
- Age
- Link
- Interpretation
-
-
-
-
- {LOADING_ROWS.map((key) => {
- return (
-
-
-
-
-
-
-
-
-
-
-
- )
- })}
-
-
-
- )
-}
diff --git a/apps/web/src/app/contract/[contract]/page.tsx b/apps/web/src/app/contract/[contract]/page.tsx
deleted file mode 100644
index 3ef4176d..00000000
--- a/apps/web/src/app/contract/[contract]/page.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import * as React from 'react'
-import TxTable from './table'
-import { aaveV2, DEFAULT_CHAIN_ID, DEFAULT_CONTRACT } from '../../data'
-import { Label } from '@/components/ui/label'
-import { Input } from '@/components/ui/input'
-import { Separator } from '@/components/ui/separator'
-import { getTransactions } from '@/lib/etherscan'
-import { decodeTransaction } from '@/lib/decode'
-import { DecodedTransaction } from '@3loop/transaction-decoder'
-
-async function getListOfDecodedTransactions(
- contract: string,
- chainID: number,
-): Promise<(DecodedTransaction | undefined)[]> {
- if (contract !== aaveV2) return []
-
- try {
- const txs = await getTransactions(1, contract)
- const decodedTxs = await Promise.all(txs.map(({ hash }) => decodeTransaction({ hash, chainID: chainID })))
-
- return decodedTxs.map(({ decoded }) => decoded)
- } catch (e) {
- console.error(e)
- return []
- }
-}
-
-export default async function Home({ params }: { params: { contract?: string } }) {
- let contract = params.contract?.toLowerCase() || DEFAULT_CONTRACT
- const decodedTxs = (await getListOfDecodedTransactions(contract, DEFAULT_CHAIN_ID)).filter(
- (tx): tx is DecodedTransaction => !!tx,
- )
-
- return (
-
-
-
-
-
-
-
-
- )
-}
diff --git a/apps/web/src/app/contract/[contract]/table.tsx b/apps/web/src/app/contract/[contract]/table.tsx
deleted file mode 100644
index f5c8117c..00000000
--- a/apps/web/src/app/contract/[contract]/table.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-'use client'
-import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
-import React from 'react'
-import type { DecodedTransaction } from '@3loop/transaction-decoder'
-import { findAndRunInterpreter, Interpretation } from '@/lib/interpreter'
-
-export default function TxTable({ txs }: { txs: DecodedTransaction[] }) {
- const [result, setResult] = React.useState([])
-
- React.useEffect(() => {
- async function run() {
- const withIntepretations = await Promise.all(
- txs.map((tx) => {
- return findAndRunInterpreter(tx)
- }),
- )
-
- setResult(withIntepretations)
- }
- run()
- }, [txs])
-
- return (
-
- A list of recent transactions.
-
-
- Age
- Link
- Interpretation
-
-
-
-
- {result.map(({ tx, interpretation }) => (
-
- {new Date(Number(tx?.timestamp + '000')).toUTCString()}
-
-
- {tx?.txHash.slice(0, 6) + '...' + tx?.txHash.slice(-4)}
-
-
-
- {typeof interpretation === 'string' ? interpretation : JSON.stringify(interpretation, null, 2)}
-
-
- ))}
-
-
- )
-}
diff --git a/apps/web/src/app/interpret/[chainID]/[hash]/form.tsx b/apps/web/src/app/interpret/[chainID]/[hash]/form.tsx
index 6fac4a16..e0e330f8 100644
--- a/apps/web/src/app/interpret/[chainID]/[hash]/form.tsx
+++ b/apps/web/src/app/interpret/[chainID]/[hash]/form.tsx
@@ -8,7 +8,7 @@ import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { useRouter } from 'next/navigation'
import { DecodedTransaction } from '@3loop/transaction-decoder'
-import { Interpretation, applyInterpreter } from '@/lib/interpreter'
+import type { Interpretation } from '@/lib/interpreter-server'
import CodeBlock from '@/components/ui/code-block'
import { NetworkSelect } from '@/components/ui/network-select'
import { fallbackInterpreter, getInterpreter } from '@3loop/transaction-interpreter'
@@ -65,10 +65,29 @@ export default function DecodingForm({ decoded, currentHash, chainID, error }: F
setIsInterpreting(true)
setResult(undefined)
- applyInterpreter(decoded, newInterpreter, userAddress || undefined)
+ // Call server-side API to run interpreter (avoids CORS issues)
+ fetch('/api/interpret', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ decodedTx: decoded,
+ interpreter: newInterpreter,
+ interpretAsUserAddress: userAddress || undefined,
+ }),
+ })
+ .then((response) => response.json())
.then((res) => {
setResult(res)
})
+ .catch((error) => {
+ setResult({
+ tx: decoded,
+ interpretation: null,
+ error: error.message || 'Failed to interpret transaction',
+ })
+ })
.finally(() => {
setIsInterpreting(false)
})
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index 75f55b9f..3aa96b21 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -4,7 +4,6 @@ import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import { MainNav } from '@/components/ui/main-nav'
import { Analytics } from '@vercel/analytics/react'
-import { aaveV2 } from '../app/data'
import {
DropdownMenu,
DropdownMenuContent,
@@ -33,11 +32,6 @@ const navLinks = [
match: 'interpret',
title: 'Transaction Interpreter',
},
- {
- href: `/contract/${aaveV2}`,
- match: 'contract',
- title: 'Test contract',
- },
]
const SOCIAL_LINKS = [
diff --git a/apps/web/src/lib/etherscan.ts b/apps/web/src/lib/etherscan.ts
deleted file mode 100644
index cd1100bb..00000000
--- a/apps/web/src/lib/etherscan.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-const endpoints: { [k: number]: string } = {
- 1: 'https://api.etherscan.io/api',
- 3: 'https://api-ropsten.etherscan.io/api',
- 4: 'https://api-rinkeby.etherscan.io/api',
- 5: 'https://api-goerli.etherscan.io/api',
- 8453: 'https://api.basescan.org/api',
- 84531: 'https://api-goerli.basescan.org/api',
- 84532: 'https://api-sepolia.basescan.org/api',
-}
-
-export interface Transfer {
- timeStamp: string
- uniqueId: string
- hash: string
- from: string
- to: string
- interpretation?: any
-}
-
-export async function getTransactions(chainId: number, address: string) {
- const url = endpoints[chainId]
-
- const data = new URLSearchParams({
- module: 'account',
- action: 'txlist',
- address,
- sort: 'desc',
- offset: '5',
- page: '1',
- apikey: process.env.ETHERSCAN_API_KEY || '',
- })
-
- const resp = await fetch(`${url}?${data.toString()}`, {
- next: { revalidate: 60 * 5 },
- })
-
- if (!resp.ok) {
- throw new Error(resp.statusText)
- }
-
- const json = (await resp.json()).result as Transfer[]
- return json
-}
diff --git a/apps/web/src/lib/interpreter.ts b/apps/web/src/lib/interpreter-server.ts
similarity index 69%
rename from apps/web/src/lib/interpreter.ts
rename to apps/web/src/lib/interpreter-server.ts
index f8870e6e..0bc1e2a6 100644
--- a/apps/web/src/lib/interpreter.ts
+++ b/apps/web/src/lib/interpreter-server.ts
@@ -4,16 +4,16 @@ import {
QuickjsInterpreterLive,
QuickjsConfig,
TransactionInterpreter,
- fallbackInterpreter,
- getInterpreter,
} from '@3loop/transaction-interpreter'
import { Effect, Layer } from 'effect'
-import variant from '@jitl/quickjs-singlefile-browser-release-sync'
+import variant from '@jitl/quickjs-singlefile-cjs-release-sync'
+// Server-side interpreter configuration using Node.js QuickJS variant
+// This runs in Node.js context where CORS restrictions don't apply
const config = Layer.succeed(QuickjsConfig, {
variant: variant,
runtimeConfig: {
- timeout: 1000,
+ timeout: 5000,
useFetch: true,
},
})
@@ -26,11 +26,11 @@ export interface Interpretation {
error?: string
}
-export async function applyInterpreter(
+export const applyInterpreterServer = async (
decodedTx: DecodedTransaction,
interpreter: Interpreter,
interpretAsUserAddress?: string,
-): Promise {
+): Promise => {
const runnable = Effect.gen(function* () {
const interpreterService = yield* TransactionInterpreter
const interpretation = yield* interpreterService.interpretTransaction(decodedTx, interpreter, {
@@ -54,18 +54,3 @@ export async function applyInterpreter(
}
})
}
-
-export async function findAndRunInterpreter(decodedTx: DecodedTransaction): Promise {
- let interpreter = getInterpreter(decodedTx)
-
- if (!interpreter) {
- interpreter = fallbackInterpreter
- }
-
- const res = await applyInterpreter(decodedTx, {
- id: 'default',
- schema: interpreter,
- })
-
- return res
-}
diff --git a/packages/transaction-interpreter/README.md b/packages/transaction-interpreter/README.md
index ae4d2580..f29d99c1 100644
--- a/packages/transaction-interpreter/README.md
+++ b/packages/transaction-interpreter/README.md
@@ -40,7 +40,7 @@ const runnable = Effect.gen(function* () {
// NOTE: Search the interpreter in the default interpreters
const interpreter = interpreterService.findInterpreter(decodedTx)
- const interpretation = yield* interpreterService.interpretTx(decodedTx, interpreter)
+ const interpretation = yield* interpreterService.interpretTransaction(decodedTx, interpreter)
return interpretation
}).pipe(Effect.provide(layer))
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ff692ff1..3e4ba653 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -81,7 +81,7 @@ importers:
'@effect/sql-pg':
specifier: ^0.36.1
version: 0.36.1(@effect/experimental@0.46.1(@effect/platform@0.82.1(effect@3.15.1))(effect@3.15.1))(@effect/platform@0.82.1(effect@3.15.1))(@effect/sql@0.35.1(@effect/experimental@0.46.1(@effect/platform@0.82.1(effect@3.15.1))(effect@3.15.1))(@effect/platform@0.82.1(effect@3.15.1))(effect@3.15.1))(effect@3.15.1)
- '@jitl/quickjs-singlefile-browser-release-sync':
+ '@jitl/quickjs-singlefile-cjs-release-sync':
specifier: ^0.31.0
version: 0.31.0
'@monaco-editor/react':
@@ -1456,8 +1456,8 @@ packages:
'@jitl/quickjs-ffi-types@0.31.0':
resolution: {integrity: sha512-1yrgvXlmXH2oNj3eFTrkwacGJbmM0crwipA3ohCrjv52gBeDaD7PsTvFYinlAnqU8iPME3LGP437yk05a2oejw==}
- '@jitl/quickjs-singlefile-browser-release-sync@0.31.0':
- resolution: {integrity: sha512-JctBiLmRpxEp83gJWhDcBuFqm5X7T683OLmncN9g0chAHkC8+y5cJmgknaAk5Rb/ANDR3pXMMnGdnGXDdysfBQ==}
+ '@jitl/quickjs-singlefile-cjs-release-sync@0.31.0':
+ resolution: {integrity: sha512-TQ6WUsmdcdlXQKPyyGE/qNAoWY83mvjn+VNru6ug5ILv1D3Y+yaFXnMx+QyNX0onx9xSRGgVNZxXN0V0U+ZKpQ==}
'@jitl/quickjs-wasmfile-debug-asyncify@0.29.2':
resolution: {integrity: sha512-YdRw2414pFkxzyyoJGv81Grbo9THp/5athDMKipaSBNNQvFE9FGRrgE9tt2DT2mhNnBx1kamtOGj0dX84Yy9bg==}
@@ -8027,7 +8027,7 @@ snapshots:
'@jitl/quickjs-ffi-types@0.31.0': {}
- '@jitl/quickjs-singlefile-browser-release-sync@0.31.0':
+ '@jitl/quickjs-singlefile-cjs-release-sync@0.31.0':
dependencies:
'@jitl/quickjs-ffi-types': 0.31.0