From 3d491fe9576308688e9d58e5836300df28f4e2b8 Mon Sep 17 00:00:00 2001 From: Anastasia Rodionova Date: Tue, 28 Oct 2025 18:42:34 +0100 Subject: [PATCH] Run interpreter in nodejs env in the playground --- apps/web/package.json | 2 +- apps/web/src/app/api/interpret/route.ts | 42 +++++++++++++++ .../src/app/contract/[contract]/loading.tsx | 47 ----------------- apps/web/src/app/contract/[contract]/page.tsx | 44 ---------------- .../web/src/app/contract/[contract]/table.tsx | 51 ------------------- .../app/interpret/[chainID]/[hash]/form.tsx | 23 ++++++++- apps/web/src/app/layout.tsx | 6 --- apps/web/src/lib/etherscan.ts | 43 ---------------- .../{interpreter.ts => interpreter-server.ts} | 27 +++------- packages/transaction-interpreter/README.md | 2 +- pnpm-lock.yaml | 8 +-- 11 files changed, 75 insertions(+), 220 deletions(-) create mode 100644 apps/web/src/app/api/interpret/route.ts delete mode 100644 apps/web/src/app/contract/[contract]/loading.tsx delete mode 100644 apps/web/src/app/contract/[contract]/page.tsx delete mode 100644 apps/web/src/app/contract/[contract]/table.tsx delete mode 100644 apps/web/src/lib/etherscan.ts rename apps/web/src/lib/{interpreter.ts => interpreter-server.ts} (69%) 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