diff --git a/.github/workflows/frontend-unit-tests.yml b/.github/workflows/frontend-unit-tests.yml new file mode 100644 index 0000000..543e35b --- /dev/null +++ b/.github/workflows/frontend-unit-tests.yml @@ -0,0 +1,48 @@ +name: CI – Frontend Unit Tests + +on: + push: + paths: + - "frontend/lib/**" + - "frontend/hooks/**" + - ".github/workflows/frontend-unit-tests.yml" + pull_request: + paths: + - "frontend/lib/**" + - "frontend/hooks/**" + - ".github/workflows/frontend-unit-tests.yml" + workflow_dispatch: + +concurrency: + group: frontend-unit-${{ github.ref }} + cancel-in-progress: true + +jobs: + unit: + name: Node.js unit tests + runs-on: ubuntu-latest + timeout-minutes: 10 + defaults: + run: + working-directory: frontend + + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + package_json_file: frontend/package.json + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + cache-dependency-path: frontend/pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run unit tests + run: pnpm test:unit diff --git a/frontend/app/api/admin/audit-log/route.ts b/frontend/app/api/admin/audit-log/route.ts new file mode 100644 index 0000000..67901cb --- /dev/null +++ b/frontend/app/api/admin/audit-log/route.ts @@ -0,0 +1,80 @@ +import { NextRequest, NextResponse } from "next/server" +import { supabase } from "@/lib/supabase" +import { readLimiter } from "@/lib/rate-limit" + +export interface AuditRow { + id: string + pool_id: string + pool_name: string | null + activity_type: string + user_address: string | null + amount: number | null + tx_hash: string | null + description: string | null + created_at: string + /** True when the DB total_saved disagrees with the activity sum for this pool. */ + inconsistent: boolean +} + +/** + * GET /api/admin/audit-log?poolId= + * + * Returns all pool_activity rows for the given pool together with a + * per-pool consistency flag: `inconsistent` is true when the pool's + * recorded `total_saved` diverges from the sum of deposit/withdrawal + * activity rows by more than 0.01 (floating-point tolerance). + * + * Only the pool creator should call this route; the component enforces + * that guard on the client side. A missing poolId returns 400. + */ +export async function GET(req: NextRequest) { + const limited = readLimiter(req) + if (limited) return limited + + const poolId = req.nextUrl.searchParams.get("poolId") + if (!poolId) { + return NextResponse.json({ error: "poolId is required" }, { status: 400 }) + } + + const { data: pool, error: poolErr } = await supabase + .from("pools") + .select("id, name, total_saved") + .eq("id", poolId) + .single() + + if (poolErr || !pool) { + return NextResponse.json({ error: "Pool not found" }, { status: 404 }) + } + + const { data: activity, error: actErr } = await supabase + .from("pool_activity") + .select("id, pool_id, activity_type, user_address, amount, tx_hash, description, created_at") + .eq("pool_id", poolId) + .order("created_at", { ascending: false }) + + if (actErr) { + return NextResponse.json({ error: "Failed to fetch activity" }, { status: 500 }) + } + + const rows = activity ?? [] + + // ── Consistency check ────────────────────────────────────────────────────── + const activityNet = rows.reduce((sum: number, r: { activity_type: string; amount: number | null }) => { + const amt = r.amount ?? 0 + const t = r.activity_type.toLowerCase() + if (t === "deposit") return sum + amt + if (t === "withdraw" || t === "payout") return sum - amt + return sum + }, 0) + + const recorded = pool.total_saved ?? 0 + const inconsistent = Math.abs(activityNet - recorded) > 0.01 + + const auditRows: AuditRow[] = rows.map((r: { id: string; pool_id: string; activity_type: string; user_address: string | null; amount: number | null; tx_hash: string | null; description: string | null; created_at: string }) => ({ + ...r, + pool_name: pool.name, + inconsistent, + })) + + return NextResponse.json({ rows: auditRows, inconsistent, activityNet, recorded }) +} diff --git a/frontend/app/dashboard/group/[id]/page.tsx b/frontend/app/dashboard/group/[id]/page.tsx index 20248cb..d79bdc7 100644 --- a/frontend/app/dashboard/group/[id]/page.tsx +++ b/frontend/app/dashboard/group/[id]/page.tsx @@ -7,6 +7,7 @@ import { GroupMembers } from "@/components/group/group-members"; import { GroupActivity } from "@/components/group/group-activity"; import { GroupActions } from "@/components/group/group-actions"; import { YieldDashboard } from "@/components/group/yield-dashboard"; +import { AdminAuditLog } from "@/components/group/admin-audit-log"; import { Button } from "@/components/ui/button"; import { ArrowLeft } from "lucide-react"; import Link from "next/link"; @@ -15,17 +16,12 @@ import { useStellar } from "@/components/web3-provider"; import { useRecentPools } from "@/hooks/useRecentPools"; interface Pool { - id: string - name: string - type: 'rotational' | 'target' | 'flexible' - contract_address: string - token_address: string - creator_address: string id: string; name: string; type: "rotational" | "target" | "flexible"; contract_address: string; token_address: string; + creator_address: string; } const isPendingAddress = (addr: string) => @@ -66,7 +62,6 @@ export default function GroupPage({ contract_address: pool.contract_address, }); } - // Reset tracker when navigating to a different pool if (!pool || loading) { trackedRef.current = false; } @@ -115,6 +110,11 @@ export default function GroupPage({ contractAddress={cacheKey} startLedger={0} /> + {/* Admin audit log with CSV export — only shown to the pool creator */} +
@@ -142,4 +142,3 @@ export default function GroupPage({
); } - diff --git a/frontend/components/group/admin-audit-log.tsx b/frontend/components/group/admin-audit-log.tsx new file mode 100644 index 0000000..876df9b --- /dev/null +++ b/frontend/components/group/admin-audit-log.tsx @@ -0,0 +1,167 @@ +"use client" + +/** + * AdminAuditLog + * + * Shows the full pool activity log with a consistency-check banner and + * an Export CSV button. Only rendered for the pool creator. + */ + +import { useState, useEffect } from "react" +import { Card } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { AlertTriangle, CheckCircle2, Download, Loader2 } from "lucide-react" +import { useStellar } from "@/components/web3-provider" +import { buildCsv, downloadCsv } from "@/lib/csv-export" +import type { AuditRow } from "@/app/api/admin/audit-log/route" + +interface AdminAuditLogProps { + groupId: string + creatorAddress: string +} + +export function AdminAuditLog({ groupId, creatorAddress }: AdminAuditLogProps) { + const { address } = useStellar() + + const isCreator = + address && + creatorAddress && + address.toLowerCase() === creatorAddress.toLowerCase() + + const [rows, setRows] = useState([]) + const [inconsistent, setInconsistent] = useState(false) + const [activityNet, setActivityNet] = useState(0) + const [recorded, setRecorded] = useState(0) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + if (!isCreator) return + setLoading(true) + fetch(`/api/admin/audit-log?poolId=${groupId}`) + .then((r) => r.json()) + .then((data) => { + if (data.error) throw new Error(data.error) + setRows(data.rows) + setInconsistent(data.inconsistent) + setActivityNet(data.activityNet) + setRecorded(data.recorded) + }) + .catch((e) => setError(e.message)) + .finally(() => setLoading(false)) + }, [isCreator, groupId]) + + if (!isCreator) return null + + const handleExport = () => { + const headers = [ + "Date", + "Activity Type", + "User Address", + "Amount (XLM)", + "Tx Hash", + "Description", + ] + const data = rows.map((r) => [ + new Date(r.created_at).toISOString().slice(0, 19).replace("T", " "), + r.activity_type, + r.user_address ?? "", + r.amount != null ? r.amount.toFixed(2) : "", + r.tx_hash ?? "", + r.description ?? "", + ]) + const csv = buildCsv(headers, data) + downloadCsv(csv, `audit-log-${groupId}-${new Date().toISOString().slice(0, 10)}.csv`) + } + + return ( + +
+
+

Admin Audit Log

+

Visible to pool creator only

+
+ +
+ + {/* Consistency banner */} + {!loading && !error && ( +
+ {inconsistent ? ( + + ) : ( + + )} + + {inconsistent + ? `Balance inconsistency: activity net ${activityNet.toFixed(2)} XLM ≠ recorded ${recorded.toFixed(2)} XLM` + : `Balance consistent: ${recorded.toFixed(2)} XLM`} + +
+ )} + + {loading && ( +
+ +
+ )} + + {error && ( +

{error}

+ )} + + {!loading && !error && rows.length === 0 && ( +

No activity recorded.

+ )} + + {!loading && !error && rows.length > 0 && ( +
+ {rows.map((r) => ( +
+
+
+ {r.activity_type} + {r.amount != null && ( + {r.amount.toFixed(2)} XLM + )} +
+

+ {r.user_address ? `${r.user_address.slice(0, 8)}…${r.user_address.slice(-6)}` : "System"} +

+ {r.tx_hash && ( + + {r.tx_hash.slice(0, 8)}… + + )} +
+ +
+ ))} +
+ )} +
+ ) +} diff --git a/frontend/components/group/group-actions.tsx b/frontend/components/group/group-actions.tsx index 5b97301..6631a15 100644 --- a/frontend/components/group/group-actions.tsx +++ b/frontend/components/group/group-actions.tsx @@ -35,23 +35,6 @@ import { fetchPoolMembers, } from "@/hooks/useJointSaveContracts"; import { validateStellarAddress } from "@/lib/form-validation"; -import { - useRotationalDeposit, useTriggerPayout, - useTargetContribute, useTargetWithdraw, useTargetRefund, - useFlexibleDeposit, useFlexibleWithdraw, -} from "@/hooks/useJointSaveContracts" -import { ExportPdfButton } from "@/components/group/export-pdf-button" - -interface GroupActionsProps { - groupId: string - poolAddress: string - poolType: "rotational" | "target" | "flexible" - tokenAddress: string - creatorAddress?: string - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; import { useOptimisticTransactions } from "@/hooks/useOptimisticTransactions"; import { toastManager } from "@/lib/toast"; import { @@ -62,12 +45,19 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { ExportPdfButton } from "@/components/group/export-pdf-button"; interface GroupActionsProps { groupId: string; poolAddress: string; poolType: "rotational" | "target" | "flexible"; tokenAddress: string; + creatorAddress?: string; isPaused?: boolean; poolAdmin?: string | null; onPauseChange?: () => void; @@ -98,17 +88,12 @@ async function logActivity( }); } catch {} } - -export function GroupActions({ groupId, poolAddress, poolType, creatorAddress }: GroupActionsProps) { - const { address } = useStellar() - const [depositAmount, setDepositAmount] = useState("") - const [withdrawAmount, setWithdrawAmount] = useState("") - const [error, setError] = useState("") - const [successMsg, setSuccessMsg] = useState("") export function GroupActions({ groupId, poolAddress, poolType, + tokenAddress, + creatorAddress, isPaused = false, poolAdmin = null, onPauseChange, @@ -875,23 +860,6 @@ export function GroupActions({ )} -
-

Your Stellar address

-

- {address || "Not connected"} -

-
- - {/* Admin-only PDF export */} - {creatorAddress && ( -
-

Admin

- -
- )} - - - ) diff --git a/frontend/lib/csv-export.test.ts b/frontend/lib/csv-export.test.ts new file mode 100644 index 0000000..ed018c4 --- /dev/null +++ b/frontend/lib/csv-export.test.ts @@ -0,0 +1,91 @@ +// Unit tests for the csv-export utility +import { test } from "node:test" +import assert from "node:assert" +import { buildCsv } from "./csv-export" + +test("buildCsv - basic output with header and rows", () => { + const csv = buildCsv(["A", "B"], [["x", "y"], ["1", "2"]]) + assert.strictEqual(csv, "A,B\nx,y\n1,2") +}) + +test("buildCsv - wraps cells containing commas in double quotes", () => { + const csv = buildCsv(["Col"], [["hello, world"]]) + assert.strictEqual(csv, 'Col\n"hello, world"') +}) + +test("buildCsv - escapes embedded double-quotes (RFC 4180)", () => { + const csv = buildCsv(["Col"], [['say "hi"']]) + assert.strictEqual(csv, 'Col\n"say ""hi"""') +}) + +test("buildCsv - handles null / undefined values as empty strings", () => { + const csv = buildCsv(["A", "B"], [[null, undefined]]) + assert.strictEqual(csv, "A,B\n,") +}) + +test("buildCsv - empty rows returns header only", () => { + const csv = buildCsv(["X", "Y"], []) + assert.strictEqual(csv, "X,Y") +}) + +test("buildCsv - wraps cells containing newlines in double quotes", () => { + const csv = buildCsv(["Note"], [["line1\nline2"]]) + assert.strictEqual(csv, 'Note\n"line1\nline2"') +}) + +// ── Consistency-check logic (mirrors the API route calculation) ────────────── + +function computeConsistency( + activities: { activity_type: string; amount: number | null }[], + recorded: number +): { inconsistent: boolean; activityNet: number } { + const activityNet = activities.reduce((sum, r) => { + const amt = r.amount ?? 0 + const t = r.activity_type.toLowerCase() + if (t === "deposit") return sum + amt + if (t === "withdraw" || t === "payout") return sum - amt + return sum + }, 0) + return { inconsistent: Math.abs(activityNet - recorded) > 0.01, activityNet } +} + +test("consistency check - consistent when net matches recorded", () => { + const acts = [ + { activity_type: "deposit", amount: 100 }, + { activity_type: "payout", amount: 40 }, + ] + const { inconsistent, activityNet } = computeConsistency(acts, 60) + assert.strictEqual(inconsistent, false) + assert.strictEqual(activityNet, 60) +}) + +test("consistency check - inconsistent when net diverges from recorded", () => { + const acts = [{ activity_type: "deposit", amount: 100 }] + const { inconsistent } = computeConsistency(acts, 90) + assert.strictEqual(inconsistent, true) +}) + +test("consistency check - tolerates floating-point differences ≤ 0.01", () => { + const acts = [{ activity_type: "deposit", amount: 100.001 }] + const { inconsistent } = computeConsistency(acts, 100) + assert.strictEqual(inconsistent, false) +}) + +test("consistency check - non-financial activity types are ignored", () => { + const acts = [ + { activity_type: "deposit", amount: 50 }, + { activity_type: "member_joined", amount: null }, + ] + const { inconsistent, activityNet } = computeConsistency(acts, 50) + assert.strictEqual(inconsistent, false) + assert.strictEqual(activityNet, 50) +}) + +test("consistency check - withdraw reduces the net", () => { + const acts = [ + { activity_type: "deposit", amount: 200 }, + { activity_type: "withdraw", amount: 80 }, + ] + const { activityNet } = computeConsistency(acts, 120) + assert.strictEqual(activityNet, 120) +}) diff --git a/frontend/lib/csv-export.ts b/frontend/lib/csv-export.ts new file mode 100644 index 0000000..04968cd --- /dev/null +++ b/frontend/lib/csv-export.ts @@ -0,0 +1,36 @@ +/** + * Minimal CSV export utility. + * + * buildCsv – pure function that converts a header row + data rows to a CSV string. + * downloadCsv – triggers a browser file-download from a CSV string (no-op in SSR). + */ + +/** Escape a single cell value per RFC 4180. */ +function escapeCell(value: unknown): string { + const str = value == null ? "" : String(value) + // Wrap in quotes if the value contains a comma, double-quote, or newline. + if (/[",\n\r]/.test(str)) { + return `"${str.replace(/"/g, '""')}"` + } + return str +} + +/** Convert a header + rows array into a CSV string. */ +export function buildCsv(headers: string[], rows: unknown[][]): string { + const lines = [headers, ...rows].map((row) => + row.map(escapeCell).join(",") + ) + return lines.join("\n") +} + +/** Trigger a browser download for the given CSV string. */ +export function downloadCsv(csv: string, filename: string): void { + if (typeof window === "undefined") return + const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }) + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + a.href = url + a.download = filename + a.click() + URL.revokeObjectURL(url) +} diff --git a/frontend/package.json b/frontend/package.json index 97c684e..d04f326 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,6 +7,7 @@ "dev": "next dev --webpack", "lint": "next lint", "start": "next start", + "test:unit": "tsx --test lib/csv-export.test.ts lib/analytics.test.ts lib/pool-health.test.ts hooks/use-keyboard-shortcuts.test.ts", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", "test:e2e:report": "playwright show-report" @@ -86,6 +87,7 @@ "@types/react-dom": "^19", "postcss": "^8.5", "tailwindcss": "^4.1.9", + "tsx": "4.19.2", "tw-animate-css": "1.3.3", "typescript": "^5" }, diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index a23bf82..6faf5cf 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -225,6 +225,9 @@ importers: tailwindcss: specifier: ^4.1.9 version: 4.2.2 + tsx: + specifier: 4.19.2 + version: 4.19.2 tw-animate-css: specifier: 1.3.3 version: 1.3.3 @@ -340,6 +343,150 @@ packages: '@emurgo/cardano-serialization-lib-nodejs@13.2.0': resolution: {integrity: sha512-Bz1zLGEqBQ0BVkqt1OgMxdBOE3BdUWUd7Ly9Ecr/aUwkA8AV1w1XzBMe4xblmJHnB1XXNlPH4SraXCvO+q0Mig==} + '@esbuild/aix-ppc64@0.23.1': + resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.23.1': + resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.23.1': + resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.23.1': + resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.23.1': + resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.23.1': + resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.23.1': + resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.23.1': + resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.23.1': + resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.23.1': + resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.23.1': + resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.23.1': + resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.23.1': + resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.23.1': + resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.23.1': + resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.23.1': + resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.23.1': + resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.23.1': + resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.23.1': + resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.23.1': + resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.23.1': + resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.23.1': + resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.23.1': + resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.23.1': + resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@ethereumjs/common@10.1.1': resolution: {integrity: sha512-NefPzPlrJ9w+NWVe06P+sHZQU98E1AEU9vhiHJEVT2wEcNBC1YX6hON9+smrfbn86C4U1pb2zbvjhkF+n/LKBw==} @@ -3183,6 +3330,11 @@ packages: es6-promisify@5.0.0: resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==} + esbuild@0.23.1: + resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -3340,6 +3492,11 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -3374,6 +3531,9 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -4333,6 +4493,9 @@ packages: require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + rgbcolor@1.0.1: resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==} engines: {node: '>= 0.8.15'} @@ -4659,6 +4822,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.19.2: + resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} + engines: {node: '>=18.0.0'} + hasBin: true + tw-animate-css@1.3.3: resolution: {integrity: sha512-tXE2TRWrskc4TU3RDd7T8n8Np/wCfoeH9gz22c7PzYqNPQ9FBGFbWWzwL0JyHcFp+jHozmF76tbHfPAx22ua2Q==} @@ -5172,6 +5340,78 @@ snapshots: '@emurgo/cardano-serialization-lib-nodejs@13.2.0': {} + '@esbuild/aix-ppc64@0.23.1': + optional: true + + '@esbuild/android-arm64@0.23.1': + optional: true + + '@esbuild/android-arm@0.23.1': + optional: true + + '@esbuild/android-x64@0.23.1': + optional: true + + '@esbuild/darwin-arm64@0.23.1': + optional: true + + '@esbuild/darwin-x64@0.23.1': + optional: true + + '@esbuild/freebsd-arm64@0.23.1': + optional: true + + '@esbuild/freebsd-x64@0.23.1': + optional: true + + '@esbuild/linux-arm64@0.23.1': + optional: true + + '@esbuild/linux-arm@0.23.1': + optional: true + + '@esbuild/linux-ia32@0.23.1': + optional: true + + '@esbuild/linux-loong64@0.23.1': + optional: true + + '@esbuild/linux-mips64el@0.23.1': + optional: true + + '@esbuild/linux-ppc64@0.23.1': + optional: true + + '@esbuild/linux-riscv64@0.23.1': + optional: true + + '@esbuild/linux-s390x@0.23.1': + optional: true + + '@esbuild/linux-x64@0.23.1': + optional: true + + '@esbuild/netbsd-x64@0.23.1': + optional: true + + '@esbuild/openbsd-arm64@0.23.1': + optional: true + + '@esbuild/openbsd-x64@0.23.1': + optional: true + + '@esbuild/sunos-x64@0.23.1': + optional: true + + '@esbuild/win32-arm64@0.23.1': + optional: true + + '@esbuild/win32-ia32@0.23.1': + optional: true + + '@esbuild/win32-x64@0.23.1': + optional: true + '@ethereumjs/common@10.1.1': dependencies: '@ethereumjs/util': 10.1.1 @@ -8538,6 +8778,33 @@ snapshots: dependencies: es6-promise: 4.2.8 + esbuild@0.23.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -8656,6 +8923,9 @@ snapshots: fsevents@2.3.2: optional: true + fsevents@2.3.3: + optional: true + function-bind@1.1.2: {} geist@1.7.0(next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.61.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)): @@ -8694,6 +8964,10 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -9817,6 +10091,8 @@ snapshots: require-main-filename@2.0.0: {} + resolve-pkg-maps@1.0.0: {} + rgbcolor@1.0.1: optional: true @@ -10180,6 +10456,13 @@ snapshots: tslib@2.8.1: {} + tsx@4.19.2: + dependencies: + esbuild: 0.23.1 + get-tsconfig: 4.14.0 + optionalDependencies: + fsevents: 2.3.3 + tw-animate-css@1.3.3: {} tweetnacl-util@0.15.1: {}