关于 example-leaderboard-dapp
面向 Midnight Network 开发者的隐私排行榜 DApp 技能,生成包含 Compact 合约和 Next.js 界面的项目。它支持匿名或自定义展示分数,并通过零知识证明验证条目归属。
主要功能
- 01生成排行榜合约与 Next.js 界面
- 02支持匿名、公开和自定义名称
- 03提交分数并读取链上状态
- 04通过零知识证明验证归属
- 05无需连接钱包即可查看排行榜
适用场景
- 01构建隐私保护的游戏排行榜
- 02提交分数并选择展示身份
- 03验证参赛者对条目的所有权
- 04允许访客查看排行榜
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ kali-decoder/ midnight-skills --skill example-leaderboard-dapp安装后在 Claude Code 中输入/example-leaderboard-dapp确认已加载。
详细介绍
Midnight Network 排行榜 DApp
一个保护隐私的排行榜,玩家可在链上提交分数,并选择三种显示模式;玩家还可使用 verifyOwnership 证明自己拥有某个条目——这是一个 ZK 电路,可检查 ownerCommitment(secretKey) 是否与存储的 ownerHash 匹配,而不会泄露秘密。
可运行模板: 复制 templates/leaderboard-dapp/,即可获得完整的 Next.js 项目(合约 + UI)。安装 1AM wallet 后,运行 npm install && npm run compact && npm run sync:assets && npm run dev。
此技能会生成:
contract/— Compact 排行榜 + TypeScript witnesses + 编译脚本app/leaderboard/— Next.js 客户端 UI(点击游戏、隐私模式选择器、排行榜表格)lib/midnight.ts— 钱包会话 + 已修补的索引器 provider(复制自references/midnight-session.md或templates/leaderboard-dapp/lib/midnight.ts)lib/leaderboard.ts— 部署、submitScore、verifyOwnership、账本解码lib/display-name.ts— 将匿名哈希字节解码为生成的名称(“Crimson Tiger”)public/zk/leaderboard/— 从合约构建同步的 ZK 证明资源
共享参考资料(规范 provider + 故障排查——请勿在提示词中重复):
references/midnight-session.md—createConnectedSession、索引器修补、部署/调用辅助函数references/gotchas.md— preprod 部署卡住、GraphQLoffset: null、ZK 资源路径references/versions.json— 固定的@midnight-ntwrk/*版本
主要参考资料:
example-locker-dapp//templates/locker-dapp/— Next.js + 1AM 模式、底层部署/调用example-payment-dapp/— provider 配置、索引器轮询compact/—disclose()、witnesses、persistentHash、Map、Counter、assertsecurity/— 链上公开数据与 witness 数据中的私密数据indexer/— 无需连接钱包即可读取合约状态
关键架构说明:
scores是公开的Map<Uint<64>, ScoreEntry>— 任何人都能通过索引器读取localSecretKeywitness 会从私有状态返回一个 32 字节的秘密;绝不会在链上公开ownerCommitment(sk)使用带有域分隔符"leaderboard:owner:"的persistentHash- 布尔值
useCustomName用于选择由 witness 提供的名称,或基于哈希的匿名显示名称 verifyOwnership是仅用于证明的电路——不会写入账本;可用于“证明这是我的分数”- 将秘密密钥保存在
localStorage中,以便在页面刷新后仍可进行所有权证明 - 使用
createUnprovenDeployTx+submitTxAsync,不要使用deployContract()(在 preprod 上会卡住) - 使用已修补的
queryContractState封装indexerPublicDataProvider(这是 GraphQLoffset: null的 bug) - 排行榜表格无需连接钱包即可从索引器加载——只有提交/验证需要连接
工作流
帮助用户时,请按以下顺序操作:
- 合约 — 编译
leaderboard.compact+ witnesses(localSecretKey、getCustomName) - Providers —
createConnectedSession(来自references/midnight-session.md) - 部署 — 底层部署,持久化私有状态 + 合约地址
- 读取状态 — 索引器 GraphQL →
ContractState.deserialize→ledger()→ 解码条目 - 提交分数 —
submitScore(score, useCustomName),可选择事先调用setCustomName - 验证所有权 — 使用
verifyOwnership(entryId)领取奖品或显示“你的”徽章 - UI — 点击游戏、隐私模式选择器、带证明按钮的排行榜表格
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)
2) Compact 合约
contract/src/leaderboard.compact:
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):displayName = persistentHash(sk)— UI 会将其解码为生成的名称 - 公开 / 自定义模式(
useCustomName = true):displayName = getCustomName()witness — 应用会传入地址或用户名 - 所有权:每个条目都会存储
ownerHash = ownerCommitment(sk);verifyOwnership会重新推导并进行比对
编译:
cd contract && npm run compact && cd ..
npm run sync:assets
3) 见证者
contract/src/witnesses.ts:
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> */ };
使用公开或自定义显示模式时,请在调用 submitScore 之前于 TypeScript 中调用 setCustomName(name)。
4) TypeScript 集成
lib/leaderboard.ts 遵循 locker-dapp 模式:
| 函数 | 用途 |
|---|---|
getOrCreateSecretKey() | 在 localStorage 中持久化 32 字节密钥 |
deployLeaderboard(session) | createUnprovenDeployTx + submitTxAsync |
submitScore(session, addr, score, customName?) | submitCallTxAsync → submitScore 电路 |
verifyOwnership(session, addr, entryId) | submitCallTxAsync → verifyOwnership 电路 |
fetchLeaderboardState(queryUrl, addr) | 轮询索引器 + decodeLeaderboardState |
ZK 资源路径:/zk/leaderboard(同步至 public/zk/leaderboard/)。
5) 浏览器 UI
LeaderboardClient.tsx 流程:
- 连接 —
detectWallet()→wallet.connect('preprod')→createConnectedSession(api, ZK_PATH) - 部署 / 加入 — 部署新合约或粘贴现有的 64 字符十六进制地址
- 游戏 — 10 秒点击挑战
- 提交 — 选择匿名 / 公开 / 自定义,然后调用
submitScore - 排行榜 — 每 15 秒从索引器自动刷新(读取无需钱包)
- 证明 —
verifyOwnership(entryId)在 UI 中将条目标记为你的条目
仅从索引器读取意味着观众无需连接钱包即可查看排行榜。
6) 前置条件
| 组件 | 版本 |
|---|---|
| Compact 编译器 | 0.31.0 |
| Compact 运行时 | 0.16.0 |
| Ledger | 8.0.3 |
| midnight-js | 4.0.4 |
| 证明服务 | 8.0.3 |
docker run -d -p 6300:6300 midnightntwrk/proof-server:8.0.3 -- \
midnight-proof-server --network preprod
钱包:在 Preprod 上使用 1AM 扩展程序,证明服务为 http://localhost:6300,并从水龙头获取 tNIGHT + tDUST。
7) 兼容性矩阵
从 references/versions.json 固定版本。升级前请对照 docs.midnight.network 支持矩阵进行检查。
8) 生产环境说明
- 将前端部署到 Vercel/Netlify;在构建步骤中设置
public/zk/leaderboard资源(npm run sync:assets) - 在环境变量或
localStorage中存储默认合约地址,方便回访玩家使用 verifyOwnership不会修改账本 — 可安全用于重复的“证明这是我的”交互- 对于主网,请通过钱包的
getConfiguration()更新网络 ID、索引器 URL 和证明服务配置
快速命令
cd templates/leaderboard-dapp
npm install
npm run compact
npm run sync:assets
npm run dev
打开 http://localhost:3000 → 连接 1AM → 部署新合约 → 开始游戏 → 提交 → 刷新排行榜。