npx skills add ...
npx skills add kali-decoder/midnight-skills --skill example-leaderboard-dapp
Build a privacy-preserving arcade leaderboard DApp on Midnight Network — players submit click-challenge scores with anonymous, public, or custom display names, and prove entry ownership via zero-knowledge proofs without revealing secret keys. Covers leaderboard.compact (Map, Counter, witnesses, persistentHash owner commitments), Next.js frontend, 1AM wallet integration, low-level deploy/call, indexer reads without wallet, and ZK asset hosting. Use for gamified on-chain rankings, pseudonymous competitions, score attestations, or teaching disclose()/witness patterns. Triggers: leaderboard dApp, privacy leaderboard, submit score, verify ownership, anonymous player name, ZK proof of ownership, persistentHash identity, arcade game on Midnight. Also use when extending locker-dapp or payment-dapp wallet/provider patterns to social or gaming flows.
npx skills add kali-decoder/midnight-skills --skill example-leaderboard-dapp
A privacy-preserving leaderboard where players submit scores on-chain with three display modes and prove they own an entry using verifyOwnership — a ZK circuit that checks ownerCommitment(secretKey) matches the stored ownerHash without revealing the secret.
Runnable template: Copy templates/leaderboard-dapp/ for a complete Next.js project (contract + UI). Run npm install && npm run compact && npm run sync:assets && npm run dev after installing the 1AM wallet.
What this skill produces:
contract/ — Compact leaderboard + TypeScript witnesses + compile scriptsapp/leaderboard/ — Next.js client UI (click game, privacy mode picker, leaderboard table)lib/midnight.ts — wallet session + patched indexer provider (copy from references/midnight-session.md or templates/leaderboard-dapp/lib/midnight.ts)lib/leaderboard.ts — deploy, submitScore, verifyOwnership, ledger decodelib/display-name.ts — decode anonymous hash bytes to generated names ("Crimson Tiger")public/zk/leaderboard/ — ZK proving assets synced from contract buildShared references (canonical provider + troubleshooting — do not duplicate in prompts):
references/midnight-session.md — createConnectedSession, indexer patch, deploy/call helpersreferences/gotchas.md — preprod deploy hangs, GraphQL offset: null, ZK asset pathsreferences/versions.json — pinned @midnight-ntwrk/* versionsPrimary references:
example-locker-dapp/ / templates/locker-dapp/ — Next.js + 1AM pattern, low-level deploy/callexample-payment-dapp/ — provider wiring, indexer pollingcompact/ — disclose(), witnesses, persistentHash, Map, Counter, assertsecurity/ — what is public on-chain vs private in witness dataindexer/ — read contract state without wallet connectionKey architecture notes:
scores is a public Map<Uint<64>, ScoreEntry> — readable from the indexer by anyonelocalSecretKey witness returns a 32-byte secret from private state; never disclosed on-chainownerCommitment(sk) uses persistentHash with domain separator "leaderboard:owner:"useCustomName boolean selects witness-fed name vs hash-based anonymous display nameverifyOwnership is a proof-only circuit — no ledger writes; use for "prove this is my score"localStorage so ownership proofs work after page refreshcreateUnprovenDeployTx + submitTxAsync — not deployContract() (hangs on preprod)indexerPublicDataProvider with patched queryContractState (GraphQL offset: null bug)When helping the user, follow this sequence:
leaderboard.compact compile + witnesses (localSecretKey, getCustomName)createConnectedSession (from references/midnight-session.md)ContractState.deserialize → ledger() → decode entriessubmitScore(score, useCustomName) with optional setCustomName beforehandverifyOwnership(entryId) for prize claims or "yours" badgescontract/src/leaderboard.compact:
Privacy design:
useCustomName = false): displayName = persistentHash(sk) — UI decodes to generated nameuseCustomName = true): displayName = getCustomName() witness — app feeds address or user nameownerHash = ownerCommitment(sk) stored on every entry; verifyOwnership re-derives and comparesCompile:
contract/src/witnesses.ts:
Call setCustomName(name) in TypeScript before submitScore when using public or custom display modes.
lib/leaderboard.ts mirrors the locker-dapp pattern:
| Function | Purpose |
|---|---|
getOrCreateSecretKey() | Persist 32-byte secret in localStorage |
deployLeaderboard(session) | createUnprovenDeployTx + submitTxAsync |
submitScore(session, addr, score, customName?) | submitCallTxAsync → submitScore circuit |
verifyOwnership(session, addr, entryId) | submitCallTxAsync → verifyOwnership circuit |
fetchLeaderboardState(queryUrl, addr) | Indexer poll + decodeLeaderboardState |
ZK asset path: /zk/leaderboard (synced to public/zk/leaderboard/).
LeaderboardClient.tsx flow:
detectWallet() → wallet.connect('preprod') → createConnectedSession(api, ZK_PATH)submitScoreverifyOwnership(entryId) marks entry as yours in UIIndexer-only reads mean spectators can watch the leaderboard without connecting a wallet.
| Component | Version |
|---|---|
| Compact compiler | 0.31.0 |
| Compact runtime | 0.16.0 |
| Ledger | 8.0.3 |
| midnight-js | 4.0.4 |
| proof server | 8.0.3 |
Wallet: 1AM extension on Preprod, proof server http://localhost:6300, tNIGHT + tDUST from faucet.
Pin versions from references/versions.json. Cross-check docs.midnight.network support matrix before upgrading.
public/zk/leaderboard assets in build step (npm run sync:assets)localStorage for returning playersverifyOwnership does not mutate ledger — safe for repeated "prove mine" UXgetConfiguration()Open http://localhost:3000 → Connect 1AM → Deploy New → play → submit → refresh leaderboard.
struct ScoreEntry {
score: Uint<64>,
displayName: Bytes<32>,
ownerHash: Bytes<32>
}
export ledger scores: Map<Uint<64>, ScoreEntry>;
export ledger nextId: Counter;
witness localSecretKey(): Bytes<32>;
witness getCustomName(): Bytes<32>;
export circuit ownerCommitment(sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([pad(32, "leaderboard:owner:"), sk]);
}
export circuit submitScore(score: Uint<64>, useCustomName: Boolean): [] { /* ... */ }
export circuit verifyOwnership(targetEntryId: Uint<64>): [] { /* ... */ }cd contract && npm run compact && cd ..
npm run sync:assetsexport type LeaderboardPrivateState = { secretKey: Uint8Array };
export const witnesses = {
localSecretKey: (ctx) => [ctx.privateState, ctx.privateState.secretKey] as const,
getCustomName: (ctx) => [ctx.privateState, customName] as const,
};
export const setCustomName = (name: string) => { /* encode to Bytes<32> */ };docker run -d -p 6300:6300 midnightntwrk/proof-server:8.0.3 -- \
midnight-proof-server --network preprodcd templates/leaderboard-dapp
npm install
npm run compact
npm run sync:assets
npm run dev