example-leaderboard-dapp
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMidnight Network Leaderboard DApp
Midnight Network Leaderboard DApp
A privacy-preserving leaderboard where players submit scores on-chain with three display modes and prove they own an entry using — a ZK circuit that checks matches the stored without revealing the secret.
verifyOwnershipownerCommitment(secretKey)ownerHashRunnable template: Copy for a complete Next.js project (contract + UI). Run after installing the 1AM wallet.
templates/leaderboard-dapp/npm install && npm run compact && npm run sync:assets && npm run devWhat this skill produces:
- — Compact leaderboard + TypeScript witnesses + compile scripts
contract/ - — Next.js client UI (click game, privacy mode picker, leaderboard table)
app/leaderboard/ - — wallet session + patched indexer provider (copy from
lib/midnight.tsorreferences/midnight-session.md)templates/leaderboard-dapp/lib/midnight.ts - — deploy,
lib/leaderboard.ts,submitScore, ledger decodeverifyOwnership - — decode anonymous hash bytes to generated names ("Crimson Tiger")
lib/display-name.ts - — ZK proving assets synced from contract build
public/zk/leaderboard/
Shared references (canonical provider + troubleshooting — do not duplicate in prompts):
- —
references/midnight-session.md, indexer patch, deploy/call helperscreateConnectedSession - — preprod deploy hangs, GraphQL
references/gotchas.md, ZK asset pathsoffset: null - — pinned
references/versions.jsonversions@midnight-ntwrk/*
Primary references:
- /
example-locker-dapp/— Next.js + 1AM pattern, low-level deploy/calltemplates/locker-dapp/ - — provider wiring, indexer polling
example-payment-dapp/ - —
compact/, witnesses,disclose(),persistentHash,Map,Counterassert - — what is public on-chain vs private in witness data
security/ - — read contract state without wallet connection
indexer/
Key architecture notes:
- is a public
scores— readable from the indexer by anyoneMap<Uint<64>, ScoreEntry> - witness returns a 32-byte secret from private state; never disclosed on-chain
localSecretKey - uses
ownerCommitment(sk)with domain separatorpersistentHash"leaderboard:owner:" - boolean selects witness-fed name vs hash-based anonymous display name
useCustomName - is a proof-only circuit — no ledger writes; use for "prove this is my score"
verifyOwnership - Persist secret key in so ownership proofs work after page refresh
localStorage - Use +
createUnprovenDeployTx— notsubmitTxAsync(hangs on preprod)deployContract() - Wrap with patched
indexerPublicDataProvider(GraphQLqueryContractStatebug)offset: null - Leaderboard table can load from indexer without wallet — only submit/verify need connection
一个隐私保护型排行榜,玩家可通过三种显示模式在链上提交分数,并使用——一个零知识(ZK)电路,在不泄露密钥的情况下检查是否与存储的匹配——来证明自己拥有某条目。
verifyOwnershipownerCommitment(secretKey)ownerHash可运行模板: 复制获取完整的Next.js项目(合约+UI)。安装1AM钱包后,运行。
templates/leaderboard-dapp/npm install && npm run compact && npm run sync:assets && npm run dev该技能产出内容:
- —— Compact排行榜 + TypeScript witnesses + 编译脚本
contract/ - —— Next.js客户端UI(点击游戏、隐私模式选择器、排行榜表格)
app/leaderboard/ - —— 钱包会话 + 补丁索引器提供者(从
lib/midnight.ts或references/midnight-session.md复制)templates/leaderboard-dapp/lib/midnight.ts - —— 部署、
lib/leaderboard.ts、submitScore、账本解码verifyOwnership - —— 将匿名哈希字节解码为生成名称(如"Crimson Tiger")
lib/display-name.ts - —— 从合约构建同步的ZK证明资产
public/zk/leaderboard/
共享参考资料(标准提供者+故障排除——请勿在提示中重复):
- ——
references/midnight-session.md、索引器补丁、部署/调用助手createConnectedSession - —— 预生产部署卡顿、GraphQL
references/gotchas.md问题、ZK资产路径offset: null - —— 固定版本的
references/versions.json依赖@midnight-ntwrk/*
主要参考资料:
- /
example-locker-dapp/—— Next.js + 1AM模式、底层部署/调用templates/locker-dapp/ - —— 提供者连接、索引器轮询
example-payment-dapp/ - ——
compact/、witnesses、disclose()、persistentHash、Map、Counterassert - —— 链上公开数据与witness中私有数据的区别
security/ - —— 无需连接钱包即可读取合约状态
indexer/
核心架构说明:
- 是一个公开的
scores——任何人都可通过索引器读取Map<Uint<64>, ScoreEntry> - witness从私有状态返回32字节密钥;绝不会在链上披露
localSecretKey - 使用带有域分隔符
ownerCommitment(sk)的"leaderboard:owner:"persistentHash - 布尔值选择由witness提供的名称还是基于哈希的匿名显示名称
useCustomName - 是一个仅证明的电路——不写入账本;用于“证明这是我的分数”场景
verifyOwnership - 将密钥持久化存储在中,以便页面刷新后仍可进行所有权证明
localStorage - 使用+
createUnprovenDeployTx——而非submitTxAsync(在预生产环境会卡顿)deployContract() - 用补丁后的包装
queryContractState(解决GraphQLindexerPublicDataProviderbug)offset: null - 排行榜表格可从索引器加载无需钱包——仅提交/验证操作需要连接钱包
Workflow
工作流程
When helping the user, follow this sequence:
- Contract — compile + witnesses (
leaderboard.compact,localSecretKey)getCustomName - Providers — (from
createConnectedSession)references/midnight-session.md - Deploy — low-level deploy, persist private state + contract address
- Read state — indexer GraphQL → →
ContractState.deserialize→ decode entriesledger() - Submit score — with optional
submitScore(score, useCustomName)beforehandsetCustomName - Verify ownership — for prize claims or "yours" badges
verifyOwnership(entryId) - UI — click game, privacy mode selector, leaderboard table with prove button
协助用户时,请遵循以下步骤:
- 合约——编译+ witnesses(
leaderboard.compact、localSecretKey)getCustomName - 提供者——(来自
createConnectedSession)references/midnight-session.md - 部署——底层部署,持久化私有状态 + 合约地址
- 读取状态——索引器GraphQL → →
ContractState.deserialize→ 解码条目ledger() - 提交分数——,可提前调用
submitScore(score, useCustomName)setCustomName - 验证所有权——用于领奖或标记“我的分数”徽章
verifyOwnership(entryId) - UI——点击游戏、隐私模式选择器、带有证明按钮的排行榜表格
1) Project Structure
1) 项目结构
leaderboard-dapp/
├── package.json
├── next.config.mjs
├── lib/
│ ├── isomorphic-ws-fix.mjs
│ ├── midnight.ts # session, patched provider, hex helpers
│ ├── leaderboard.ts # deploy, submitScore, verifyOwnership, decode
│ └── display-name.ts # anonymous name generator
├── app/
│ ├── layout.tsx
│ └── leaderboard/
│ └── LeaderboardClient.tsx # game + wallet + leaderboard UI
├── contract/
│ ├── package.json
│ └── src/
│ ├── leaderboard.compact
│ ├── witnesses.ts
│ ├── index.ts
│ └── managed/leaderboard/ # compiler output (gitignored)
├── scripts/
│ └── sync-zk-assets.mjs # → public/zk/leaderboard/
└── public/zk/leaderboard/ # keys + zkir (gitignored until sync)leaderboard-dapp/
├── package.json
├── next.config.mjs
├── lib/
│ ├── isomorphic-ws-fix.mjs
│ ├── midnight.ts # 会话、补丁提供者、十六进制工具
│ ├── leaderboard.ts # 部署、submitScore、verifyOwnership、解码
│ └── display-name.ts # 匿名名称生成器
├── app/
│ ├── layout.tsx
│ └── leaderboard/
│ └── LeaderboardClient.tsx # 游戏 + 钱包 + 排行榜UI
├── contract/
│ ├── package.json
│ └── src/
│ ├── leaderboard.compact
│ ├── witnesses.ts
│ ├── index.ts
│ └── managed/leaderboard/ # 编译器输出(Git忽略)
├── scripts/
│ └── sync-zk-assets.mjs # → public/zk/leaderboard/
└── public/zk/leaderboard/ # 密钥 + zkir(同步前Git忽略)2) Compact Contract
2) Compact合约
contract/src/leaderboard.compactcompact
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>): [] { /* ... */ }Privacy design:
- Anonymous mode ():
useCustomName = false— UI decodes to generated namedisplayName = persistentHash(sk) - Public / Custom mode ():
useCustomName = truewitness — app feeds address or user namedisplayName = getCustomName() - Ownership: stored on every entry;
ownerHash = ownerCommitment(sk)re-derives and comparesverifyOwnership
Compile:
bash
cd contract && npm run compact && cd ..
npm run sync:assetscontract/src/leaderboard.compactcompact
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>): [] { /* ... */ }隐私设计:
- 匿名模式():
useCustomName = false——UI解码为生成名称displayName = persistentHash(sk) - 公开/自定义模式():
useCustomName = truewitness——应用传入地址或用户名displayName = getCustomName() - 所有权:每个条目都存储;
ownerHash = ownerCommitment(sk)重新推导并进行比较verifyOwnership
编译命令:
bash
cd contract && npm run compact && cd ..
npm run sync:assets3) Witnesses
3) Witnesses
contract/src/witnesses.tstypescript
export 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> */ };Call in TypeScript before when using public or custom display modes.
setCustomName(name)submitScorecontract/src/witnesses.tstypescript
export 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) => { /* 编码为Bytes<32> */ };使用公开或自定义显示模式时,需在TypeScript中调用 之后再调用。
setCustomName(name)submitScore4) TypeScript Integration
4) TypeScript集成
lib/leaderboard.ts| Function | Purpose |
|---|---|
| Persist 32-byte secret in |
| |
| |
| |
| Indexer poll + |
ZK asset path: (synced to ).
/zk/leaderboardpublic/zk/leaderboard/lib/leaderboard.ts| 函数 | 用途 |
|---|---|
| 在 |
| |
| |
| |
| 索引器轮询 + |
ZK资产路径:(同步至)。
/zk/leaderboardpublic/zk/leaderboard/5) Browser UI
5) 浏览器UI
LeaderboardClient.tsx- Connect — →
detectWallet()→wallet.connect('preprod')createConnectedSession(api, ZK_PATH) - Deploy / Join — deploy new contract or paste existing 64-char hex address
- Play — 10-second click challenge
- Submit — pick Anonymous / Public / Custom, then
submitScore - Leaderboard — auto-refresh from indexer every 15s (no wallet required to read)
- Prove — marks entry as yours in UI
verifyOwnership(entryId)
Indexer-only reads mean spectators can watch the leaderboard without connecting a wallet.
LeaderboardClient.tsx- 连接——→
detectWallet()→wallet.connect('preprod')createConnectedSession(api, ZK_PATH) - 部署/加入——部署新合约或粘贴现有64位十六进制地址
- 游玩——10秒点击挑战
- 提交——选择匿名/公开/自定义模式,然后调用
submitScore - 排行榜——每15秒从索引器自动刷新(读取无需钱包)
- 证明——在UI中标记条目为“我的”
verifyOwnership(entryId)
仅通过索引器读取意味着观众无需连接钱包即可查看排行榜。
6) Prerequisites
6) 前置条件
| 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 |
bash
docker run -d -p 6300:6300 midnightntwrk/proof-server:8.0.3 -- \
midnight-proof-server --network preprodWallet: 1AM extension on Preprod, proof server , tNIGHT + tDUST from faucet.
http://localhost:6300| 组件 | 版本 |
|---|---|
| Compact编译器 | 0.31.0 |
| Compact运行时 | 0.16.0 |
| Ledger | 8.0.3 |
| midnight-js | 4.0.4 |
| 证明服务器 | 8.0.3 |
bash
docker run -d -p 6300:6300 midnightntwrk/proof-server:8.0.3 -- \
midnight-proof-server --network preprod钱包:**预生产环境(Preprod)**上的1AM扩展,证明服务器,从水龙头获取tNIGHT + tDUST。
http://localhost:63007) Compatibility Matrix
7) 兼容性矩阵
Pin versions from . Cross-check docs.midnight.network support matrix before upgrading.
references/versions.json从固定版本。升级前请交叉检查docs.midnight.network支持矩阵。
references/versions.json8) Production Notes
8) 生产环境注意事项
- Deploy frontend to Vercel/Netlify; set assets in build step (
public/zk/leaderboard)npm run sync:assets - Store default contract address in env or for returning players
localStorage - does not mutate ledger — safe for repeated "prove mine" UX
verifyOwnership - For mainnet, update network ID, indexer URLs, and proof server config via wallet
getConfiguration()
- 将前端部署到Vercel/Netlify;在构建步骤中设置资产(
public/zk/leaderboard)npm run sync:assets - 将默认合约地址存储在环境变量或中,方便回头玩家使用
localStorage - 不修改账本——可安全用于重复的“证明是我的”用户体验
verifyOwnership - 主网部署时,通过钱包更新网络ID、索引器URL和证明服务器配置
getConfiguration()
Quick Commands
快速命令
bash
cd templates/leaderboard-dapp
npm install
npm run compact
npm run sync:assets
npm run devOpen http://localhost:3000 → Connect 1AM → Deploy New → play → submit → refresh leaderboard.
bash
cd templates/leaderboard-dapp
npm install
npm run compact
npm run sync:assets
npm run dev打开http://localhost:3000 → 连接1AM钱包 → 部署新合约 → 游玩 → 提交分数 → 刷新排行榜。