diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..a52b0fb --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +# Build outputs +dist +build + +# Dependencies +node_modules + +# Auto-generated +*.lock +pnpm-lock.yaml +package-lock.json diff --git a/README.md b/README.md index 460a9ee..76befc2 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,10 @@ Decode raw contract calls into plain English, visualise contract relationships, The Octraban Soroban contracts are **deployed and verifiable on the Stellar test network** right now: -| Contract | Contract ID | Stellar Explorer | -|----------|-------------|------------------| +| Contract | Contract ID | Stellar Explorer | +| ----------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | **Explorer / registry** | `CBKPNRQ4D3KTAAE7MMJ4HL6JNF2J2EBG2PSSRW4YHOMHTRHUU734CFWJ` | [View on stellar.expert ↗](https://stellar.expert/explorer/testnet/contract/CBKPNRQ4D3KTAAE7MMJ4HL6JNF2J2EBG2PSSRW4YHOMHTRHUU734CFWJ) | -| **Ticket** | `CDX3V6OE72KUIEEJTBLFCQZFXZCAKOYWYXK2KPRM57M6FLZFAVUSVL42` | [View on stellar.expert ↗](https://stellar.expert/explorer/testnet/contract/CDX3V6OE72KUIEEJTBLFCQZFXZCAKOYWYXK2KPRM57M6FLZFAVUSVL42) | +| **Ticket** | `CDX3V6OE72KUIEEJTBLFCQZFXZCAKOYWYXK2KPRM57M6FLZFAVUSVL42` | [View on stellar.expert ↗](https://stellar.expert/explorer/testnet/contract/CDX3V6OE72KUIEEJTBLFCQZFXZCAKOYWYXK2KPRM57M6FLZFAVUSVL42) | > **Network:** `Test SDF Network ; September 2015` · **RPC:** `https://soroban-testnet.stellar.org` @@ -35,7 +35,7 @@ Point the app at these contracts by keeping the default testnet configuration in Raw Soroban activity is opaque — a swap looks like an unlabelled base64 XDR blob. **Octraban** turns that noise into signal. It pairs an ABI-aware indexer (in the [backend](https://github.com/octraban/octraban_backend) repo) with a rich React UI that renders contract calls, events, and relationships the way a human actually reads them: -> *"Address `GABC…` swapped 100 USDC → 98.7 XLM on StellarSwap at ledger 4,521,983."* +> _"Address `GABC…` swapped 100 USDC → 98.7 XLM on StellarSwap at ledger 4,521,983."_ Beyond read-only exploration, Octraban ships a **developer workspace** — an in-browser editor and sandbox for prototyping contract interactions against live testnet contracts without leaving the tab. @@ -44,54 +44,58 @@ Beyond read-only exploration, Octraban ships a **developer workspace** — an in ## ✨ Features ### 🔍 Explore -| Feature | Description | -|---|---| -| **Contract dashboard** | Metadata, ABI, privileged roles, recent transactions and events for any contract (`/contract/:id`) | -| **Human-readable events** | XDR decoded into plain-English event rows via the ABI registry | -| **Event & sub-invocation views** | Drill into a single event (`/event/:seq`) or the full sub-invocation tree of a call | -| **Wallet history** | All transactions and events for an address (`/wallet/:address`) | -| **Universal search** | Look up contracts, wallets, transactions, and events from one bar (`/search`) | -| **XDR inspector** | Paste any XDR envelope and decode it field-by-field (`/xdr`) | + +| Feature | Description | +| -------------------------------- | -------------------------------------------------------------------------------------------------- | +| **Contract dashboard** | Metadata, ABI, privileged roles, recent transactions and events for any contract (`/contract/:id`) | +| **Human-readable events** | XDR decoded into plain-English event rows via the ABI registry | +| **Event & sub-invocation views** | Drill into a single event (`/event/:seq`) or the full sub-invocation tree of a call | +| **Wallet history** | All transactions and events for an address (`/wallet/:address`) | +| **Universal search** | Look up contracts, wallets, transactions, and events from one bar (`/search`) | +| **XDR inspector** | Paste any XDR envelope and decode it field-by-field (`/xdr`) | ### 🕸️ Visualise -| Feature | Description | -|---|---| -| **Relationship graph** | Interactive contract/address relationship graph (`/graph`) built with Cytoscape | -| **3D dependency graph** | `3d-force-graph` view of contract dependencies and factory deployment trees | -| **Invocation & batch flow charts** | React-Flow diagrams of call flows and batched multi-calls | -| **Address connection graph** | Trace value and call flow between accounts | + +| Feature | Description | +| ---------------------------------- | ------------------------------------------------------------------------------- | +| **Relationship graph** | Interactive contract/address relationship graph (`/graph`) built with Cytoscape | +| **3D dependency graph** | `3d-force-graph` view of contract dependencies and factory deployment trees | +| **Invocation & batch flow charts** | React-Flow diagrams of call flows and batched multi-calls | +| **Address connection graph** | Trace value and call flow between accounts | ### 🛠️ Build -| Feature | Description | -|---|---| -| **Developer workspace** | Monaco-powered editor + file explorer scoped to a contract (`/contract/:id/workspace`) | -| **Sandbox** | Run and share snippets against live contracts using a WebContainer runtime (`/sandbox`, `/sandbox/:id`) | -| **Batch multi-call** | Compose and preview batched contract invocations (`/batch`) | -| **ABI upload** | Drop a local ABI to decode events for unregistered contracts | -| **Guided setup** | First-run configuration walkthrough (`/setup`) | + +| Feature | Description | +| ----------------------- | ------------------------------------------------------------------------------------------------------- | +| **Developer workspace** | Monaco-powered editor + file explorer scoped to a contract (`/contract/:id/workspace`) | +| **Sandbox** | Run and share snippets against live contracts using a WebContainer runtime (`/sandbox`, `/sandbox/:id`) | +| **Batch multi-call** | Compose and preview batched contract invocations (`/batch`) | +| **ABI upload** | Drop a local ABI to decode events for unregistered contracts | +| **Guided setup** | First-run configuration walkthrough (`/setup`) | ### 📊 Operate -| Feature | Description | -|---|---| -| **RPC metrics dashboard** | Latency, throughput, and circuit-breaker status for the RPC layer (`/rpc-metrics`) | -| **Rate-limit dashboard** | Admin view of rate-limit hits (`/admin/rate-limits`) | -| **Network switcher & comparison** | Toggle networks and compare state side-by-side | -| **Wallet integration** | Connect with **Freighter** for signing and account context | + +| Feature | Description | +| --------------------------------- | ---------------------------------------------------------------------------------- | +| **RPC metrics dashboard** | Latency, throughput, and circuit-breaker status for the RPC layer (`/rpc-metrics`) | +| **Rate-limit dashboard** | Admin view of rate-limit hits (`/admin/rate-limits`) | +| **Network switcher & comparison** | Toggle networks and compare state side-by-side | +| **Wallet integration** | Connect with **Freighter** for signing and account context | --- ## 🧱 Tech Stack -| Layer | Technology | -|---|---| -| **Framework** | React 18 + TypeScript, bundled with **Vite** | -| **Routing** | React Router | -| **Data** | TanStack Query (server state, caching, retries) | -| **Stellar** | `@stellar/stellar-sdk`, `@stellar/freighter-api` | -| **Visualisation** | Cytoscape, `3d-force-graph`, React-Flow | -| **In-browser IDE** | Monaco Editor + `@webcontainer/api` sandbox | -| **Testing** | Vitest + Testing Library (jsdom) | -| **Tooling** | ESLint, Prettier, TypeScript strict mode | +| Layer | Technology | +| ------------------ | ------------------------------------------------ | +| **Framework** | React 18 + TypeScript, bundled with **Vite** | +| **Routing** | React Router | +| **Data** | TanStack Query (server state, caching, retries) | +| **Stellar** | `@stellar/stellar-sdk`, `@stellar/freighter-api` | +| **Visualisation** | Cytoscape, `3d-force-graph`, React-Flow | +| **In-browser IDE** | Monaco Editor + `@webcontainer/api` sandbox | +| **Testing** | Vitest + Testing Library (jsdom) | +| **Tooling** | ESLint, Prettier, TypeScript strict mode | --- @@ -114,11 +118,13 @@ src/ ## 🚀 Getting Started ### Prerequisites + - **Node.js ≥ 18** - The [Octraban backend](https://github.com/octraban/octraban_backend) running locally (provides the indexer API the UI reads from), or a reachable indexer URL - A [Freighter](https://www.freighter.app/) wallet for signing (optional, for write flows) ### Installation + ```bash git clone https://github.com/octraban/octraban_frontend.git cd octraban_frontend @@ -129,13 +135,14 @@ npm run dev # http://localhost:5173 ``` ### Scripts -| Command | Description | -|---|---| -| `npm run dev` | Start the Vite dev server | -| `npm run build` | Type-check and build for production | -| `npm run preview` | Preview the production build locally | -| `npm test` | Run the Vitest suite | -| `npm run test:watch` | Run tests in watch mode | + +| Command | Description | +| -------------------- | ------------------------------------ | +| `npm run dev` | Start the Vite dev server | +| `npm run build` | Type-check and build for production | +| `npm run preview` | Preview the production build locally | +| `npm test` | Run the Vitest suite | +| `npm run test:watch` | Run tests in watch mode | --- @@ -143,11 +150,11 @@ npm run dev # http://localhost:5173 Copy `.env.example` → `.env` and adjust: -| Variable | Default | Description | -|---|---|---| -| `VITE_INDEXER_URL` | `http://localhost:3001` | Octraban indexer API the UI reads from | -| `VITE_SOROBAN_RPC_URL` | `https://soroban-testnet.stellar.org` | Soroban RPC endpoint (used by the Sandbox) | -| `VITE_NETWORK_PASSPHRASE` | `Test SDF Network ; September 2015` | Stellar network passphrase | +| Variable | Default | Description | +| ------------------------- | ------------------------------------- | ------------------------------------------ | +| `VITE_INDEXER_URL` | `http://localhost:3001` | Octraban indexer API the UI reads from | +| `VITE_SOROBAN_RPC_URL` | `https://soroban-testnet.stellar.org` | Soroban RPC endpoint (used by the Sandbox) | +| `VITE_NETWORK_PASSPHRASE` | `Test SDF Network ; September 2015` | Stellar network passphrase | The defaults point at **Stellar testnet**, so a fresh checkout talks to the live testnet deployment out of the box. @@ -157,7 +164,7 @@ The defaults point at **Stellar testnet**, so a fresh checkout talks to the live Octraban is split across three repositories: -- **octraban_frontend** *(this repo)* — the explorer & developer workspace UI. +- **octraban_frontend** _(this repo)_ — the explorer & developer workspace UI. - **[octraban_backend](https://github.com/octraban/octraban_backend)** — the API + indexer that ingests on-chain data and serves it to the UI (`:3001`). - **[octraban_contract](https://github.com/octraban/octraban_contract)** — the Soroban smart contracts (explorer registry + ticket), deployed to testnet. diff --git a/eslint.config.js b/eslint.config.js index 7593ac5..340d125 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -6,17 +6,45 @@ export default [ ...tseslint.configs.recommended, { rules: { - "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_" }, + ], "@typescript-eslint/no-explicit-any": "warn", }, }, { - files: [ "src/components/AddressConnectionGraph.tsx", "src/components/ContractDependencyGraph3D.tsx", "src/components/DependencyVisualizer.tsx", "src/components/FileExplorer.tsx", "src/components/RawDataViewer.tsx", "src/components/ReadContract.tsx", "src/components/SimulateButton.tsx", "src/components/WriteContract.tsx", "src/pages/ContractPage.tsx", "src/pages/RpcMetricsDashboard.tsx", "src/pages/SandboxPage.tsx", "src/pages/SetupPage.tsx", "src/pages/XdrInspector.tsx", "src/services/dependencies.ts", "src/services/sandbox-api.ts", "src/services/webcontainer.ts", "src/env.d.ts", "test/api.test.ts", "test/hooks.test.tsx", "test/ErrorBoundary.test.tsx"], + files: [ + "src/components/AddressConnectionGraph.tsx", + "src/components/ContractDependencyGraph3D.tsx", + "src/components/DependencyVisualizer.tsx", + "src/components/FileExplorer.tsx", + "src/components/RawDataViewer.tsx", + "src/components/ReadContract.tsx", + "src/components/SimulateButton.tsx", + "src/components/SubInvocationGraph.tsx", + "src/components/WriteContract.tsx", + "src/pages/BatchMultiCall.tsx", + "src/pages/ContractPage.tsx", + "src/pages/RateLimitDashboard.tsx", + "src/pages/RpcMetricsDashboard.tsx", + "src/pages/SandboxPage.tsx", + "src/pages/SetupPage.tsx", + "src/pages/XdrInspector.tsx", + "src/services/dependencies.ts", + "src/services/sandbox-api.ts", + "src/services/webcontainer.ts", + "src/env.d.ts", + "test/api.test.ts", + "test/hooks.test.tsx", + "test/ErrorBoundary.test.tsx", + "test/SearchPage.test.tsx", + ], rules: { "@typescript-eslint/no-explicit-any": "off", }, }, { - ignores: ["node_modules/**", "dist/**"], + ignores: ["node_modules/**", "dist/**", "build/**"], }, ]; diff --git a/index.html b/index.html index 8840c98..0c4ccb0 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ - + diff --git a/package.json b/package.json index 34322bc..abc522f 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,9 @@ "build": "tsc && vite build", "preview": "vite preview", "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --check .", + "format:fix": "prettier --write .", "test": "vitest run", "test:watch": "vitest" }, diff --git a/src/App.tsx b/src/App.tsx index 256243f..ef05549 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,7 +21,11 @@ const RateLimitDashboard = lazy(() => import("./pages/RateLimitDashboard")); const NotFound = lazy(() => import("./pages/NotFound")); function Fallback() { - return

Loading…

; + return ( +

+ Loading… +

+ ); } export default function App() { @@ -33,7 +37,10 @@ export default function App() { } /> } /> - } /> + } + /> } /> } /> } /> diff --git a/src/api.ts b/src/api.ts index 849facf..1f65dba 100644 --- a/src/api.ts +++ b/src/api.ts @@ -48,7 +48,13 @@ export interface FactoryDeploymentTree { export interface ZkHostCall { fn_name: string; curve: "BN254" | "BLS12-381"; - kind: "msm" | "pairing" | "scalar_field" | "map_to_curve" | "hash_to_curve" | "other"; + kind: + | "msm" + | "pairing" + | "scalar_field" + | "map_to_curve" + | "hash_to_curve" + | "other"; cpu_native: number; cpu_legacy: number; } @@ -63,7 +69,14 @@ export interface ZkCostDelta { export interface HeuristicParam { index: number; raw: string; - type: "Address" | "ContractId" | "Amount" | "Hash" | "Symbol" | "Boolean" | "Unknown"; + type: + | "Address" + | "ContractId" + | "Amount" + | "Hash" + | "Symbol" + | "Boolean" + | "Unknown"; value: string; confidence: "likely" | "possible"; } @@ -396,7 +409,12 @@ export interface TransactionTreeDiff { } export const api = { - events: (params: { contract?: string; fn?: string; page?: number; type?: string }) => { + events: (params: { + contract?: string; + fn?: string; + page?: number; + type?: string; + }) => { const q = new URLSearchParams(); if (params.contract) q.set("contract", params.contract); if (params.fn) q.set("fn", params.fn); @@ -411,29 +429,40 @@ export const api = { params.set("limit", String(limit)); return get(`/search?${params}`); }, - zkCosts: (seq: number) => get<{ calls: ZkHostCall[]; delta: ZkCostDelta | null }>(`/events/${seq}/zk-costs`), + zkCosts: (seq: number) => + get<{ calls: ZkHostCall[]; delta: ZkCostDelta | null }>( + `/events/${seq}/zk-costs`, + ), contract: (id: string) => get(`/contracts/${id}`), - burnAlerts: (contract: string) => get(`/burn-alerts?contract=${contract}`), - migrationStatus: (id: string) => get(`/contracts/${id}/migration-status`), + burnAlerts: (contract: string) => + get(`/burn-alerts?contract=${contract}`), + migrationStatus: (id: string) => + get(`/contracts/${id}/migration-status`), wallet: (address: string) => get(`/wallet/${address}`), roles: (id: string) => get(`/contracts/${id}/roles`), - networkComparison: (id: string) => get(`/contracts/${id}/network-comparison`), - addressGraph: (id: string) => get(`/contracts/${id}/address-graph`), + networkComparison: (id: string) => + get(`/contracts/${id}/network-comparison`), + addressGraph: (id: string) => + get(`/contracts/${id}/address-graph`), // sub-invocations for a transaction - subInvocations: (txHash: string) => get(`/transactions/${txHash}/sub-invocations`), + subInvocations: (txHash: string) => + get(`/transactions/${txHash}/sub-invocations`), // Events where contract appears directly OR as sub-invocation eventsDeep: (contractId: string, page = 1) => get(`/v1/contracts/${contractId}/events-deep?page=${page}`), // transaction status (polling fallback; SSE via useTxStatus hook) - txStatus: (txHash: string) => get(`/transactions/${txHash}/status`), + txStatus: (txHash: string) => + get(`/transactions/${txHash}/status`), // Circuit breaker status - circuitBreakerStatus: (id: string) => get(`/contracts/${id}/circuit-breaker`), + circuitBreakerStatus: (id: string) => + get(`/contracts/${id}/circuit-breaker`), // RWA token metadata - rwaMetadata: (id: string) => get(`/contracts/${id}/rwa-metadata`), + rwaMetadata: (id: string) => + get(`/contracts/${id}/rwa-metadata`), downloadAbi: async (id: string) => { const res = await fetch(`${BASE}/contracts/${id}/abi`); @@ -452,7 +481,9 @@ export const api = { // multi-sig source verification sourceVerifications: (id: string, wasmHash?: string) => { const q = wasmHash ? `?wasm_hash=${encodeURIComponent(wasmHash)}` : ""; - return get(`/contracts/${id}/source-verifications${q}`); + return get( + `/contracts/${id}/source-verifications${q}`, + ); }, submitSourceVerification: ( id: string, @@ -482,7 +513,8 @@ export const api = { }, // global contract dependency graph - contractGraph: (limit = 500) => get(`/contract-graph?limit=${limit}`), + contractGraph: (limit = 500) => + get(`/contract-graph?limit=${limit}`), // CAP-0077 quorum freeze status quorumFreeze: (id: string) => @@ -607,15 +639,19 @@ export const api = { if (filter.date_from) q.set("date_from", filter.date_from); if (filter.date_to) q.set("date_to", filter.date_to); if (filter.arg_query) q.set("arg_query", filter.arg_query); - if (filter.has_reentrancy != null) q.set("has_reentrancy", String(filter.has_reentrancy)); + if (filter.has_reentrancy != null) + q.set("has_reentrancy", String(filter.has_reentrancy)); if (filter.tx_hash) q.set("tx_hash", filter.tx_hash); - if (filter.ledger_min != null) q.set("ledger_min", String(filter.ledger_min)); - if (filter.ledger_max != null) q.set("ledger_max", String(filter.ledger_max)); + if (filter.ledger_min != null) + q.set("ledger_min", String(filter.ledger_min)); + if (filter.ledger_max != null) + q.set("ledger_max", String(filter.ledger_max)); return get(`/sub-invocations/search?${q}`); }, // Issue #210: Global analytics for sub-invocations - subInvocationAnalytics: () => get(`/sub-invocations/analytics`), + subInvocationAnalytics: () => + get(`/sub-invocations/analytics`), // Issue #210: Call-path metrics for a single transaction (6 analytical metrics) callPathMetrics: (txHash: string) => @@ -628,7 +664,9 @@ export const api = { ), // Issue #210: SSE stream URL builder for live sub-invocation feed - subInvocationStreamUrl: (filter?: Pick) => { + subInvocationStreamUrl: ( + filter?: Pick, + ) => { const q = new URLSearchParams(); if (filter?.contract) q.set("contract", filter.contract); if (filter?.function) q.set("function", filter.function); diff --git a/src/components/AbiUploadZone.tsx b/src/components/AbiUploadZone.tsx index e44307f..b697fe8 100644 --- a/src/components/AbiUploadZone.tsx +++ b/src/components/AbiUploadZone.tsx @@ -12,7 +12,13 @@ * - A "Clear" button to remove the local ABI */ -import { useRef, useState, useCallback, type DragEvent, type ChangeEvent } from "react"; +import { + useRef, + useState, + useCallback, + type DragEvent, + type ChangeEvent, +} from "react"; import type { LocalAbi } from "../hooks/useLocalAbi"; interface Props { @@ -26,7 +32,12 @@ interface Props { parseError: string | null; } -export default function AbiUploadZone({ onLoad, onClear, localAbi, parseError }: Props) { +export default function AbiUploadZone({ + onLoad, + onClear, + localAbi, + parseError, +}: Props) { const [dragging, setDragging] = useState(false); const [readError, setReadError] = useState(null); const inputRef = useRef(null); @@ -116,7 +127,11 @@ export default function AbiUploadZone({ onLoad, onClear, localAbi, parseError }: {/* Icon + title row */}
📄 - Local ABI loaded + + Local ABI loaded + - File: {localAbi.fileName} + File:{" "} + {localAbi.fileName} {localAbi.name && ( - Name: {localAbi.name} + Name:{" "} + {localAbi.name} )} - Functions: {localAbi.functions.length} + Functions:{" "} + {localAbi.functions.length} {localAbi.types && localAbi.types.length > 0 && ( - Types: {localAbi.types.length} + Types:{" "} + {localAbi.types.length} )} - Loaded: {new Date(localAbi.loadedAt).toLocaleTimeString()} + Loaded:{" "} + {new Date(localAbi.loadedAt).toLocaleTimeString()}
{/* Function list */} -
+
{localAbi.functions.map((f) => ( {f.name} @@ -201,7 +223,13 @@ export default function AbiUploadZone({ onLoad, onClear, localAbi, parseError }:
{/* Hidden file input for Replace */} - +
); } @@ -218,7 +246,9 @@ export default function AbiUploadZone({ onLoad, onClear, localAbi, parseError }: onDragLeave={onDragLeave} onDrop={onDrop} onClick={() => inputRef.current?.click()} - onKeyDown={(e) => (e.key === "Enter" || e.key === " ") && inputRef.current?.click()} + onKeyDown={(e) => + (e.key === "Enter" || e.key === " ") && inputRef.current?.click() + } style={{ border: `2px dashed ${dragging ? "var(--accent)" : "var(--border)"}`, borderRadius: 8, @@ -232,10 +262,12 @@ export default function AbiUploadZone({ onLoad, onClear, localAbi, parseError }: >
📂

- Drop a contract ABI .json file here + Drop a contract ABI{" "} + .json file here

- or click to browse — stored in session memory only, never sent to the server + or click to browse — stored in session memory only, never sent to the + server

diff --git a/src/components/ActionBar.tsx b/src/components/ActionBar.tsx index 5621bdb..9e408a3 100644 --- a/src/components/ActionBar.tsx +++ b/src/components/ActionBar.tsx @@ -28,11 +28,19 @@ const ActionBar: React.FC = ({ files, sandboxId }) => { return (
- {sandboxId && ( - )} diff --git a/src/components/AddressConnectionGraph.tsx b/src/components/AddressConnectionGraph.tsx index f319f4c..94070f5 100644 --- a/src/components/AddressConnectionGraph.tsx +++ b/src/components/AddressConnectionGraph.tsx @@ -55,7 +55,10 @@ export default function AddressConnectionGraph({ contractId }: Props) { ...nodes.map((n) => ({ data: { id: n.id, - label: n.label.length > 12 ? `${n.label.slice(0, 6)}…${n.label.slice(-4)}` : n.label, + label: + n.label.length > 12 + ? `${n.label.slice(0, 6)}…${n.label.slice(-4)}` + : n.label, type: n.type, }, })), @@ -124,11 +127,19 @@ export default function AddressConnectionGraph({ contractId }: Props) { }, [data]); if (isLoading) return

Loading graph…

; - if (error) return

Failed to load address graph.

; + if (error) + return

Failed to load address graph.

; if (nodes.length === 0) { return (
- + No address graph data available No wallet or contract connections were returned for this contract. @@ -193,7 +204,8 @@ export default function AddressConnectionGraph({ contractId }: Props) { }} />

- Scroll to zoom · Drag to pan · {nodes.length} nodes · {edges.length} connections + Scroll to zoom · Drag to pan · {nodes.length} nodes · {edges.length}{" "} + connections

); diff --git a/src/components/BatchFlowChart.tsx b/src/components/BatchFlowChart.tsx index c2d5da4..392a3dd 100644 --- a/src/components/BatchFlowChart.tsx +++ b/src/components/BatchFlowChart.tsx @@ -20,17 +20,25 @@ import ReactFlow, { NodeChange, } from "react-flow-renderer"; import { BatchCall, ExecutionMode } from "../types/batch"; -import { BATCH_TEMPLATES, fillTemplateParameters } from "../services/batchTemplates"; +import { + BATCH_TEMPLATES, + fillTemplateParameters, +} from "../services/batchTemplates"; interface BatchFlowChartProps { initialCalls?: BatchCall[]; onCallsChange?: (calls: BatchCall[]) => void; - onSimulate?: (mode: ExecutionMode, calls: BatchCall[]) => Promise | void; + onSimulate?: ( + mode: ExecutionMode, + calls: BatchCall[], + ) => Promise | void; } function toBatchCall(node: Node): BatchCall { const label = String(node.data.label ?? ""); - const [functionName, contractId] = label.split("@").map((part) => part.trim()); + const [functionName, contractId] = label + .split("@") + .map((part) => part.trim()); return { id: node.id, @@ -81,8 +89,11 @@ export default function BatchFlowChart({ const [nodes, setNodes] = useNodesState([]); const [edges, setEdges] = useEdgesState([]); const [selectedTemplate, setSelectedTemplate] = useState(""); - const [templateValues, setTemplateValues] = useState>({}); - const [executionMode, setExecutionMode] = useState("sequential"); + const [templateValues, setTemplateValues] = useState>( + {}, + ); + const [executionMode, setExecutionMode] = + useState("sequential"); const [showTemplateSelector, setShowTemplateSelector] = useState(true); // Convert calls to nodes @@ -138,7 +149,7 @@ export default function BatchFlowChart({ const handleApplyTemplate = () => { const template = BATCH_TEMPLATES[selectedTemplate]; if (!template) return; - + const filledCalls = fillTemplateParameters(template, templateValues); initializeNodes(filledCalls); setShowTemplateSelector(false); @@ -157,18 +168,37 @@ export default function BatchFlowChart({ setNodes((nds) => [...nds, newNode]); setEdges((eds) => { const previous = nodes[nodes.length - 1]; - return previous ? [...eds, { id: `${previous.id}-${newId}`, source: previous.id, target: newId }] : eds; + return previous + ? [ + ...eds, + { + id: `${previous.id}-${newId}`, + source: previous.id, + target: newId, + }, + ] + : eds; }); }; const handleSimulate = () => { - const calls = nodes.map(toBatchCall).filter((call) => call.contractId && call.functionName); + const calls = nodes + .map(toBatchCall) + .filter((call) => call.contractId && call.functionName); if (!calls.length) return; onSimulate?.(executionMode, calls); }; return ( -
+
); -} \ No newline at end of file +} diff --git a/src/components/CircuitBreakerStatus.tsx b/src/components/CircuitBreakerStatus.tsx index a0bd517..9aea049 100644 --- a/src/components/CircuitBreakerStatus.tsx +++ b/src/components/CircuitBreakerStatus.tsx @@ -5,7 +5,9 @@ interface CircuitBreakerStatusProps { contractId: string; } -export default function CircuitBreakerStatus({ contractId }: CircuitBreakerStatusProps) { +export default function CircuitBreakerStatus({ + contractId, +}: CircuitBreakerStatusProps) { const { data: status, isLoading } = useQuery({ queryKey: ["circuit-breaker", contractId], queryFn: () => api.circuitBreakerStatus(contractId), @@ -13,7 +15,9 @@ export default function CircuitBreakerStatus({ contractId }: CircuitBreakerStatu }); if (isLoading) { - return
Loading status…
; + return ( +
Loading status…
+ ); } if (!status?.has_circuit_breaker) { @@ -21,11 +25,15 @@ export default function CircuitBreakerStatus({ contractId }: CircuitBreakerStatu } const isPaused = status.is_paused; - const bgColor = isPaused ? "rgba(239, 68, 68, 0.1)" : "rgba(34, 197, 94, 0.1)"; + const bgColor = isPaused + ? "rgba(239, 68, 68, 0.1)" + : "rgba(34, 197, 94, 0.1)"; const borderColor = isPaused ? "#ef4444" : "#22c55e"; const textColor = isPaused ? "#dc2626" : "#16a34a"; const icon = isPaused ? "⛔" : "✓"; - const statusText = isPaused ? "Status: Paused by Emergency Administration" : "Status: Operational"; + const statusText = isPaused + ? "Status: Paused by Emergency Administration" + : "Status: Operational"; return (
{icon}
-
{statusText}
+
+ {statusText} +
{status.pause_status_ledger && (
Last status change at ledger {status.pause_status_ledger} diff --git a/src/components/ContractDashboard.tsx b/src/components/ContractDashboard.tsx index 96acfa7..6ddc387 100644 --- a/src/components/ContractDashboard.tsx +++ b/src/components/ContractDashboard.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState } from "react"; /** * Multi-Network Contract Management Dashboard @@ -6,13 +6,13 @@ import React, { useState } from 'react'; * upgrade execution, DAO voting integrations, pausing, and admin transfer. */ export const ContractDashboard = () => { - const [network, setNetwork] = useState('testnet'); + const [network, setNetwork] = useState("testnet"); const [contractDetails] = useState({ - status: 'Deployed', - address: 'CABCD...789', - version: 'v2.1.0', - admin: 'GABC...123', - lastUpgrade: '2024-06-01 14:30 UTC' + status: "Deployed", + address: "CABCD...789", + version: "v2.1.0", + admin: "GABC...123", + lastUpgrade: "2024-06-01 14:30 UTC", }); const handleTransferAdmin = () => { @@ -39,9 +39,9 @@ export const ContractDashboard = () => {

ExplorerContract Manager

- setQ(e.target.value)} diff --git a/src/components/NetworkComparison.tsx b/src/components/NetworkComparison.tsx index 181a8f0..60a6edb 100644 --- a/src/components/NetworkComparison.tsx +++ b/src/components/NetworkComparison.tsx @@ -6,8 +6,15 @@ import { useQuery } from "@tanstack/react-query"; import { api } from "../api"; -function StatusBadge({ deployed, error }: { deployed: boolean; error?: string }) { - if (error) return Error; +function StatusBadge({ + deployed, + error, +}: { + deployed: boolean; + error?: string; +}) { + if (error) + return Error; return deployed ? ( ✓ Live ) : ( @@ -15,7 +22,11 @@ function StatusBadge({ deployed, error }: { deployed: boolean; error?: string }) ); } -export default function NetworkComparison({ contractId }: { contractId: string }) { +export default function NetworkComparison({ + contractId, +}: { + contractId: string; +}) { const { data, isLoading, error } = useQuery({ queryKey: ["network-comparison", contractId], queryFn: () => api.networkComparison(contractId), @@ -23,12 +34,23 @@ export default function NetworkComparison({ contractId }: { contractId: string } }); const statuses = Array.isArray(data?.statuses) ? data.statuses : []; - if (isLoading) return

Checking networks…

; - if (error) return

Failed to load network comparison.

; + if (isLoading) + return

Checking networks…

; + if (error) + return ( +

Failed to load network comparison.

+ ); if (statuses.length === 0) { return (
- + No network data available No deployment status was returned for this contract. @@ -38,7 +60,9 @@ export default function NetworkComparison({ contractId }: { contractId: string } return (
-

Network Deployment Status

+

+ Network Deployment Status +

{data?.hasVersionMismatch && (
- ⚠ WASM hash mismatch detected across networks — contract versions differ. + ⚠ WASM hash mismatch detected across networks — contract versions + differ.
)} @@ -73,9 +98,16 @@ export default function NetworkComparison({ contractId }: { contractId: string } {statuses.map((s) => ( - + - {s.network} + + {s.network} + diff --git a/src/components/NetworkSwitcher.tsx b/src/components/NetworkSwitcher.tsx index c4b2386..419f3a8 100644 --- a/src/components/NetworkSwitcher.tsx +++ b/src/components/NetworkSwitcher.tsx @@ -1,5 +1,9 @@ import { useEffect, useRef, useState } from "react"; -import { NETWORK_COLORS, useNetwork, type NetworkConfig } from "../contexts/NetworkContext"; +import { + NETWORK_COLORS, + useNetwork, + type NetworkConfig, +} from "../contexts/NetworkContext"; type HealthState = "checking" | "connected" | "degraded" | "down"; @@ -47,11 +51,25 @@ function NetworkBadge({ network }: { network: NetworkConfig }) { } export default function NetworkSwitcher() { - const { networks, active, setActiveId, addCustomNetwork, removeCustomNetwork } = useNetwork(); + const { + networks, + active, + setActiveId, + addCustomNetwork, + removeCustomNetwork, + } = useNetwork(); const [open, setOpen] = useState(false); const [showAddForm, setShowAddForm] = useState(false); - const [health, setHealth] = useState({ state: "checking", latencyMs: null }); - const [form, setForm] = useState({ name: "", rpcUrl: "", horizonUrl: "", passphrase: "" }); + const [health, setHealth] = useState({ + state: "checking", + latencyMs: null, + }); + const [form, setForm] = useState({ + name: "", + rpcUrl: "", + horizonUrl: "", + passphrase: "", + }); const [formError, setFormError] = useState(null); const [validating, setValidating] = useState(false); const rootRef = useRef(null); @@ -59,17 +77,19 @@ export default function NetworkSwitcher() { useEffect(() => { let cancelled = false; setHealth({ state: "checking", latencyMs: null }); - probeNetwork(active.rpcUrl).then(h => { + probeNetwork(active.rpcUrl).then((h) => { if (!cancelled) setHealth(h); }); - return () => { cancelled = true; }; + return () => { + cancelled = true; + }; }, [active.id, active.rpcUrl]); useEffect(() => { function onKey(e: KeyboardEvent) { if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === "n") { e.preventDefault(); - setOpen(o => !o); + setOpen((o) => !o); } if (e.key === "Escape") setOpen(false); } @@ -98,7 +118,9 @@ export default function NetworkSwitcher() { const probed = await probeNetwork(form.rpcUrl.trim()); setValidating(false); if (probed.state === "down") { - setFormError("Could not reach this RPC URL. Check the address and try again."); + setFormError( + "Could not reach this RPC URL. Check the address and try again.", + ); return; } const network = addCustomNetwork({ @@ -116,7 +138,7 @@ export default function NetworkSwitcher() {
@@ -153,7 +179,7 @@ export default function NetworkSwitcher() { padding: 8, }} > - {networks.map(n => ( + {networks.map((n) => (
{ + setActiveId(n.id); + setOpen(false); }} - onClick={() => { setActiveId(n.id); setOpen(false); }} > - + {n.name} {n.custom && ( @@ -184,7 +227,9 @@ export default function NetworkSwitcher() {
))} -
+
{!showAddForm ? ( -
diff --git a/src/components/Preview.tsx b/src/components/Preview.tsx index cd81e7b..2856b95 100644 --- a/src/components/Preview.tsx +++ b/src/components/Preview.tsx @@ -12,13 +12,20 @@ const Preview: React.FC = ({ packageJsonContent = "" }) => {
Preview -
{showDeps ? ( - + ) : ( <>
diff --git a/src/components/PrivilegedRoles.tsx b/src/components/PrivilegedRoles.tsx index 59bd40d..1f2d17e 100644 --- a/src/components/PrivilegedRoles.tsx +++ b/src/components/PrivilegedRoles.tsx @@ -16,14 +16,21 @@ function fmtAddr(addr: string) { return addr.length > 12 ? `${addr.slice(0, 6)}…${addr.slice(-4)}` : addr; } -export default function PrivilegedRoles({ contractId }: { contractId: string }) { +export default function PrivilegedRoles({ + contractId, +}: { + contractId: string; +}) { const { data: roles = [], isLoading } = useQuery({ queryKey: ["roles", contractId], queryFn: () => api.roles(contractId), enabled: !!contractId, }); - if (isLoading) return

Loading roles…

; + if (isLoading) + return ( +

Loading roles…

+ ); if (roles.length === 0) { return ( @@ -38,7 +45,9 @@ export default function PrivilegedRoles({ contractId }: { contractId: string }) return (

Privileged Roles

- +
@@ -70,8 +79,12 @@ export default function PrivilegedRoles({ contractId }: { contractId: string }) - - + + ))} diff --git a/src/components/QuorumFreezeBadge.tsx b/src/components/QuorumFreezeBadge.tsx index 8e372cc..8f98bb2 100644 --- a/src/components/QuorumFreezeBadge.tsx +++ b/src/components/QuorumFreezeBadge.tsx @@ -48,17 +48,20 @@ export default function QuorumFreezeBadge({ contractId }: Props) {

- This contract has been collectively frozen by network validators during a security incident. All invocations - targeting this contract will be rejected with a quarantine error. + This contract has been collectively frozen by network validators during + a security incident. All invocations targeting this contract will be + rejected with a quarantine error.

{data.ledger && (
- Freeze enacted at ledger {data.ledger} + Freeze enacted at ledger{" "} + {data.ledger} {data.tx_hash && ( {" "} - · tx {data.tx_hash.slice(0, 12)}… + · tx{" "} + {data.tx_hash.slice(0, 12)}… )}
@@ -66,7 +69,9 @@ export default function QuorumFreezeBadge({ contractId }: Props) { {data.frozen_ids.length > 1 && (
- {data.frozen_ids.length} frozen keys in this event + + {data.frozen_ids.length} frozen keys in this event +
    No rate limit hits in the selected window.

    ; + return ( +

    + No rate limit hits in the selected window. +

    + ); } const maxHits = Math.max(...data.map((d) => Number(d.hits)), 1); @@ -52,9 +56,15 @@ export default function RateLimitHitsChart({ data }: { data: HitDataPoint[] }) { marginTop: 4, }} > - {data[0] ? new Date(data[0].minute).toLocaleTimeString() : ""} + + {data[0] ? new Date(data[0].minute).toLocaleTimeString() : ""} + Peak: {maxHits} hits/min - {data[data.length - 1] ? new Date(data[data.length - 1].minute).toLocaleTimeString() : ""} + + {data[data.length - 1] + ? new Date(data[data.length - 1].minute).toLocaleTimeString() + : ""} + ); diff --git a/src/components/ResourceCosts.tsx b/src/components/ResourceCosts.tsx index 0f182e1..940a28a 100644 --- a/src/components/ResourceCosts.tsx +++ b/src/components/ResourceCosts.tsx @@ -6,7 +6,8 @@ import type { DecodedEvent } from "../api"; export default function ResourceCosts({ event }: { event: DecodedEvent }) { const { cpu_instructions, mem_bytes, fee_charged } = event; - if (cpu_instructions == null && mem_bytes == null && fee_charged == null) return null; + if (cpu_instructions == null && mem_bytes == null && fee_charged == null) + return null; return (
    @@ -19,16 +20,40 @@ export default function ResourceCosts({ event }: { event: DecodedEvent }) { }} > {cpu_instructions != null && ( - + + )} + {mem_bytes != null && ( + + )} + {fee_charged != null && ( + )} - {mem_bytes != null && } - {fee_charged != null && }
    ); } -function ResourceTile({ label, value, unit }: { label: string; value: string; unit: string }) { +function ResourceTile({ + label, + value, + unit, +}: { + label: string; + value: string; + unit: string; +}) { return (
    - {restore.keyCount} ledger key{restore.keyCount !== 1 ? "s" : ""} revived from archive + {restore.keyCount} ledger key{restore.keyCount !== 1 ? "s" : ""}{" "} + revived from archive {restore.feePaid != null && ( - - Fee paid: {restore.feePaid.toLocaleString()} stroops + + Fee paid:{" "} + {restore.feePaid.toLocaleString()} stroops )}
    diff --git a/src/components/RustCodeViewer.tsx b/src/components/RustCodeViewer.tsx index 3236fab..1e56330 100644 --- a/src/components/RustCodeViewer.tsx +++ b/src/components/RustCodeViewer.tsx @@ -7,7 +7,11 @@ interface Props { // Escape HTML to prevent XSS function escapeHtml(s: string): string { - return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + return s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); } // Token types and their colours (matches project CSS vars) diff --git a/src/components/RwaMetadataDisplay.tsx b/src/components/RwaMetadataDisplay.tsx index 7fff07e..a079adf 100644 --- a/src/components/RwaMetadataDisplay.tsx +++ b/src/components/RwaMetadataDisplay.tsx @@ -43,8 +43,12 @@ export default function RwaMetadataDisplay({ contractId }: RwaMetadataProps) { > 🏢
    -
    Real-World Asset (RWA) Token
    -
    Type: {typeLabel}
    +
    + Real-World Asset (RWA) Token +
    +
    + Type: {typeLabel} +
    ); diff --git a/src/components/SdkSnippet.tsx b/src/components/SdkSnippet.tsx index 6d7f102..b09a43a 100644 --- a/src/components/SdkSnippet.tsx +++ b/src/components/SdkSnippet.tsx @@ -21,7 +21,13 @@ function buildArgList(args: { name: string; type?: string }[] = []): string { return args.map((a) => a.name).join(", "); } -function jsSnippet(contractId: string, fnName: string, rpcUrl: string, passphrase: string, args: Props["args"] = []): string { +function jsSnippet( + contractId: string, + fnName: string, + rpcUrl: string, + passphrase: string, + args: Props["args"] = [], +): string { const argList = buildArgList(args); return `import { Contract, SorobanRpc, TransactionBuilder, BASE_FEE } from "@stellar/stellar-sdk"; @@ -40,7 +46,13 @@ const result = await rpc.sendTransaction(prepared); console.log(result);`; } -function pythonSnippet(contractId: string, fnName: string, rpcUrl: string, passphrase: string, args: Props["args"] = []): string { +function pythonSnippet( + contractId: string, + fnName: string, + rpcUrl: string, + passphrase: string, + args: Props["args"] = [], +): string { const argList = buildArgList(args); return `from stellar_sdk import SorobanServer, Keypair, TransactionBuilder from stellar_sdk.soroban_rpc import SendTransactionStatus @@ -66,8 +78,15 @@ response = server.send_transaction(tx) print(response.status)`; } -function rustSnippet(contractId: string, fnName: string, args: Props["args"] = []): string { - const argList = args.length ? args.map((a) => ` // ${a.name}: ${a.type ?? "ScVal"}`).join("\n") + "\n" : ""; +function rustSnippet( + contractId: string, + fnName: string, + args: Props["args"] = [], +): string { + const argList = args.length + ? args.map((a) => ` // ${a.name}: ${a.type ?? "ScVal"}`).join("\n") + + "\n" + : ""; return `use soroban_sdk::{contract, contractimpl, Address, Env}; // Client generated from contract spec: // stellar contract bindings rust --contract-id ${contractId} --output-dir ./bindings @@ -93,9 +112,21 @@ export default function SdkSnippet({ contractId, fnName, args = [] }: Props) { const [copied, setCopied] = useState(false); const snippets: Record = { - javascript: jsSnippet(contractId, fnName, active.rpcUrl, active.passphrase, args), - python: pythonSnippet(contractId, fnName, active.rpcUrl, active.passphrase, args), - rust: rustSnippet(contractId, fnName, args), + javascript: jsSnippet( + contractId, + fnName, + active.rpcUrl, + active.passphrase, + args, + ), + python: pythonSnippet( + contractId, + fnName, + active.rpcUrl, + active.passphrase, + args, + ), + rust: rustSnippet(contractId, fnName, args), }; const snippet = snippets[lang]; @@ -126,7 +157,10 @@ export default function SdkSnippet({ contractId, fnName, args = [] }: Props) { style={{ padding: "3px 10px", fontSize: 12, - background: lang === l.key ? "var(--accent, #7c3aed)" : "var(--bg2, #1e1e2e)", + background: + lang === l.key + ? "var(--accent, #7c3aed)" + : "var(--bg2, #1e1e2e)", color: lang === l.key ? "#fff" : "var(--muted)", border: "1px solid var(--border, #333)", borderRadius: 4, diff --git a/src/components/SimulateButton.tsx b/src/components/SimulateButton.tsx index d34ee8d..90a04bb 100644 --- a/src/components/SimulateButton.tsx +++ b/src/components/SimulateButton.tsx @@ -14,7 +14,11 @@ interface Props { args?: string; // JSON array string } -export default function SimulateButton({ contractId, fnName, args = "[]" }: Props) { +export default function SimulateButton({ + contractId, + fnName, + args = "[]", +}: Props) { const [result, setResult] = useState(null); const [loading, setLoading] = useState(false); const [argsInput, setArgsInput] = useState(args); @@ -57,7 +61,11 @@ export default function SimulateButton({ contractId, fnName, args = "[]" }: Prop placeholder='Args JSON, e.g. ["GABC...", 100]' style={{ flex: 1, minWidth: 200 }} /> - @@ -86,9 +94,17 @@ export default function SimulateButton({ contractId, fnName, args = "[]" }: Prop > {result.success ? "✓ Would succeed" : "✗ Would revert"} - {result.returnValue && → {result.returnValue}} + {result.returnValue && ( + + → {result.returnValue} + + )} - {result.error &&

    {result.error}

    } + {result.error && ( +

    + {result.error} +

    + )} {result.cost && (
    - CPU: {result.cost.cpuInsns} insns + CPU:{" "} + + {result.cost.cpuInsns} + {" "} + insns - Mem: {result.cost.memBytes} bytes + Mem:{" "} + + {result.cost.memBytes} + {" "} + bytes
    )} diff --git a/src/components/SourceFileTree.tsx b/src/components/SourceFileTree.tsx index 408c21e..cc6ef5f 100644 --- a/src/components/SourceFileTree.tsx +++ b/src/components/SourceFileTree.tsx @@ -68,7 +68,14 @@ interface NodeRowProps { onToggle: (path: string) => void; } -function NodeRow({ node, depth, selected, onSelect, expanded, onToggle }: NodeRowProps) { +function NodeRow({ + node, + depth, + selected, + onSelect, + expanded, + onToggle, +}: NodeRowProps) { const isExpanded = expanded.has(node.fullPath); const isSelected = selected === node.fullPath; @@ -224,7 +231,9 @@ export default function SourceFileTree({ files }: Props) { {selected ? ( ) : ( -
    Select a file to view its source.
    +
    + Select a file to view its source. +
    )} diff --git a/src/components/SourceVerificationBadge.tsx b/src/components/SourceVerificationBadge.tsx index 2a98f1a..8aa7962 100644 --- a/src/components/SourceVerificationBadge.tsx +++ b/src/components/SourceVerificationBadge.tsx @@ -18,7 +18,10 @@ interface Props { wasmHash?: string; } -export default function SourceVerificationBadge({ contractId, wasmHash }: Props) { +export default function SourceVerificationBadge({ + contractId, + wasmHash, +}: Props) { const qc = useQueryClient(); const [form, setForm] = useState({ signer: "", @@ -34,8 +37,12 @@ export default function SourceVerificationBadge({ contractId, wasmHash }: Props) }); const mutation = useMutation({ - mutationFn: (body: { wasm_hash: string; signer: string; signature: string; compiler_hash: string }) => - api.submitSourceVerification(contractId, body), + mutationFn: (body: { + wasm_hash: string; + signer: string; + signature: string; + compiler_hash: string; + }) => api.submitSourceVerification(contractId, body), onSuccess: () => { qc.invalidateQueries({ queryKey: ["source-verifications", contractId] }); setForm({ signer: "", signature: "", compiler_hash: "" }); @@ -46,7 +53,11 @@ export default function SourceVerificationBadge({ contractId, wasmHash }: Props) const count = verifications.length; const isVerified = count >= MIN_VERIFIED; - const badgeColor = isVerified ? "var(--green, #22c55e)" : count > 0 ? "var(--yellow, #eab308)" : "var(--muted)"; + const badgeColor = isVerified + ? "var(--green, #22c55e)" + : count > 0 + ? "var(--yellow, #eab308)" + : "var(--muted)"; const badgeIcon = isVerified ? "✔" : count > 0 ? "⚠" : "✗"; const handleSubmit = (e: React.FormEvent) => { @@ -70,7 +81,9 @@ export default function SourceVerificationBadge({ contractId, wasmHash }: Props) width: "fit-content", }} > - {badgeIcon} + + {badgeIcon} + {isVerified ? `Source Verified by ${count} Independent Developer Signature${count !== 1 ? "s" : ""}` @@ -94,7 +107,9 @@ export default function SourceVerificationBadge({ contractId, wasmHash }: Props) alignItems: "flex-start", }} > - + + ✔ +
    {v.signer}
    -
    - compiler hash: {v.compiler_hash.slice(0, 16)}… +
    + compiler hash:{" "} + + {v.compiler_hash.slice(0, 16)}… + {" · "} {new Date(v.submitted_at).toLocaleDateString()}
    @@ -147,9 +167,13 @@ export default function SourceVerificationBadge({ contractId, wasmHash }: Props) borderRadius: 8, }} > -

    Submit Source Verification

    +

    + Submit Source Verification +

    {!wasmHash && ( -

    No WASM hash available for this contract yet.

    +

    + No WASM hash available for this contract yet. +

    )} {[ { @@ -181,7 +205,9 @@ export default function SourceVerificationBadge({ contractId, wasmHash }: Props) )[field]} - onChange={(e) => setForm((f) => ({ ...f, [field]: e.target.value }))} + onChange={(e) => + setForm((f) => ({ ...f, [field]: e.target.value })) + } placeholder={placeholder} style={{ padding: "6px 10px", @@ -212,7 +238,9 @@ export default function SourceVerificationBadge({ contractId, wasmHash }: Props) {mutation.isPending ? "Submitting…" : "Submit"} {mutation.isError && ( -

    {(mutation.error as Error).message}

    +

    + {(mutation.error as Error).message} +

    )} )} diff --git a/src/components/StateDiffTimeline.tsx b/src/components/StateDiffTimeline.tsx index b767ead..6b606da 100644 --- a/src/components/StateDiffTimeline.tsx +++ b/src/components/StateDiffTimeline.tsx @@ -52,7 +52,11 @@ export default function StateDiffTimeline({ contractId }: Props) { const maxIndex = ledgerGroups.length - 1; const safeSliderIndex = Math.min(sliderIndex, Math.max(maxIndex, 0)); const current = ledgerGroups[safeSliderIndex]; - const visibleEntries = current ? (filterKey ? current.entries.filter((e) => e.key === filterKey) : current.entries) : []; + const visibleEntries = current + ? filterKey + ? current.entries.filter((e) => e.key === filterKey) + : current.entries + : []; // Cumulative state up to current ledger for context const cumulativeState = useMemo(() => { @@ -67,7 +71,8 @@ export default function StateDiffTimeline({ contractId }: Props) { return state; }, [ledgerGroups, safeSliderIndex]); - if (isLoading) return

    Loading state diffs…

    ; + if (isLoading) + return

    Loading state diffs…

    ; if (!current) { return ( @@ -80,7 +85,10 @@ export default function StateDiffTimeline({ contractId }: Props) { return (
    {/* Slider control */} -
    +
    - Block {current.ledger.toLocaleString()} + + Block {current.ledger.toLocaleString()} + {safeSliderIndex + 1} / {ledgerGroups.length} snapshots @@ -105,7 +115,9 @@ export default function StateDiffTimeline({ contractId }: Props) { /> {/* Mini timeline ticks */} -
    +
    {ledgerGroups.map((g, i) => (
    - Filter key: + + Filter key: +
Role {fmtAddr(r.address)} {r.ledger ?? "—"}{new Date(r.updated_at).toLocaleString()} + {r.ledger ?? "—"} + + {new Date(r.updated_at).toLocaleString()} +
{writes.map((w: StorageWrite, i: number) => ( - +
{w.key} diff --git a/src/components/StructuredInput.tsx b/src/components/StructuredInput.tsx index fb1d2cd..d188790 100644 --- a/src/components/StructuredInput.tsx +++ b/src/components/StructuredInput.tsx @@ -21,7 +21,11 @@ import type { TypeIndex } from "./StructuredValue"; function primitiveInputType(type: string): string { const t = type.toLowerCase(); if (t === "bool") return "checkbox"; - if (t.includes("int") || ["u32", "i32", "u64", "i64", "u128", "i128", "u256", "i256"].includes(t)) return "number"; + if ( + t.includes("int") || + ["u32", "i32", "u64", "i64", "u128", "i128", "u256", "i256"].includes(t) + ) + return "number"; return "text"; } @@ -40,7 +44,13 @@ interface StructuredInputProps { label?: string; } -export default function StructuredInput({ type, value, onChange, typeIndex, label }: StructuredInputProps) { +export default function StructuredInput({ + type, + value, + onChange, + typeIndex, + label, +}: StructuredInputProps) { const typeDef = typeIndex.get(type); // ── Struct ──────────────────────────────────────────────────────────────── @@ -58,7 +68,14 @@ export default function StructuredInput({ type, value, onChange, typeIndex, labe // ── Enum ────────────────────────────────────────────────────────────────── if (typeDef?.kind === "enum" || typeDef?.kind === "error_enum") { - return ; + return ( + + ); } // ── Union ───────────────────────────────────────────────────────────────── @@ -76,7 +93,12 @@ export default function StructuredInput({ type, value, onChange, typeIndex, labe // ── Primitive / unknown type ────────────────────────────────────────────── return ( - + ); } @@ -90,7 +112,13 @@ interface StructInputProps { label: string; } -function StructInput({ typeDef, value, onChange, typeIndex, label }: StructInputProps) { +function StructInput({ + typeDef, + value, + onChange, + typeIndex, + label, +}: StructInputProps) { const fields = typeDef.fields ?? []; const current = value ?? {}; @@ -101,7 +129,10 @@ function StructInput({ typeDef, value, onChange, typeIndex, label }: StructInput return (
- {label} ({typeDef.name}) + {label}{" "} + + ({typeDef.name}) +
{fields.map((field) => ( -
+
{label} ({typeDef.name}) - onChange(Number(e.target.value))} + style={{ width: "100%" }} + > {cases.map((c) => (