Skip to content

Commit ebfcef2

Browse files
ensi321claude
andcommitted
feat: support custom container definitions
Add a "custom" fork option that reveals a Python-DSL textarea for defining ad-hoc SSZ containers using consensus-specs syntax. Lets users serialize/ deserialize types from unreleased forks or one-off shapes without waiting for them to land in @lodestar/types. Parser accepts multiple `class Foo(Container):` blocks, resolves field type idents against forks.fulu and built-in primitives (uint8..256, boolean), and accepts either integer literals or @lodestar/params constants for List/Vector/Bitlist/Bitvector/ByteList/ByteVector sizes. Compilation runs in both main thread (for the UI Type instance) and the worker (for serialize/deserialize), debounced 300ms. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b458ac3 commit ebfcef2

8 files changed

Lines changed: 399 additions & 27 deletions

File tree

package-lock.json

Lines changed: 1 addition & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
},
1717
"dependencies": {
1818
"@chainsafe/ssz": "^1.2.1",
19+
"@lodestar/params": "^1.34.0",
1920
"@lodestar/types": "^1.34.0",
2021
"comlink": "^4.4.2",
2122
"js-yaml": "^4.1.0",

src/app.tsx

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import type {Type} from "@chainsafe/ssz";
2-
import {useCallback, useEffect, useState} from "react";
2+
import {useCallback, useEffect, useMemo, useState} from "react";
3+
import {CustomTypeEditor} from "./components/custom-type-editor";
34
import {Footer} from "./components/footer";
45
import {Header} from "./components/header";
56
import {InputPanel} from "./components/input-panel";
67
import {OutputPanel} from "./components/output-panel";
78
import {StructureView} from "./components/structure-view/structure-view";
89
import {Toolbar} from "./components/toolbar";
10+
import {useDebounce} from "./hooks/use-debounce";
911
import {useSsz} from "./hooks/use-ssz";
1012
import {useWorker} from "./hooks/use-worker";
13+
import {type CompileResult, compileCustomDsl} from "./lib/custom-type";
1114
import {inputFormats, serializeOutputFormats} from "./lib/formats";
12-
import {type ForkName, forks, typeNames} from "./lib/types";
15+
import {CUSTOM_FORK, type ForkName, forks, typeNames} from "./lib/types";
1316

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

31+
// Custom-type DSL state
32+
const [customDsl, setCustomDsl] = useState("");
33+
const [customCompile, setCustomCompile] = useState<CompileResult | null>(null);
34+
const debouncedDsl = useDebounce(customDsl, 300);
35+
2836
// Worker
2937
const worker = useWorker();
3038

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

3442
// Get current SSZ type
35-
const sszType: Type<unknown> | null = forks[forkName]?.[typeName] ?? null;
43+
const sszType: Type<unknown> | null = useMemo(() => {
44+
if (forkName === CUSTOM_FORK) {
45+
return customCompile?.ok ? (customCompile.types.get(typeName) ?? null) : null;
46+
}
47+
return forks[forkName]?.[typeName] ?? null;
48+
}, [forkName, typeName, customCompile]);
49+
50+
// TYPE dropdown options
51+
const typeOptions = useMemo(() => {
52+
if (forkName === CUSTOM_FORK) {
53+
return customCompile?.ok ? customCompile.order : [];
54+
}
55+
return typeNames(forks[forkName] ?? {});
56+
}, [forkName, customCompile]);
57+
58+
// Compile custom DSL (main thread for UI, worker for serialize/deserialize)
59+
useEffect(() => {
60+
if (forkName !== CUSTOM_FORK) return;
61+
if (!debouncedDsl.trim()) {
62+
setCustomCompile(null);
63+
return;
64+
}
65+
const local = compileCustomDsl(debouncedDsl);
66+
setCustomCompile(local);
67+
if (worker) worker.compileCustom(debouncedDsl);
68+
}, [debouncedDsl, forkName, worker]);
69+
70+
// Keep typeName valid after custom compile changes
71+
useEffect(() => {
72+
if (forkName !== CUSTOM_FORK) return;
73+
if (!customCompile?.ok) return;
74+
if (!customCompile.types.has(typeName)) {
75+
setTypeName(customCompile.order.at(-1) ?? "");
76+
}
77+
}, [forkName, customCompile, typeName]);
3678

3779
// Generate default value — callable from button and auto-trigger
3880
const generateDefault = useCallback(async () => {
@@ -109,12 +151,19 @@ export default function App() {
109151
const handleForkChange = useCallback(
110152
(newFork: ForkName) => {
111153
setForkName(newFork);
154+
if (newFork === CUSTOM_FORK) {
155+
const order = customCompile?.ok ? customCompile.order : [];
156+
if (!order.includes(typeName)) {
157+
setTypeName(order.at(-1) ?? "");
158+
}
159+
return;
160+
}
112161
const types = typeNames(forks[newFork]);
113162
if (!types.includes(typeName)) {
114163
setTypeName(DEFAULT_TYPE);
115164
}
116165
},
117-
[typeName]
166+
[typeName, customCompile]
118167
);
119168

120169
// Handle builder value change — sync to text input
@@ -185,12 +234,22 @@ export default function App() {
185234
<Toolbar
186235
forkName={forkName}
187236
typeName={typeName}
237+
typeOptions={typeOptions}
188238
serializeMode={serializeMode}
189239
onForkChange={handleForkChange}
190240
onTypeChange={setTypeName}
191241
onModeChange={handleModeChange}
192242
/>
193243

244+
{forkName === CUSTOM_FORK && (
245+
<CustomTypeEditor
246+
dsl={customDsl}
247+
onDslChange={setCustomDsl}
248+
error={customCompile && !customCompile.ok ? customCompile.error : null}
249+
parsedNames={customCompile?.ok ? customCompile.order : []}
250+
/>
251+
)}
252+
194253
<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">
195254
{/* Left: Input */}
196255
<div className="bg-[var(--color-surface-raised)] rounded-xl border border-[var(--color-border)] p-4 min-h-0">
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
type CustomTypeEditorProps = {
2+
dsl: string;
3+
onDslChange: (dsl: string) => void;
4+
error: string | null;
5+
parsedNames: string[];
6+
};
7+
8+
const PLACEHOLDER = `class MyContainer(Container):
9+
slot: uint64
10+
parent_root: Bytes32
11+
attestations: List[Attestation, 128]`;
12+
13+
export function CustomTypeEditor({dsl, onDslChange, error, parsedNames}: CustomTypeEditorProps) {
14+
return (
15+
<div className="border-b border-[var(--color-border)] bg-[var(--color-surface-raised)]/40 px-5 py-3">
16+
<div className="max-w-[1800px] mx-auto flex flex-col gap-2">
17+
<div className="flex items-center justify-between">
18+
<span className="text-[10px] font-medium text-[var(--color-text-muted)] uppercase tracking-widest">
19+
Custom Container Definition
20+
</span>
21+
<span className="text-[10px] font-mono text-[var(--color-text-muted)]">
22+
{error ? "parse error" : parsedNames.length > 0 ? `parsed: ${parsedNames.join(", ")}` : "empty"}
23+
</span>
24+
</div>
25+
<textarea
26+
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"
27+
value={dsl}
28+
onChange={(e) => onDslChange(e.target.value)}
29+
placeholder={PLACEHOLDER}
30+
spellCheck={false}
31+
/>
32+
{error && (
33+
<div className="text-[11px] font-mono text-red-400 bg-red-500/10 border border-red-500/30 rounded px-2 py-1">
34+
{error}
35+
</div>
36+
)}
37+
</div>
38+
</div>
39+
);
40+
}

src/components/toolbar.tsx

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
1-
import {type ForkName, forkNames, forks, typeNames} from "../lib/types";
1+
import {type ForkName, forkNames} from "../lib/types";
22

33
type ToolbarProps = {
44
forkName: string;
55
typeName: string;
6+
typeOptions: string[];
67
serializeMode: boolean;
78
onForkChange: (fork: ForkName) => void;
89
onTypeChange: (type: string) => void;
910
onModeChange: (serialize: boolean) => void;
1011
};
1112

12-
export function Toolbar({forkName, typeName, serializeMode, onForkChange, onTypeChange, onModeChange}: ToolbarProps) {
13-
const types = typeNames(forks[forkName]);
13+
export function Toolbar({
14+
forkName,
15+
typeName,
16+
typeOptions,
17+
serializeMode,
18+
onForkChange,
19+
onTypeChange,
20+
onModeChange,
21+
}: ToolbarProps) {
22+
const types = typeOptions;
1423

1524
return (
1625
<div className="border-b border-[var(--color-border)] bg-[var(--color-surface-raised)]/40 px-5 py-2.5">
@@ -68,13 +77,18 @@ export function Toolbar({forkName, typeName, serializeMode, onForkChange, onType
6877
<select
6978
value={typeName}
7079
onChange={(e) => onTypeChange(e.target.value)}
71-
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]"
80+
disabled={types.length === 0}
81+
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"
7282
>
73-
{types.map((name) => (
74-
<option key={name} value={name}>
75-
{name}
76-
</option>
77-
))}
83+
{types.length === 0 ? (
84+
<option value=""></option>
85+
) : (
86+
types.map((name) => (
87+
<option key={name} value={name}>
88+
{name}
89+
</option>
90+
))
91+
)}
7892
</select>
7993
</div>
8094
</div>

0 commit comments

Comments
 (0)