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
56 changes: 56 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,38 @@ h3 {
gap: 7px;
}

.challenge-nav {
display: inline-flex;
align-items: center;
gap: 4px;
border: 1px solid var(--line);
border-radius: 10px;
padding: 3px;
background: linear-gradient(180deg, var(--panel-strong) 0%, var(--panel-soft) 100%);
}

.challenge-link {
border: 1px solid transparent;
border-radius: 7px;
color: var(--ink-soft);
font-family: 'Space Mono', 'Courier New', monospace;
font-size: 0.69rem;
letter-spacing: 0.08em;
text-decoration: none;
text-transform: uppercase;
padding: 5px 8px;
}

.challenge-link:hover {
color: var(--accent);
}

.challenge-link.active {
color: var(--accent);
border-color: color-mix(in srgb, var(--accent-soft) 52%, transparent);
background: color-mix(in srgb, var(--accent) 10%, transparent);
}

.terminal-link,
.theme-toggle {
border: 1px solid var(--line);
Expand Down Expand Up @@ -948,6 +980,20 @@ h3 {
background: linear-gradient(180deg, var(--chart-bg-top) 0%, var(--chart-bg-bottom) 100%);
}

html[data-theme='dark'] .prop-amm-page .chart-wrap {
padding: 10px;
border: 1px solid rgba(76, 96, 126, 0.52);
border-radius: 10px;
background: linear-gradient(180deg, rgba(18, 29, 44, 0.8) 0%, rgba(10, 16, 28, 0.86) 100%);
}

html[data-theme='dark'] .prop-amm-page #curveChart {
border-radius: 8px;
background:
radial-gradient(circle at 30% 18%, rgba(42, 62, 89, 0.22) 0%, transparent 50%),
linear-gradient(180deg, #101b2c 0%, #0a131f 100%);
}

.market-bottom {
flex: 0 0 auto;
display: grid;
Expand Down Expand Up @@ -1338,6 +1384,16 @@ h3 {
gap: 6px;
}

.challenge-nav {
grid-column: 1 / -1;
width: 100%;
}

.challenge-link {
flex: 1 1 0;
text-align: center;
}

.terminal-link {
justify-content: center;
width: 100%;
Expand Down
182 changes: 182 additions & 0 deletions app/prop-amm/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
'use client'

import { useEffect, useMemo, useState } from 'react'
import { HeaderActions } from '../../components/HeaderActions'
import { PropCodePanel } from '../../components/PropCodePanel'
import { PropMarketPanel } from '../../components/PropMarketPanel'
import { usePropSimulationWorker } from '../../hooks/usePropSimulationWorker'
import { useUiStore } from '../../store/useUiStore'
import type { PropStrategyRef, PropWorkerUiState } from '../../lib/prop-sim/types'
import { PROP_BUILTIN_STRATEGIES, getPropBuiltinStrategyById } from '../../lib/prop-strategies/builtins'

function buildFallbackUiState(strategyRef: PropStrategyRef, playbackSpeed: number, maxTapeRows: number): PropWorkerUiState {
const builtin = getPropBuiltinStrategyById(strategyRef.id) ?? PROP_BUILTIN_STRATEGIES[0]

const snapshot: PropWorkerUiState['snapshot'] = {
step: 0,
fairPrice: 100,
strategy: {
x: 100,
y: 10_000,
k: 1_000_000,
impliedBidBps: builtin.feeBps,
impliedAskBps: builtin.feeBps,
},
normalizer: {
x: 100,
y: 10_000,
k: 1_000_000,
feeBps: 30,
liquidityMult: 1.0,
},
edge: { total: 0, retail: 0, arb: 0 },
simulationParams: { volatility: 0.003, arrivalRate: 0.8 },
}

return {
config: {
seed: 1337,
strategyRef: { kind: 'builtin', id: builtin.id },
playbackSpeed,
maxTapeRows,
},
currentStrategy: {
kind: 'builtin',
id: builtin.id,
name: builtin.name,
code: builtin.code,
feeBps: builtin.feeBps,
},
isPlaying: false,
tradeCount: 0,
snapshot,
lastEvent: {
id: 0,
step: 0,
flow: 'system',
ammName: builtin.name,
isStrategyTrade: false,
trade: null,
order: null,
arbProfit: 0,
fairPrice: 100,
priceMove: { from: 100, to: 100 },
edgeDelta: 0,
codeLines: [],
codeExplanation: 'Initializing simulation...',
stateBadge: `implied: ${builtin.feeBps}/${builtin.feeBps} bps`,
summary: 'Simulation worker is initializing.',
snapshot,
},
history: [],
reserveTrail: [{ x: 100, y: 10_000 }],
viewWindow: null,
availableStrategies: PROP_BUILTIN_STRATEGIES.map((s) => ({
kind: 'builtin' as const,
id: s.id,
name: s.name,
})),
normalizerConfig: { feeBps: 30, liquidityMult: 1.0 },
}
}

export default function PropAmmPage() {
const theme = useUiStore((state) => state.theme)
const playbackSpeed = useUiStore((state) => state.playbackSpeed)
const maxTapeRows = useUiStore((state) => state.maxTapeRows)
const showCodeExplanation = useUiStore((state) => state.showCodeExplanation)
const chartAutoZoom = useUiStore((state) => state.chartAutoZoom)

const setTheme = useUiStore((state) => state.setTheme)
const setPlaybackSpeed = useUiStore((state) => state.setPlaybackSpeed)
const setShowCodeExplanation = useUiStore((state) => state.setShowCodeExplanation)
const setChartAutoZoom = useUiStore((state) => state.setChartAutoZoom)

// Local strategy ref state for prop-amm
const [propStrategyRef, setPropStrategyRef] = useState<PropStrategyRef>({
kind: 'builtin',
id: 'starter-500bps',
})

const {
ready,
workerState,
workerError,
controls,
} = usePropSimulationWorker({
seed: 1337,
playbackSpeed,
maxTapeRows,
strategyRef: propStrategyRef,
})

useEffect(() => {
document.documentElement.setAttribute('data-theme', theme)
}, [theme])

const fallbackState = useMemo(
() => buildFallbackUiState(propStrategyRef, playbackSpeed, maxTapeRows),
[maxTapeRows, playbackSpeed, propStrategyRef],
)
const effectiveState = workerState ?? fallbackState
const simulationLoading = !ready || !workerState

return (
<>
<div className="backdrop" />
<div className="app-shell prop-amm-page">
<HeaderActions
theme={theme}
onToggleTheme={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
subtitle="Prop AMM Challenge"
subtitleLink="https://ammchallenge.com/prop-amm"
/>

{workerError ? <div className="worker-error">Worker error: {workerError}</div> : null}

<main className="layout">
<PropCodePanel
availableStrategies={effectiveState.availableStrategies}
selectedStrategy={propStrategyRef}
code={effectiveState.currentStrategy.code}
highlightedLines={effectiveState.lastEvent.codeLines}
codeExplanation={effectiveState.lastEvent.codeExplanation}
showExplanationOverlay={showCodeExplanation}
onSelectStrategy={(next) => {
setPropStrategyRef(next)
controls.setStrategy(next)
}}
onToggleExplanationOverlay={() => setShowCodeExplanation(!showCodeExplanation)}
/>

<PropMarketPanel
state={effectiveState}
theme={theme}
playbackSpeed={playbackSpeed}
autoZoom={chartAutoZoom}
isInitializing={simulationLoading}
onPlaybackSpeedChange={setPlaybackSpeed}
onToggleAutoZoom={() => setChartAutoZoom(!chartAutoZoom)}
onPlayPause={() => {
if (!workerState) return
if (workerState.isPlaying) {
controls.pause()
return
}
controls.play()
}}
onStep={() => {
if (!workerState) return
controls.step()
}}
onReset={() => {
if (!workerState) return
controls.reset()
}}
/>
</main>

</div>
</>
)
}
58 changes: 41 additions & 17 deletions components/AmmChart.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import { useEffect, useId, useMemo, useState } from 'react'
import { CHART_THEME } from '../lib/sim/constants'
import { CHART_THEME, PROP_CHART_THEME } from '../lib/sim/constants'
import {
buildArrowPath,
buildCurvePath,
Expand All @@ -20,10 +20,15 @@ interface AmmChartProps {
viewWindow: { xMin: number; xMax: number; yMin: number; yMax: number } | null
autoZoom: boolean
chartSize?: { width: number; height: number }
variant?: 'classic' | 'prop'
}

export function AmmChart({ snapshot, reserveTrail, lastEvent, theme, viewWindow, autoZoom, chartSize }: AmmChartProps) {
const palette = CHART_THEME[theme]
export function AmmChart({ snapshot, reserveTrail, lastEvent, theme, viewWindow, autoZoom, chartSize, variant = 'classic' }: AmmChartProps) {
const palette = variant === 'prop'
? theme === 'dark'
? PROP_CHART_THEME.dark
: CHART_THEME.light
: CHART_THEME[theme]
const uid = useId().replace(/:/g, '')
const clipId = `plotClip-${uid}`
const markerId = `arrowHead-${uid}`
Expand Down Expand Up @@ -114,6 +119,25 @@ export function AmmChart({ snapshot, reserveTrail, lastEvent, theme, viewWindow,
}
}, [autoZoom, baseView.liveWindow, baseView.targetX, baseView.targetY, frozenWindow, geometry.height, geometry.margin.bottom, geometry.margin.left, geometry.margin.right, geometry.margin.top, geometry.width, lastEvent, reserveTrail, snapshot])

const titleLabel = variant === 'prop' ? 'compute_swap()' : 'x · y = k'
const subtitleLabel = variant === 'prop' ? 'output curve' : 'Δy / Δx'
const xAxisLabel = variant === 'prop' ? 'Input' : 'Reserve X'
const yAxisLabel = variant === 'prop' ? 'Output' : 'Reserve Y'
const strategyLabel = variant === 'prop' ? 'strategy curve' : 'strategy'
const normalizerLabel = variant === 'prop' ? 'normalizer ref' : 'normalizer'
const trailLabel =
variant === 'prop'
? autoZoom
? 'tracked reserves (auto)'
: 'tracked reserves (fixed)'
: autoZoom
? 'recent trail (auto-zoom)'
: 'recent trail (fixed view)'
const titleFont = variant === 'prop' ? 'Space Mono' : 'Cormorant Garamond'
const titleStyle = variant === 'prop' ? 'normal' : 'italic'
const axisFont = variant === 'prop' ? 'IBM Plex Sans' : 'Cormorant Garamond'
const labelFont = variant === 'prop' ? 'Space Mono' : 'Space Mono'

return (
<svg
id="curveChart"
Expand Down Expand Up @@ -217,35 +241,35 @@ export function AmmChart({ snapshot, reserveTrail, lastEvent, theme, viewWindow,
<circle cx={chart.targetPoint.x} cy={chart.targetPoint.y} r="2.8" fill={palette.targetDot} fillOpacity="0.62" />
</g>

<text x={geometry.width - 188} y="42" fill={palette.labelMain} fontSize="28" fontFamily="Cormorant Garamond" fontStyle="italic">
x · y = k
<text x={geometry.width - 218} y="42" fill={palette.labelMain} fontSize={variant === 'prop' ? '20' : '28'} fontFamily={titleFont} fontStyle={titleStyle} letterSpacing={variant === 'prop' ? '0.03em' : 'normal'}>
{titleLabel}
</text>
<text x={geometry.width - 166} y="61" fill={palette.labelSoft} fontSize="15" fontFamily="Cormorant Garamond" fontStyle="italic">
Δy / Δx
<text x={geometry.width - 188} y="61" fill={palette.labelSoft} fontSize={variant === 'prop' ? '12' : '15'} fontFamily={titleFont} fontStyle={titleStyle} letterSpacing={variant === 'prop' ? '0.06em' : 'normal'}>
{subtitleLabel}
</text>

<text x={geometry.width / 2 - 38} y={geometry.height - 12} fill={palette.axisLabel} fontSize="13" fontFamily="Cormorant Garamond">
Reserve X
<text x={geometry.width / 2 - 38} y={geometry.height - 12} fill={palette.axisLabel} fontSize="13" fontFamily={axisFont}>
{xAxisLabel}
</text>
<text
x="27"
y={geometry.height / 2 + 24}
fill={palette.axisLabel}
fontSize="13"
fontFamily="Cormorant Garamond"
fontFamily={axisFont}
transform={`rotate(-90 27 ${geometry.height / 2 + 24})`}
>
Reserve Y
{yAxisLabel}
</text>

<text x={geometry.margin.left + 10} y={geometry.margin.top + 14} fill={palette.legendStrategy} fontSize="10" fontFamily="Space Mono">
strategy
<text x={geometry.margin.left + 10} y={geometry.margin.top + 14} fill={palette.legendStrategy} fontSize="10" fontFamily={labelFont}>
{strategyLabel}
</text>
<text x={geometry.margin.left + 10} y={geometry.margin.top + 27} fill={palette.legendNormalizer} fontSize="10" fontFamily="Space Mono">
normalizer
<text x={geometry.margin.left + 10} y={geometry.margin.top + 27} fill={palette.legendNormalizer} fontSize="10" fontFamily={labelFont}>
{normalizerLabel}
</text>
<text x={geometry.margin.left + 10} y={geometry.margin.top + 40} fill={palette.legendTrail} fontSize="9" fontFamily="Space Mono">
{autoZoom ? 'recent trail (auto-zoom)' : 'recent trail (fixed view)'}
<text x={geometry.margin.left + 10} y={geometry.margin.top + 40} fill={palette.legendTrail} fontSize="9" fontFamily={labelFont}>
{trailLabel}
</text>
</svg>
)
Expand Down
2 changes: 1 addition & 1 deletion components/FooterLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function FooterLinks() {
aria-label="X profile for devrelius"
>
<span className="x-logo" aria-hidden="true">
<svg viewBox="0 0 24 24" role="img" focusable="false">
<svg viewBox="0 0 24 24" role="img" focusable="false" className="x-logo-icon" width="12" height="12">
<path d="M4 3h5.2l4 5.6L18 3H20l-5.7 7.2L20.6 21h-5.1l-4.4-6.2L6 21H4l6-7.7z" />
</svg>
</span>
Expand Down
Loading