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: 4 additions & 4 deletions packages/enclave-dashboard/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
# VITE_SEPOLIA_RPC=https://eth-sepolia.g.alchemy.com/v2/<your-key>

# Contract addresses — point the dashboard at a different deployment.
# VITE_ENCLAVE_ADDRESS=0xB47B267876B60a06138Bc9dfCee7aa3E26907CCB
# VITE_CIPHERNODE_REGISTRY_ADDRESS=0x497Feea9abB72229aab1584c22b5416ff128926B
# VITE_CRISP_PROGRAM_ADDRESS=0xba3B07aBFd0B8cad68aa1E946CC7AF5C1B1c8B5D
# VITE_ENCLAVE_ADDRESS=0x670eFE043d1D340148037b4b76c4F9dfED294309
# VITE_CIPHERNODE_REGISTRY_ADDRESS=0x4D707127F72a216EA116AF0B4262dD7382F84259
# VITE_CRISP_PROGRAM_ADDRESS=0xbCc418F4dd1266Cc6070b1e2AC728ef56De946e7

# First block to scan from (the Enclave deploy block). Lower = slower initial
# load (more getLogs chunks); set to the actual deploy block of the above.
# VITE_DEPLOY_BLOCK=10697349
# VITE_DEPLOY_BLOCK=10939869

# E3 timeout windows (seconds), from the deployment's timeoutConfig. Used to
# decide whether an E3 is still active vs. expired (input close + these windows).
Expand Down
27 changes: 22 additions & 5 deletions packages/enclave-dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,15 @@ function SiteFooter() {
// Fixed presentation density (the live tweak panel was removed).
const DENSITY = 'comfortable'

const pollStateForStage = (uiStageIdx: number) => (uiStageIdx >= 6 ? 'published' : uiStageIdx >= 4 ? 'computing' : 'open')
// Derive the poll-card state from the UI stage + ballot count. Specifically,
// when the input window has closed (uiStageIdx >= 4) but no ballots ever arrived,
// the committee isn't actually tallying anything — surface that as a distinct
// "idle" state instead of falsely claiming a tally is in progress.
const pollStateForStage = (uiStageIdx: number, ballotCount: number): string => {
if (uiStageIdx >= 6) return 'published'
if (uiStageIdx >= 4) return ballotCount === 0 ? 'idle' : 'computing'
return 'open'
}

// Synthetic poll used only for the "Watch the lifecycle" demo when nothing is live.
const DEMO_POLL: Poll = {
Expand All @@ -155,7 +163,7 @@ export default function App() {
const [pollState, setPollState] = useState('open')
const [stageIdx, setStageIdx] = useState(3)

const [, setNowTick] = useState(0)
const [nowTick, setNowTick] = useState(0)
const [liveMode, setLiveMode] = useState(false)
// Demo autoplay step, persisted so pausing/resuming continues where it left off.
const liveStepRef = useRef(0)
Expand Down Expand Up @@ -189,8 +197,17 @@ export default function App() {
// (archived). Card data comes straight from the list summary — no per-poll fetch.
const crispReady = crispPolls.status === 'ready'
const polls = useMemo(() => crispPolls.data ?? [], [crispPolls.data])
const activePolls = useMemo<E3Summary[]>(() => polls.filter((p) => isE3Active(p.stage, p.inputWindow[1])), [polls])
const pastPolls = useMemo<E3Summary[]>(() => polls.filter((p) => !isE3Active(p.stage, p.inputWindow[1])), [polls])
// `nowTick` is in the deps so isE3Active (which reads Date.now()) re-evaluates
// each second-tick and polls move from active → past as their windows close,
// rather than waiting for the next 15s on-chain refresh.
const activePolls = useMemo<E3Summary[]>(
() => polls.filter((p) => isE3Active(p.stage, p.inputWindow[1], { e3Program: p.e3Program, ballotCount: p.ballotCount })),
[polls, nowTick],
)
const pastPolls = useMemo<E3Summary[]>(
() => polls.filter((p) => !isE3Active(p.stage, p.inputWindow[1], { e3Program: p.e3Program, ballotCount: p.ballotCount })),
[polls, nowTick],
)
Comment thread
ctrlc03 marked this conversation as resolved.
const liveHistory = useMemo(() => adaptHistoryEntries(pastPolls, detailsCache), [pastPolls, detailsCache])

// Inspector tab state.
Expand Down Expand Up @@ -298,7 +315,7 @@ export default function App() {
<Fragment key={s.id.toString()}>
<PollCard
poll={poll}
pollState={pollStateForStage(stageIdx)}
pollState={pollStateForStage(stageIdx, s.ballotCount)}
currentStageIdx={stageIdx}
ballotCount={s.ballotCount}
onNavigate={setView}
Expand Down
13 changes: 9 additions & 4 deletions packages/enclave-dashboard/src/Inspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ function ExplorerLink({ value, href }: { value: string; href: string }) {
const TxLink = ({ hash }: { hash: string }) => <ExplorerLink value={hash} href={explorerTx(hash)} />
const AddrLink = ({ address }: { address: string }) => <ExplorerLink value={address} href={explorerAddress(address)} />

function InspStatusBadge({ stageIdx }: { stageIdx: number }) {
function InspStatusBadge({ stageIdx, label }: { stageIdx: number; label?: string }) {
const s = STAGES[stageIdx]
const variant = stageIdx >= 6 ? 'published' : stageIdx === 3 ? 'open' : 'working'
return (
<span className={`stage-badge stage-badge--${variant}`}>
<span className='stage-badge__dot' />
<span>{s.label}</span>
<span>{label ?? s.label}</span>
</span>
)
}
Expand Down Expand Up @@ -236,8 +236,13 @@ export default function Inspector({

// Section status derived from the E3's current UI stage index (see STAGES order).
// The final stage (Published, index 6) is terminal: reaching it = complete.
// When `noBallots` is set the chain stayed in KeyPublished (currentStage=4) but
// nothing was ever submitted, so post-input sections aren't actually "in progress".
const lastStage = STAGES.length - 1
const stageStatus = (targetStage: number) => {
if (e3.noBallots && targetStage >= 4) {
return targetStage === 4 ? { kind: 'pending', label: 'No ballots' } : { kind: 'pending', label: 'Skipped' }
}
if (e3.currentStage > targetStage) return { kind: 'done', label: 'Done' }
if (e3.currentStage === targetStage) {
return targetStage >= lastStage ? { kind: 'done', label: 'Complete' } : { kind: 'live', label: 'In progress' }
Expand Down Expand Up @@ -300,7 +305,7 @@ export default function Inspector({
<div className='insp-stat'>
<div className='insp-stat__label'>Status</div>
<div className='insp-stat__value'>
<InspStatusBadge stageIdx={e3.currentStage} />
<InspStatusBadge stageIdx={e3.currentStage} label={e3.noBallots ? 'Complete · no ballots' : undefined} />
</div>
</div>
<div className='insp-stat'>
Expand Down Expand Up @@ -329,7 +334,7 @@ export default function Inspector({
items={[
['Requested at', <Mono>{e3.requestedAt}</Mono>],
['Request tx', <TxLink hash={e3.requestedTx} />],
['Block', <Mono>#{e3.requestedBlock.toLocaleString()}</Mono>],
['Block', <Mono>{e3.requestedBlock != null ? `#${e3.requestedBlock.toLocaleString()}` : '—'}</Mono>],
['Requested by', <AddrLink address={e3.requestedBy} />],
['Program', <Mono>{e3.program}</Mono>],
['Program address', <AddrLink address={e3.programAddr} />],
Expand Down
23 changes: 14 additions & 9 deletions packages/enclave-dashboard/src/PollCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ function formatRemaining(closesTs: number): string {
return `${secs}s remaining`
}

function StageBadge({ stageIdx }: { stageIdx: number }) {
function StageBadge({ stageIdx, label }: { stageIdx: number; label?: string }) {
const s = STAGES[stageIdx]
const variant = stageIdx >= 6 ? 'published' : stageIdx === 3 ? 'open' : 'working'
return (
<span className={`stage-badge stage-badge--${variant}`}>
<span className='stage-badge__dot' />
<span>{s.label}</span>
<span>{label ?? s.label}</span>
</span>
)
}
Expand Down Expand Up @@ -138,8 +138,11 @@ export default function PollCard({
const isPublished = pollState === 'published'
const isOpen = pollState === 'open'
const isComputing = pollState === 'computing'
// Live countdown only meaningful while the input window is open.
const timeValue = stageId === 'input' ? formatRemaining(poll.closesTs) : status.label
const isIdle = pollState === 'idle' // input window closed with no ballots
// Live countdown only meaningful while the input window is open. For idle
// (window closed with no ballots) override the canned "In progress" copy.
const timeValue = stageId === 'input' ? formatRemaining(poll.closesTs) : isIdle ? 'Closed · no ballots' : status.label
const statusSub = isIdle ? 'No encrypted ballots were submitted before close' : status.sub

return (
<section className='poll-card' aria-label="Today's CRISP poll">
Expand Down Expand Up @@ -172,7 +175,7 @@ export default function PollCard({
<span>{liveMode ? 'Pause demo' : 'Watch the lifecycle'}</span>
</button>
)}
<StageBadge stageIdx={safeStageIdx} />
<StageBadge stageIdx={safeStageIdx} label={isIdle ? 'No ballots' : undefined} />
</header>

<h1 className='poll-card__question'>{poll.question}</h1>
Expand All @@ -182,7 +185,7 @@ export default function PollCard({
<div>
<div className='poll-card__timing-label'>Time</div>
<div className='poll-card__timing-value'>{timeValue}</div>
<div className='poll-card__timing-sub'>{status.sub}</div>
<div className='poll-card__timing-sub'>{statusSub}</div>
</div>
<div>
<div className='poll-card__timing-label'>Opened</div>
Expand All @@ -200,9 +203,11 @@ export default function PollCard({
<span className='poll-card__cta-note'>
{isOpen
? "Voting is open. Ballots are encrypted on the voter's device and submitted to the network."
: isComputing
? 'Voting has closed. The committee is now tallying the encrypted ballots.'
: 'Voting has not opened yet for this poll.'}
: isIdle
? 'Voting has closed without any ballots submitted.'
: isComputing
? 'Voting has closed. The committee is now tallying the encrypted ballots.'
: 'Voting has not opened yet for this poll.'}
</span>
<a
className='link-inline'
Expand Down
29 changes: 21 additions & 8 deletions packages/enclave-dashboard/src/lib/adapt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ function historyResult(s: E3Summary, detail: E3FullDetails | undefined, meta: Re
}
if (s.stage === E3Stage.Complete) return 'Completed'
if (s.stage === E3Stage.Failed) return 'Failed'
if (isE3Active(s.stage, s.inputWindow[1])) return 'In progress'
if (isE3Active(s.stage, s.inputWindow[1], { e3Program: s.e3Program, ballotCount: s.ballotCount })) return 'In progress'
// Past the input window without inputs (and not flagged Complete/Failed on chain)
// — distinguish from generic "Expired" so empty rounds are obvious in history.
if (s.ballotCount === 0) return 'No ballots'
return 'Expired'
}

Expand Down Expand Up @@ -97,7 +100,8 @@ export type InspectorDetail = {
requestedByLabel: string
requestedTx: string
requestedAt: string
requestedBlock: number

requestedBlock: number | null
currentStage: number
summary: string
committee: { size: number; threshold: number; selectionSeed: string; drawnAt: string }
Expand All @@ -123,6 +127,11 @@ export type InspectorDetail = {
compute: { status: string; note: string }
decryption: { status: string; note: string; threshold: number; committeeSize: number }
publication: { status: string; note: string; resultTx?: string }
// True when the input window has closed without a single ballot being submitted.
// The chain stage still reports KeyPublished (no transition is triggered without
// inputs), so without this flag the compute/decryption sections look "in progress"
// when in fact nothing is happening.
noBallots: boolean
events: InspectorEvent[]
}

Expand All @@ -145,6 +154,8 @@ export function adaptInspectorDetail(detail: E3FullDetails | null): InspectorDet
const inputsReceived = detail.inputsTracked ? detail.ballotCount.toLocaleString() : '—'
const sharesRequired = detail.committeeThreshold[0] || 0
const committeeSize = detail.committeeThreshold[1] || detail.committeeMembers.length
// Past the input window with zero ballots — see `noBallots` on InspectorDetail.
const noBallots = detail.inputsTracked && detail.ballotCount === 0 && detail.uiStageIdx >= 4

return {
id: formatE3Id(detail.id),
Expand All @@ -153,8 +164,8 @@ export function adaptInspectorDetail(detail: E3FullDetails | null): InspectorDet
requestedBy: detail.requester,
requestedByLabel: 'Requester',
requestedTx: detail.requestTxHash,
requestedAt: detail.requestedAt ? fmtUtcFromUnix(detail.requestedAt) : `block #${detail.requestBlock.toString()}`,
requestedBlock: Number(detail.requestBlock),
requestedAt: detail.requestedAt ? fmtUtcFromUnix(detail.requestedAt) : '—',
requestedBlock: detail.requestEventBlock != null ? Number(detail.requestEventBlock) : null,
currentStage: detail.uiStageIdx,
summary: isCrisp ? meta.question : `Encrypted execution ${formatE3Id(detail.id)}`,

Expand Down Expand Up @@ -188,9 +199,10 @@ export function adaptInspectorDetail(detail: E3FullDetails | null): InspectorDet
},

compute: {
status: detail.uiStageIdx >= 4 ? 'active' : 'pending',
note:
detail.uiStageIdx < 4
status: noBallots ? 'idle' : detail.uiStageIdx >= 4 ? 'active' : 'pending',
note: noBallots
? 'The input window closed without any ballots being submitted. There is nothing to compute over.'
: detail.uiStageIdx < 4
? 'Compute begins automatically when the input window closes.'
: "The program's FHE computation runs over the encrypted inputs, without decrypting any individual input.",
},
Expand All @@ -213,6 +225,7 @@ export function adaptInspectorDetail(detail: E3FullDetails | null): InspectorDet
resultTx: detail.resultTxHash,
},

noBallots,
events: buildEventLog(detail),
}
}
Expand All @@ -226,7 +239,7 @@ function buildEventLog(d: E3FullDetails): InspectorEvent[] {
const evs: InspectorEvent[] = []
evs.push({
t: d.requestedAt ? fmtClock(d.requestedAt) : '—',
block: Number(d.requestBlock),
block: d.requestEventBlock != null ? Number(d.requestEventBlock) : '—',
name: 'E3Requested',
stage: 'Requested',
tx: shortHash(d.requestTxHash),
Expand Down
8 changes: 4 additions & 4 deletions packages/enclave-dashboard/src/lib/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ export const publicClient = createPublicClient({
})

export const CONTRACTS = {
Enclave: envStr('VITE_ENCLAVE_ADDRESS', '0xB47B267876B60a06138Bc9dfCee7aa3E26907CCB') as Address,
CiphernodeRegistry: envStr('VITE_CIPHERNODE_REGISTRY_ADDRESS', '0x497Feea9abB72229aab1584c22b5416ff128926B') as Address,
CRISPProgram: envStr('VITE_CRISP_PROGRAM_ADDRESS', '0xba3B07aBFd0B8cad68aa1E946CC7AF5C1B1c8B5D') as Address,
Enclave: envStr('VITE_ENCLAVE_ADDRESS', '0x670eFE043d1D340148037b4b76c4F9dfED294309') as Address,
CiphernodeRegistry: envStr('VITE_CIPHERNODE_REGISTRY_ADDRESS', '0x4D707127F72a216EA116AF0B4262dD7382F84259') as Address,
CRISPProgram: envStr('VITE_CRISP_PROGRAM_ADDRESS', '0xbCc418F4dd1266Cc6070b1e2AC728ef56De946e7') as Address,
}

// First block to scan from — lower bound for getLogs (the Enclave deploy block).
export const DEPLOY_BLOCK = BigInt(envStr('VITE_DEPLOY_BLOCK', '10697349'))
export const DEPLOY_BLOCK = BigInt(envStr('VITE_DEPLOY_BLOCK', '10939869'))

// E3 timeout windows (seconds), matching the deployment's timeoutConfig. Used to
// decide whether an E3 is still genuinely active vs. expired without completing.
Expand Down
Loading
Loading