example-private-party-dapp
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMidnight Network Private Party DApp
Midnight Network 私有派对DApp
A private party contract lets an organizer collect RSVPs while attendee identities stay private until guests check in and pay the entry fee in unshielded NIGHT. That payment is the privacy boundary — unshielded token flows are always public on Midnight.
Runnable template: Copy for a complete Next.js project (contract + UI). Run after installing the 1AM wallet.
templates/private-party-dapp/npm install && npm run compact && npm run sync:assets && npm run devOfficial reference: — Compact tutorial + vitest harness ( on Docker devnet).
github.com/midnightntwrk/example-private-partyyarn test:localTwo ways to use this skill:
- Scaffold mode (default) — the user wants a working dApp. Use every code block below verbatim; they are the tested reference implementation. Don't rederive them.
- Tutorial mode — the user explicitly wants to learn Compact by writing it themselves. In that case walk through circuit-by-circuit (§5) and let them type it, but still scaffold the surrounding TS/UI from the code blocks — those aren't the teaching surface.
private-party.compact
What this skill produces:
- —
contract/(no witnesses) + compile scriptsprivate-party.compact - — Next.js client UI (organizer deploy/start/close/claim + attendee RSVP/check-in)
app/party/ - — wallet session + patched indexer provider (full implementation in §9)
lib/midnight.ts - — deploy,
lib/party.ts,rsvp,startParty,checkIn,closeEntry, ledger decodeclaimFees - — Bech32 unshielded address →
lib/address.tsfor{ bytes: Uint8Array }circuit argsUserAddress - — generate/store 32-byte DApp secrets in
lib/secret.tslocalStorage - — ZK proving assets synced from contract build
public/zk/private-party/
Shared references (canonical provider + troubleshooting — do not duplicate in prompts):
- — 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, low-level deploy/calltemplates/locker-dapp/ - — unshielded
example-payment-dapp//receiveUnshieldedpatternssendUnshielded - — vitest + Docker devnet test harness (test script provided in official repo)
example-hello-world/ - —
compact/,disclose(),persistentCommit,persistentHash, sealed ledger, enumsSet - — privacy boundary checklist, what becomes public and when
security/ - — unshielded NIGHT units (Stars), Bech32 address decoding
token-transfers/
Key architecture notes:
- No witnesses — caller auth uses circuit-private →
_secretcompared to on-chaingetDappPublicKey(_secret)organizer - RSVP privacy — guest + secret committed via
UserAddress; only the hash is stored inpersistentCommithashedPartyGoers - Privacy boundary — calls
checkInthenreceiveUnshielded(nativeToken(), entryFee)— guest address becomes publiccheckedInParty.insert(disclose(address)) - Organizer becomes public — calls
claimFeesto organizer'ssendUnshielded(...)UserAddress - is a developer assertion — it marks values safe for public domains; it does not perform the disclosure itself
disclose() - output is safe on ledger without
persistentCommit— sufficiently random salt (disclose()) required_secret - Use +
createUnprovenDeployTx— notsubmitTxAsync(hangs on preprod)deployContract() - Wrap with patched
indexerPublicDataProvider(GraphQLqueryContractStatebug)offset: null - Entry fee is on ledger but cast to
Uint<16>for unshielded ops; 1 NIGHT = 1_000_000 StarsUint<128> - Persist organizer/attendee in
_secret— losing it means losing auth for that rolelocalStorage - Network: this template targets everywhere (wallet connect, indexer, proof server). If your deployment target is
preprod, changepreviewinDEFAULT_NETWORK(§9) — that's the single source of truth; nothing else should hardcode a network string.lib/midnight.ts
私有派对合约允许组织者收集RSVP信息,同时参会者身份保持私密,直到宾客签到并使用未屏蔽的NIGHT代币支付入场费。该支付行为即为隐私边界——未屏蔽的代币流转在Midnight上始终是公开的。
可运行模板: 复制获取完整的Next.js项目(合约+UI)。安装1AM钱包后,执行。
templates/private-party-dapp/npm install && npm run compact && npm run sync:assets && npm run dev官方参考: —— Compact教程+vitest测试套件(在Docker测试网执行)。
github.com/midnightntwrk/example-private-partyyarn test:local两种使用方式:
- 脚手架模式(默认)——用户需要可运行的dApp。直接使用下方所有代码块,它们是经过测试的参考实现,无需重新推导。
- 教程模式——用户明确希望通过编写代码学习Compact。这种情况下,需逐步讲解的每个电路(第5节),让用户自行编写,但仍需从代码块中搭建周边的TS/UI代码——这些内容不属于教学重点。
private-party.compact
本教程产出内容:
- ——
contract/(无见证者)+编译脚本private-party.compact - —— Next.js客户端UI(组织者部署/启动/关闭/提现 + 参会者RSVP/签到)
app/party/ - —— 钱包会话+修补后的索引器提供者(完整实现见第9节)
lib/midnight.ts - —— 部署、
lib/party.ts、rsvp、startParty、checkIn、closeEntry、账本解码claimFees - —— 将Bech32未屏蔽地址转换为
lib/address.ts格式,用于{ bytes: Uint8Array }电路参数UserAddress - —— 在
lib/secret.ts中生成/存储32字节的DApp密钥localStorage - —— 从合约构建同步而来的ZK证明资产
public/zk/private-party/
共享参考资料(标准提供者+故障排查——请勿在提示中重复):
- —— 预生产环境部署卡顿、GraphQL
references/gotchas.md问题、ZK资产路径offset: null - —— 固定版本的
references/versions.json依赖@midnight-ntwrk/*
主要参考资料:
- /
example-locker-dapp/—— Next.js + 1AM,底层部署/调用templates/locker-dapp/ - —— 未屏蔽的
example-payment-dapp//receiveUnshielded模式sendUnshielded - —— vitest + Docker测试网测试套件(官方仓库提供测试脚本)
example-hello-world/ - ——
compact/、disclose()、persistentCommit、persistentHash、密封账本、枚举Set - —— 隐私边界检查清单、何时及何种数据会公开
security/ - —— 未屏蔽NIGHT单位(Stars)、Bech32地址解码
token-transfers/
核心架构说明:
- 无见证者——调用者认证使用电路私有,通过
_secret与链上getDappPublicKey(_secret)对比organizer - RSVP隐私性——宾客+密钥通过
UserAddress生成承诺;仅哈希值存储在persistentCommit中hashedPartyGoers - 隐私边界——调用
checkIn,随后receiveUnshielded(nativeToken(), entryFee)——宾客地址变为公开checkedInParty.insert(disclose(address)) - 组织者身份公开——调用
claimFees将资金转入组织者的sendUnshielded(...)UserAddress - 是开发者断言——标记值可安全进入公开域;本身不执行披露操作
disclose() - 输出无需
persistentCommit即可安全存储在账本上——需要足够随机的盐值(disclose())_secret - 使用+
createUnprovenDeployTx——而非submitTxAsync(在预生产环境会卡顿)deployContract() - 用修补后的包装
queryContractState(解决GraphQLindexerPublicDataProviderbug)offset: null - 入场费在账本上是类型,但在未屏蔽操作中需转换为
Uint<16>;1 NIGHT = 1_000_000 StarsUint<128> - 将组织者/参会者的持久化在
_secret中——丢失密钥意味着失去对应角色的权限localStorage - 网络:本模板默认目标为环境(钱包连接、索引器、证明服务器)。若部署目标为
preprod,修改preview(第9节)中的lib/midnight.ts——这是唯一的数据源;其他地方不应硬编码网络字符串。DEFAULT_NETWORK
Workflow
工作流程
When helping the user, follow this sequence:
- Contract — write/scaffold (§5); compile with
private-party.compactyarn compile - Understand privacy boundary — private RSVP → public check-in (unshielded) → public payout
- Providers — (§9,
createConnectedSession)lib/midnight.ts - Deploy — organizer passes to constructor
(partySize, entryFee, organizerSecret) - RSVP — attendees call before party starts
rsvp(userAddress, secret) - Start — organizer calls when ready (or auto when list full →
startParty(secret))READY - Check in — RSVP'd guests call + pay entry fee (crosses boundary)
checkIn(address, secret) - Close — organizer if not everyone checked in; or auto when full
closeEntry(secret) - Claim — organizer after doors closed
claimFees(organizerAddress, secret) - UI — role picker, party status panel, indexer polling for public state
协助用户时,请遵循以下步骤:
- 合约——编写/搭建(第5节);执行
private-party.compact编译yarn compile - 理解隐私边界——私密RSVP → 公开签到(未屏蔽)→ 公开提现
- 提供者——(第9节,
createConnectedSession)lib/midnight.ts - 部署——组织者将传入构造函数
(partySize, entryFee, organizerSecret) - RSVP——参会者在派对开始前调用
rsvp(userAddress, secret) - 启动——组织者准备就绪后调用(或宾客名单满员时自动切换为
startParty(secret)状态)READY - 签到——已RSVP的宾客调用+支付入场费(跨越隐私边界)
checkIn(address, secret) - 关闭——若未全员签到,组织者调用;或满员时自动关闭
closeEntry(secret) - 提现——组织者在入场关闭后调用
claimFees(organizerAddress, secret) - UI——角色选择器、派对状态面板、索引器轮询获取公开状态
1) Project Structure
1) 项目结构
private-party-dapp/
├── package.json
├── next.config.mjs
├── postcss.config.mjs
├── lib/
│ ├── isomorphic-ws-fix.mjs
│ ├── midnight.ts # session, patched provider, hex helpers
│ ├── party.ts # deploy, circuits, decode state
│ ├── address.ts # Bech32 → UserAddress bytes
│ └── secret.ts # crypto.getRandomValues + localStorage
├── app/
│ ├── layout.tsx
│ ├── globals.css
│ └── party/
│ └── PartyClient.tsx # organizer + attendee UI
├── contract/
│ ├── package.json
│ └── src/
│ ├── private-party.compact
│ ├── index.ts # CompiledContract.withVacantWitnesses
│ └── managed/private-party/ # compiler output (gitignored)
├── scripts/
│ └── sync-zk-assets.mjs # → public/zk/private-party/
└── public/zk/private-party/ # keys + zkir (gitignored until sync)Optional test harness (official repo — not in browser template):
example-private-party/
├── contract/private-party.compact
├── src/test/party.test.ts # vitest: Alice organizer, Bob/Claire guests
├── compose.yml # node + indexer + proof-server
└── package.json # yarn test:localprivate-party-dapp/
├── package.json
├── next.config.mjs
├── postcss.config.mjs
├── lib/
│ ├── isomorphic-ws-fix.mjs
│ ├── midnight.ts # session, patched provider, hex helpers
│ ├── party.ts # deploy, circuits, decode state
│ ├── address.ts # Bech32 → UserAddress bytes
│ └── secret.ts # crypto.getRandomValues + localStorage
├── app/
│ ├── layout.tsx
│ ├── globals.css
│ └── party/
│ └── PartyClient.tsx # organizer + attendee UI
├── contract/
│ ├── package.json
│ └── src/
│ ├── private-party.compact
│ ├── index.ts # CompiledContract.withVacantWitnesses
│ └── managed/private-party/ # compiler output (gitignored)
├── scripts/
│ └── sync-zk-assets.mjs # → public/zk/private-party/
└── public/zk/private-party/ # keys + zkir (gitignored until sync)可选测试套件(官方仓库——不在浏览器模板中):
example-private-party/
├── contract/private-party.compact
├── src/test/party.test.ts # vitest: Alice organizer, Bob/Claire guests
├── compose.yml # node + indexer + proof-server
└── package.json # yarn test:local2) Prerequisites
2) 前置要求
bash
node --version # 22+ for vitest harness; 20+ for Next.js frontend
docker --version # optional local devnet tests
curl --proto '=https' --tlsv1.2 -sSf \
https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh
source $HOME/.local/bin/envBrowser: 1AM wallet, network set to match in (preprod by default), funded with tNIGHT for entry fees.
DEFAULT_NETWORKlib/midnight.tsbash
node --version # 22+ for vitest harness; 20+ for Next.js frontend
docker --version # optional local devnet tests
curl --proto '=https' --tlsv1.2 -sSf \
https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh
source $HOME/.local/bin/env浏览器:1AM钱包,网络设置需与中的匹配(默认preprod),并充值tNIGHT用于支付入场费。
lib/midnight.tsDEFAULT_NETWORK3) Root package.json
package.json3) 根目录package.json
package.jsonjson
{
"name": "private-party-dapp",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "next dev --webpack",
"build": "npm run sync:assets && next build --webpack",
"compact": "npm run compact --prefix contract",
"sync:assets": "node scripts/sync-zk-assets.mjs",
"postinstall": "npm install --prefix contract"
},
"dependencies": {
"@midnight-ntwrk/compact-runtime": "0.16.0",
"@midnight-ntwrk/compact-js": "4.0.4",
"@midnight-ntwrk/ledger-v8": "8.0.3",
"@midnight-ntwrk/midnight-js-contracts": "4.0.4",
"@midnight-ntwrk/midnight-js-fetch-zk-config-provider": "4.0.4",
"@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.0.4",
"@midnight-ntwrk/midnight-js-network-id": "4.0.4",
"@midnight-ntwrk/midnight-js-types": "4.0.4",
"@midnight-ntwrk/wallet-sdk-address-format": "3.1.0",
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.7.0",
"stream-browserify": "^3.0.0",
"@tailwindcss/postcss": "^4.0.0",
"tailwindcss": "^4.0.0"
}
}json
{
"name": "private-party-dapp",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "next dev --webpack",
"build": "npm run sync:assets && next build --webpack",
"compact": "npm run compact --prefix contract",
"sync:assets": "node scripts/sync-zk-assets.mjs",
"postinstall": "npm install --prefix contract"
},
"dependencies": {
"@midnight-ntwrk/compact-runtime": "0.16.0",
"@midnight-ntwrk/compact-js": "4.0.4",
"@midnight-ntwrk/ledger-v8": "8.0.3",
"@midnight-ntwrk/midnight-js-contracts": "4.0.4",
"@midnight-ntwrk/midnight-js-fetch-zk-config-provider": "4.0.4",
"@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.0.4",
"@midnight-ntwrk/midnight-js-network-id": "4.0.4",
"@midnight-ntwrk/midnight-js-types": "4.0.4",
"@midnight-ntwrk/wallet-sdk-address-format": "3.1.0",
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.7.0",
"stream-browserify": "^3.0.0",
"@tailwindcss/postcss": "^4.0.0",
"tailwindcss": "^4.0.0"
}
}4) contract/package.json
contract/package.json4) contract/package.json
contract/package.jsonjson
{
"name": "@private-party/contract",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"compact": "compact compile src/private-party.compact src/managed/private-party"
},
"dependencies": {
"@midnight-ntwrk/compact-runtime": "0.16.0"
}
}Do not run inside — a second copy of there causes the dual-WASM-instance bug ( identity error). All deps live at root; exists only for the compile script.
npm installcontract/@midnight-ntwrk/compact-runtimeContractMaintenanceAuthoritycontract/package.jsoncompactCompile:
bash
npm run compactjson
{
"name": "@private-party/contract",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"compact": "compact compile src/private-party.compact src/managed/private-party"
},
"dependencies": {
"@midnight-ntwrk/compact-runtime": "0.16.0"
}
}请勿在目录下执行——该目录下的副本会导致双WASM实例bug(身份错误)。所有依赖均在根目录;仅用于编译脚本。
contract/npm install@midnight-ntwrk/compact-runtimeContractMaintenanceAuthoritycontract/package.jsoncompact编译:
bash
npm run compact→ contract/src/managed/private-party/{contract,keys,zkir}/
→ contract/src/managed/private-party/{contract,keys,zkir}/
npm run sync:assets
npm run sync:assets
→ public/zk/private-party/
→ public/zk/private-party/
Expected circuits: `rsvp`, `startParty`, `checkIn`, `closeEntry`, `claimFees`.
---
预期生成的电路:`rsvp`, `startParty`, `checkIn`, `closeEntry`, `claimFees`。
---5) contract/src/private-party.compact
contract/src/private-party.compact5) contract/src/private-party.compact
contract/src/private-party.compactIn scaffold mode, use this exactly. In tutorial mode, build it up circuit by circuit in this order: ledger declarations → constructor → → → → → → helper circuits.
rsvpstartPartycheckIncloseEntryclaimFeescompact
pragma language_version 0.23;
import CompactStandardLibrary;
export enum PartyState {
NOT_STARTED,
READY,
STARTED,
DOORS_CLOSED,
FEES_CLAIMED
}
export sealed ledger organizer: Bytes<32>;
export sealed ledger maxListSize: Uint<16>;
export sealed ledger entryFee: Uint<16>;
export ledger partyState: PartyState;
export ledger hashedPartyGoers: Set<Bytes<32>>;
export ledger checkedInParty: Set<UserAddress>;
constructor (partySize: Uint<16>, fee: Uint<16>, _secret: Bytes<32>) {
assert(partySize > 0, "The party size must be greater than zero");
assert(fee > 0, "Fee must be greater than zero");
const pubKey = getDappPublicKey(_secret);
organizer = disclose(pubKey);
entryFee = disclose(fee);
maxListSize = disclose(partySize);
partyState = PartyState.NOT_STARTED;
}
export circuit rsvp(_address: UserAddress, _secret: Bytes<32>): [] {
const pubKey = getDappPublicKey(_secret);
assert(pubKey != organizer, "Organizer cannot RSVP to the party");
assert(partyState == PartyState.NOT_STARTED, "The party has already started");
assert(hashedPartyGoers.size() < maxListSize, "The list is full");
const commitHash = commitAddress(_secret, _address.bytes);
assert(!hashedPartyGoers.member(commitHash), "You are already on the list");
hashedPartyGoers.insert(commitHash);
if (hashedPartyGoers.size() == maxListSize) {
partyState = PartyState.READY;
}
}
export circuit startParty(_secret: Bytes<32>): [] {
const pubKey = getDappPublicKey(_secret);
assert(organizer == pubKey, "Only the organizer can start the party");
assert(partyState == PartyState.READY || partyState == PartyState.NOT_STARTED,
"The party is not in the correct state for this operation");
partyState = PartyState.STARTED;
}
export circuit checkIn(address: UserAddress, _secret: Bytes<32>): [] {
assert(partyState == PartyState.STARTED, "The party has not been started. Call the party police");
assert(checkedInParty.size() < hashedPartyGoers.size(), "All guests have already checked in");
const commitHash = commitAddress(_secret, address.bytes);
assert(hashedPartyGoers.member(commitHash), "You are not on the list");
assert(!checkedInParty.member(disclose(address)), "You have already checked in");
// Privacy boundary: unshielded payment makes guest address public
receiveUnshielded(nativeToken(), entryFee as Uint<128>);
checkedInParty.insert(disclose(address));
if (checkedInParty.size() == maxListSize) {
partyState = PartyState.DOORS_CLOSED;
}
}
export circuit closeEntry(_secret: Bytes<32>): [] {
const pubKey = getDappPublicKey(_secret);
assert(organizer == pubKey, "Only organizer can close the doors");
assert(partyState == PartyState.STARTED, "Party in wrong state");
partyState = PartyState.DOORS_CLOSED;
}
export circuit claimFees(address: UserAddress, _secret: Bytes<32>): [] {
const pubKey = getDappPublicKey(_secret);
assert(organizer == pubKey, "You are not the organizer");
assert(partyState == PartyState.DOORS_CLOSED, "The doors are not yet closed");
assert(checkedInParty.size() > 0, "No fees to claim");
const totalCollected = checkedInParty.size() * entryFee;
assert(unshieldedBalanceGte(nativeToken(), totalCollected), "Contract balance wrong");
sendUnshielded(
nativeToken(),
disclose(totalCollected) as Uint<128>,
right<ContractAddress, UserAddress>(disclose(address))
);
partyState = PartyState.FEES_CLAIMED;
}
circuit commitAddress(_address: Bytes<32>, _secret: Bytes<32>): Bytes<32> {
return persistentCommit<Bytes<32>>(_address, _secret);
}
circuit getDappPublicKey(_secret: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([pad(32, "private-party:pk:"), _secret]);
}脚手架模式下,请直接使用此代码。教程模式下,按以下顺序逐步构建电路:账本声明→构造函数→→→→→→辅助电路。
rsvpstartPartycheckIncloseEntryclaimFeescompact
pragma language_version 0.23;
import CompactStandardLibrary;
export enum PartyState {
NOT_STARTED,
READY,
STARTED,
DOORS_CLOSED,
FEES_CLAIMED
}
export sealed ledger organizer: Bytes<32>;
export sealed ledger maxListSize: Uint<16>;
export sealed ledger entryFee: Uint<16>;
export ledger partyState: PartyState;
export ledger hashedPartyGoers: Set<Bytes<32>>;
export ledger checkedInParty: Set<UserAddress>;
constructor (partySize: Uint<16>, fee: Uint<16>, _secret: Bytes<32>) {
assert(partySize > 0, "The party size must be greater than zero");
assert(fee > 0, "Fee must be greater than zero");
const pubKey = getDappPublicKey(_secret);
organizer = disclose(pubKey);
entryFee = disclose(fee);
maxListSize = disclose(partySize);
partyState = PartyState.NOT_STARTED;
}
export circuit rsvp(_address: UserAddress, _secret: Bytes<32>): [] {
const pubKey = getDappPublicKey(_secret);
assert(pubKey != organizer, "Organizer cannot RSVP to the party");
assert(partyState == PartyState.NOT_STARTED, "The party has already started");
assert(hashedPartyGoers.size() < maxListSize, "The list is full");
const commitHash = commitAddress(_secret, _address.bytes);
assert(!hashedPartyGoers.member(commitHash), "You are already on the list");
hashedPartyGoers.insert(commitHash);
if (hashedPartyGoers.size() == maxListSize) {
partyState = PartyState.READY;
}
}
export circuit startParty(_secret: Bytes<32>): [] {
const pubKey = getDappPublicKey(_secret);
assert(organizer == pubKey, "Only the organizer can start the party");
assert(partyState == PartyState.READY || partyState == PartyState.NOT_STARTED,
"The party is not in the correct state for this operation");
partyState = PartyState.STARTED;
}
export circuit checkIn(address: UserAddress, _secret: Bytes<32>): [] {
assert(partyState == PartyState.STARTED, "The party has not been started. Call the party police");
assert(checkedInParty.size() < hashedPartyGoers.size(), "All guests have already checked in");
const commitHash = commitAddress(_secret, address.bytes);
assert(hashedPartyGoers.member(commitHash), "You are not on the list");
assert(!checkedInParty.member(disclose(address)), "You have already checked in");
// Privacy boundary: unshielded payment makes guest address public
receiveUnshielded(nativeToken(), entryFee as Uint<128>);
checkedInParty.insert(disclose(address));
if (checkedInParty.size() == maxListSize) {
partyState = PartyState.DOORS_CLOSED;
}
}
export circuit closeEntry(_secret: Bytes<32>): [] {
const pubKey = getDappPublicKey(_secret);
assert(organizer == pubKey, "Only organizer can close the doors");
assert(partyState == PartyState.STARTED, "Party in wrong state");
partyState = PartyState.DOORS_CLOSED;
}
export circuit claimFees(address: UserAddress, _secret: Bytes<32>): [] {
const pubKey = getDappPublicKey(_secret);
assert(organizer == pubKey, "You are not the organizer");
assert(partyState == PartyState.DOORS_CLOSED, "The doors are not yet closed");
assert(checkedInParty.size() > 0, "No fees to claim");
const totalCollected = checkedInParty.size() * entryFee;
assert(unshieldedBalanceGte(nativeToken(), totalCollected), "Contract balance wrong");
sendUnshielded(
nativeToken(),
disclose(totalCollected) as Uint<128>,
right<ContractAddress, UserAddress>(disclose(address))
);
partyState = PartyState.FEES_CLAIMED;
}
circuit commitAddress(_address: Bytes<32>, _secret: Bytes<32>): Bytes<32> {
return persistentCommit<Bytes<32>>(_address, _secret);
}
circuit getDappPublicKey(_secret: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([pad(32, "private-party:pk:"), _secret]);
}Privacy model summary
隐私模型总结
| Phase | Attendee identity | On-chain data |
|---|---|---|
| RSVP | Private | Commitment hash in |
| Before check-in | Private | Hash count visible; no addresses |
| Check-in | Public | |
| Claim fees | Organizer public | |
| 阶段 | 参会者身份 | 链上数据 |
|---|---|---|
| RSVP | 私密 | 仅 |
| 签到前 | 私密 | 仅可见哈希数量;无地址信息 |
| 签到 | 公开 | |
| 提现 | 组织者公开 | |
Always-public Compact domains
始终公开的Compact域
- Ledger fields (after or safe commits)
disclose() - Circuit return values from exported circuits
- Contract-to-contract calls
- Unshielded token transfers (,
receiveUnshielded)sendUnshielded
- 账本字段(经过或安全承诺后)
disclose() - 导出电路的返回值
- 合约间调用
- 未屏蔽代币转账(,
receiveUnshielded)sendUnshielded
6) contract/src/index.ts
contract/src/index.ts6) contract/src/index.ts
contract/src/index.tsNo witnesses — use . Use lazy pattern to avoid SSR issues and ensure is resolved from the correct module.
withVacantWitnessesawait import()CompiledContracttypescript
import { CompiledContract } from '@midnight-ntwrk/compact-js';
import { sampleSigningKey, ContractState } from '@midnight-ntwrk/compact-runtime';
let _contractModule: any = null;
let _compiledContract: any = null;
let _ledgerFn: any = null;
export async function getCompiledContract(): Promise<any> {
if (!_compiledContract) {
if (!_contractModule) {
_contractModule = await import('./managed/private-party/contract/index.js');
}
_compiledContract = CompiledContract.make(
'private-party',
_contractModule.Contract,
);
_compiledContract = CompiledContract.withVacantWitnesses(_compiledContract);
}
return _compiledContract;
}
export async function getLedger(): Promise<any> {
if (!_ledgerFn) {
if (!_contractModule) {
_contractModule = await import('./managed/private-party/contract/index.js');
}
_ledgerFn = _contractModule.ledger;
}
return _ledgerFn;
}
export { sampleSigningKey, ContractState };Key points:
- imported from
CompiledContract(not@midnight-ntwrk/compact-js) — the two packages both export something with this name and only one matches the runtime'scompact-runtime.withVacantWitnesses - Lazy singleton (,
getCompiledContract) — avoids the dual-instance WASM identity bug.getLedger - and
ContractStatere-exported fromsampleSigningKeyso callers never import that package directly (keeps the single-copy guarantee from §4).compact-runtime
无见证者——使用。使用延迟模式,避免SSR问题,并确保从正确模块解析。
withVacantWitnessesawait import()CompiledContracttypescript
import { CompiledContract } from '@midnight-ntwrk/compact-js';
import { sampleSigningKey, ContractState } from '@midnight-ntwrk/compact-runtime';
let _contractModule: any = null;
let _compiledContract: any = null;
let _ledgerFn: any = null;
export async function getCompiledContract(): Promise<any> {
if (!_compiledContract) {
if (!_contractModule) {
_contractModule = await import('./managed/private-party/contract/index.js');
}
_compiledContract = CompiledContract.make(
'private-party',
_contractModule.Contract,
);
_compiledContract = CompiledContract.withVacantWitnesses(_compiledContract);
}
return _compiledContract;
}
export async function getLedger(): Promise<any> {
if (!_ledgerFn) {
if (!_contractModule) {
_contractModule = await import('./managed/private-party/contract/index.js');
}
_ledgerFn = _contractModule.ledger;
}
return _ledgerFn;
}
export { sampleSigningKey, ContractState };核心要点:
- 从
CompiledContract导入(而非@midnight-ntwrk/compact-js)——两个包都导出同名内容,但只有前者匹配运行时的compact-runtime。withVacantWitnesses - 延迟单例模式(,
getCompiledContract)——避免双实例WASM身份bug。getLedger - 和
ContractState从sampleSigningKey重新导出,调用者无需直接导入该包(保持第4节的单副本保证)。compact-runtime
7) lib/address.ts
lib/address.ts7) lib/address.ts
lib/address.tsDecode Bech32 unshielded addresses for circuit args. The format is required for — not for args, which take a raw .
UserAddress{ bytes: Uint8Array }UserAddressBytes<32>Uint8Arraytypescript
import { MidnightBech32m, UnshieldedAddress } from '@midnight-ntwrk/wallet-sdk-address-format';
export function bech32ToUserAddress(bech32: string, networkId: string): { bytes: Uint8Array } {
const parsed = MidnightBech32m.parse(bech32).decode(UnshieldedAddress, networkId);
return { bytes: new Uint8Array(parsed.data) };
}解码Bech32未屏蔽地址,用于电路参数。格式是的要求——不适用于参数,后者接受原始。
UserAddress{ bytes: Uint8Array }UserAddressBytes<32>Uint8Arraytypescript
import { MidnightBech32m, UnshieldedAddress } from '@midnight-ntwrk/wallet-sdk-address-format';
export function bech32ToUserAddress(bech32: string, networkId: string): { bytes: Uint8Array } {
const parsed = MidnightBech32m.parse(bech32).decode(UnshieldedAddress, networkId);
return { bytes: new Uint8Array(parsed.data) };
}8) lib/secret.ts
lib/secret.ts8) lib/secret.ts
lib/secret.tstypescript
import { toHex, fromHex } from './midnight';
export function generateSecret(): Uint8Array {
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
return bytes;
}
export function saveSecret(role: 'organizer' | 'attendee', contractAddress: string, secret: Uint8Array) {
localStorage.setItem(`private-party:${role}:${contractAddress}`, toHex(secret));
}
export function loadSecret(role: 'organizer' | 'attendee', contractAddress: string): Uint8Array | null {
const hex = localStorage.getItem(`private-party:${role}:${contractAddress}`);
return hex ? fromHex(hex) : null;
}typescript
import { toHex, fromHex } from './midnight';
export function generateSecret(): Uint8Array {
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
return bytes;
}
export function saveSecret(role: 'organizer' | 'attendee', contractAddress: string, secret: Uint8Array) {
localStorage.setItem(`private-party:${role}:${contractAddress}`, toHex(secret));
}
export function loadSecret(role: 'organizer' | 'attendee', contractAddress: string): Uint8Array | null {
const hex = localStorage.getItem(`private-party:${role}:${contractAddress}`);
return hex ? fromHex(hex) : null;
}9) lib/midnight.ts
— full implementation
lib/midnight.ts9) lib/midnight.ts
—— 完整实现
lib/midnight.tsThis is the file that causes the most real-world breakage (see §15). It is given in full here rather than as bullets — do not improvise it.
typescript
import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
import { FetchZkConfigProvider } from '@midnight-ntwrk/midnight-js-fetch-zk-config-provider';
export type NetworkName = 'preprod' | 'preview';
// Single source of truth for network — change this, not any hardcoded
// 'preprod' string elsewhere in the app, if you target a different network.
export const DEFAULT_NETWORK: NetworkName = 'preprod';
export interface NetworkConfig {
networkId: NetworkName;
indexerUri: string;
indexerWsUri: string;
proofServerUri: string;
}
const NETWORKS: Record<NetworkName, NetworkConfig> = {
preprod: {
networkId: 'preprod',
indexerUri: 'https://indexer.preprod.midnight.network/api/v1/graphql',
indexerWsUri: 'wss://indexer.preprod.midnight.network/api/v1/graphql/ws',
proofServerUri: 'https://proof-server.preprod.midnight.network',
},
preview: {
networkId: 'preview',
indexerUri: 'https://indexer.preview.midnight.network/api/v1/graphql',
indexerWsUri: 'wss://indexer.preview.midnight.network/api/v1/graphql/ws',
proofServerUri: 'https://proof-server.preview.midnight.network',
},
};
export interface ConnectedSession {
unshieldedAddress: string;
config: NetworkConfig;
providers: {
walletProvider: any;
zkConfigProvider: any;
privateStateProvider: any;
publicDataProvider: any;
};
}
export function toHex(bytes: Uint8Array): string {
return Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('');
}
export function fromHex(hex: string): Uint8Array {
const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
const out = new Uint8Array(clean.length / 2);
for (let i = 0; i < out.length; i++) {
out[i] = parseInt(clean.substr(i * 2, 2), 16);
}
return out;
}
// Wallet APIs return coin public keys in inconsistent shapes depending on
// wallet version — normalize all of them to a 32-byte Uint8Array.
export function coinPublicKeyToBytes(cpk: unknown): Uint8Array {
if (cpk instanceof Uint8Array) return cpk;
if (typeof cpk === 'string') return fromHex(cpk);
if (Array.isArray(cpk)) return new Uint8Array(cpk);
if (cpk && typeof cpk === 'object' && 'bytes' in (cpk as any)) {
return coinPublicKeyToBytes((cpk as any).bytes);
}
throw new Error(`Unrecognized coin public key shape: ${JSON.stringify(cpk)}`);
}
export async function detectWallet(): Promise<any> {
const w = (window as any).midnight;
if (!w) throw new Error('No Midnight-compatible wallet found in window.midnight');
if (w['1am']) return w['1am'];
const first = Object.values(w)[0];
if (!first) throw new Error('window.midnight is present but empty');
return first;
}
function createPrivateStateProvider() {
const store = new Map<string, unknown>();
const contractAddresses = new Map<string, string>();
const signingKeys = new Map<string, unknown>();
return {
async get(id: string) { return store.get(id) ?? null; },
async set(id: string, state: unknown) { store.set(id, state); },
async remove(id: string) { store.delete(id); },
async setContractAddress(address: string) { contractAddresses.set('__current__', address); },
async getContractAddress() { return contractAddresses.get('__current__') ?? null; },
async setSigningKey(contractAddress: string, key: unknown) { signingKeys.set(contractAddress, key); },
async getSigningKey(contractAddress: string) { return signingKeys.get(contractAddress) ?? null; },
};
}
// The default indexer provider throws on `offset: null` against the
// preprod/preview indexer (see §15). This wraps it with a raw fetch that
// omits the offending field.
function createPatchedPublicDataProvider(queryUrl: string, subscriptionUrl: string) {
const base = indexerPublicDataProvider(queryUrl, subscriptionUrl);
return {
...base,
async queryContractState(contractAddress: string) {
const res = await fetch(queryUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query ContractState($address: String!) {
contractAction(address: $address) {
state
}
}
`,
variables: { address: contractAddress },
}),
});
const json = await res.json();
const state = json?.data?.contractAction?.state;
return state ? { state } : null;
},
};
}
export async function pollForState(
queryUrl: string,
contractAddress: string,
{ retries = 20, delayMs = 1500 }: { retries?: number; delayMs?: number } = {},
): Promise<string> {
for (let i = 0; i < retries; i++) {
const res = await fetch(queryUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query ContractState($address: String!) {
contractAction(address: $address) { state }
}
`,
variables: { address: contractAddress },
}),
});
const json = await res.json();
const state = json?.data?.contractAction?.state;
if (state) return state as string;
await new Promise((r) => setTimeout(r, delayMs));
}
throw new Error(`Timed out waiting for contract state at ${contractAddress}`);
}
export async function createConnectedSession(
walletApi: any,
zkPath: string,
network: NetworkName = DEFAULT_NETWORK,
): Promise<ConnectedSession> {
const config = NETWORKS[network];
const state = await walletApi.state();
const unshieldedAddress: string = state.address ?? state.unshieldedAddress;
const walletProvider = {
coinPublicKey: coinPublicKeyToBytes(state.coinPublicKey),
balanceTx: (tx: unknown) => walletApi.balanceTransaction(tx),
submitTx: (tx: unknown) => walletApi.submitTransaction(tx),
};
const zkConfigProvider = new FetchZkConfigProvider(zkPath);
const privateStateProvider = createPrivateStateProvider();
const publicDataProvider = createPatchedPublicDataProvider(config.indexerUri, config.indexerWsUri);
return {
unshieldedAddress,
config,
providers: { walletProvider, zkConfigProvider, privateStateProvider, publicDataProvider },
};
}该文件是实际开发中最容易出问题的部分(见第15节)。此处提供完整代码,请勿自行编写。
typescript
import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
import { FetchZkConfigProvider } from '@midnight-ntwrk/midnight-js-fetch-zk-config-provider';
export type NetworkName = 'preprod' | 'preview';
// Single source of truth for network — change this, not any hardcoded
// 'preprod' string elsewhere in the app, if you target a different network.
export const DEFAULT_NETWORK: NetworkName = 'preprod';
export interface NetworkConfig {
networkId: NetworkName;
indexerUri: string;
indexerWsUri: string;
proofServerUri: string;
}
const NETWORKS: Record<NetworkName, NetworkConfig> = {
preprod: {
networkId: 'preprod',
indexerUri: 'https://indexer.preprod.midnight.network/api/v1/graphql',
indexerWsUri: 'wss://indexer.preprod.midnight.network/api/v1/graphql/ws',
proofServerUri: 'https://proof-server.preprod.midnight.network',
},
preview: {
networkId: 'preview',
indexerUri: 'https://indexer.preview.midnight.network/api/v1/graphql',
indexerWsUri: 'wss://indexer.preview.midnight.network/api/v1/graphql/ws',
proofServerUri: 'https://proof-server.preview.midnight.network',
},
};
export interface ConnectedSession {
unshieldedAddress: string;
config: NetworkConfig;
providers: {
walletProvider: any;
zkConfigProvider: any;
privateStateProvider: any;
publicDataProvider: any;
};
}
export function toHex(bytes: Uint8Array): string {
return Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('');
}
export function fromHex(hex: string): Uint8Array {
const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
const out = new Uint8Array(clean.length / 2);
for (let i = 0; i < out.length; i++) {
out[i] = parseInt(clean.substr(i * 2, 2), 16);
}
return out;
}
// Wallet APIs return coin public keys in inconsistent shapes depending on
// wallet version — normalize all of them to a 32-byte Uint8Array.
export function coinPublicKeyToBytes(cpk: unknown): Uint8Array {
if (cpk instanceof Uint8Array) return cpk;
if (typeof cpk === 'string') return fromHex(cpk);
if (Array.isArray(cpk)) return new Uint8Array(cpk);
if (cpk && typeof cpk === 'object' && 'bytes' in (cpk as any)) {
return coinPublicKeyToBytes((cpk as any).bytes);
}
throw new Error(`Unrecognized coin public key shape: ${JSON.stringify(cpk)}`);
}
export async function detectWallet(): Promise<any> {
const w = (window as any).midnight;
if (!w) throw new Error('No Midnight-compatible wallet found in window.midnight');
if (w['1am']) return w['1am'];
const first = Object.values(w)[0];
if (!first) throw new Error('window.midnight is present but empty');
return first;
}
function createPrivateStateProvider() {
const store = new Map<string, unknown>();
const contractAddresses = new Map<string, string>();
const signingKeys = new Map<string, unknown>();
return {
async get(id: string) { return store.get(id) ?? null; },
async set(id: string, state: unknown) { store.set(id, state); },
async remove(id: string) { store.delete(id); },
async setContractAddress(address: string) { contractAddresses.set('__current__', address); },
async getContractAddress() { return contractAddresses.get('__current__') ?? null; },
async setSigningKey(contractAddress: string, key: unknown) { signingKeys.set(contractAddress, key); },
async getSigningKey(contractAddress: string) { return signingKeys.get(contractAddress) ?? null; },
};
}
// The default indexer provider throws on `offset: null` against the
// preprod/preview indexer (see §15). This wraps it with a raw fetch that
// omits the offending field.
function createPatchedPublicDataProvider(queryUrl: string, subscriptionUrl: string) {
const base = indexerPublicDataProvider(queryUrl, subscriptionUrl);
return {
...base,
async queryContractState(contractAddress: string) {
const res = await fetch(queryUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query ContractState($address: String!) {
contractAction(address: $address) {
state
}
}
`,
variables: { address: contractAddress },
}),
});
const json = await res.json();
const state = json?.data?.contractAction?.state;
return state ? { state } : null;
},
};
}
export async function pollForState(
queryUrl: string,
contractAddress: string,
{ retries = 20, delayMs = 1500 }: { retries?: number; delayMs?: number } = {},
): Promise<string> {
for (let i = 0; i < retries; i++) {
const res = await fetch(queryUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query ContractState($address: String!) {
contractAction(address: $address) { state }
}
`,
variables: { address: contractAddress },
}),
});
const json = await res.json();
const state = json?.data?.contractAction?.state;
if (state) return state as string;
await new Promise((r) => setTimeout(r, delayMs));
}
throw new Error(`Timed out waiting for contract state at ${contractAddress}`);
}
export async function createConnectedSession(
walletApi: any,
zkPath: string,
network: NetworkName = DEFAULT_NETWORK,
): Promise<ConnectedSession> {
const config = NETWORKS[network];
const state = await walletApi.state();
const unshieldedAddress: string = state.address ?? state.unshieldedAddress;
const walletProvider = {
coinPublicKey: coinPublicKeyToBytes(state.coinPublicKey),
balanceTx: (tx: unknown) => walletApi.balanceTransaction(tx),
submitTx: (tx: unknown) => walletApi.submitTransaction(tx),
};
const zkConfigProvider = new FetchZkConfigProvider(zkPath);
const privateStateProvider = createPrivateStateProvider();
const publicDataProvider = createPatchedPublicDataProvider(config.indexerUri, config.indexerWsUri);
return {
unshieldedAddress,
config,
providers: { walletProvider, zkConfigProvider, privateStateProvider, publicDataProvider },
};
}10) lib/party.ts
lib/party.ts10) lib/party.ts
lib/party.tsUses (lazy singleton) and the correct arg format for Compact 0.23+.
getCompiledContract()typescript
import { createUnprovenDeployTx, submitCallTxAsync, submitTxAsync } from '@midnight-ntwrk/midnight-js-contracts';
import { getCompiledContract, getLedger, sampleSigningKey, ContractState } from '../contract/src/index';
import type { ConnectedSession } from './midnight';
import { fromHex, pollForState } from './midnight';
import { bech32ToUserAddress } from './address';
const PRIVATE_STATE_ID = 'PrivatePartyState';
export const ZK_PATH = '/zk/private-party';
const PARTY_STATE_NAMES = [
'NOT_STARTED',
'READY',
'STARTED',
'DOORS_CLOSED',
'FEES_CLAIMED',
] as const;
function setSize(value: unknown): number {
if (typeof value === 'number') return value;
if (value && typeof value === 'object' && 'size' in value) {
const size = (value as { size: unknown }).size;
if (typeof size === 'function') return Number((size as () => number)());
if (typeof size === 'number') return size;
}
return 0;
}
export async function deployParty(
session: ConnectedSession,
partySize: number,
entryFeeStars: number,
organizerSecret: Uint8Array,
): Promise<string> {
const cc = await getCompiledContract();
const deployTxData = await (createUnprovenDeployTx as any)(
{
zkConfigProvider: session.providers.zkConfigProvider,
walletProvider: session.providers.walletProvider,
},
{
compiledContract: cc,
args: [BigInt(partySize), BigInt(entryFeeStars), organizerSecret],
privateStateId: PRIVATE_STATE_ID,
initialPrivateState: {},
signingKey: sampleSigningKey(),
},
);
const contractAddress = deployTxData.public.contractAddress;
await (submitTxAsync as any)(session.providers, { unprovenTx: deployTxData.private.unprovenTx });
await session.providers.privateStateProvider.setContractAddress(contractAddress);
await session.providers.privateStateProvider.set(PRIVATE_STATE_ID, {});
await session.providers.privateStateProvider.setSigningKey(
contractAddress,
deployTxData.private.signingKey,
);
return contractAddress;
}
async function call(
session: ConnectedSession,
contractAddress: string,
circuitId: string,
args: unknown[],
) {
const cc = await getCompiledContract();
await (submitCallTxAsync as any)(session.providers, {
compiledContract: cc,
contractAddress,
circuitId,
args,
privateStateId: PRIVATE_STATE_ID,
});
}
export const rsvp = (session: ConnectedSession, contractAddress: string, userAddress: { bytes: Uint8Array }, attendeeSecret: Uint8Array) =>
call(session, contractAddress, 'rsvp', [userAddress, attendeeSecret]);
export const startParty = (session: ConnectedSession, contractAddress: string, organizerSecret: Uint8Array) =>
call(session, contractAddress, 'startParty', [organizerSecret]);
export const checkIn = (session: ConnectedSession, contractAddress: string, userAddress: { bytes: Uint8Array }, attendeeSecret: Uint8Array) =>
call(session, contractAddress, 'checkIn', [userAddress, attendeeSecret]);
export const closeEntry = (session: ConnectedSession, contractAddress: string, organizerSecret: Uint8Array) =>
call(session, contractAddress, 'closeEntry', [organizerSecret]);
export const claimFees = (session: ConnectedSession, contractAddress: string, organizerAddress: { bytes: Uint8Array }, organizerSecret: Uint8Array) =>
call(session, contractAddress, 'claimFees', [organizerAddress, organizerSecret]);
export async function decodePartyState(stateHex: string) {
const contractState = ContractState.deserialize(fromHex(stateHex));
const ledger = await getLedger();
const l = ledger(contractState.data) as any;
const stateIdx = Number(l.partyState);
return {
partyState: PARTY_STATE_NAMES[stateIdx] ?? `UNKNOWN(${stateIdx})`,
partyStateIndex: stateIdx,
maxListSize: Number(l.maxListSize),
entryFee: Number(l.entryFee),
rsvpCount: setSize(l.hashedPartyGoers),
checkedInCount: setSize(l.checkedInParty),
};
}
export async function fetchPartyState(queryUrl: string, contractAddress: string) {
const hex = await pollForState(queryUrl, contractAddress);
return decodePartyState(hex);
}
export function userAddressFromSession(session: ConnectedSession) {
return bech32ToUserAddress(session.unshieldedAddress, session.config.networkId);
}Key details:
- use
argsforBigInt(...)fields, not plain numbers.Uint<16> - args are passed as raw
Bytes<32>, not wrapped inUint8Array.{ bytes: ... }args require theUserAddresswrapper.{ bytes } - is resolved via
ledger()from the contract's own compiled module — never a static top-level import (breaks the lazy-singleton guarantee from §6).getLedger() - handles both
setSize()(property) and.size(method) since compiled.size()ledger fields vary by SDK version.Set
使用(延迟单例)和Compact 0.23+的正确参数格式。
getCompiledContract()typescript
import { createUnprovenDeployTx, submitCallTxAsync, submitTxAsync } from '@midnight-ntwrk/midnight-js-contracts';
import { getCompiledContract, getLedger, sampleSigningKey, ContractState } from '../contract/src/index';
import type { ConnectedSession } from './midnight';
import { fromHex, pollForState } from './midnight';
import { bech32ToUserAddress } from './address';
const PRIVATE_STATE_ID = 'PrivatePartyState';
export const ZK_PATH = '/zk/private-party';
const PARTY_STATE_NAMES = [
'NOT_STARTED',
'READY',
'STARTED',
'DOORS_CLOSED',
'FEES_CLAIMED',
] as const;
function setSize(value: unknown): number {
if (typeof value === 'number') return value;
if (value && typeof value === 'object' && 'size' in value) {
const size = (value as { size: unknown }).size;
if (typeof size === 'function') return Number((size as () => number)());
if (typeof size === 'number') return size;
}
return 0;
}
export async function deployParty(
session: ConnectedSession,
partySize: number,
entryFeeStars: number,
organizerSecret: Uint8Array,
): Promise<string> {
const cc = await getCompiledContract();
const deployTxData = await (createUnprovenDeployTx as any)(
{
zkConfigProvider: session.providers.zkConfigProvider,
walletProvider: session.providers.walletProvider,
},
{
compiledContract: cc,
args: [BigInt(partySize), BigInt(entryFeeStars), organizerSecret],
privateStateId: PRIVATE_STATE_ID,
initialPrivateState: {},
signingKey: sampleSigningKey(),
},
);
const contractAddress = deployTxData.public.contractAddress;
await (submitTxAsync as any)(session.providers, { unprovenTx: deployTxData.private.unprovenTx });
await session.providers.privateStateProvider.setContractAddress(contractAddress);
await session.providers.privateStateProvider.set(PRIVATE_STATE_ID, {});
await session.providers.privateStateProvider.setSigningKey(
contractAddress,
deployTxData.private.signingKey,
);
return contractAddress;
}
async function call(
session: ConnectedSession,
contractAddress: string,
circuitId: string,
args: unknown[],
) {
const cc = await getCompiledContract();
await (submitCallTxAsync as any)(session.providers, {
compiledContract: cc,
contractAddress,
circuitId,
args,
privateStateId: PRIVATE_STATE_ID,
});
}
export const rsvp = (session: ConnectedSession, contractAddress: string, userAddress: { bytes: Uint8Array }, attendeeSecret: Uint8Array) =>
call(session, contractAddress, 'rsvp', [userAddress, attendeeSecret]);
export const startParty = (session: ConnectedSession, contractAddress: string, organizerSecret: Uint8Array) =>
call(session, contractAddress, 'startParty', [organizerSecret]);
export const checkIn = (session: ConnectedSession, contractAddress: string, userAddress: { bytes: Uint8Array }, attendeeSecret: Uint8Array) =>
call(session, contractAddress, 'checkIn', [userAddress, attendeeSecret]);
export const closeEntry = (session: ConnectedSession, contractAddress: string, organizerSecret: Uint8Array) =>
call(session, contractAddress, 'closeEntry', [organizerSecret]);
export const claimFees = (session: ConnectedSession, contractAddress: string, userAddress: { bytes: Uint8Array }, organizerSecret: Uint8Array) =>
call(session, contractAddress, 'claimFees', [userAddress, organizerSecret]);
export async function decodePartyState(stateHex: string) {
const contractState = ContractState.deserialize(fromHex(stateHex));
const ledger = await getLedger();
const l = ledger(contractState.data) as any;
const stateIdx = Number(l.partyState);
return {
partyState: PARTY_STATE_NAMES[stateIdx] ?? `UNKNOWN(${stateIdx})`,
partyStateIndex: stateIdx,
maxListSize: Number(l.maxListSize),
entryFee: Number(l.entryFee),
rsvpCount: setSize(l.hashedPartyGoers),
checkedInCount: setSize(l.checkedInParty),
};
}
export async function fetchPartyState(queryUrl: string, contractAddress: string) {
const hex = await pollForState(queryUrl, contractAddress);
return decodePartyState(hex);
}
export function userAddressFromSession(session: ConnectedSession) {
return bech32ToUserAddress(session.unshieldedAddress, session.config.networkId);
}核心细节:
- 对
args字段使用Uint<16>,而非普通数字。BigInt(...) - 参数以原始
Bytes<32>传递,不包裹在Uint8Array中。{ bytes: ... }参数需要UserAddress包裹。{ bytes } - 通过合约自身编译模块的
ledger()解析——切勿使用静态顶层导入(破坏第6节的延迟单例保证)。getLedger() - 处理
setSize()(属性)和.size(方法),因为编译后的.size()账本字段因SDK版本而异。Set
11) Frontend — app/party/PartyClient.tsx
app/party/PartyClient.tsx11) 前端 —— app/party/PartyClient.tsx
app/party/PartyClient.tsxFull component — no placeholder handlers.
tsx
'use client';
import { useCallback, useEffect, useState } from 'react';
import {
checkIn,
claimFees,
closeEntry,
deployParty,
fetchPartyState,
rsvp,
startParty,
userAddressFromSession,
ZK_PATH,
} from '@/lib/party';
import { createConnectedSession, detectWallet, DEFAULT_NETWORK, type ConnectedSession } from '@/lib/midnight';
import { generateSecret, loadSecret, saveSecret } from '@/lib/secret';
type Role = 'organizer' | 'attendee';
export default function PartyClient() {
const [session, setSession] = useState<ConnectedSession | null>(null);
const [role, setRole] = useState<Role>('attendee');
const [contractAddress, setContractAddress] = useState('');
const [partySize, setPartySize] = useState('2');
const [entryFee, setEntryFee] = useState('5');
const [status, setStatus] = useState<Awaited<ReturnType<typeof fetchPartyState>> | null>(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const refresh = useCallback(async () => {
if (!session || !contractAddress) return;
try {
setStatus(await fetchPartyState(session.config.indexerUri, contractAddress));
} catch (e) {
setError(String(e));
}
}, [session, contractAddress]);
useEffect(() => { void refresh(); }, [refresh]);
async function guard(fn: () => Promise<void>) {
setBusy(true);
setError(null);
try {
await fn();
await refresh();
} catch (e) {
setError(String(e));
} finally {
setBusy(false);
}
}
const onConnect = () => guard(async () => {
const wallet = await detectWallet();
const api = await wallet.connect(DEFAULT_NETWORK);
setSession(await createConnectedSession(api, ZK_PATH));
});
const onDeploy = () => guard(async () => {
if (!session) return;
const secret = generateSecret();
const addr = await deployParty(session, Number(partySize), Number(entryFee), secret);
setContractAddress(addr);
saveSecret('organizer', addr, secret);
});
const onRsvp = () => guard(async () => {
if (!session || !contractAddress) return;
let secret = loadSecret('attendee', contractAddress);
if (!secret) {
secret = generateSecret();
saveSecret('attendee', contractAddress, secret);
}
await rsvp(session, contractAddress, userAddressFromSession(session), secret);
});
const onStartParty = () => guard(async () => {
if (!session || !contractAddress) return;
const secret = loadSecret('organizer', contractAddress);
if (!secret) throw new Error('No organizer secret found for this contract in this browser');
await startParty(session, contractAddress, secret);
});
const onCheckIn = () => guard(async () => {
if (!session || !contractAddress) return;
const secret = loadSecret('attendee', contractAddress);
if (!secret) throw new Error('RSVP first — no attendee secret found for this contract');
await checkIn(session, contractAddress, userAddressFromSession(session), secret);
});
const onCloseEntry = () => guard(async () => {
if (!session || !contractAddress) return;
const secret = loadSecret('organizer', contractAddress);
if (!secret) throw new Error('No organizer secret found for this contract in this browser');
await closeEntry(session, contractAddress, secret);
});
const onClaimFees = () => guard(async () => {
if (!session || !contractAddress) return;
const secret = loadSecret('organizer', contractAddress);
if (!secret) throw new Error('No organizer secret found for this contract in this browser');
await claimFees(session, contractAddress, userAddressFromSession(session), secret);
});
return (
<div className="mx-auto max-w-xl p-6 text-zinc-100">
<h1 className="text-2xl font-semibold">Private Party</h1>
<p className="mt-1 text-muted">Attendees stay private until check-in pays unshielded NIGHT.</p>
{!session ? (
<button type="button" onClick={onConnect} disabled={busy}
className="mt-4 rounded bg-brand px-4 py-2 hover:bg-brand-hover disabled:opacity-50">
Connect Wallet
</button>
) : (
<div className="mt-4 space-y-4">
<p className="text-sm text-muted">Connected: {session.unshieldedAddress}</p>
<label className="block">
Role
<select value={role} onChange={(e) => setRole(e.target.value as Role)}
className="ml-2 rounded border border-border bg-surface px-2 py-1 text-zinc-100">
<option value="organizer">Organizer</option>
<option value="attendee">Attendee</option>
</select>
</label>
<label className="block">
Contract address
<input value={contractAddress} onChange={(e) => setContractAddress(e.target.value.trim())}
placeholder="mn_contract1..."
className="mt-1 w-full rounded border border-border bg-surface px-2 py-1 text-zinc-100 placeholder:text-zinc-500" />
</label>
{status ? (
<p className="rounded border border-border bg-card p-3 text-sm">
State: {status.partyState} · RSVPs: {status.rsvpCount}/{status.maxListSize} ·
Checked in: {status.checkedInCount} · Fee: {status.entryFee} Stars
</p>
) : null}
{role === 'organizer' && !contractAddress ? (
<div className="space-y-2 rounded border border-border bg-card p-3">
<label className="block">Max guests
<input value={partySize} onChange={(e) => setPartySize(e.target.value)}
className="ml-2 w-20 rounded border border-border bg-surface px-2 py-1 text-zinc-100" />
</label>
<label className="block">Entry fee (Stars)
<input value={entryFee} onChange={(e) => setEntryFee(e.target.value)}
className="ml-2 w-24 rounded border border-border bg-surface px-2 py-1 text-zinc-100" />
</label>
<button type="button" onClick={onDeploy} disabled={busy}
className="rounded bg-brand px-3 py-1.5 hover:bg-brand-hover disabled:opacity-50">
Deploy Party
</button>
</div>
) : null}
{role === 'organizer' && contractAddress ? (
<div className="flex flex-wrap gap-2">
<button type="button" onClick={onStartParty} disabled={busy}
className="rounded bg-brand px-3 py-1.5 hover:bg-brand-hover disabled:opacity-50">Start party</button>
<button type="button" onClick={onCloseEntry} disabled={busy}
className="rounded bg-card border border-border px-3 py-1.5 hover:bg-[#1e1e38] disabled:opacity-50">Close doors</button>
<button type="button" onClick={onClaimFees} disabled={busy}
className="rounded bg-success px-3 py-1.5 hover:opacity-90 disabled:opacity-50">Claim fees</button>
</div>
) : null}
{role === 'attendee' && contractAddress ? (
<div className="flex flex-wrap gap-2">
<button type="button" onClick={onRsvp} disabled={busy}
className="rounded bg-brand px-3 py-1.5 hover:bg-brand-hover disabled:opacity-50">RSVP</button>
<button type="button" onClick={onCheckIn} disabled={busy}
className="rounded bg-danger px-3 py-1.5 hover:opacity-90 disabled:opacity-50">
Check in (pays {status?.entryFee ?? '?'} Stars — becomes public)
</button>
</div>
) : null}
</div>
)}
{error ? <p role="alert" className="mt-4 rounded border border-danger bg-[#3a1414] p-2 text-sm text-red-300">{error}</p> : null}
</div>
);
}完整组件——无占位符处理函数。
tsx
'use client';
import { useCallback, useEffect, useState } from 'react';
import {
checkIn,
claimFees,
closeEntry,
deployParty,
fetchPartyState,
rsvp,
startParty,
userAddressFromSession,
ZK_PATH,
} from '@/lib/party';
import { createConnectedSession, detectWallet, DEFAULT_NETWORK, type ConnectedSession } from '@/lib/midnight';
import { generateSecret, loadSecret, saveSecret } from '@/lib/secret';
type Role = 'organizer' | 'attendee';
export default function PartyClient() {
const [session, setSession] = useState<ConnectedSession | null>(null);
const [role, setRole] = useState<Role>('attendee');
const [contractAddress, setContractAddress] = useState('');
const [partySize, setPartySize] = useState('2');
const [entryFee, setEntryFee] = useState('5');
const [status, setStatus] = useState<Awaited<ReturnType<typeof fetchPartyState>> | null>(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const refresh = useCallback(async () => {
if (!session || !contractAddress) return;
try {
setStatus(await fetchPartyState(session.config.indexerUri, contractAddress));
} catch (e) {
setError(String(e));
}
}, [session, contractAddress]);
useEffect(() => { void refresh(); }, [refresh]);
async function guard(fn: () => Promise<void>) {
setBusy(true);
setError(null);
try {
await fn();
await refresh();
} catch (e) {
setError(String(e));
} finally {
setBusy(false);
}
}
const onConnect = () => guard(async () => {
const wallet = await detectWallet();
const api = await wallet.connect(DEFAULT_NETWORK);
setSession(await createConnectedSession(api, ZK_PATH));
});
const onDeploy = () => guard(async () => {
if (!session) return;
const secret = generateSecret();
const addr = await deployParty(session, Number(partySize), Number(entryFee), secret);
setContractAddress(addr);
saveSecret('organizer', addr, secret);
});
const onRsvp = () => guard(async () => {
if (!session || !contractAddress) return;
let secret = loadSecret('attendee', contractAddress);
if (!secret) {
secret = generateSecret();
saveSecret('attendee', contractAddress, secret);
}
await rsvp(session, contractAddress, userAddressFromSession(session), secret);
});
const onStartParty = () => guard(async () => {
if (!session || !contractAddress) return;
const secret = loadSecret('organizer', contractAddress);
if (!secret) throw new Error('No organizer secret found for this contract in this browser');
await startParty(session, contractAddress, secret);
});
const onCheckIn = () => guard(async () => {
if (!session || !contractAddress) return;
const secret = loadSecret('attendee', contractAddress);
if (!secret) throw new Error('RSVP first — no attendee secret found for this contract');
await checkIn(session, contractAddress, userAddressFromSession(session), secret);
});
const onCloseEntry = () => guard(async () => {
if (!session || !contractAddress) return;
const secret = loadSecret('organizer', contractAddress);
if (!secret) throw new Error('No organizer secret found for this contract in this browser');
await closeEntry(session, contractAddress, secret);
});
const onClaimFees = () => guard(async () => {
if (!session || !contractAddress) return;
const secret = loadSecret('organizer', contractAddress);
if (!secret) throw new Error('No organizer secret found for this contract in this browser');
await claimFees(session, contractAddress, userAddressFromSession(session), secret);
});
return (
<div className="mx-auto max-w-xl p-6 text-zinc-100">
<h1 className="text-2xl font-semibold">Private Party</h1>
<p className="mt-1 text-muted">Attendees stay private until check-in pays unshielded NIGHT.</p>
{!session ? (
<button type="button" onClick={onConnect} disabled={busy}
className="mt-4 rounded bg-brand px-4 py-2 hover:bg-brand-hover disabled:opacity-50">
Connect Wallet
</button>
) : (
<div className="mt-4 space-y-4">
<p className="text-sm text-muted">Connected: {session.unshieldedAddress}</p>
<label className="block">
Role
<select value={role} onChange={(e) => setRole(e.target.value as Role)}
className="ml-2 rounded border border-border bg-surface px-2 py-1 text-zinc-100">
<option value="organizer">Organizer</option>
<option value="attendee">Attendee</option>
</select>
</label>
<label className="block">
Contract address
<input value={contractAddress} onChange={(e) => setContractAddress(e.target.value.trim())}
placeholder="mn_contract1..."
className="mt-1 w-full rounded border border-border bg-surface px-2 py-1 text-zinc-100 placeholder:text-zinc-500" />
</label>
{status ? (
<p className="rounded border border-border bg-card p-3 text-sm">
State: {status.partyState} · RSVPs: {status.rsvpCount}/{status.maxListSize} ·
Checked in: {status.checkedInCount} · Fee: {status.entryFee} Stars
</p>
) : null}
{role === 'organizer' && !contractAddress ? (
<div className="space-y-2 rounded border border-border bg-card p-3">
<label className="block">Max guests
<input value={partySize} onChange={(e) => setPartySize(e.target.value)}
className="ml-2 w-20 rounded border border-border bg-surface px-2 py-1 text-zinc-100" />
</label>
<label className="block">Entry fee (Stars)
<input value={entryFee} onChange={(e) => setEntryFee(e.target.value)}
className="ml-2 w-24 rounded border border-border bg-surface px-2 py-1 text-zinc-100" />
</label>
<button type="button" onClick={onDeploy} disabled={busy}
className="rounded bg-brand px-3 py-1.5 hover:bg-brand-hover disabled:opacity-50">
Deploy Party
</button>
</div>
) : null}
{role === 'organizer' && contractAddress ? (
<div className="flex flex-wrap gap-2">
<button type="button" onClick={onStartParty} disabled={busy}
className="rounded bg-brand px-3 py-1.5 hover:bg-brand-hover disabled:opacity-50">Start party</button>
<button type="button" onClick={onCloseEntry} disabled={busy}
className="rounded bg-card border border-border px-3 py-1.5 hover:bg-[#1e1e38] disabled:opacity-50">Close doors</button>
<button type="button" onClick={onClaimFees} disabled={busy}
className="rounded bg-success px-3 py-1.5 hover:opacity-90 disabled:opacity-50">Claim fees</button>
</div>
) : null}
{role === 'attendee' && contractAddress ? (
<div className="flex flex-wrap gap-2">
<button type="button" onClick={onRsvp} disabled={busy}
className="rounded bg-brand px-3 py-1.5 hover:bg-brand-hover disabled:opacity-50">RSVP</button>
<button type="button" onClick={onCheckIn} disabled={busy}
className="rounded bg-danger px-3 py-1.5 hover:opacity-90 disabled:opacity-50">
Check in (pays {status?.entryFee ?? '?'} Stars — becomes public)
</button>
</div>
) : null}
</div>
)}
{error ? <p role="alert" className="mt-4 rounded border border-danger bg-[#3a1414] p-2 text-sm text-red-300">{error}</p> : null}
</div>
);
}12) ZK Asset Sync — scripts/sync-zk-assets.mjs
scripts/sync-zk-assets.mjs12) ZK资产同步 —— scripts/sync-zk-assets.mjs
scripts/sync-zk-assets.mjsjavascript
import { cpSync, mkdirSync, rmSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const src = join(root, 'contract/src/managed/private-party');
const dest = join(root, 'public/zk/private-party');
rmSync(dest, { recursive: true, force: true });
mkdirSync(dest, { recursive: true });
for (const dir of ['keys', 'zkir']) {
cpSync(join(src, dir), join(dest, dir), { recursive: true });
}Verify: returns 200.
http://localhost:3000/zk/private-party/keys/rsvp.proverjavascript
import { cpSync, mkdirSync, rmSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const src = join(root, 'contract/src/managed/private-party');
const dest = join(root, 'public/zk/private-party');
rmSync(dest, { recursive: true, force: true });
mkdirSync(dest, { recursive: true });
for (const dir of ['keys', 'zkir']) {
cpSync(join(src, dir), join(dest, dir), { recursive: true });
}验证:返回200状态码。
http://localhost:3000/zk/private-party/keys/rsvp.prover13) next.config.mjs
next.config.mjs13) next.config.mjs
next.config.mjsRequired — without this, webpack fails to resolve the Midnight SDK's Node built-ins and WASM.
javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config) => {
config.experiments = { ...config.experiments, asyncWebAssembly: true, topLevelAwait: true };
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
net: false,
tls: false,
child_process: false,
stream: 'stream-browserify',
};
config.resolve.alias = {
...config.resolve.alias,
'isomorphic-ws': join(process.cwd(), 'lib/isomorphic-ws-fix.mjs'),
};
return config;
},
// Static generation tries to resolve the SDK's Node deps and can hang the build.
images: { unoptimized: true },
};
import { join } from 'node:path';
export default nextConfig;必填项——若无此配置,webpack无法解析Midnight SDK的Node内置模块和WASM。
javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config) => {
config.experiments = { ...config.experiments, asyncWebAssembly: true, topLevelAwait: true };
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
net: false,
tls: false,
child_process: false,
stream: 'stream-browserify',
};
config.resolve.alias = {
...config.resolve.alias,
'isomorphic-ws': join(process.cwd(), 'lib/isomorphic-ws-fix.mjs'),
};
return config;
},
// Static generation tries to resolve the SDK's Node deps and can hang the build.
images: { unoptimized: true },
};
import { join } from 'node:path';
export default nextConfig;14) postcss.config.mjs
and app/globals.css
postcss.config.mjsapp/globals.css14) postcss.config.mjs
和 app/globals.css
postcss.config.mjsapp/globals.cssjavascript
// postcss.config.mjs
export default {
plugins: { '@tailwindcss/postcss': {} },
};css
/* app/globals.css */
@import "tailwindcss";
@theme {
--color-brand: #7c3aed;
--color-brand-hover: #6d28d9;
--color-surface: #1a1a2e;
--color-card: #16213e;
--color-border: #2a2a4a;
--color-muted: #9ca3af;
--color-success: #16a34a;
--color-danger: #dc2626;
}tsx
/* app/layout.tsx */
import './globals.css';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className="bg-[#0f0f23] text-zinc-100 antialiased">{children}</body>
</html>
);
}No light theme — everything is dark surface/card/border/muted. Inputs and selects must use explicitly (a raw defaults to a white background that renders invisible text on this theme).
bg-surface text-zinc-100<input>javascript
// postcss.config.mjs
export default {
plugins: { '@tailwindcss/postcss': {} },
};css
/* app/globals.css */
@import "tailwindcss";
@theme {
--color-brand: #7c3aed;
--color-brand-hover: #6d28d9;
--color-surface: #1a1a2e;
--color-card: #16213e;
--color-border: #2a2a4a;
--color-muted: #9ca3af;
--color-success: #16a34a;
--color-danger: #dc2626;
}tsx
/* app/layout.tsx */
import './globals.css';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className="bg-[#0f0f23] text-zinc-100 antialiased">{children}</body>
</html>
);
}不支持浅色主题——所有元素均为深色背景/卡片/边框/弱化文本。输入框和选择器必须显式设置(原生默认白色背景,在此主题下文本会不可见)。
bg-surface text-zinc-100<input>15) Local Devnet Tests (Official Repo)
15) 本地测试网测试(官方仓库)
bash
git clone git@github.com:midnightntwrk/example-private-party.git
cd example-private-party
yarn install
yarn compile
yarn env:up # Docker: node + indexer + proof-server
yarn test:local # vitest party.test.tsExpected flow (11 tests):
- Deploy contract (Alice organizer)
- Bob RSVPs privately
- Alice (organizer) rejected from RSVP
- Claire RSVPs
- Bob rejected from startParty
- Alice starts party
- Bob checks in → becomes public
- Bob rejected from closeEntry
- Alice closes doors
- Alice claimFees → NIGHT balance increases by
checkedInCount * entryFee - Hard-way deploy test
Read for MidnightJS provider patterns with .
/src/test/party.test.tsFluentWalletBuilderbash
git clone git@github.com:midnightntwrk/example-private-party.git
cd example-private-party
yarn install
yarn compile
yarn env:up # Docker: node + indexer + proof-server
yarn test:local # vitest party.test.ts预期流程(11个测试用例):
- 部署合约(Alice作为组织者)
- Bob私密RSVP
- Alice(组织者)被拒绝RSVP
- Claire RSVP
- Bob被拒绝启动派对
- Alice启动派对
- Bob签到→身份变为公开
- Bob被拒绝关闭入场
- Alice关闭入场
- Alice提现→NIGHT余额增加
checkedInCount * entryFee - 复杂部署测试
查看了解使用的MidnightJS提供者模式。
/src/test/party.test.tsFluentWalletBuilder16) End-to-End Browser Flow
16) 端到端浏览器流程
1. npm install && npm run compact && npm run sync:assets
2. npm run dev
3. Organizer: Connect 1AM → Deploy (max 2 guests, fee 5 Stars) → copy contract address
4. Attendee (other browser/wallet): Connect → paste address → RSVP
5. Organizer: Start party (or wait until RSVP list full → READY)
6. Attendee: Check in (wallet pays 5 Stars unshielded — address now public on ledger)
7. Organizer: Close doors (if needed) → Claim fees to unshielded address
8. Poll indexer — verify checkedInCount and partyState transitions1. npm install && npm run compact && npm run sync:assets
2. npm run dev
3. 组织者:连接1AM钱包→部署(最多2位宾客,费用5 Stars)→复制合约地址
4. 参会者(其他浏览器/钱包):连接钱包→粘贴地址→RSVP
5. 组织者:启动派对(或等待RSVP名单满员→READY状态)
6. 参会者:签到(钱包支付5 Stars未屏蔽代币——地址现在在账本上公开)
7. 组织者:关闭入场(若需要)→提现至未屏蔽地址
8. 轮询索引器——验证checkedInCount和partyState状态转换17) Troubleshooting
17) 故障排查
| Symptom | Cause | Fix |
|---|---|---|
| Organizer secret used for RSVP | Use separate attendee secret |
| Wrong secret or address at check-in | Same |
| RSVP after startParty | RSVP only in NOT_STARTED |
| Wrong organizer secret | Reload from |
| Fee mismatch or partial check-ins | Verify |
| Bech32 passed as bytes | Use |
| Deploy hangs 30–120s | Used | Use |
GraphQL | Default indexer provider | Use patched |
| ZK 404 | Assets not synced | |
| Lost organizer secret | No recovery on-chain | Redeploy contract; store secret in localStorage |
| | Import |
| Dual | Never |
| Static import fails in browser | Lazy |
| Passed | Wrap in |
| Confusing it with | Raw |
| Passed hex string directly | |
| Called a static import instead of the contract's own export | |
| Missing Node polyfills | |
| Missing | |
| Tailwind v4 not applied | Missing PostCSS config | |
| Input/select text invisible on dark bg | Browser default white input bg | |
| | |
| Static generation tries to resolve SDK's Node deps | |
| Wallet rejects connection / wrong chain | App and wallet on different networks | Both must match |
| 症状 | 原因 | 修复方案 |
|---|---|---|
| 使用组织者密钥进行RSVP | 使用独立的参会者密钥 |
| 签到时密钥或地址错误 | 使用与RSVP时相同的 |
| 派对启动后进行RSVP | 仅在NOT_STARTED状态下RSVP |
| 组织者密钥错误 | 从 |
| 费用不匹配或部分签到 | 验证 |
| 将Bech32地址作为字节传递 | 使用 |
| 部署卡顿30–120秒 | 使用了 | 使用 |
GraphQL | 使用默认索引器提供者 | 使用修补后的 |
| ZK资源404错误 | 资产未同步 | 执行 |
| 丢失组织者密钥 | 链上无恢复机制 | 重新部署合约;将密钥存储在localStorage中 |
| | 从 |
| 存在双 | 切勿在 |
| 静态导入在浏览器中失败 | 使用延迟 |
构造函数参数 | 传入 | 对 |
| 混淆了 | |
| 直接传入十六进制字符串 | |
| 调用了静态导入而非合约自身导出的函数 | 使用 |
webpack中找不到 | 缺少Node polyfills | 在 |
| 缺少 | 在 |
| Tailwind v4未生效 | 缺少PostCSS配置 | 使用包含 |
| 输入框/选择器文本在深色背景下不可见 | 浏览器默认白色输入框背景 | 为所有输入框设置 |
Set账本字段的 | 部分SDK版本中 | 使用 |
| 静态生成尝试解析SDK的Node依赖 | 在 |
| 钱包拒绝连接/链错误 | 应用与钱包网络不一致 | 两者必须与 |
18) Agent Checklist
18) 代理检查清单
- Write with all six exported circuits + helper circuits (§5)
private-party.compact - Compile; sync ZK assets to
public/zk/private-party/ - from
CompiledContract.withVacantWitnesses, not@midnight-ntwrk/compact-jscompact-runtime - Lazy /
getCompiledContract()singleton (§6)getLedger() - copied in full from §9 — do not improvise the provider layer
lib/midnight.ts - and
next.config.mjscopied in full from §13–14postcss.config.mjs - Constructor args:
[BigInt(partySize), BigInt(entryFeeStars), rawUint8Array] - Decode unshielded Bech32 via
wallet-sdk-address-format - Store organizer/attendee secrets in per contract address
localStorage - UI explains the privacy boundary before the check-in button (see copy in §11)
- Never run inside
npm installcontract/ - Confirm in
DEFAULT_NETWORKmatches the target network before demoinglib/midnight.ts
- 编写包含所有6个导出电路+辅助电路的(第5节)
private-party.compact - 编译合约;将ZK资产同步至
public/zk/private-party/ - 从而非
@midnight-ntwrk/compact-js导入compact-runtimeCompiledContract.withVacantWitnesses - 使用延迟/
getCompiledContract()单例模式(第6节)getLedger() - 完整复制第9节的——请勿自行编写提供者层代码
lib/midnight.ts - 完整复制第13–14节的和
next.config.mjspostcss.config.mjs - 构造函数参数:
[BigInt(partySize), BigInt(entryFeeStars), rawUint8Array] - 通过解码未屏蔽Bech32地址
wallet-sdk-address-format - 按合约地址将组织者/参会者密钥存储在中
localStorage - UI在签到按钮前说明隐私边界(见第11节文案)
- 切勿在目录下执行
contract/npm install - 演示前确认中的
lib/midnight.ts与目标网络匹配DEFAULT_NETWORK
19) Extensions
19) 扩展功能
Shielded entry fees
屏蔽入场费
To keep attendees private through payment, rework to use shielded tokens instead of — see tutorial conclusion and skill.
checkInreceiveUnshieldedtoken-transfers/若要在支付过程中保持参会者隐私,需重新设计以使用屏蔽代币替代——详见教程结论和技能。
checkInreceiveUnshieldedtoken-transfers/Multi-party testing UI
多方测试UI
Add "copy invite link" with contract address query param; show public addresses after boundary crossed.
checkedInParty添加“复制邀请链接”功能,包含合约地址查询参数;跨越隐私边界后显示公开的地址。
checkedInPartyHeadless CI
无头CI
Port official into the template monorepo using Docker compose pattern.
party.test.tsexample-hello-world/使用的Docker compose模式,将官方移植到模板单仓库中。
example-hello-world/party.test.ts20) Related Skills
20) 相关技能
| Next step | Skill |
|---|---|
| Wallet connect only | |
| Unshielded token flows | |
| Payment vault pattern | |
| Privacy audit | |
| Compact language reference | |
| Local vitest harness | |
| 下一步 | 技能 |
|---|---|
| 仅钱包连接 | |
| 未屏蔽代币流转 | |
| 支付 vault 模式 | |
| 隐私审计 | |
| Compact语言参考 | |
| 本地vitest测试套件 | |