example-leaderboard-dapp

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Midnight 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
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 scripts
  • app/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 decode
  • lib/display-name.ts
    — decode anonymous hash bytes to generated names ("Crimson Tiger")
  • public/zk/leaderboard/
    — ZK proving assets synced from contract build
Shared references (canonical provider + troubleshooting — do not duplicate in prompts):
  • references/midnight-session.md
    createConnectedSession
    , indexer patch, deploy/call helpers
  • references/gotchas.md
    — preprod deploy hangs, GraphQL
    offset: null
    , ZK asset paths
  • references/versions.json
    — pinned
    @midnight-ntwrk/*
    versions
Primary references:
  • example-locker-dapp/
    /
    templates/locker-dapp/
    — Next.js + 1AM pattern, low-level deploy/call
  • example-payment-dapp/
    — provider wiring, indexer polling
  • compact/
    disclose()
    , witnesses,
    persistentHash
    ,
    Map
    ,
    Counter
    ,
    assert
  • security/
    — what is public on-chain vs private in witness data
  • indexer/
    — read contract state without wallet connection
Key architecture notes:
  • scores
    is a public
    Map<Uint<64>, ScoreEntry>
    — readable from the indexer by anyone
  • localSecretKey
    witness returns a 32-byte secret from private state; never disclosed on-chain
  • ownerCommitment(sk)
    uses
    persistentHash
    with domain separator
    "leaderboard:owner:"
  • useCustomName
    boolean selects witness-fed name vs hash-based anonymous display name
  • verifyOwnership
    is a proof-only circuit — no ledger writes; use for "prove this is my score"
  • Persist secret key in
    localStorage
    so ownership proofs work after page refresh
  • Use
    createUnprovenDeployTx
    +
    submitTxAsync
    — not
    deployContract()
    (hangs on preprod)
  • Wrap
    indexerPublicDataProvider
    with patched
    queryContractState
    (GraphQL
    offset: null
    bug)
  • Leaderboard table can load from indexer without wallet — only submit/verify need connection

一个隐私保护型排行榜,玩家可通过三种显示模式在链上提交分数,并使用
verifyOwnership
——一个零知识(ZK)电路,在不泄露密钥的情况下检查
ownerCommitment(secretKey)
是否与存储的
ownerHash
匹配——来证明自己拥有某条目。
可运行模板: 复制
templates/leaderboard-dapp/
获取完整的Next.js项目(合约+UI)。安装1AM钱包后,运行
npm install && npm run compact && npm run sync:assets && npm run dev
该技能产出内容:
  • contract/
    —— Compact排行榜 + TypeScript witnesses + 编译脚本
  • app/leaderboard/
    —— Next.js客户端UI(点击游戏、隐私模式选择器、排行榜表格)
  • lib/midnight.ts
    —— 钱包会话 + 补丁索引器提供者(
    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证明资产
共享参考资料(标准提供者+故障排除——请勿在提示中重复):
  • references/midnight-session.md
    ——
    createConnectedSession
    、索引器补丁、部署/调用助手
  • references/gotchas.md
    —— 预生产部署卡顿、GraphQL
    offset: null
    问题、ZK资产路径
  • references/versions.json
    —— 固定版本的
    @midnight-ntwrk/*
    依赖
主要参考资料:
  • example-locker-dapp/
    /
    templates/locker-dapp/
    —— Next.js + 1AM模式、底层部署/调用
  • example-payment-dapp/
    —— 提供者连接、索引器轮询
  • compact/
    ——
    disclose()
    、witnesses、
    persistentHash
    Map
    Counter
    assert
  • security/
    —— 链上公开数据与witness中私有数据的区别
  • indexer/
    —— 无需连接钱包即可读取合约状态
核心架构说明:
  • scores
    是一个公开的
    Map<Uint<64>, ScoreEntry>
    ——任何人都可通过索引器读取
  • localSecretKey
    witness从私有状态返回32字节密钥;绝不会在链上披露
  • ownerCommitment(sk)
    使用带有域分隔符
    "leaderboard:owner:"
    persistentHash
  • useCustomName
    布尔值选择由witness提供的名称还是基于哈希的匿名显示名称
  • verifyOwnership
    是一个仅证明的电路——不写入账本;用于“证明这是我的分数”场景
  • 将密钥持久化存储在
    localStorage
    中,以便页面刷新后仍可进行所有权证明
  • 使用
    createUnprovenDeployTx
    +
    submitTxAsync
    ——而非
    deployContract()
    (在预生产环境会卡顿)
  • 用补丁后的
    queryContractState
    包装
    indexerPublicDataProvider
    (解决GraphQL
    offset: null
    bug)
  • 排行榜表格可从索引器加载无需钱包——仅提交/验证操作需要连接钱包

Workflow

工作流程

When helping the user, follow this sequence:
  1. Contract
    leaderboard.compact
    compile + witnesses (
    localSecretKey
    ,
    getCustomName
    )
  2. Providers
    createConnectedSession
    (from
    references/midnight-session.md
    )
  3. Deploy — low-level deploy, persist private state + contract address
  4. Read state — indexer GraphQL →
    ContractState.deserialize
    ledger()
    → decode entries
  5. Submit score
    submitScore(score, useCustomName)
    with optional
    setCustomName
    beforehand
  6. Verify ownership
    verifyOwnership(entryId)
    for prize claims or "yours" badges
  7. UI — click game, privacy mode selector, leaderboard table with prove button

协助用户时,请遵循以下步骤:
  1. 合约——编译
    leaderboard.compact
    + witnesses(
    localSecretKey
    getCustomName
  2. 提供者——
    createConnectedSession
    (来自
    references/midnight-session.md
  3. 部署——底层部署,持久化私有状态 + 合约地址
  4. 读取状态——索引器GraphQL →
    ContractState.deserialize
    ledger()
    → 解码条目
  5. 提交分数——
    submitScore(score, useCustomName)
    ,可提前调用
    setCustomName
  6. 验证所有权——
    verifyOwnership(entryId)
    用于领奖或标记“我的分数”徽章
  7. 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.compact
:
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>): [] { /* ... */ }
Privacy design:
  • Anonymous mode (
    useCustomName = false
    ):
    displayName = persistentHash(sk)
    — UI decodes to generated name
  • Public / Custom mode (
    useCustomName = true
    ):
    displayName = getCustomName()
    witness — app feeds address or user name
  • Ownership:
    ownerHash = ownerCommitment(sk)
    stored on every entry;
    verifyOwnership
    re-derives and compares
Compile:
bash
cd contract && npm run compact && cd ..
npm run sync:assets

contract/src/leaderboard.compact
:
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
    重新推导并进行比较
编译命令:
bash
cd contract && npm run compact && cd ..
npm run sync:assets

3) Witnesses

3) Witnesses

contract/src/witnesses.ts
:
typescript
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
setCustomName(name)
in TypeScript before
submitScore
when using public or custom display modes.

contract/src/witnesses.ts
:
typescript
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)
之后再调用
submitScore

4) TypeScript Integration

4) TypeScript集成

lib/leaderboard.ts
mirrors the locker-dapp pattern:
FunctionPurpose
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/
).

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) Browser UI

5) 浏览器UI

LeaderboardClient.tsx
flow:
  1. Connect
    detectWallet()
    wallet.connect('preprod')
    createConnectedSession(api, ZK_PATH)
  2. Deploy / Join — deploy new contract or paste existing 64-char hex address
  3. Play — 10-second click challenge
  4. Submit — pick Anonymous / Public / Custom, then
    submitScore
  5. Leaderboard — auto-refresh from indexer every 15s (no wallet required to read)
  6. Prove
    verifyOwnership(entryId)
    marks entry as yours in UI
Indexer-only reads mean spectators can watch the leaderboard without connecting a wallet.

LeaderboardClient.tsx
流程:
  1. 连接——
    detectWallet()
    wallet.connect('preprod')
    createConnectedSession(api, ZK_PATH)
  2. 部署/加入——部署新合约或粘贴现有64位十六进制地址
  3. 游玩——10秒点击挑战
  4. 提交——选择匿名/公开/自定义模式,然后调用
    submitScore
  5. 排行榜——每15秒从索引器自动刷新(读取无需钱包)
  6. 证明——
    verifyOwnership(entryId)
    在UI中标记条目为“我的”
仅通过索引器读取意味着观众无需连接钱包即可查看排行榜。

6) Prerequisites

6) 前置条件

ComponentVersion
Compact compiler0.31.0
Compact runtime0.16.0
Ledger8.0.3
midnight-js4.0.4
proof server8.0.3
bash
docker run -d -p 6300:6300 midnightntwrk/proof-server:8.0.3 -- \
  midnight-proof-server --network preprod
Wallet: 1AM extension on Preprod, proof server
http://localhost:6300
, tNIGHT + tDUST from faucet.

组件版本
Compact编译器0.31.0
Compact运行时0.16.0
Ledger8.0.3
midnight-js4.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扩展,证明服务器
http://localhost:6300
,从水龙头获取tNIGHT + tDUST。

7) Compatibility Matrix

7) 兼容性矩阵

Pin versions from
references/versions.json
. Cross-check docs.midnight.network support matrix before upgrading.

references/versions.json
固定版本。升级前请交叉检查docs.midnight.network支持矩阵

8) Production Notes

8) 生产环境注意事项

  • Deploy frontend to Vercel/Netlify; set
    public/zk/leaderboard
    assets in build step (
    npm run sync:assets
    )
  • Store default contract address in env or
    localStorage
    for returning players
  • verifyOwnership
    does not mutate ledger — safe for repeated "prove mine" UX
  • 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
    不修改账本——可安全用于重复的“证明是我的”用户体验
  • 主网部署时,通过钱包
    getConfiguration()
    更新网络ID、索引器URL和证明服务器配置

Quick Commands

快速命令

bash
cd templates/leaderboard-dapp
npm install
npm run compact
npm run sync:assets
npm run dev
Open 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钱包 → 部署新合约 → 游玩 → 提交分数 → 刷新排行榜。