Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
},
"dependencies": {
"@chainsafe/ssz": "^1.2.1",
"@lodestar/params": "^1.34.0",
"@lodestar/types": "^1.34.0",
"comlink": "^4.4.2",
"js-yaml": "^4.1.0",
Expand Down
67 changes: 63 additions & 4 deletions src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import type {Type} from "@chainsafe/ssz";
import {useCallback, useEffect, useState} from "react";
import {useCallback, useEffect, useMemo, useState} from "react";
import {CustomTypeEditor} from "./components/custom-type-editor";
import {Footer} from "./components/footer";
import {Header} from "./components/header";
import {InputPanel} from "./components/input-panel";
import {OutputPanel} from "./components/output-panel";
import {StructureView} from "./components/structure-view/structure-view";
import {Toolbar} from "./components/toolbar";
import {useDebounce} from "./hooks/use-debounce";
import {useSsz} from "./hooks/use-ssz";
import {useWorker} from "./hooks/use-worker";
import {type CompileResult, compileCustomDsl} from "./lib/custom-type";
import {inputFormats, serializeOutputFormats} from "./lib/formats";
import {type ForkName, forks, typeNames} from "./lib/types";
import {CUSTOM_FORK, type ForkName, forks, typeNames} from "./lib/types";

const DEFAULT_FORK = "fulu";
const DEFAULT_TYPE = "BeaconBlock";
Expand All @@ -25,14 +28,53 @@ export default function App() {
const [parsedValue, setParsedValue] = useState<unknown>(null);
const [inputMode, setInputMode] = useState<"editor" | "builder">("builder");

// Custom-type DSL state
const [customDsl, setCustomDsl] = useState("");
const [customCompile, setCustomCompile] = useState<CompileResult | null>(null);
const debouncedDsl = useDebounce(customDsl, 300);

// Worker
const worker = useWorker();

// SSZ processing
const result = useSsz(worker, serializeMode ? "serialize" : "deserialize", forkName, typeName, input, inputFormat);

// Get current SSZ type
const sszType: Type<unknown> | null = forks[forkName]?.[typeName] ?? null;
const sszType: Type<unknown> | null = useMemo(() => {
if (forkName === CUSTOM_FORK) {
return customCompile?.ok ? (customCompile.types.get(typeName) ?? null) : null;
}
return forks[forkName]?.[typeName] ?? null;
}, [forkName, typeName, customCompile]);

// TYPE dropdown options
const typeOptions = useMemo(() => {
if (forkName === CUSTOM_FORK) {
return customCompile?.ok ? customCompile.order : [];
}
return typeNames(forks[forkName] ?? {});
}, [forkName, customCompile]);

// Compile custom DSL (main thread for UI, worker for serialize/deserialize)
useEffect(() => {
if (forkName !== CUSTOM_FORK) return;
if (!debouncedDsl.trim()) {
setCustomCompile(null);
return;
}
const local = compileCustomDsl(debouncedDsl);
setCustomCompile(local);
if (worker) worker.compileCustom(debouncedDsl);
}, [debouncedDsl, forkName, worker]);

// Keep typeName valid after custom compile changes
useEffect(() => {
if (forkName !== CUSTOM_FORK) return;
if (!customCompile?.ok) return;
if (!customCompile.types.has(typeName)) {
setTypeName(customCompile.order.at(-1) ?? "");
}
}, [forkName, customCompile, typeName]);

// Generate default value — callable from button and auto-trigger
const generateDefault = useCallback(async () => {
Expand Down Expand Up @@ -109,12 +151,19 @@ export default function App() {
const handleForkChange = useCallback(
(newFork: ForkName) => {
setForkName(newFork);
if (newFork === CUSTOM_FORK) {
const order = customCompile?.ok ? customCompile.order : [];
if (!order.includes(typeName)) {
setTypeName(order.at(-1) ?? "");
}
return;
}
const types = typeNames(forks[newFork]);
if (!types.includes(typeName)) {
setTypeName(DEFAULT_TYPE);
}
},
[typeName]
[typeName, customCompile]
);

// Handle builder value change — sync to text input
Expand Down Expand Up @@ -185,12 +234,22 @@ export default function App() {
<Toolbar
forkName={forkName}
typeName={typeName}
typeOptions={typeOptions}
serializeMode={serializeMode}
onForkChange={handleForkChange}
onTypeChange={setTypeName}
onModeChange={handleModeChange}
/>

{forkName === CUSTOM_FORK && (
<CustomTypeEditor
dsl={customDsl}
onDslChange={setCustomDsl}
error={customCompile && !customCompile.ok ? customCompile.error : null}
parsedNames={customCompile?.ok ? customCompile.order : []}
/>
)}

<main className="flex-1 grid grid-cols-1 lg:grid-cols-2 gap-2.5 p-2.5 max-w-[1800px] mx-auto w-full">
{/* Left: Input */}
<div className="bg-[var(--color-surface-raised)] rounded-xl border border-[var(--color-border)] p-4 min-h-0">
Expand Down
40 changes: 40 additions & 0 deletions src/components/custom-type-editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
type CustomTypeEditorProps = {
dsl: string;
onDslChange: (dsl: string) => void;
error: string | null;
parsedNames: string[];
};

const PLACEHOLDER = `class MyContainer(Container):
slot: uint64
parent_root: Bytes32
attestations: List[Attestation, 128]`;

export function CustomTypeEditor({dsl, onDslChange, error, parsedNames}: CustomTypeEditorProps) {
return (
<div className="border-b border-[var(--color-border)] bg-[var(--color-surface-raised)]/40 px-5 py-3">
<div className="max-w-[1800px] mx-auto flex flex-col gap-2">
<div className="flex items-center justify-between">
<span className="text-[10px] font-medium text-[var(--color-text-muted)] uppercase tracking-widest">
Custom Container Definition
</span>
<span className="text-[10px] font-mono text-[var(--color-text-muted)]">
{error ? "parse error" : parsedNames.length > 0 ? `parsed: ${parsedNames.join(", ")}` : "empty"}
</span>
</div>
<textarea
className="w-full min-h-[140px] bg-[var(--color-surface)] text-[var(--color-text-primary)] font-mono text-[12px] leading-relaxed rounded-lg border border-[var(--color-border)] p-3 resize-y focus:border-[var(--color-border-focus)] focus:outline-none placeholder:text-[var(--color-text-muted)]/50 transition-colors"
value={dsl}
onChange={(e) => onDslChange(e.target.value)}
placeholder={PLACEHOLDER}
spellCheck={false}
/>
{error && (
<div className="text-[11px] font-mono text-red-400 bg-red-500/10 border border-red-500/30 rounded px-2 py-1">
{error}
</div>
)}
</div>
</div>
);
}
32 changes: 23 additions & 9 deletions src/components/toolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import {type ForkName, forkNames, forks, typeNames} from "../lib/types";
import {type ForkName, forkNames} from "../lib/types";

type ToolbarProps = {
forkName: string;
typeName: string;
typeOptions: string[];
serializeMode: boolean;
onForkChange: (fork: ForkName) => void;
onTypeChange: (type: string) => void;
onModeChange: (serialize: boolean) => void;
};

export function Toolbar({forkName, typeName, serializeMode, onForkChange, onTypeChange, onModeChange}: ToolbarProps) {
const types = typeNames(forks[forkName]);
export function Toolbar({
forkName,
typeName,
typeOptions,
serializeMode,
onForkChange,
onTypeChange,
onModeChange,
}: ToolbarProps) {
const types = typeOptions;

return (
<div className="border-b border-[var(--color-border)] bg-[var(--color-surface-raised)]/40 px-5 py-2.5">
Expand Down Expand Up @@ -68,13 +77,18 @@ export function Toolbar({forkName, typeName, serializeMode, onForkChange, onType
<select
value={typeName}
onChange={(e) => onTypeChange(e.target.value)}
className="bg-[var(--color-surface-overlay)] text-[var(--color-text-primary)] text-[12px] font-mono rounded-md px-2.5 py-1 border border-[var(--color-border)] focus:border-[var(--color-border-focus)] focus:outline-none cursor-pointer max-w-[280px]"
disabled={types.length === 0}
className="bg-[var(--color-surface-overlay)] text-[var(--color-text-primary)] text-[12px] font-mono rounded-md px-2.5 py-1 border border-[var(--color-border)] focus:border-[var(--color-border-focus)] focus:outline-none cursor-pointer max-w-[280px] disabled:opacity-50 disabled:cursor-not-allowed"
>
{types.map((name) => (
<option key={name} value={name}>
{name}
</option>
))}
{types.length === 0 ? (
<option value="">—</option>
) : (
types.map((name) => (
<option key={name} value={name}>
{name}
</option>
))
)}
</select>
</div>
</div>
Expand Down
Loading
Loading