Skip to content
Merged
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
8 changes: 3 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
# Test and build every PR and every push to master, so pool contributions get
# checked automatically instead of a maintainer running the suite by hand.
# No lint gate yet: the tree has ~20 pre-existing eslint errors, and a gate
# that fails on day one teaches everyone to ignore it. Add `pnpm lint` here
# once those are fixed (tracked in the issues).
# Lint, test and build every PR and every push to master, so pool contributions
# get checked automatically instead of a maintainer running the suite by hand.

name: CI

Expand All @@ -25,5 +22,6 @@ jobs:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm test
- run: pnpm build
11 changes: 6 additions & 5 deletions src/components/analyzer/AnalyzerPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ export default function AnalyzerPage() {
const [result, setResult] = useState<CompilationResult | null>(null);
const [selectedContract, setSelectedContract] = useState(0);
const [selectedSlots, setSelectedSlots] = useState<Set<number>>(new Set());
const [autoAnalyzed, setAutoAnalyzed] = useState(false);
// Not state: it only guards the one-time auto-analyze and never renders.
const autoAnalyzed = useRef(false);

// Trace state
const [selectedFunction, setSelectedFunction] = useState<string>("");
Expand Down Expand Up @@ -256,16 +257,16 @@ export default function AnalyzerPage() {
analyzeProgress.finish();
setLoading(false);
},
[analyzeProgress]
[analyzeProgress, setTab]
);

// Auto-analyze when navigated with ?q= param
useEffect(() => {
if (queryInput && !autoAnalyzed) {
setAutoAnalyzed(true);
if (queryInput && !autoAnalyzed.current) {
autoAnalyzed.current = true;
handleAnalyze();
}
}, [queryInput, autoAnalyzed, handleAnalyze]);
}, [queryInput, handleAnalyze]);

const handleSelectContract = useCallback(
(idx: number) => {
Expand Down
36 changes: 21 additions & 15 deletions src/components/btx/ConstructionSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import Hint from "./Hint";

const COMMITTEE_N = 5;
const BATCH = 5;
const ALL_BATCH: boolean[] = new Array(BATCH).fill(true);
const ALL_COMMITTEE: boolean[] = new Array(COMMITTEE_N).fill(true);

export default function ConstructionSection() {
const { ref, isVisible } = useInView(0.1);
Expand Down Expand Up @@ -208,6 +210,13 @@ function CommitteeAnimation() {
const playingRef = useRef(false);
const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
const nextDotId = useRef(0);
// Reduced motion shows the final frame. The preference is only known after
// hydration, so it is applied here at render time instead of being written
// into state from an effect.
const ctShown = reduced ? ALL_BATCH : ctVisible;
const serversShown = reduced ? ALL_COMMITTEE : serverOn;
const ptShown = reduced ? ALL_BATCH : ptVisible;
const dotsShown = reduced ? [] : dots;

const cx = 320;
const cy = 140;
Expand Down Expand Up @@ -345,14 +354,9 @@ function CommitteeAnimation() {
const el = svgRef.current;
if (!el) return;
if (reduced) {
// The initializers above ran during hydration, when the media query
// isn't known yet (reduced=false) — force the final frame here.
// The final frame is rendered from `reduced` above; just stop playback.
clearTimers();
playingRef.current = false;
setCtVisible(new Array(BATCH).fill(true));
setServerOn(new Array(COMMITTEE_N).fill(true));
setPtVisible(new Array(BATCH).fill(true));
setDots([]);
return;
}
const io = new IntersectionObserver(
Expand Down Expand Up @@ -397,7 +401,7 @@ function CommitteeAnimation() {
{cts.map((c, i) => (
<g
key={i}
opacity={ctVisible[i] ? 1 : 0}
opacity={ctShown[i] ? 1 : 0}
style={{ transition: "opacity 0.4s" }}
>
<rect
Expand Down Expand Up @@ -465,7 +469,7 @@ function CommitteeAnimation() {
cx={s.sx}
cy={s.sy}
r={9}
fill={serverOn[i] ? colors.solutionAccent : colors.solutionAccentLight}
fill={serversShown[i] ? colors.solutionAccent : colors.solutionAccentLight}
stroke={colors.solutionAccent}
strokeWidth={1.2}
style={{ transition: "fill 0.3s" }}
Expand All @@ -476,7 +480,7 @@ function CommitteeAnimation() {
textAnchor="middle"
fontFamily="var(--font-plex-mono), ui-monospace, monospace"
fontSize={8}
fill={serverOn[i] ? "white" : colors.solutionAccent}
fill={serversShown[i] ? "white" : colors.solutionAccent}
fontWeight={600}
>
σ{i + 1}
Expand All @@ -487,7 +491,7 @@ function CommitteeAnimation() {
{pts.map((p, i) => (
<g
key={i}
opacity={ptVisible[i] ? 1 : 0}
opacity={ptShown[i] ? 1 : 0}
style={{ transition: "opacity 0.4s" }}
>
<rect
Expand Down Expand Up @@ -522,7 +526,7 @@ function CommitteeAnimation() {
</g>
))}

{dots.map((d) => (
{dotsShown.map((d) => (
<FlyingDotEl key={d.id} dot={d} />
))}
</svg>
Expand Down Expand Up @@ -582,6 +586,9 @@ function FftComparison() {
reduced ? btxActiveOrder.length : 0,
);

const naiveShown = reduced ? 64 : naiveCount;
const btxShown = reduced ? btxActiveOrder.length : btxCount;

useEffect(() => {
if (reduced) {
naiveRefs.current.forEach((el) => {
Expand All @@ -597,8 +604,7 @@ function FftComparison() {
el.style.opacity = "0.95";
}
});
setNaiveCount(64);
setBtxCount(btxActiveOrder.length);
// The counts are rendered from `reduced` below.
return;
}

Expand Down Expand Up @@ -757,11 +763,11 @@ function FftComparison() {
>
<span style={{ color: colors.problemAccentStrong }}>
Without FFT:{" "}
<span className="font-semibold tabular-nums">{naiveCount}</span> / 64
<span className="font-semibold tabular-nums">{naiveShown}</span> / 64
</span>
<span className="text-solution-accent">
With FFT:{" "}
<span className="font-semibold tabular-nums">{btxCount}</span> / ~
<span className="font-semibold tabular-nums">{btxShown}</span> / ~
{btxActiveOrder.length}
</span>
</div>
Expand Down
53 changes: 34 additions & 19 deletions src/components/infra/InfraContent.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { motion } from "framer-motion";
import { useState, useEffect } from "react";
import { useState, useSyncExternalStore } from "react";
import OraclePlayground from "./OraclePlayground";
import IndexerPlayground from "./IndexerPlayground";
import SwapPlayground from "./SwapPlayground";
Expand All @@ -25,25 +25,40 @@ const CATEGORIES: Category[] = [
{ id: "auth", label: "Add Auth", icon: "⊡", ready: false },
];

/** The ready category named by the URL hash, if any. */
export function readHashCategory(): string | null {
const hash = window.location.hash.slice(1);
return CATEGORIES.some((c) => c.id === hash && c.ready) ? hash : null;
}

export function serverHashCategory(): string | null {
return null;
}

// The hash is only restored on load, so there is nothing to subscribe to.
const subscribeHash = () => () => {};

export default function InfraContent() {
const [active, setActive] = useState("oracles");
// Server and hydration render the default tab; a valid hash applies right
// after hydration. A tab the user picks takes over from there.
const hashCategory = useSyncExternalStore(
subscribeHash,
readHashCategory,
serverHashCategory
);
const [picked, setPicked] = useState<string | null>(null);
const [visited, setVisited] = useState(() => new Set(["oracles"]));

// Restore active tab from URL hash on mount
useEffect(() => {
const hash = window.location.hash.slice(1);
const valid = CATEGORIES.find((c) => c.id === hash && c.ready);
if (valid) {
setActive(hash);
setVisited((prev) => new Set(prev).add(hash));
}
}, []);
const active = picked ?? hashCategory ?? "oracles";
// The open tab always counts as visited, including one restored from the hash.
const opened = new Set(visited).add(active);

const selectCategory = (id: string) => {
setActive(id);
setPicked(id);
// Keep the tab being left mounted too, so a tab restored from the hash
// keeps its state like any other visited one.
setVisited((prev) => {
if (prev.has(id)) return prev;
return new Set(prev).add(id);
if (prev.has(id) && prev.has(active)) return prev;
return new Set(prev).add(active).add(id);
});
window.history.replaceState(null, "", `#${id}`);
};
Expand Down Expand Up @@ -104,28 +119,28 @@ export default function InfraContent() {

{/* Active playground — lazy mount, keep alive */}
<div className="w-full max-w-5xl mb-28">
{visited.has("oracles") && (
{opened.has("oracles") && (
<div className={active !== "oracles" ? "hidden" : "playground-enter"}>
<ErrorBoundary>
<OraclePlayground />
</ErrorBoundary>
</div>
)}
{visited.has("indexers") && (
{opened.has("indexers") && (
<div className={active !== "indexers" ? "hidden" : "playground-enter"}>
<ErrorBoundary>
<IndexerPlayground />
</ErrorBoundary>
</div>
)}
{visited.has("swaps") && (
{opened.has("swaps") && (
<div className={active !== "swaps" ? "hidden" : "playground-enter"}>
<ErrorBoundary>
<SwapPlayground />
</ErrorBoundary>
</div>
)}
{visited.has("payments") && (
{opened.has("payments") && (
<div className={active !== "payments" ? "hidden" : "playground-enter"}>
<ErrorBoundary>
<PaymentsPlayground />
Expand Down
15 changes: 10 additions & 5 deletions src/components/infra/OraclePlayground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,15 @@ export default function OraclePlayground() {
const [error, setError] = useState<string | null>(null);
const latestPriceRef = useRef<number | null>(null);

// A new token starts from an empty chart; the effect below fetches it.
const selectToken = (i: number) => {
if (i === selectedToken) return;
setSelectedToken(i);
setLoading(true);
setError(null);
setPrices([]);
};

const token = TOKENS[selectedToken];

// Fetch real prices from RedStone API
Expand Down Expand Up @@ -231,10 +240,6 @@ export default function OraclePlayground() {
}
};

// Reset state for new token
setLoading(true);
setError(null);
setPrices([]);
latestPriceRef.current = null;

fetchPrice();
Expand Down Expand Up @@ -294,7 +299,7 @@ export default function OraclePlayground() {
{TOKENS.map((t, i) => (
<button
key={t.symbol}
onClick={() => setSelectedToken(i)}
onClick={() => selectToken(i)}
className={`font-mono text-sm px-3 py-2 rounded-lg border transition-all ${
selectedToken === i
? "bg-text-primary text-surface border-text-primary"
Expand Down
14 changes: 7 additions & 7 deletions src/components/mip3/MemoryPoolSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
if (stepIdx < demoSteps.length - 1) {
setStepIdx((s) => s + 1);
}
}, [stepIdx]);

Check warning on line 72 in src/components/mip3/MemoryPoolSection.tsx

View workflow job for this annotation

GitHub Actions / test

React Hook useCallback has a missing dependency: 'demoSteps.length'. Either include it or remove the dependency array

const handlePrev = useCallback(() => {
if (stepIdx > 0) {
Expand Down Expand Up @@ -100,15 +100,15 @@
}, [handleNext, handlePrev, isPlaying]);

// Auto-advance when playing using a timer
// Playback stops in the same tick that reaches the last step.
useEffect(() => {
if (!isPlaying) return;
if (stepIdx >= demoSteps.length - 1) {
setIsPlaying(false);
return;
}
const timer = setTimeout(() => setStepIdx((s) => s + 1), 1200);
if (!isPlaying || finished) return;
const timer = setTimeout(() => {
setStepIdx(stepIdx + 1);
if (stepIdx + 1 >= demoSteps.length - 1) setIsPlaying(false);
}, 1200);
return () => clearTimeout(timer);
}, [isPlaying, stepIdx]);
}, [isPlaying, stepIdx, finished, demoSteps.length]);

return (
<section ref={ref} className="py-24 px-6 bg-surface-elevated relative">
Expand Down
35 changes: 10 additions & 25 deletions src/components/mip3/Mip3HeroSection.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"use client";

import { motion } from "framer-motion";
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";
import { useLanguage } from "@/i18n/LanguageContext";
import SpecDisclaimer from "@/components/SpecDisclaimer";
import { HERO_STEPS, nextHeroTick, type HeroTick } from "./heroTimeline";

// Quadratic: words²/512 + 3*words
function ethMemoryCost(bytes: number): number {
Expand All @@ -18,38 +19,22 @@ function mip3MemoryCost(bytes: number): number {
}

const TARGET_BYTES = 1_048_576; // 1 MB
const STEPS = 40;
const STEP_DELAY = 80;

export default function Mip3HeroSection() {
const { t } = useLanguage();
const [step, setStep] = useState(0);
const [running, setRunning] = useState(false);
const [tick, setTick] = useState<HeroTick>({ step: 0, cycle: 0 });
const { step } = tick;

// One timer per frame, so a cleanup can never cancel the restart.
useEffect(() => {
const timer = setTimeout(() => setRunning(true), 1000);
const { delay, ...next } = nextHeroTick(tick);
const timer = setTimeout(() => setTick(next), delay);
return () => clearTimeout(timer);
}, []);

const innerTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
useEffect(() => {
if (!running) return;
if (step >= STEPS) {
// Reset after pause
const timer = setTimeout(() => {
setStep(0);
innerTimerRef.current = setTimeout(() => setRunning(true), 500);
}, 3000);
setRunning(false);
return () => { clearTimeout(timer); clearTimeout(innerTimerRef.current); };
}
const timer = setTimeout(() => setStep((s) => s + 1), STEP_DELAY);
return () => clearTimeout(timer);
}, [step, running]);
}, [tick]);

// Exponential scale so early steps show small sizes, later steps show big
const currentBytes = Math.round(
32 * Math.pow(TARGET_BYTES / 32, step / STEPS)
32 * Math.pow(TARGET_BYTES / 32, step / HERO_STEPS)
);
const ethGas = ethMemoryCost(currentBytes);
const mip3Gas = mip3MemoryCost(currentBytes);
Expand Down Expand Up @@ -151,7 +136,7 @@ export default function Mip3HeroSection() {
{/* Ratio */}
<div className="flex items-center justify-between mt-4 pt-4 border-t border-border">
<p className="font-mono text-xs text-text-tertiary">
{step >= STEPS
{step >= HERO_STEPS
? t("mip3.hero.memoryOf")
: `${t("mip3.hero.expanding")} ${formatBytes(currentBytes)}...`}
</p>
Expand Down
Loading
Loading