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
2 changes: 1 addition & 1 deletion crates/dashboard/assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<meta name="description" content="Interfold ciphernode observability dashboard" />
<title>Interfold · Node Observatory</title>
<script type="module" crossorigin src="/assets/app.js"></script>
<link rel="stylesheet" crossorigin href="/assets/app.css">
<link rel="stylesheet" crossorigin href="/assets/app.css" />
</head>
<body>
<div id="root"></div>
Expand Down
13 changes: 11 additions & 2 deletions examples/CRISP/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"deploy": "gh-pages -d dist"
},
"dependencies": {
"@crisp-e3/sdk": "0.11.0",
"@crisp-e3/sdk": "0.14.0",
"@emotion/babel-plugin": "^11.11.0",
"@emotion/react": "^11.11.4",
"@phosphor-icons/react": "^2.1.4",
Expand Down Expand Up @@ -53,5 +53,14 @@
"typescript": "^5.8.3",
"vite": "^5.2.0"
},
"packageManager": "pnpm@10.7.1+sha512.2d92c86b7928dc8284f53494fb4201f983da65f0fb4f0d40baafa5cf628fa31dae3e5968f12466f17df7e97310e30f343a648baea1b9b350685dafafffdf5808"
"packageManager": "pnpm@10.7.1+sha512.2d92c86b7928dc8284f53494fb4201f983da65f0fb4f0d40baafa5cf628fa31dae3e5968f12466f17df7e97310e30f343a648baea1b9b350685dafafffdf5808",
"pnpm": {
"overrides": {
"@types/node": "22.7.5",
"tsup": "8.5.0",
"typescript": "5.8.3",
"undici-types": "6.19.8",
"viem": "2.38.6"
}
}
}
1,396 changes: 580 additions & 816 deletions examples/CRISP/client/pnpm-lock.yaml

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions examples/CRISP/client/src/components/CircularTiles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@
// without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE.

import { memo, useEffect, useState } from 'react'
import { memo, useState } from 'react'
import CircularTile from './CircularTile'

const generateRotations = (count: number) => [...Array(count)].map(() => [0, 90, 180, 270][Math.floor(Math.random() * 4)])

const CircularTiles = ({ count = 1, className }: { count?: number; className?: string }) => {
const [rotations, setRotations] = useState(() => generateRotations(count))
const [renderedCount, setRenderedCount] = useState(count)

useEffect(() => {
// Re-roll the rotations when the number of tiles changes, adjusting state
// during render rather than in an effect.
if (renderedCount !== count) {
setRenderedCount(count)
setRotations(generateRotations(count))
}, [count])
}

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const getVoteCacheKey = (sessionId: string, roundId: number, address: string): s
return `crisp-vote-status-${sessionId}-${roundId}-${address.toLowerCase()}`
}

const nowInSeconds = (): number => Math.floor(Date.now() / 1000)

const VOTE_CACHE_DURATION = 5 * 60 * 1000

const VoteManagementProvider = ({ children }: VoteManagementProviderProps) => {
Expand All @@ -41,11 +43,9 @@ const VoteManagementProvider = ({ children }: VoteManagementProviderProps) => {
/**
* Voting Management States
**/
const [user, setUser] = useState<{ address: string } | null>(null)
const [roundState, setRoundState] = useState<VoteStateLite | null>(null)
const [votingRound, setVotingRound] = useState<VotingRound | null>(null)
const [roundEndDate, setRoundEndDate] = useState<Date | null>(null)
const [isLoading, setIsLoading] = useState<boolean>(false)
const [pollOptions, setPollOptions] = useState<Poll[]>([])
const [pastPolls, setPastPolls] = useState<PollResult[]>([])
const [txUrl, setTxUrl] = useState<string | undefined>(undefined)
Expand All @@ -56,6 +56,13 @@ const VoteManagementProvider = ({ children }: VoteManagementProviderProps) => {
const [voteStatusLoading, setVoteStatusLoading] = useState<boolean>(false)
const voteStatusCache = useRef<Map<string, VoteStatus>>(new Map())

/**
* The connected wallet is the source of truth for the user, so it is derived
* rather than mirrored into state.
**/
const user = useMemo(() => (isConnected && address ? { address } : null), [isConnected, address])
const userAddress = user?.address

/**
* Voting Management Methods
**/
Expand Down Expand Up @@ -107,9 +114,9 @@ const VoteManagementProvider = ({ children }: VoteManagementProviderProps) => {

const markVotedInRound = useCallback(
(roundId: number) => {
if (!user?.address) return
if (!userAddress) return

const cacheKey = getVoteCacheKey(sessionId, roundId, user.address)
const cacheKey = getVoteCacheKey(sessionId, roundId, userAddress)
const status: VoteStatus = {
hasVoted: true,
roundId: roundId,
Expand All @@ -121,7 +128,7 @@ const VoteManagementProvider = ({ children }: VoteManagementProviderProps) => {
return roundId === currentRoundId ? true : prevHasVoted
})
},
[sessionId, user?.address, currentRoundId],
[sessionId, userAddress, currentRoundId],
)

const initialLoad = async () => {
Expand All @@ -135,8 +142,7 @@ const VoteManagementProvider = ({ children }: VoteManagementProviderProps) => {
const fetched = await getRoundStateLiteRequest(currentRound.id)
if (!fetched) return

const nowSec = Math.floor(Date.now() / 1000)
const ended = Number(fetched.end_time) <= nowSec
const ended = Number(fetched.end_time) <= nowInSeconds()
let fallbackRoundId: number | null = null

if (ended) {
Expand Down Expand Up @@ -185,33 +191,22 @@ const VoteManagementProvider = ({ children }: VoteManagementProviderProps) => {
}
} catch (error) {
handleGenericError('getPastPolls', error as Error)
} finally {
setIsLoading(false)
}
}

// The cached vote statuses are keyed by address, so drop them when the wallet
// disconnects.
useEffect(() => {
if (interfoldLoading) {
return setIsLoading(true)
}
setIsLoading(false)
}, [interfoldLoading])

useEffect(() => {
if (isConnected && address) {
setUser({ address })
} else {
setUser(null)
setHasVotedInCurrentRound(false)
if (!userAddress) {
voteStatusCache.current.clear()
}
}, [isConnected, address])
}, [userAddress])

useEffect(() => {
let cancelled = false
const checkStatus = async () => {
if (user?.address && currentRoundId !== null && currentRoundId >= 0) {
const hasVoted = await checkVoteStatus(currentRoundId, user.address)
if (userAddress && currentRoundId !== null && currentRoundId >= 0) {
const hasVoted = await checkVoteStatus(currentRoundId, userAddress)
if (!cancelled) {
setHasVotedInCurrentRound(hasVoted)
}
Expand All @@ -223,12 +218,12 @@ const VoteManagementProvider = ({ children }: VoteManagementProviderProps) => {
return () => {
cancelled = true
}
}, [user?.address, currentRoundId, checkVoteStatus])
}, [userAddress, currentRoundId, checkVoteStatus])

return (
<VoteManagementContextProvider
value={{
isLoading,
isLoading: interfoldLoading,
user,
votingRound,
roundEndDate,
Expand All @@ -253,7 +248,6 @@ const VoteManagementProvider = ({ children }: VoteManagementProviderProps) => {
initialLoad,
broadcastVote,
setVotingRound,
setUser,
checkVoteStatus,
markVotedInRound,
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ export type VoteManagementContextType = {
initialLoad: () => Promise<void>
getPastPolls: () => Promise<void>
setVotingRound: React.Dispatch<React.SetStateAction<VotingRound | null>>
setUser: React.Dispatch<React.SetStateAction<{ address: string } | null>>
broadcastVote: (vote: BroadcastVoteRequest) => Promise<BroadcastVoteResponse | undefined>
getRoundStateLite: (roundCount: number) => Promise<void>
setPastPolls: React.Dispatch<React.SetStateAction<PollResult[]>>
Expand Down
1 change: 1 addition & 0 deletions examples/CRISP/client/src/model/vote.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export interface VoteStateLite {
start_time: number
end_time: number
start_block: number
snapshot_block: number

committee_public_key: number[]
emojis: [string, string]
Expand Down
10 changes: 1 addition & 9 deletions examples/CRISP/client/src/pages/AllPolls/AllPolls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { debounce } from '@/utils/methods'

const AllPolls: React.FC = () => {
const { votingRound, pastPolls, getPastPolls, isLoading } = useVoteManagementContext()
const [visiblePolls, setVisiblePolls] = useState<PollResult[]>([])
const [page, setPage] = useState<number>(0)
const [loadingMore, setLoadingMore] = useState<boolean>(false)

Expand All @@ -41,14 +40,7 @@ const AllPolls: React.FC = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [votingRound])

useEffect(() => {
setVisiblePolls(pastPolls.slice(0, 12))
}, [pastPolls])

useEffect(() => {
const newVisiblePolls = pastPolls.slice(0, (page + 1) * 12)
setVisiblePolls(newVisiblePolls)
}, [page, pastPolls])
const visiblePolls = useMemo(() => pastPolls.slice(0, (page + 1) * 12), [page, pastPolls])

const handleScroll = useMemo(
() =>
Expand Down
55 changes: 24 additions & 31 deletions examples/CRISP/client/src/pages/PollResult/PollResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE.

import React, { Fragment, useEffect, useState } from 'react'
import React, { Fragment, useEffect, useMemo } from 'react'
import CardContent from '@/components/Cards/CardContent'
import VotesBadge from '@/components/VotesBadge'
import PollCardResult from '@/components/Cards/PollCardResult'
Expand All @@ -21,55 +21,48 @@ const PollResult: React.FC = () => {
const params = useParams()
const { roundId, type } = params
const { pastPolls, getWebResultByRound, pollResult, setPollResult } = useVoteManagementContext()
const [loading, setLoading] = useState<boolean>(true)
const { roundEndDate, txUrl, roundState } = useVoteManagementContext()

const activeTotalCount = type === 'confirmation' ? roundState?.vote_count : pollResult?.totalVotes

const fetchPoll = async () => {
const pollResult = await getWebResultByRound(parseInt(roundId as string))
if (pollResult) {
const convertedPoll = convertPollData([pollResult])
setPollResult(convertedPoll[0])
setLoading(false)
}
}
// Right after voting the tally is not published yet, so the live round state
// is rendered instead of the fetched result.
const confirmationPoll = useMemo(() => {
if (type !== 'confirmation' || !roundState || !activeTotalCount) return null
return convertVoteStateLite(roundState)
}, [type, roundState, activeTotalCount])

useEffect(() => {
if (!pollResult && roundId && loading) {
fetchPoll()
} else if (activeTotalCount && roundState && type === 'confirmation') {
const currentPoll = convertVoteStateLite(roundState)
if (currentPoll) {
setPollResult(currentPoll)
setLoading(false)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pastPolls, roundId, roundState, activeTotalCount])
const displayedPoll = confirmationPoll ?? pollResult
const loading = !displayedPoll

useEffect(() => {
if (pollResult && loading) {
setLoading(false)
if (pollResult || confirmationPoll || !roundId) return

const fetchPoll = async () => {
const fetched = await getWebResultByRound(parseInt(roundId))
if (fetched) {
setPollResult(convertPollData([fetched])[0])
}
}
fetchPoll()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pollResult])
}, [pastPolls, roundId, confirmationPoll, pollResult])

return (
<EditorialShell className='flex w-full flex-1 flex-col'>
<section className='pad-section col' style={{ flex: 1, alignItems: 'center', gap: 36 }}>
{loading && !pollResult && (
{loading && (
<div className='flex items-center justify-center'>
<LoadingAnimation isLoading={loading} />
</div>
)}
{!loading && pollResult && (
{displayedPoll && (
<Fragment>
<div className='col' style={{ alignItems: 'center', gap: 24, width: '100%' }}>
<div className='col' style={{ alignItems: 'center', gap: 8, textAlign: 'center' }}>
<p className='mono muted'>Poll {pollResult.roundId}</p>
<p className='mono muted'>Poll {displayedPoll.roundId}</p>
<h1 className='h1'>{type === 'confirmation' ? 'Thanks for voting!' : 'Poll Results'}</h1>
{type !== 'confirmation' && <p className='cap'>{formatDate(pollResult.date)}</p>}
{type !== 'confirmation' && <p className='cap'>{formatDate(displayedPoll.date)}</p>}
</div>
{type === 'confirmation' && roundEndDate && (
<div className='col' style={{ alignItems: 'center', gap: 6 }}>
Expand All @@ -79,8 +72,8 @@ const PollResult: React.FC = () => {
)}
<VotesBadge totalVotes={activeTotalCount ?? 0} />
<PollCardResult
results={markWinner(pollResult.options)}
totalVotes={pollResult.totalVotes}
results={markWinner(displayedPoll.options)}
totalVotes={displayedPoll.totalVotes}
isResult
isActive={type === 'confirmation' ? true : false}
/>
Expand Down
7 changes: 4 additions & 3 deletions examples/CRISP/crates/zk-inputs/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ pub fn numbers_to_strings_for_js(value: serde_json::Value) -> serde_json::Value
if u > JS_SAFE_INT_MAX as u64 {
return serde_json::Value::String(u.to_string());
}
} else if n.as_f64().is_none_or(|f| {
f < -JS_SAFE_INT_MAX as f64 || f > JS_SAFE_INT_MAX as f64
}) {
} else if n
.as_f64()
.is_none_or(|f| f < -JS_SAFE_INT_MAX as f64 || f > JS_SAFE_INT_MAX as f64)
{
return serde_json::Value::String(n.to_string());
}

Expand Down
4 changes: 2 additions & 2 deletions examples/CRISP/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
"@interfold/config": "^0",
"eslint": "^9.39.1",
"@playwright/test": "1.52.0",
"@synthetixio/synpress": "^4.1.0",
"@synthetixio/synpress-cache": "^0.0.12",
"@synthetixio/synpress": "4.1.1",
"@synthetixio/synpress-cache": "0.0.13",
"@types/node": "^22.18.0",
"concurrently": "^9.1.2",
"dotenv": "^16.4.5",
Expand Down
26 changes: 26 additions & 0 deletions examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,32 @@ contract CRISPProgram is IE3Program, Ownable {
return e3Data[e3Id].paramsHash;
}

/// @notice Get the details about an E3 such as the merkle root of the census
/// @dev RoundData cannot be returned directly as it contains nested mappings
/// @param e3Id The E3 program ID
/// @return merkleRoot The census merkle root
/// @return paramsHash The hash of the E3 program params
/// @return numOptions The number of vote options
/// @return creditMode The credit mode for the round
/// @return inputRoot The current root of the input (votes) merkle tree
/// @return numberOfVotes The number of leaves in the input merkle tree
function getRoundData(
uint256 e3Id
)
public
view
returns (uint256 merkleRoot, bytes32 paramsHash, uint256 numOptions, CreditMode creditMode, uint256 inputRoot, uint40 numberOfVotes)
{
RoundData storage round = e3Data[e3Id];

merkleRoot = round.merkleRoot;
paramsHash = round.paramsHash;
numOptions = round.numOptions;
creditMode = round.creditMode;
inputRoot = round.votes._root(TREE_DEPTH);
numberOfVotes = round.votes.numberOfLeaves;
}

/// @inheritdoc IE3Program
function validate(
uint256 e3Id,
Expand Down
3 changes: 2 additions & 1 deletion examples/CRISP/packages/crisp-contracts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@crisp-e3/contracts",
"version": "0.11.0",
"version": "0.14.0",
"type": "module",
"files": [
"contracts",
Expand Down Expand Up @@ -50,6 +50,7 @@
"devDependencies": {
"@crisp-e3/sdk": "workspace:^",
"@crisp-e3/zk-inputs": "workspace:^",
"@nomicfoundation/hardhat-keystore": "3.0.3",
"@nomicfoundation/hardhat-toolbox-mocha-ethers": "3.0.0",
"@openzeppelin/contracts": "^5.0.2",
"@typechain/ethers-v6": "^0.5.0",
Expand Down
Loading
Loading