example-private-reserve-auction-dapp
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMidnight Network Private Reserve Auction DApp
Midnight Network 私有保留拍卖DApp
A private reserve auction contract lets a seller hide the reserve price on-chain while bidders place public bids with private identities. The winner crosses the privacy boundary when claiming the item via unshielded NIGHT payment.
Runnable template: Run after installing the 1AM wallet.
npm install && npm run compact && npm run sync:assets && npm run devWhat this skill produces:
- —
contract/(no witnesses, Map, Counter) + compile scriptsprivate-reserve-auction.compact - — Next.js client UI (seller deploy/close/reveal/claim + bidder place bid/claim item)
app/auction/ - — wallet session + patched indexer provider
lib/midnight.ts - — deploy,
lib/auction.ts,bid,closeAuction,revealPrice,claimItem, ledger decodeclaimProceeds - — 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-reserve-auction/
Key architecture notes:
- No witnesses — caller auth uses circuit-private →
_secretcompared to on-chaingetDappPublicKey(_secret)organizer - Hidden reserve price — committed via , revealed later by seller via
persistentCommit(price, secret)revealPrice - Public bids, private identities — discloses bid amount but identity is
bidhashgetDappPublicKey(secret) - Bid overwriting — bidders can update their bid if higher; tracked in
Map<Bytes<32>, Uint<32>> - Auto-close — auction closes when
bidCount == maxBids - Privacy boundary — calls
claimItemthenreceiveUnshielded(nativeToken(), publicPrice)winnerClaimed.insert(disclose(address)) - Seller becomes public — calls
claimProceedsto seller'ssendUnshielded(...)UserAddress - is a developer assertion — it marks values safe for public domains; it does not perform the disclosure itself
disclose() - Use +
createUnprovenDeployTx— notsubmitTxAsync(hangs on preview)deployContract() - Wrap with patched
indexerPublicDataProvider(GraphQLqueryContractStatebug)offset: null - Reserve price is on ledger (in Stars) but cast to
Uint<32>for unshielded ops; 1 NIGHT = 1_000_000 StarsUint<128> - UI accepts/display NIGHT; converts to Stars (×1,000,000) for contract
- Persist seller/bidder in
_secret— losing it means losing auth for that rolelocalStorage - Network: Preview (hardcoded in )
AuctionClient.tsx
私有保留拍卖合约允许卖家在链上隐藏保留价,而出价者以私有身份进行公开出价。当获胜者通过非屏蔽NIGHT支付申领物品时,将跨越隐私边界。
可运行模板:安装1AM钱包后,运行 。
npm install && npm run compact && npm run sync:assets && npm run dev本项目产出内容:
- —
contract/(无见证者、Map、Counter)+ 编译脚本private-reserve-auction.compact - — Next.js客户端UI(卖家部署/关闭/揭示/申领 + 出价者出价/申领物品)
app/auction/ - — 钱包会话 + 修补后的索引器提供者
lib/midnight.ts - — 部署、
lib/auction.ts、bid、closeAuction、revealPrice、claimItem、账本解码claimProceeds - — Bech32非屏蔽地址 →
lib/address.ts用于{ bytes: Uint8Array }电路参数UserAddress - — 在
lib/secret.ts中生成/存储32字节DApp密钥localStorage - — 从合约构建同步的ZK证明资产
public/zk/private-reserve-auction/
核心架构说明:
- 无见证者 — 调用者认证使用电路私有→ 将
_secret与链上getDappPublicKey(_secret)对比organizer - 隐藏保留价 — 通过提交,之后由卖家通过
persistentCommit(price, secret)揭示revealPrice - 公开出价,私有身份 — 披露出价金额,但身份为
bid哈希值getDappPublicKey(secret) - 出价覆盖 — 出价者可更新更高出价;通过跟踪
Map<Bytes<32>, Uint<32>> - 自动关闭 — 当时拍卖自动关闭
bidCount == maxBids - 隐私边界 — 调用
claimItem,然后receiveUnshielded(nativeToken(), publicPrice)winnerClaimed.insert(disclose(address)) - 卖家身份公开 — 调用
claimProceeds到卖家的sendUnshielded(...)UserAddress - 是开发者断言 — 标记值可安全用于公开领域;本身不执行披露操作
disclose() - 使用+
createUnprovenDeployTx— 而非submitTxAsync(预览环境会挂起)deployContract() - 用修补后的包装
queryContractState(解决GraphQLindexerPublicDataProviderbug)offset: null - 账本上的保留价为(单位Stars),但在非屏蔽操作中转换为
Uint<32>;1 NIGHT = 1_000_000 StarsUint<128> - UI接受/显示NIGHT;转换为Stars(×1,000,000)后传给合约
- 卖家/出价者的持久化在
_secret中 — 丢失密钥意味着失去对应角色的权限localStorage - 网络:Preview(在中硬编码)
AuctionClient.tsx
Workflow
工作流程
When helping the user, follow this sequence:
- Contract — write ; compile with
private-reserve-auction.compactnpm run compact - Understand privacy boundary — hidden price → public bids → reveal → claim (unshielded) → public settlement
- Providers — (from
createConnectedSession)references/midnight-session.md - Deploy — seller passes to constructor
(reservePriceStars, maxBidders, sellerSecret) - Bid — bidders call (public amount, private identity)
bid(bidAmountStars, userAddress, secret) - Close — seller calls (or auto-closes when full)
closeAuction(secret) - Reveal — seller calls (verified against commitment)
revealPrice(reservePriceStars, secret) - Claim — winner calls + pays reserve (crosses boundary)
claimItem(address, secret) - Proceeds — seller calls after claim
claimProceeds(address, secret) - UI — role cards (seller/bidder), auction status panel, indexer polling for public state
协助用户时,请遵循以下步骤:
- 合约 — 编写;用
private-reserve-auction.compact编译npm run compact - 理解隐私边界 — 隐藏价格 → 公开出价 → 揭示 → 申领(非屏蔽)→ 公开结算
- 提供者 — (来自
createConnectedSession)references/midnight-session.md - 部署 — 卖家将传入构造函数
(reservePriceStars, maxBidders, sellerSecret) - 出价 — 出价者调用(公开金额,私有身份)
bid(bidAmountStars, userAddress, secret) - 关闭 — 卖家调用(或满额时自动关闭)
closeAuction(secret) - 揭示 — 卖家调用(与提交值验证匹配)
revealPrice(reservePriceStars, secret) - 申领 — 获胜者调用+ 支付保留价(跨越边界)
claimItem(address, secret) - 收款 — 卖家在申领完成后调用
claimProceeds(address, secret) - UI — 角色卡片(卖家/出价者)、拍卖状态面板、索引器轮询获取公开状态
1) Project Structure
1) 项目结构
private-reserve-auction-dapp/
├── package.json
├── next.config.mjs
├── postcss.config.mjs
├── lib/
│ ├── isomorphic-ws-fix.mjs
│ ├── midnight.ts # session, patched provider, hex helpers
│ ├── auction.ts # deploy, circuits, decode state
│ ├── address.ts # Bech32 → UserAddress bytes
│ └── secret.ts # crypto.getRandomValues + localStorage
├── app/
│ ├── globals.css
│ ├── layout.tsx
│ ├── page.tsx # landing page
│ └── auction/
│ ├── page.tsx # server shell
│ └── AuctionClient.tsx # seller + bidder UI
├── contract/
│ ├── package.json
│ └── src/
│ ├── private-reserve-auction.compact
│ ├── index.ts # CompiledContract.withVacantWitnesses
│ └── managed/private-reserve-auction/ # compiler output (gitignored)
├── scripts/
│ └── sync-zk-assets.mjs # → public/zk/private-reserve-auction/
└── public/zk/private-reserve-auction/ # keys + zkir (gitignored until sync)private-reserve-auction-dapp/
├── package.json
├── next.config.mjs
├── postcss.config.mjs
├── lib/
│ ├── isomorphic-ws-fix.mjs
│ ├── midnight.ts # 会话、修补后的提供者、十六进制工具
│ ├── auction.ts # 部署、电路、解码状态
│ ├── address.ts # Bech32 → UserAddress字节
│ └── secret.ts # crypto.getRandomValues + localStorage
├── app/
│ ├── globals.css
│ ├── layout.tsx
│ ├── page.tsx # 着陆页
│ └── auction/
│ ├── page.tsx # 服务端外壳
│ └── AuctionClient.tsx # 卖家 + 出价者UI
├── contract/
│ ├── package.json
│ └── src/
│ ├── private-reserve-auction.compact
│ ├── index.ts # CompiledContract.withVacantWitnesses
│ └── managed/private-reserve-auction/ # 编译器输出(已加入git忽略)
├── scripts/
│ └── sync-zk-assets.mjs # → public/zk/private-reserve-auction/
└── public/zk/private-reserve-auction/ # 密钥 + zkir(同步前加入git忽略)2) Prerequisites
2) 前置条件
bash
node --version # 20+
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 on with tNIGHT for bids and reserve payment.
previewbash
node --version # 20+
docker --version # 可选,用于本地测试网
curl --proto '=https' --tlsv1.2 -sSf \
https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh
source $HOME/.local/bin/env浏览器:在网络上使用1AM钱包,并持有tNIGHT用于出价和保留价支付。
preview3) Root package.json
package.json3) 根目录package.json
package.jsonjson
{
"name": "private-reserve-auction-dapp",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "next dev",
"build": "npm run sync:assets && next build",
"compact": "npm run compact --prefix contract",
"sync:assets": "node scripts/sync-zk-assets.mjs",
"postinstall": "echo 'All deps at root level'"
},
"dependencies": {
"@midnight-ntwrk/compact-js": "^2.5.0",
"@midnight-ntwrk/compact-runtime": "0.16.0",
"@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",
"@tailwindcss/postcss": "^4.3.2",
"next": "^15.0.0",
"postcss": "^8.5.16",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^4.3.2"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.7.0"
}
}json
{
"name": "private-reserve-auction-dapp",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "next dev",
"build": "npm run sync:assets && next build",
"compact": "npm run compact --prefix contract",
"sync:assets": "node scripts/sync-zk-assets.mjs",
"postinstall": "echo 'All deps at root level'"
},
"dependencies": {
"@midnight-ntwrk/compact-js": "^2.5.0",
"@midnight-ntwrk/compact-runtime": "0.16.0",
"@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",
"@tailwindcss/postcss": "^4.3.2",
"next": "^15.0.0",
"postcss": "^8.5.16",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^4.3.2"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.7.0"
}
}4) contract/package.json
contract/package.json4) contract/package.json
contract/package.jsonjson
{
"name": "@private-reserve-auction/contract",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"compact": "compact compile src/private-reserve-auction.compact src/managed/private-reserve-auction"
},
"devDependencies": {
"@midnight-ntwrk/compact-runtime": "0.16.0"
}
}Compile:
bash
npm run compactjson
{
"name": "@private-reserve-auction/contract",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"compact": "compact compile src/private-reserve-auction.compact src/managed/private-reserve-auction"
},
"devDependencies": {
"@midnight-ntwrk/compact-runtime": "0.16.0"
}
}编译:
bash
npm run compact→ contract/src/managed/private-reserve-auction/{contract,keys,zkir}/
→ contract/src/managed/private-reserve-auction/{contract,keys,zkir}/
npm run sync:assets
npm run sync:assets
→ public/zk/private-reserve-auction/
→ public/zk/private-reserve-auction/
Expected circuits: `bid`, `closeAuction`, `revealPrice`, `claimItem`, `claimProceeds`.
---
预期电路:`bid`、`closeAuction`、`revealPrice`、`claimItem`、`claimProceeds`。
---5) contract/src/private-reserve-auction.compact
contract/src/private-reserve-auction.compact5) contract/src/private-reserve-auction.compact
contract/src/private-reserve-auction.compactcompact
pragma language_version 0.23;
import CompactStandardLibrary;
export enum AuctionState {
OPEN,
CLOSED,
SETTLED
}
export sealed ledger organizer: Bytes<32>;
export sealed ledger hiddenPrice: Bytes<32>;
export sealed ledger maxBids: Uint<16>;
export ledger publicPrice: Uint<32>;
export ledger auctionState: AuctionState;
export ledger bidders: Map<Bytes<32>, Uint<32>>;
export ledger bidCount: Counter;
export ledger highestBid: Uint<32>;
export ledger winnerClaimed: Set<UserAddress>;
constructor(minPrice: Uint<32>, maxBidCount: Uint<16>, _secret: Bytes<32>) {
assert(minPrice > 0, "Reserve price must be greater than zero");
assert(maxBidCount > 0, "Max bids must be greater than zero");
const pubKey = getDappPublicKey(_secret);
organizer = disclose(pubKey);
hiddenPrice = commitPrice(minPrice as Bytes<32>, _secret);
maxBids = disclose(maxBidCount);
publicPrice = 0;
highestBid = 0;
auctionState = AuctionState.OPEN;
}
export circuit bid(bidAmount: Uint<32>, _address: UserAddress, _secret: Bytes<32>): [] {
assert(auctionState == AuctionState.OPEN, "Auction is not open");
assert(bidCount < maxBids, "Bids are full");
assert(bidAmount > 0, "Bid must be greater than zero");
const pubKey = getDappPublicKey(_secret);
assert(pubKey != organizer, "Organizer cannot bid");
const bidderId = disclose(pubKey);
const publicBid = disclose(bidAmount);
if (bidders.member(bidderId)) {
assert(bidders.lookup(bidderId) < publicBid, "New bid must be higher");
}
bidders.insert(bidderId, publicBid);
bidCount.increment(1);
if (publicBid > highestBid) {
highestBid = publicBid;
}
if (bidCount == maxBids) {
auctionState = AuctionState.CLOSED;
}
}
export circuit closeAuction(_secret: Bytes<32>): [] {
const pubKey = getDappPublicKey(_secret);
assert(organizer == pubKey, "Only organizer can close");
assert(auctionState == AuctionState.OPEN, "Auction already closed");
auctionState = AuctionState.CLOSED;
}
export circuit revealPrice(minPrice: Uint<32>, _secret: Bytes<32>): [] {
const pubKey = getDappPublicKey(_secret);
assert(organizer == pubKey, "Only organizer can reveal");
assert(auctionState == AuctionState.CLOSED, "Auction not closed");
const hashedPrice = commitPrice(minPrice as Bytes<32>, _secret);
assert(hashedPrice == hiddenPrice, "Price mismatch — cannot change reserve");
publicPrice = disclose(minPrice);
auctionState = AuctionState.SETTLED;
}
export circuit claimItem(_address: UserAddress, _secret: Bytes<32>): [] {
assert(auctionState == AuctionState.SETTLED, "Auction not settled");
assert(highestBid >= publicPrice, "No valid winning bid");
assert(!winnerClaimed.member(disclose(_address)), "Already claimed");
const pubKey = getDappPublicKey(_secret);
const bidderId = disclose(pubKey);
assert(bidders.lookup(bidderId) == highestBid, "Not the highest bidder");
// Privacy boundary: winner pays reserve, identity revealed
receiveUnshielded(nativeToken(), publicPrice as Uint<128>);
winnerClaimed.insert(disclose(_address));
}
export circuit claimProceeds(_address: UserAddress, _secret: Bytes<32>): [] {
const pubKey = getDappPublicKey(_secret);
assert(organizer == pubKey, "Not organizer");
assert(auctionState == AuctionState.SETTLED, "Auction not settled");
assert(winnerClaimed.size() > 0, "No winner claimed");
sendUnshielded(
nativeToken(),
publicPrice as Uint<128>,
right<ContractAddress, UserAddress>(disclose(_address))
);
}
circuit commitPrice(_price: Bytes<32>, _secret: Bytes<32>): Bytes<32> {
return persistentCommit<Bytes<32>>(_price, _secret);
}
circuit getDappPublicKey(_secret: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([pad(32, "private-auction:pk:"), _secret]);
}compact
pragma language_version 0.23;
import CompactStandardLibrary;
export enum AuctionState {
OPEN,
CLOSED,
SETTLED
}
export sealed ledger organizer: Bytes<32>;
export sealed ledger hiddenPrice: Bytes<32>;
export sealed ledger maxBids: Uint<16>;
export ledger publicPrice: Uint<32>;
export ledger auctionState: AuctionState;
export ledger bidders: Map<Bytes<32>, Uint<32>>;
export ledger bidCount: Counter;
export ledger highestBid: Uint<32>;
export ledger winnerClaimed: Set<UserAddress>;
constructor(minPrice: Uint<32>, maxBidCount: Uint<16>, _secret: Bytes<32>) {
assert(minPrice > 0, "Reserve price must be greater than zero");
assert(maxBidCount > 0, "Max bids must be greater than zero");
const pubKey = getDappPublicKey(_secret);
organizer = disclose(pubKey);
hiddenPrice = commitPrice(minPrice as Bytes<32>, _secret);
maxBids = disclose(maxBidCount);
publicPrice = 0;
highestBid = 0;
auctionState = AuctionState.OPEN;
}
export circuit bid(bidAmount: Uint<32>, _address: UserAddress, _secret: Bytes<32>): [] {
assert(auctionState == AuctionState.OPEN, "Auction is not open");
assert(bidCount < maxBids, "Bids are full");
assert(bidAmount > 0, "Bid must be greater than zero");
const pubKey = getDappPublicKey(_secret);
assert(pubKey != organizer, "Organizer cannot bid");
const bidderId = disclose(pubKey);
const publicBid = disclose(bidAmount);
if (bidders.member(bidderId)) {
assert(bidders.lookup(bidderId) < publicBid, "New bid must be higher");
}
bidders.insert(bidderId, publicBid);
bidCount.increment(1);
if (publicBid > highestBid) {
highestBid = publicBid;
}
if (bidCount == maxBids) {
auctionState = AuctionState.CLOSED;
}
}
export circuit closeAuction(_secret: Bytes<32>): [] {
const pubKey = getDappPublicKey(_secret);
assert(organizer == pubKey, "Only organizer can close");
assert(auctionState == AuctionState.OPEN, "Auction already closed");
auctionState = AuctionState.CLOSED;
}
export circuit revealPrice(minPrice: Uint<32>, _secret: Bytes<32>): [] {
const pubKey = getDappPublicKey(_secret);
assert(organizer == pubKey, "Only organizer can reveal");
assert(auctionState == AuctionState.CLOSED, "Auction not closed");
const hashedPrice = commitPrice(minPrice as Bytes<32>, _secret);
assert(hashedPrice == hiddenPrice, "Price mismatch — cannot change reserve");
publicPrice = disclose(minPrice);
auctionState = AuctionState.SETTLED;
}
export circuit claimItem(_address: UserAddress, _secret: Bytes<32>): [] {
assert(auctionState == AuctionState.SETTLED, "Auction not settled");
assert(highestBid >= publicPrice, "No valid winning bid");
assert(!winnerClaimed.member(disclose(_address)), "Already claimed");
const pubKey = getDappPublicKey(_secret);
const bidderId = disclose(pubKey);
assert(bidders.lookup(bidderId) == highestBid, "Not the highest bidder");
// 隐私边界:获胜者支付保留价,身份公开
receiveUnshielded(nativeToken(), publicPrice as Uint<128>);
winnerClaimed.insert(disclose(_address));
}
export circuit claimProceeds(_address: UserAddress, _secret: Bytes<32>): [] {
const pubKey = getDappPublicKey(_secret);
assert(organizer == pubKey, "Not organizer");
assert(auctionState == AuctionState.SETTLED, "Auction not settled");
assert(winnerClaimed.size() > 0, "No winner claimed");
sendUnshielded(
nativeToken(),
publicPrice as Uint<128>,
right<ContractAddress, UserAddress>(disclose(_address))
);
}
circuit commitPrice(_price: Bytes<32>, _secret: Bytes<32>): Bytes<32> {
return persistentCommit<Bytes<32>>(_price, _secret);
}
circuit getDappPublicKey(_secret: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([pad(32, "private-auction:pk:"), _secret]);
}Privacy model summary
隐私模型总结
| Phase | Bidder identity | On-chain data |
|---|---|---|
| Deploy | Seller hidden | |
| Bid | Hidden (hash only) | Bid amount public in |
| Close | Hidden | |
| Reveal | Hidden | |
| Claim | Public | |
| Proceeds | Seller public | |
| 阶段 | 出价者身份 | 链上数据 |
|---|---|---|
| 部署 | 卖家隐藏 | |
| 出价 | 隐藏(仅哈希值) | 出价金额公开存储在 |
| 关闭 | 隐藏 | |
| 揭示 | 隐藏 | |
| 申领 | 公开 | |
| 收款 | 卖家公开 | |
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 - Bid amounts (disclosed in circuit)
bid
- 账本字段(后或安全提交的字段)
disclose() - 导出电路的返回值
- 合约间调用
- 非屏蔽代币转账(、
receiveUnshielded)sendUnshielded - 出价金额(在电路中披露)
bid
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(zkPath?: string): Promise<any> {
if (!_compiledContract) {
if (!_contractModule) {
_contractModule = await import('./managed/private-reserve-auction/contract/index.js');
}
_compiledContract = CompiledContract.make(
'private-reserve-auction',
_contractModule.Contract,
);
_compiledContract = CompiledContract.withVacantWitnesses(_compiledContract);
}
return _compiledContract;
}
export async function getLedger(): Promise<any> {
if (!_ledgerFn) {
if (!_contractModule) {
_contractModule = await import('./managed/private-reserve-auction/contract/index.js');
}
_ledgerFn = _contractModule.ledger;
}
return _ledgerFn;
}
export { sampleSigningKey, ContractState };无见证者 — 使用。使用延迟模式以避免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(zkPath?: string): Promise<any> {
if (!_compiledContract) {
if (!_contractModule) {
_contractModule = await import('./managed/private-reserve-auction/contract/index.js');
}
_compiledContract = CompiledContract.make(
'private-reserve-auction',
_contractModule.Contract,
);
_compiledContract = CompiledContract.withVacantWitnesses(_compiledContract);
}
return _compiledContract;
}
export async function getLedger(): Promise<any> {
if (!_ledgerFn) {
if (!_contractModule) {
_contractModule = await import('./managed/private-reserve-auction/contract/index.js');
}
_ledgerFn = _contractModule.ledger;
}
return _ledgerFn;
}
export { sampleSigningKey, ContractState };7) lib/address.ts
lib/address.ts7) lib/address.ts
lib/address.tsDecode Bech32 unshielded addresses for circuit args.
UserAddresstypescript
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) };
}Never pass raw Bech32 strings or shielded coin public keys where is expected. The wrapper is required for but not for — those are raw .
UserAddress{ bytes }UserAddressBytes<32>Uint8Array解码Bech32非屏蔽地址,用于电路参数。
UserAddresstypescript
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 }UserAddressBytes<32>Uint8Array8) lib/secret.ts
lib/secret.ts8) lib/secret.ts
lib/secret.tstypescript
import { fromHex, toHex } from './midnight';
export function generateSecret(): Uint8Array {
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
return bytes;
}
export function saveSecret(
role: 'seller' | 'bidder',
contractAddress: string,
secret: Uint8Array,
) {
localStorage.setItem(`private-auction:${role}:${contractAddress}`, toHex(secret));
}
export function loadSecret(
role: 'seller' | 'bidder',
contractAddress: string,
): Uint8Array | null {
const hex = localStorage.getItem(`private-auction:${role}:${contractAddress}`);
return hex ? fromHex(hex) : null;
}typescript
import { fromHex, toHex } from './midnight';
export function generateSecret(): Uint8Array {
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
return bytes;
}
export function saveSecret(
role: 'seller' | 'bidder',
contractAddress: string,
secret: Uint8Array,
) {
localStorage.setItem(`private-auction:${role}:${contractAddress}`, toHex(secret));
}
export function loadSecret(
role: 'seller' | 'bidder',
contractAddress: string,
): Uint8Array | null {
const hex = localStorage.getItem(`private-auction:${role}:${contractAddress}`);
return hex ? fromHex(hex) : null;
}9) Provider Setup — lib/midnight.ts
lib/midnight.ts9) 提供者设置 — lib/midnight.ts
lib/midnight.tstypescript
import { setNetworkId } from '@midnight-ntwrk/midnight-js-network-id';
import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
import { FetchZkConfigProvider } from '@midnight-ntwrk/midnight-js-fetch-zk-config-provider';
import { ContractState } from '@midnight-ntwrk/compact-runtime';
import type { MidnightProvider, WalletProvider } from '@midnight-ntwrk/midnight-js-types';
export type ConnectedSession = {
api: any;
config: any;
providers: {
privateStateProvider: ReturnType<typeof createPrivateStateProvider>;
publicDataProvider: ReturnType<typeof createPatchedPublicDataProvider>;
zkConfigProvider: FetchZkConfigProvider<any>;
proofProvider: { proveTx: (unprovenTx: any) => Promise<any> };
walletProvider: WalletProvider;
midnightProvider: MidnightProvider;
};
unshieldedAddress: string;
coinPublicKeyBytes: Uint8Array;
};
export function fromHex(hex: string): Uint8Array {
const h = hex.startsWith('0x') ? hex.slice(2) : hex;
return Uint8Array.from(h.match(/.{1,2}/g)!.map((b) => parseInt(b, 16)));
}
export function toHex(bytes: Uint8Array): string {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
function coinPublicKeyToBytes(pk: unknown): Uint8Array {
if (pk instanceof Uint8Array) return pk.length === 32 ? pk : pk.slice(0, 32);
if (typeof pk === 'string') {
const hex = pk.startsWith('0x') ? pk.slice(2) : pk;
if (hex.length === 64 && /^[0-9a-fA-F]+$/.test(hex)) return fromHex(hex);
return new Uint8Array(32);
}
if (Array.isArray(pk)) {
return new Uint8Array(pk.length >= 32 ? pk.slice(0, 32) : [...pk, ...new Uint8Array(32 - pk.length)]);
}
if (pk && typeof pk === 'object' && 'bytes' in (pk as object)) {
return coinPublicKeyToBytes((pk as { bytes: unknown }).bytes);
}
return new Uint8Array(32);
}
function createPrivateStateProvider() {
let scope = '';
const stateStore = new Map<string, unknown>();
const signingKeyStore = new Map<string, unknown>();
const key = (id: string) => `${scope}:${id}`;
return {
setContractAddress(address: string) {
scope = address;
},
async set(id: string, state: unknown) {
stateStore.set(key(id), state);
},
async get(id: string) {
return stateStore.get(key(id)) ?? null;
},
async remove(id: string) {
stateStore.delete(key(id));
},
async clear() {
stateStore.clear();
},
async setSigningKey(addr: string, k: unknown) {
signingKeyStore.set(addr, k);
},
async getSigningKey(addr: string) {
return signingKeyStore.get(addr) ?? null;
},
async removeSigningKey(addr: string) {
signingKeyStore.delete(addr);
},
async clearSigningKeys() {
signingKeyStore.clear();
},
async exportPrivateStates(): Promise<never> {
throw new Error('Not implemented.');
},
async importPrivateStates(): Promise<never> {
throw new Error('Not implemented.');
},
async exportSigningKeys(): Promise<never> {
throw new Error('Not implemented.');
},
async importSigningKeys(): Promise<never> {
throw new Error('Not implemented.');
},
};
}
function createPatchedPublicDataProvider(queryUrl: string, subscriptionUrl: string) {
const base = indexerPublicDataProvider(queryUrl, subscriptionUrl);
return {
...base,
async queryContractState(contractAddress: string, config?: unknown) {
if (config) return base.queryContractState(contractAddress, config as never);
const res = await fetch(queryUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
query: `query LATEST_CONTRACT_STATE($address: HexEncoded!) {
contractAction(address: $address) { state }
}`,
variables: { address: contractAddress },
}),
});
if (!res.ok) throw new Error(`Indexer HTTP error: ${res.status}`);
const payload = await res.json();
if (payload.errors?.length) {
throw new Error(payload.errors.map((e: { message: string }) => e.message).join('; '));
}
const action = payload.data?.contractAction ?? null;
return action ? ContractState.deserialize(fromHex(action.state)) : null;
},
};
}
export async function createConnectedSession(
api: any,
zkAssetBasePath: string,
): Promise<ConnectedSession> {
const [config, unshieldedAddress, shieldedAddress] = await Promise.all([
api.getConfiguration(),
api.getUnshieldedAddress(),
api.getShieldedAddresses(),
]);
setNetworkId(config.networkId);
const zkConfigProvider = new FetchZkConfigProvider(
new URL(zkAssetBasePath, window.location.origin).toString(),
window.fetch.bind(window),
);
const provingProvider = await api.getProvingProvider(zkConfigProvider);
const proofProvider = {
async proveTx(unprovenTx: any) {
const { CostModel } = await import('@midnight-ntwrk/ledger-v8');
return unprovenTx.prove(provingProvider, CostModel.initialCostModel());
},
};
const walletProvider: WalletProvider = {
getCoinPublicKey: () => shieldedAddress.shieldedCoinPublicKey,
getEncryptionPublicKey: () => shieldedAddress.shieldedEncryptionPublicKey,
balanceTx: async (tx: any) => {
const txHex = toHex(tx.serialize());
const balanced = await api.balanceUnsealedTransaction(txHex);
if (!balanced?.tx) throw new Error('balanceUnsealedTransaction returned invalid result');
const { Transaction } = await import('@midnight-ntwrk/ledger-v8');
return Transaction.deserialize('signature', 'proof', 'binding', fromHex(balanced.tx));
},
};
const midnightProvider: MidnightProvider = {
submitTx: async (tx: any) => {
const txHex = toHex(tx.serialize());
const result = await api.submitTransaction(txHex);
if (typeof result === 'string' && result) return result;
if (result?.transactionId) return result.transactionId;
if (result?.id) return result.id;
return txHex.slice(0, 64);
},
};
return {
api,
config,
providers: {
privateStateProvider: createPrivateStateProvider(),
publicDataProvider: createPatchedPublicDataProvider(config.indexerUri, config.indexerWsUri),
zkConfigProvider,
proofProvider,
walletProvider,
midnightProvider,
},
unshieldedAddress: unshieldedAddress.unshieldedAddress,
coinPublicKeyBytes: coinPublicKeyToBytes(shieldedAddress.shieldedCoinPublicKey),
};
}
export async function pollForState(
queryUrl: string,
contractAddress: string,
maxAttempts = 120,
intervalMs = 2000,
): Promise<string> {
for (let i = 0; i < maxAttempts; i++) {
const res = await fetch(queryUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
query: `query LATEST_CONTRACT_STATE($address: HexEncoded!) {
contractAction(address: $address) { state }
}`,
variables: { address: contractAddress },
}),
});
if (res.ok) {
const payload = await res.json();
const state = payload.data?.contractAction?.state;
if (state) return state;
}
await new Promise((r) => setTimeout(r, intervalMs));
}
throw new Error(`Contract state not indexed after ${(maxAttempts * intervalMs) / 1000}s`);
}
export async function detectWallet(): Promise<any> {
const w =
(window as any).midnight?.['1am'] ??
Object.values((window as any).midnight ?? {})[0];
if (!w) throw new Error('No Midnight wallet extension found');
return w;
}Key implementation details:
-
wraps the SDK's
createPatchedPublicDataProviderand interceptsindexerPublicDataProvider. When no config is passed (the polling path), it falls back to a rawqueryContractState+ hand-written GraphQL query that omits thefetchparameter entirely — this sidesteps the GraphQLoffsetbug without needing a separate SDK version fix.offset: null -
defensively normalizes four input shapes:
coinPublicKeyToBytes, hex string, plainUint8Array, andnumber[]wrapper. This is necessary because different wallet SDK versions return coin public keys in different formats.{ bytes: ... } -
is a clean in-memory Map-based provider with no
createPrivateStateProviderdependency — avoids pulling in the SDK's own private state provider which would add another WASM bundle.@midnight-ntwrk -
Lazy WASM imports —dynamically imports
proofProvider.proveTxfor@midnight-ntwrk/ledger-v8, andCostModeldoes the same forbalanceTx. This avoids loading WASM-backed modules at module-evaluation time, which matters because the same pattern prevents the "dual WASM instance" bug (see troubleshooting). If you statically importTransactionat the top of this file, it will conflict withledger-v8's own WASM instance.compact-runtime -
front-loads
createConnectedSession,getConfiguration, andgetUnshieldedAddressinto a singlegetShieldedAddresses. If any of these throw (e.g. wallet not fully initialized, or user rejected the connection prompt), the entire session creation fails. The caller should catch this and surface a meaningful error — seePromise.allinconnectWallet.AuctionClient.tsx -
throws if no wallet is found rather than returning
detectWallet. Callers mustnullor the rejection will be unhandled. The UI component uses.catch()to translate this into a.then().catch()state — see the bug note in troubleshooting.false
typescript
import { setNetworkId } from '@midnight-ntwrk/midnight-js-network-id';
import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
import { FetchZkConfigProvider } from '@midnight-ntwrk/midnight-js-fetch-zk-config-provider';
import { ContractState } from '@midnight-ntwrk/compact-runtime';
import type { MidnightProvider, WalletProvider } from '@midnight-ntwrk/midnight-js-types';
export type ConnectedSession = {
api: any;
config: any;
providers: {
privateStateProvider: ReturnType<typeof createPrivateStateProvider>;
publicDataProvider: ReturnType<typeof createPatchedPublicDataProvider>;
zkConfigProvider: FetchZkConfigProvider<any>;
proofProvider: { proveTx: (unprovenTx: any) => Promise<any> };
walletProvider: WalletProvider;
midnightProvider: MidnightProvider;
};
unshieldedAddress: string;
coinPublicKeyBytes: Uint8Array;
};
export function fromHex(hex: string): Uint8Array {
const h = hex.startsWith('0x') ? hex.slice(2) : hex;
return Uint8Array.from(h.match(/.{1,2}/g)!.map((b) => parseInt(b, 16)));
}
export function toHex(bytes: Uint8Array): string {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
function coinPublicKeyToBytes(pk: unknown): Uint8Array {
if (pk instanceof Uint8Array) return pk.length === 32 ? pk : pk.slice(0, 32);
if (typeof pk === 'string') {
const hex = pk.startsWith('0x') ? pk.slice(2) : pk;
if (hex.length === 64 && /^[0-9a-fA-F]+$/.test(hex)) return fromHex(hex);
return new Uint8Array(32);
}
if (Array.isArray(pk)) {
return new Uint8Array(pk.length >= 32 ? pk.slice(0, 32) : [...pk, ...new Uint8Array(32 - pk.length)]);
}
if (pk && typeof pk === 'object' && 'bytes' in (pk as object)) {
return coinPublicKeyToBytes((pk as { bytes: unknown }).bytes);
}
return new Uint8Array(32);
}
function createPrivateStateProvider() {
let scope = '';
const stateStore = new Map<string, unknown>();
const signingKeyStore = new Map<string, unknown>();
const key = (id: string) => `${scope}:${id}`;
return {
setContractAddress(address: string) {
scope = address;
},
async set(id: string, state: unknown) {
stateStore.set(key(id), state);
},
async get(id: string) {
return stateStore.get(key(id)) ?? null;
},
async remove(id: string) {
stateStore.delete(key(id));
},
async clear() {
stateStore.clear();
},
async setSigningKey(addr: string, k: unknown) {
signingKeyStore.set(addr, k);
},
async getSigningKey(addr: string) {
return signingKeyStore.get(addr) ?? null;
},
async removeSigningKey(addr: string) {
signingKeyStore.delete(addr);
},
async clearSigningKeys() {
signingKeyStore.clear();
},
async exportPrivateStates(): Promise<never> {
throw new Error('Not implemented.');
},
async importPrivateStates(): Promise<never> {
throw new Error('Not implemented.');
},
async exportSigningKeys(): Promise<never> {
throw new Error('Not implemented.');
},
async importSigningKeys(): Promise<never> {
throw new Error('Not implemented.');
},
};
}
function createPatchedPublicDataProvider(queryUrl: string, subscriptionUrl: string) {
const base = indexerPublicDataProvider(queryUrl, subscriptionUrl);
return {
...base,
async queryContractState(contractAddress: string, config?: unknown) {
if (config) return base.queryContractState(contractAddress, config as never);
const res = await fetch(queryUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
query: `query LATEST_CONTRACT_STATE($address: HexEncoded!) {
contractAction(address: $address) { state }
}`,
variables: { address: contractAddress },
}),
});
if (!res.ok) throw new Error(`Indexer HTTP error: ${res.status}`);
const payload = await res.json();
if (payload.errors?.length) {
throw new Error(payload.errors.map((e: { message: string }) => e.message).join('; '));
}
const action = payload.data?.contractAction ?? null;
return action ? ContractState.deserialize(fromHex(action.state)) : null;
},
};
}
export async function createConnectedSession(
api: any,
zkAssetBasePath: string,
): Promise<ConnectedSession> {
const [config, unshieldedAddress, shieldedAddress] = await Promise.all([
api.getConfiguration(),
api.getUnshieldedAddress(),
api.getShieldedAddresses(),
]);
setNetworkId(config.networkId);
const zkConfigProvider = new FetchZkConfigProvider(
new URL(zkAssetBasePath, window.location.origin).toString(),
window.fetch.bind(window),
);
const provingProvider = await api.getProvingProvider(zkConfigProvider);
const proofProvider = {
async proveTx(unprovenTx: any) {
const { CostModel } = await import('@midnight-ntwrk/ledger-v8');
return unprovenTx.prove(provingProvider, CostModel.initialCostModel());
},
};
const walletProvider: WalletProvider = {
getCoinPublicKey: () => shieldedAddress.shieldedCoinPublicKey,
getEncryptionPublicKey: () => shieldedAddress.shieldedEncryptionPublicKey,
balanceTx: async (tx: any) => {
const txHex = toHex(tx.serialize());
const balanced = await api.balanceUnsealedTransaction(txHex);
if (!balanced?.tx) throw new Error('balanceUnsealedTransaction returned invalid result');
const { Transaction } = await import('@midnight-ntwrk/ledger-v8');
return Transaction.deserialize('signature', 'proof', 'binding', fromHex(balanced.tx));
},
};
const midnightProvider: MidnightProvider = {
submitTx: async (tx: any) => {
const txHex = toHex(tx.serialize());
const result = await api.submitTransaction(txHex);
if (typeof result === 'string' && result) return result;
if (result?.transactionId) return result.transactionId;
if (result?.id) return result.id;
return txHex.slice(0, 64);
},
};
return {
api,
config,
providers: {
privateStateProvider: createPrivateStateProvider(),
publicDataProvider: createPatchedPublicDataProvider(config.indexerUri, config.indexerWsUri),
zkConfigProvider,
proofProvider,
walletProvider,
midnightProvider,
},
unshieldedAddress: unshieldedAddress.unshieldedAddress,
coinPublicKeyBytes: coinPublicKeyToBytes(shieldedAddress.shieldedCoinPublicKey),
};
}
export async function pollForState(
queryUrl: string,
contractAddress: string,
maxAttempts = 120,
intervalMs = 2000,
): Promise<string> {
for (let i = 0; i < maxAttempts; i++) {
const res = await fetch(queryUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
query: `query LATEST_CONTRACT_STATE($address: HexEncoded!) {
contractAction(address: $address) { state }
}`,
variables: { address: contractAddress },
}),
});
if (res.ok) {
const payload = await res.json();
const state = payload.data?.contractAction?.state;
if (state) return state;
}
await new Promise((r) => setTimeout(r, intervalMs));
}
throw new Error(`Contract state not indexed after ${(maxAttempts * intervalMs) / 1000}s`);
}
export async function detectWallet(): Promise<any> {
const w =
(window as any).midnight?.['1am'] ??
Object.values((window as any).midnight ?? {})[0];
if (!w) throw new Error('No Midnight wallet extension found');
return w;
}核心实现细节:
-
包装SDK的
createPatchedPublicDataProvider,并拦截indexerPublicDataProvider。当未传入config时(轮询路径),回退到原生queryContractState+ 手写GraphQL查询,完全省略fetch参数 — 无需单独修复SDK版本即可解决GraphQLoffsetbug。offset: null -
防御性地标准化四种输入格式:
coinPublicKeyToBytes、十六进制字符串、普通Uint8Array和number[]包装器。这是必要的,因为不同钱包SDK版本返回的代币公钥格式不同。{ bytes: ... } -
是一个基于内存Map的简洁提供者,无
createPrivateStateProvider依赖 — 避免引入SDK自身的私有状态提供者,后者会添加额外的WASM包。@midnight-ntwrk -
延迟WASM导入 —动态导入
proofProvider.proveTx获取@midnight-ntwrk/ledger-v8,CostModel同理导入balanceTx。这避免了在模块评估时加载WASM-backed模块,这种模式可防止“双重WASM实例”bug(见故障排除)。如果在文件顶部静态导入Transaction,会与ledger-v8自身的WASM实例冲突。compact-runtime -
将
createConnectedSession、getConfiguration和getUnshieldedAddress前置到单个getShieldedAddresses中。如果其中任何一个抛出错误(例如钱包未完全初始化,或用户拒绝连接提示),整个会话创建会失败。调用者应捕获错误并显示有意义的提示 — 见Promise.all中的AuctionClient.tsx。connectWallet -
在未找到钱包时抛出错误而非返回
detectWallet。调用者必须使用null,否则拒绝会未处理。UI组件使用.catch()将其转换为.then().catch()状态 — 见故障排除中的bug说明。false
10) lib/auction.ts
lib/auction.ts10) lib/auction.ts
lib/auction.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 = 'PrivateAuctionState';
export const ZK_PATH = '/zk/private-reserve-auction';
const AUCTION_STATE_NAMES = ['OPEN', 'CLOSED', 'SETTLED'] as const;
export type AuctionStateName = (typeof AUCTION_STATE_NAMES)[number];
let _compiledContract: any = null;
async function makeCompiledContract() {
if (!_compiledContract) {
_compiledContract = await getCompiledContract(ZK_PATH);
}
return _compiledContract;
}
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 deployAuction(
session: ConnectedSession,
reservePriceNight: number,
maxBidders: number,
sellerSecret: Uint8Array,
): Promise<string> {
const reservePriceStars = reservePriceNight * 1_000_000;
const cc = await makeCompiledContract();
const deployTxData = await (createUnprovenDeployTx as any)(
{
zkConfigProvider: session.providers.zkConfigProvider,
walletProvider: session.providers.walletProvider,
},
{
compiledContract: cc,
args: [BigInt(reservePriceStars), BigInt(maxBidders), sellerSecret],
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 makeCompiledContract();
await (submitCallTxAsync as any)(session.providers, {
compiledContract: cc,
contractAddress,
circuitId,
args,
privateStateId: PRIVATE_STATE_ID,
});
}
export const placeBid = (session: ConnectedSession, contractAddress: string, bidAmountNight: number, userAddress: { bytes: Uint8Array }, bidderSecret: Uint8Array) =>
call(session, contractAddress, 'bid', [BigInt(bidAmountNight * 1_000_000), userAddress, bidderSecret]);
export const closeAuction = (session: ConnectedSession, contractAddress: string, sellerSecret: Uint8Array) =>
call(session, contractAddress, 'closeAuction', [sellerSecret]);
export const revealPrice = (session: ConnectedSession, contractAddress: string, reservePriceNight: number, sellerSecret: Uint8Array) =>
call(session, contractAddress, 'revealPrice', [BigInt(reservePriceNight * 1_000_000), sellerSecret]);
export const claimItem = (session: ConnectedSession, contractAddress: string, userAddress: { bytes: Uint8Array }, bidderSecret: Uint8Array) =>
call(session, contractAddress, 'claimItem', [userAddress, bidderSecret]);
export const claimProceeds = (session: ConnectedSession, contractAddress: string, sellerAddress: { bytes: Uint8Array }, sellerSecret: Uint8Array) =>
call(session, contractAddress, 'claimProceeds', [sellerAddress, sellerSecret]);
export async function decodeAuctionState(stateHex: string) {
const contractState = ContractState.deserialize(fromHex(stateHex));
const ledger = await getLedger();
const l = ledger(contractState.data) as any;
const stateIdx = Number(l.auctionState);
return {
auctionState: (AUCTION_STATE_NAMES[stateIdx] ?? `UNKNOWN(${stateIdx})`) as AuctionStateName | string,
auctionStateIndex: stateIdx,
maxBidders: Number(l.maxBids),
publicPriceNight: Number(l.publicPrice) / 1_000_000,
highestBidNight: Number(l.highestBid) / 1_000_000,
bidCount: setSize(l.bidCount),
bidderCount: setSize(l.bidders),
};
}
export async function fetchAuctionState(queryUrl: string, contractAddress: string) {
const hex = await pollForState(queryUrl, contractAddress);
return decodeAuctionState(hex);
}
export function userAddressFromSession(session: ConnectedSession) {
return bech32ToUserAddress(session.unshieldedAddress, session.config.networkId);
}Key details:
- use
argsforBigInt(...)fields, not plain numbersUint<32> - 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 modulegetLedger() - handles both
setSize()(property) and.size(method) since compiled.size()/Setledger fields vary by SDK versionCounter - Reserve price in NIGHT is converted to Stars (×1,000,000) before passing to contract
使用(延迟单例)和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 = 'PrivateAuctionState';
export const ZK_PATH = '/zk/private-reserve-auction';
const AUCTION_STATE_NAMES = ['OPEN', 'CLOSED', 'SETTLED'] as const;
export type AuctionStateName = (typeof AUCTION_STATE_NAMES)[number];
let _compiledContract: any = null;
async function makeCompiledContract() {
if (!_compiledContract) {
_compiledContract = await getCompiledContract(ZK_PATH);
}
return _compiledContract;
}
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 deployAuction(
session: ConnectedSession,
reservePriceNight: number,
maxBidders: number,
sellerSecret: Uint8Array,
): Promise<string> {
const reservePriceStars = reservePriceNight * 1_000_000;
const cc = await makeCompiledContract();
const deployTxData = await (createUnprovenDeployTx as any)(
{
zkConfigProvider: session.providers.zkConfigProvider,
walletProvider: session.providers.walletProvider,
},
{
compiledContract: cc,
args: [BigInt(reservePriceStars), BigInt(maxBidders), sellerSecret],
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 makeCompiledContract();
await (submitCallTxAsync as any)(session.providers, {
compiledContract: cc,
contractAddress,
circuitId,
args,
privateStateId: PRIVATE_STATE_ID,
});
}
export const placeBid = (session: ConnectedSession, contractAddress: string, bidAmountNight: number, userAddress: { bytes: Uint8Array }, bidderSecret: Uint8Array) =>
call(session, contractAddress, 'bid', [BigInt(bidAmountNight * 1_000_000), userAddress, bidderSecret]);
export const closeAuction = (session: ConnectedSession, contractAddress: string, sellerSecret: Uint8Array) =>
call(session, contractAddress, 'closeAuction', [sellerSecret]);
export const revealPrice = (session: ConnectedSession, contractAddress: string, reservePriceNight: number, sellerSecret: Uint8Array) =>
call(session, contractAddress, 'revealPrice', [BigInt(reservePriceNight * 1_000_000), sellerSecret]);
export const claimItem = (session: ConnectedSession, contractAddress: string, userAddress: { bytes: Uint8Array }, bidderSecret: Uint8Array) =>
call(session, contractAddress, 'claimItem', [userAddress, bidderSecret]);
export const claimProceeds = (session: ConnectedSession, contractAddress: string, sellerAddress: { bytes: Uint8Array }, sellerSecret: Uint8Array) =>
call(session, contractAddress, 'claimProceeds', [sellerAddress, sellerSecret]);
export async function decodeAuctionState(stateHex: string) {
const contractState = ContractState.deserialize(fromHex(stateHex));
const ledger = await getLedger();
const l = ledger(contractState.data) as any;
const stateIdx = Number(l.auctionState);
return {
auctionState: (AUCTION_STATE_NAMES[stateIdx] ?? `UNKNOWN(${stateIdx})`) as AuctionStateName | string,
auctionStateIndex: stateIdx,
maxBidders: Number(l.maxBids),
publicPriceNight: Number(l.publicPrice) / 1_000_000,
highestBidNight: Number(l.highestBid) / 1_000_000,
bidCount: setSize(l.bidCount),
bidderCount: setSize(l.bidders),
};
}
export async function fetchAuctionState(queryUrl: string, contractAddress: string) {
const hex = await pollForState(queryUrl, contractAddress);
return decodeAuctionState(hex);
}
export function userAddressFromSession(session: ConnectedSession) {
return bech32ToUserAddress(session.unshieldedAddress, session.config.networkId);
}核心细节:
- 对
args字段使用Uint<32>,而非普通数字BigInt(...) - 参数作为原始
Bytes<32>传递,不包装在Uint8Array中。{ bytes: ... }参数需要UserAddress包装器{ bytes } - 通过合约自身编译模块的
ledger()解析getLedger() - 处理
setSize()(属性)和.size(方法),因为编译后的.size()/Set账本字段因SDK版本而异Counter - UI中的NIGHT保留价转换为Stars(×1,000,000)后传给合约
11) Frontend — app/auction/AuctionClient.tsx
app/auction/AuctionClient.tsx11) 前端 — app/auction/AuctionClient.tsx
app/auction/AuctionClient.tsxtsx
'use client';
import { useState, useCallback, useEffect, useRef } from 'react';
import { detectWallet, createConnectedSession, pollForState } from '@/lib/midnight';
import {
deployAuction,
placeBid,
closeAuction,
revealPrice,
claimItem,
claimProceeds,
fetchAuctionState,
userAddressFromSession,
ZK_PATH,
} from '@/lib/auction';
import { generateSecret, loadSecret, saveSecret } from '@/lib/secret';
import type { ConnectedSession } from '@/lib/midnight';
type Role = 'seller' | 'bidder';
const STATE_LABELS: Record<string, string> = {
OPEN: 'Open for Bids',
CLOSED: 'Bidding Closed',
SETTLED: 'Settled',
};
export default function AuctionClient() {
const [session, setSession] = useState<ConnectedSession | null>(null);
const [role, setRole] = useState<Role>('bidder');
const [contractAddress, setContractAddress] = useState('');
const [auctionState, setAuctionState] = useState<Awaited<ReturnType<typeof fetchAuctionState>> | null>(null);
const [error, setError] = useState('');
const [connecting, setConnecting] = useState(false);
const [busy, setBusy] = useState(false);
const [statusMessage, setStatusMessage] = useState('');
const [walletInstalled, setWalletInstalled] = useState<boolean | null>(null);
const mountedRef = useRef(true);
const [reservePriceNight, setReservePriceNight] = useState('0.01');
const [maxBidders, setMaxBidders] = useState('5');
const [bidAmountNight, setBidAmountNight] = useState('0.02');
useEffect(() => {
detectWallet()
.then((w) => setWalletInstalled(w !== null))
.catch(() => setWalletInstalled(false));
return () => { mountedRef.current = false; };
}, []);
const withLoading = useCallback(async <T,>(
message: string,
fn: (setStatus: (msg: string) => void) => Promise<T>,
): Promise<T> => {
setBusy(true);
setError('');
setStatusMessage(message);
try {
const result = await fn((msg: string) => {
if (mountedRef.current) setStatusMessage(msg);
});
return result;
} catch (e) {
if (mountedRef.current) {
setError(e instanceof Error ? e.message : String(e));
}
throw e;
} finally {
if (mountedRef.current) {
setBusy(false);
setStatusMessage('');
}
}
}, []);
const refresh = useCallback(async () => {
if (!session || !contractAddress) return;
try {
const state = await fetchAuctionState(session.config.indexerUri, contractAddress);
if (mountedRef.current) setAuctionState(state);
} catch (e) {
if (mountedRef.current) setError(e instanceof Error ? e.message : 'Refresh failed');
}
}, [session, contractAddress]);
useEffect(() => { void refresh(); }, [refresh]);
const connectWallet = useCallback(async () => {
setConnecting(true);
setError('');
try {
const wallet = await detectWallet();
if (!wallet) {
setError('1AM wallet not detected. Please install the 1AM browser extension.');
return;
}
const api = await wallet.connect('preview');
const s = await createConnectedSession(api, ZK_PATH);
setSession(s);
} catch (e) {
setError(e instanceof Error ? e.message : 'Failed to connect wallet');
} finally {
setConnecting(false);
}
}, []);
const handleDeploy = useCallback(async () => {
if (!session) return;
await withLoading('Deploying auction contract…', async (setStatus) => {
const secret = generateSecret();
const addr = await deployAuction(
session,
Number(reservePriceNight),
Number(maxBidders),
secret,
);
setContractAddress(addr);
saveSecret('seller', addr, secret);
setStatus('Waiting for indexer…');
const state = await fetchAuctionState(session.config.indexerUri, addr);
setAuctionState(state);
});
}, [session, withLoading, reservePriceNight, maxBidders]);
const handlePlaceBid = useCallback(async () => {
if (!session || !contractAddress) return;
await withLoading('Placing bid (proving + submitting)…', async (setStatus) => {
let secret = loadSecret('bidder', contractAddress);
if (!secret) {
secret = generateSecret();
saveSecret('bidder', contractAddress, secret);
}
await placeBid(session, contractAddress, Number(bidAmountNight), userAddressFromSession(session), secret);
setStatus('Waiting for indexer…');
const state = await fetchAuctionState(session.config.indexerUri, contractAddress);
setAuctionState(state);
});
}, [session, contractAddress, withLoading, bidAmountNight]);
const handleCloseAuction = useCallback(async () => {
if (!session || !contractAddress) return;
const secret = loadSecret('seller', contractAddress);
if (!secret) { setError('Seller secret not found.'); return; }
await withLoading('Closing auction…', async (setStatus) => {
await closeAuction(session, contractAddress, secret);
setStatus('Waiting for indexer…');
const state = await fetchAuctionState(session.config.indexerUri, contractAddress);
setAuctionState(state);
});
}, [session, contractAddress, withLoading]);
const handleRevealPrice = useCallback(async () => {
if (!session || !contractAddress) return;
const secret = loadSecret('seller', contractAddress);
if (!secret) { setError('Seller secret not found.'); return; }
await withLoading('Revealing reserve price…', async (setStatus) => {
await revealPrice(session, contractAddress, Number(reservePriceNight), secret);
setStatus('Waiting for indexer…');
const state = await fetchAuctionState(session.config.indexerUri, contractAddress);
setAuctionState(state);
});
}, [session, contractAddress, withLoading, reservePriceNight]);
const handleClaimItem = useCallback(async () => {
if (!session || !contractAddress) return;
const secret = loadSecret('bidder', contractAddress);
if (!secret) { setError('Bidder secret not found. Place a bid first.'); return; }
await withLoading('Claiming item (proving + submitting)…', async (setStatus) => {
await claimItem(session, contractAddress, userAddressFromSession(session), secret);
setStatus('Waiting for indexer…');
const state = await fetchAuctionState(session.config.indexerUri, contractAddress);
setAuctionState(state);
});
}, [session, contractAddress, withLoading]);
const handleClaimProceeds = useCallback(async () => {
if (!session || !contractAddress) return;
const secret = loadSecret('seller', contractAddress);
if (!secret) { setError('Seller secret not found.'); return; }
await withLoading('Claiming proceeds…', async (setStatus) => {
await claimProceeds(session, contractAddress, userAddressFromSession(session), secret);
setStatus('Waiting for indexer…');
const state = await fetchAuctionState(session.config.indexerUri, contractAddress);
setAuctionState(state);
});
}, [session, contractAddress, withLoading]);
const reset = useCallback(() => {
setContractAddress('');
setAuctionState(null);
setError('');
}, []);
if (walletInstalled === false) {
return (
<div className="text-center py-20">
<h2 className="text-xl font-semibold mb-4">1AM Wallet Required</h2>
<p className="text-zinc-600 dark:text-zinc-400 mb-6">
Please install the <strong>1AM</strong> browser extension for Midnight Network.
</p>
<a
href="https://1am.xyz"
target="_blank"
rel="noopener noreferrer"
className="inline-flex h-11 items-center justify-center rounded-full bg-zinc-900 px-6 text-sm font-medium text-white hover:bg-zinc-800 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
>
Install 1AM Wallet
</a>
</div>
);
}
const state = auctionState;
return (
<div className="mx-auto max-w-lg w-full">
<div className="text-center mb-8">
<h1 className="text-2xl font-bold tracking-tight">Private Reserve Auction</h1>
<p className="mt-2 text-sm text-zinc-500 dark:text-zinc-400">
Hidden reserve price, public bids, private bidder identities
</p>
</div>
{!session && (
<div className="text-center">
<button
onClick={connectWallet}
disabled={connecting}
className="inline-flex h-11 items-center justify-center rounded-full bg-zinc-900 px-8 text-sm font-medium text-white hover:bg-zinc-800 disabled:opacity-50 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
>
{connecting ? 'Connecting…' : 'Connect 1AM Wallet'}
</button>
</div>
)}
{session && (
<div className="mb-6 rounded-lg border border-zinc-200 bg-zinc-50 p-4 text-xs dark:border-zinc-800 dark:bg-zinc-900/50">
<p className="font-medium text-zinc-500 uppercase tracking-wider mb-2">Wallet</p>
<p className="text-zinc-700 dark:text-zinc-300 truncate">
<span className="text-zinc-400">Unshielded: </span>
{session.unshieldedAddress}
</p>
<p className="text-zinc-500 mt-1">
Network: <span className="font-medium text-zinc-700 dark:text-zinc-300">{session.config.networkId}</span>
</p>
</div>
)}
{session && !contractAddress && (
<div className="grid grid-cols-2 gap-4 mb-6">
<button
onClick={() => setRole('seller')}
className={`rounded-lg border p-5 text-left transition ${
role === 'seller'
? 'border-zinc-900 dark:border-white bg-zinc-100 dark:bg-zinc-800'
: 'border-zinc-200 dark:border-zinc-800 hover:border-zinc-400 dark:hover:border-zinc-600'
}`}
>
<p className="text-sm font-semibold">I'm a Seller</p>
<p className="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
Deploy an auction with a hidden reserve price
</p>
</button>
<button
onClick={() => setRole('bidder')}
className={`rounded-lg border p-5 text-left transition ${
role === 'bidder'
? 'border-zinc-900 dark:border-white bg-zinc-100 dark:bg-zinc-800'
: 'border-zinc-200 dark:border-zinc-800 hover:border-zinc-400 dark:hover:border-zinc-600'
}`}
>
<p className="text-sm font-semibold">I'm a Bidder</p>
<p className="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
Place private bids on an existing auction
</p>
</button>
</div>
)}
{/* Deploy form — seller only, no contract yet */}
{session && !contractAddress && role === 'seller' && !busy && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<label className="flex flex-col gap-1 text-xs text-zinc-500 dark:text-zinc-400">
Reserve price (NIGHT)
<input
type="number"
min={0.000001}
step={0.01}
value={reservePriceNight}
onChange={(e) => setReservePriceNight(e.target.value)}
className="h-10 rounded-lg border border-zinc-200 bg-white px-3 text-sm dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100"
/>
</label>
<label className="flex flex-col gap-1 text-xs text-zinc-500 dark:text-zinc-400">
Max bidders
<input
type="number"
min={1}
value={maxBidders}
onChange={(e) => setMaxBidders(e.target.value)}
className="h-10 rounded-lg border border-zinc-200 bg-white px-3 text-sm dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100"
/>
</label>
</div>
<button
onClick={handleDeploy}
className="w-full h-11 rounded-full bg-zinc-900 text-sm font-medium text-white hover:bg-zinc-800 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
>
Deploy Auction
</button>
</div>
)}
{/* Bidder hint — no contract yet */}
{session && !contractAddress && role === 'bidder' && !busy && (
<div className="text-center text-sm text-zinc-500 dark:text-zinc-400 py-8">
<p>Paste a deployed auction contract address to participate.</p>
<p className="mt-2">Ask the seller for the contract address.</p>
</div>
)}
{/* Auction state + actions */}
{session && contractAddress && (
<div className="space-y-6">
<div className="flex items-center justify-center gap-2 text-xs">
<span className="rounded-full border border-zinc-200 dark:border-zinc-800 px-3 py-1 text-zinc-500 dark:text-zinc-400">
Viewing as <strong className="text-zinc-700 dark:text-zinc-300 capitalize">{role}</strong>
</span>
<button
onClick={reset}
className="text-zinc-400 underline underline-offset-2 hover:text-zinc-600 dark:hover:text-zinc-300"
>
switch role
</button>
</div>
{state && (
<div className="rounded-lg border border-zinc-200 p-6 text-center dark:border-zinc-800">
<p className="text-xs text-zinc-400 uppercase tracking-wider mb-1">Auction State</p>
<p className="text-2xl font-bold tracking-tight">
{STATE_LABELS[state.auctionState] ?? state.auctionState}
</p>
<div className="mt-4 flex justify-center gap-6 text-sm text-zinc-500 dark:text-zinc-400">
<span>Bids: <strong className="text-zinc-700 dark:text-zinc-300">{state.bidCount}</strong> / {state.maxBidders}</span>
{state.highestBidNight > 0 && (
<span>Highest: <strong className="text-zinc-700 dark:text-zinc-300">{state.highestBidNight}</strong> NIGHT</span>
)}
</div>
{state.publicPriceNight > 0 && (
<p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
Reserve: <strong className="text-zinc-700 dark:text-zinc-300">{state.publicPriceNight}</strong> NIGHT
</p>
)}
</div>
)}
{/* Seller actions */}
{role === 'seller' && (
<div className="space-y-3">
{state?.auctionState === 'OPEN' && (
<button
onClick={handleCloseAuction}
disabled={busy}
className="w-full h-12 rounded-full bg-zinc-900 text-sm font-medium text-white hover:bg-zinc-800 disabled:opacity-50 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
>
{busy ? statusMessage || 'Processing…' : 'Close Auction'}
</button>
)}
{state?.auctionState === 'CLOSED' && (
<>
<label className="flex flex-col gap-1 text-xs text-zinc-500 dark:text-zinc-400">
Reveal reserve price (NIGHT) — must match deploy value
<input
type="number"
min={0.000001}
step={0.01}
value={reservePriceNight}
onChange={(e) => setReservePriceNight(e.target.value)}
className="h-10 rounded-lg border border-zinc-200 bg-white px-3 text-sm dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100"
/>
</label>
<button
onClick={handleRevealPrice}
disabled={busy}
className="w-full h-12 rounded-full bg-zinc-900 text-sm font-medium text-white hover:bg-zinc-800 disabled:opacity-50 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
>
{busy ? statusMessage || 'Processing…' : 'Reveal Reserve Price'}
</button>
</>
)}
{state?.auctionState === 'SETTLED' && (
<button
onClick={handleClaimProceeds}
disabled={busy}
className="w-full h-12 rounded-full bg-zinc-900 text-sm font-medium text-white hover:bg-zinc-800 disabled:opacity-50 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
>
{busy ? statusMessage || 'Processing…' : 'Claim Proceeds'}
</button>
)}
</div>
)}
{/* Bidder actions */}
{role === 'bidder' && (
<div className="space-y-3">
{state?.auctionState === 'OPEN' && (
<>
<label className="flex flex-col gap-1 text-xs text-zinc-500 dark:text-zinc-400">
Your bid (NIGHT)
<input
type="number"
min={0.000001}
step={0.01}
value={bidAmountNight}
onChange={(e) => setBidAmountNight(e.target.value)}
className="h-10 rounded-lg border border-zinc-200 bg-white px-3 text-sm dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100"
/>
</label>
<button
onClick={handlePlaceBid}
disabled={busy}
className="w-full h-12 rounded-full bg-zinc-900 text-sm font-medium text-white hover:bg-zinc-800 disabled:opacity-50 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
>
{busy ? statusMessage || 'Processing…' : 'Place Bid'}
</button>
</>
)}
{state?.auctionState === 'SETTLED' && state.highestBidNight >= state.publicPriceNight && (
<button
onClick={handleClaimItem}
disabled={busy}
className="w-full h-12 rounded-full bg-zinc-900 text-sm font-medium text-white hover:bg-zinc-800 disabled:opacity-50 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
>
{busy ? statusMessage || 'Processing…' : 'Claim Item (pays reserve → public)'}
</button>
)}
</div>
)}
<div className="rounded-lg border border-zinc-200 bg-zinc-50 p-3 text-xs dark:border-zinc-800 dark:bg-zinc-900/50">
<p className="text-zinc-500">
<span className="text-zinc-400">Contract: </span>
<span className="font-mono text-zinc-700 dark:text-zinc-300 break-all">{contractAddress}</span>
</p>
</div>
<div className="flex justify-center gap-3">
<button
onClick={() => void refresh()}
disabled={busy}
className="text-xs text-zinc-400 underline underline-offset-2 hover:text-zinc-600 disabled:opacity-40 dark:hover:text-zinc-300"
>
refresh
</button>
<button
onClick={reset}
disabled={busy}
className="text-xs text-zinc-400 underline underline-offset-2 hover:text-zinc-600 disabled:opacity-40 dark:hover:text-zinc-300"
>
new contract
</button>
</div>
</div>
)}
{busy && !contractAddress && (
<div className="mt-6 text-center">
<div className="inline-flex items-center gap-2 rounded-full bg-zinc-100 px-4 py-2 text-xs text-zinc-600 dark:bg-zinc-900 dark:text-zinc-400">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-zinc-400 animate-pulse" />
{statusMessage}
</div>
</div>
)}
{error && (
<div className="mt-6 rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700 dark:border-red-900 dark:bg-red-950 dark:text-red-400">
{error}
</div>
)}
</div>
);
}tsx
'use client';
import { useState, useCallback, useEffect, useRef } from 'react';
import { detectWallet, createConnectedSession, pollForState } from '@/lib/midnight';
import {
deployAuction,
placeBid,
closeAuction,
revealPrice,
claimItem,
claimProceeds,
fetchAuctionState,
userAddressFromSession,
ZK_PATH,
} from '@/lib/auction';
import { generateSecret, loadSecret, saveSecret } from '@/lib/secret';
import type { ConnectedSession } from '@/lib/midnight';
type Role = 'seller' | 'bidder';
const STATE_LABELS: Record<string, string> = {
OPEN: '开放出价',
CLOSED: '出价已关闭',
SETTLED: '已结算',
};
export default function AuctionClient() {
const [session, setSession] = useState<ConnectedSession | null>(null);
const [role, setRole] = useState<Role>('bidder');
const [contractAddress, setContractAddress] = useState('');
const [auctionState, setAuctionState] = useState<Awaited<ReturnType<typeof fetchAuctionState>> | null>(null);
const [error, setError] = useState('');
const [connecting, setConnecting] = useState(false);
const [busy, setBusy] = useState(false);
const [statusMessage, setStatusMessage] = useState('');
const [walletInstalled, setWalletInstalled] = useState<boolean | null>(null);
const mountedRef = useRef(true);
const [reservePriceNight, setReservePriceNight] = useState('0.01');
const [maxBidders, setMaxBidders] = useState('5');
const [bidAmountNight, setBidAmountNight] = useState('0.02');
useEffect(() => {
detectWallet()
.then((w) => setWalletInstalled(w !== null))
.catch(() => setWalletInstalled(false));
return () => { mountedRef.current = false; };
}, []);
const withLoading = useCallback(async <T,>(
message: string,
fn: (setStatus: (msg: string) => void) => Promise<T>,
): Promise<T> => {
setBusy(true);
setError('');
setStatusMessage(message);
try {
const result = await fn((msg: string) => {
if (mountedRef.current) setStatusMessage(msg);
});
return result;
} catch (e) {
if (mountedRef.current) {
setError(e instanceof Error ? e.message : String(e));
}
throw e;
} finally {
if (mountedRef.current) {
setBusy(false);
setStatusMessage('');
}
}
}, []);
const refresh = useCallback(async () => {
if (!session || !contractAddress) return;
try {
const state = await fetchAuctionState(session.config.indexerUri, contractAddress);
if (mountedRef.current) setAuctionState(state);
} catch (e) {
if (mountedRef.current) setError(e instanceof Error ? e.message : '刷新失败');
}
}, [session, contractAddress]);
useEffect(() => { void refresh(); }, [refresh]);
const connectWallet = useCallback(async () => {
setConnecting(true);
setError('');
try {
const wallet = await detectWallet();
if (!wallet) {
setError('未检测到1AM钱包,请安装1AM浏览器扩展。');
return;
}
const api = await wallet.connect('preview');
const s = await createConnectedSession(api, ZK_PATH);
setSession(s);
} catch (e) {
setError(e instanceof Error ? e.message : '连接钱包失败');
} finally {
setConnecting(false);
}
}, []);
const handleDeploy = useCallback(async () => {
if (!session) return;
await withLoading('部署拍卖合约…', async (setStatus) => {
const secret = generateSecret();
const addr = await deployAuction(
session,
Number(reservePriceNight),
Number(maxBidders),
secret,
);
setContractAddress(addr);
saveSecret('seller', addr, secret);
setStatus('等待索引器同步…');
const state = await fetchAuctionState(session.config.indexerUri, addr);
setAuctionState(state);
});
}, [session, withLoading, reservePriceNight, maxBidders]);
const handlePlaceBid = useCallback(async () => {
if (!session || !contractAddress) return;
await withLoading('提交出价(证明 + 上链)…', async (setStatus) => {
let secret = loadSecret('bidder', contractAddress);
if (!secret) {
secret = generateSecret();
saveSecret('bidder', contractAddress, secret);
}
await placeBid(session, contractAddress, Number(bidAmountNight), userAddressFromSession(session), secret);
setStatus('等待索引器同步…');
const state = await fetchAuctionState(session.config.indexerUri, contractAddress);
setAuctionState(state);
});
}, [session, contractAddress, withLoading, bidAmountNight]);
const handleCloseAuction = useCallback(async () => {
if (!session || !contractAddress) return;
const secret = loadSecret('seller', contractAddress);
if (!secret) { setError('未找到卖家密钥。'); return; }
await withLoading('关闭拍卖…', async (setStatus) => {
await closeAuction(session, contractAddress, secret);
setStatus('等待索引器同步…');
const state = await fetchAuctionState(session.config.indexerUri, contractAddress);
setAuctionState(state);
});
}, [session, contractAddress, withLoading]);
const handleRevealPrice = useCallback(async () => {
if (!session || !contractAddress) return;
const secret = loadSecret('seller', contractAddress);
if (!secret) { setError('未找到卖家密钥。'); return; }
await withLoading('揭示保留价…', async (setStatus) => {
await revealPrice(session, contractAddress, Number(reservePriceNight), secret);
setStatus('等待索引器同步…');
const state = await fetchAuctionState(session.config.indexerUri, contractAddress);
setAuctionState(state);
});
}, [session, contractAddress, withLoading, reservePriceNight]);
const handleClaimItem = useCallback(async () => {
if (!session || !contractAddress) return;
const secret = loadSecret('bidder', contractAddress);
if (!secret) { setError('未找到出价者密钥,请先出价。'); return; }
await withLoading('申领物品(证明 + 上链)…', async (setStatus) => {
await claimItem(session, contractAddress, userAddressFromSession(session), secret);
setStatus('等待索引器同步…');
const state = await fetchAuctionState(session.config.indexerUri, contractAddress);
setAuctionState(state);
});
}, [session, contractAddress, withLoading]);
const handleClaimProceeds = useCallback(async () => {
if (!session || !contractAddress) return;
const secret = loadSecret('seller', contractAddress);
if (!secret) { setError('未找到卖家密钥。'); return; }
await withLoading('申领收款…', async (setStatus) => {
await claimProceeds(session, contractAddress, userAddressFromSession(session), secret);
setStatus('等待索引器同步…');
const state = await fetchAuctionState(session.config.indexerUri, contractAddress);
setAuctionState(state);
});
}, [session, contractAddress, withLoading]);
const reset = useCallback(() => {
setContractAddress('');
setAuctionState(null);
setError('');
}, []);
if (walletInstalled === false) {
return (
<div className="text-center py-20">
<h2 className="text-xl font-semibold mb-4">需要1AM钱包</h2>
<p className="text-zinc-600 dark:text-zinc-400 mb-6">
请为Midnight Network安装<strong>1AM</strong>浏览器扩展。
</p>
<a
href="https://1am.xyz"
target="_blank"
rel="noopener noreferrer"
className="inline-flex h-11 items-center justify-center rounded-full bg-zinc-900 px-6 text-sm font-medium text-white hover:bg-zinc-800 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
>
安装1AM钱包
</a>
</div>
);
}
const state = auctionState;
return (
<div className="mx-auto max-w-lg w-full">
<div className="text-center mb-8">
<h1 className="text-2xl font-bold tracking-tight">私有保留拍卖</h1>
<p className="mt-2 text-sm text-zinc-500 dark:text-zinc-400">
隐藏保留价、公开出价、私有出价者身份
</p>
</div>
{!session && (
<div className="text-center">
<button
onClick={connectWallet}
disabled={connecting}
className="inline-flex h-11 items-center justify-center rounded-full bg-zinc-900 px-8 text-sm font-medium text-white hover:bg-zinc-800 disabled:opacity-50 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
>
{connecting ? '连接中…' : '连接1AM钱包'}
</button>
</div>
)}
{session && (
<div className="mb-6 rounded-lg border border-zinc-200 bg-zinc-50 p-4 text-xs dark:border-zinc-800 dark:bg-zinc-900/50">
<p className="font-medium text-zinc-500 uppercase tracking-wider mb-2">钱包信息</p>
<p className="text-zinc-700 dark:text-zinc-300 truncate">
<span className="text-zinc-400">非屏蔽地址: </span>
{session.unshieldedAddress}
</p>
<p className="text-zinc-500 mt-1">
网络: <span className="font-medium text-zinc-700 dark:text-zinc-300">{session.config.networkId}</span>
</p>
</div>
)}
{session && !contractAddress && (
<div className="grid grid-cols-2 gap-4 mb-6">
<button
onClick={() => setRole('seller')}
className={`rounded-lg border p-5 text-left transition ${
role === 'seller'
? 'border-zinc-900 dark:border-white bg-zinc-100 dark:bg-zinc-800'
: 'border-zinc-200 dark:border-zinc-800 hover:border-zinc-400 dark:hover:border-zinc-600'
}`}
>
<p className="text-sm font-semibold">我是卖家</p>
<p className="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
部署带有隐藏保留价的拍卖
</p>
</button>
<button
onClick={() => setRole('bidder')}
className={`rounded-lg border p-5 text-left transition ${
role === 'bidder'
? 'border-zinc-900 dark:border-white bg-zinc-100 dark:bg-zinc-800'
: 'border-zinc-200 dark:border-zinc-800 hover:border-zinc-400 dark:hover:border-zinc-600'
}`}
>
<p className="text-sm font-semibold">我是出价者</p>
<p className="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
参与已部署的拍卖并提交私有出价
</p>
</button>
</div>
)}
{/* 部署表单 — 仅卖家可用,无合约时显示 */}
{session && !contractAddress && role === 'seller' && !busy && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<label className="flex flex-col gap-1 text-xs text-zinc-500 dark:text-zinc-400">
保留价(NIGHT)
<input
type="number"
min={0.000001}
step={0.01}
value={reservePriceNight}
onChange={(e) => setReservePriceNight(e.target.value)}
className="h-10 rounded-lg border border-zinc-200 bg-white px-3 text-sm dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100"
/>
</label>
<label className="flex flex-col gap-1 text-xs text-zinc-500 dark:text-zinc-400">
最大出价人数
<input
type="number"
min={1}
value={maxBidders}
onChange={(e) => setMaxBidders(e.target.value)}
className="h-10 rounded-lg border border-zinc-200 bg-white px-3 text-sm dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100"
/>
</label>
</div>
<button
onClick={handleDeploy}
className="w-full h-11 rounded-full bg-zinc-900 text-sm font-medium text-white hover:bg-zinc-800 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
>
部署拍卖
</button>
</div>
)}
{/* 出价者提示 — 无合约时显示 */}
{session && !contractAddress && role === 'bidder' && !busy && (
<div className="text-center text-sm text-zinc-500 dark:text-zinc-400 py-8">
<p>粘贴已部署的拍卖合约地址以参与。</p>
<p className="mt-2">请向卖家索要合约地址。</p>
</div>
)}
{/* 拍卖状态 + 操作按钮 */}
{session && contractAddress && (
<div className="space-y-6">
<div className="flex items-center justify-center gap-2 text-xs">
<span className="rounded-full border border-zinc-200 dark:border-zinc-800 px-3 py-1 text-zinc-500 dark:text-zinc-400">
当前身份:<strong className="text-zinc-700 dark:text-zinc-300 capitalize">{role}</strong>
</span>
<button
onClick={reset}
className="text-zinc-400 underline underline-offset-2 hover:text-zinc-600 dark:hover:text-zinc-300"
>
切换身份
</button>
</div>
{state && (
<div className="rounded-lg border border-zinc-200 p-6 text-center dark:border-zinc-800">
<p className="text-xs text-zinc-400 uppercase tracking-wider mb-1">拍卖状态</p>
<p className="text-2xl font-bold tracking-tight">
{STATE_LABELS[state.auctionState] ?? state.auctionState}
</p>
<div className="mt-4 flex justify-center gap-6 text-sm text-zinc-500 dark:text-zinc-400">
<span>出价次数: <strong className="text-zinc-700 dark:text-zinc-300">{state.bidCount}</strong> / {state.maxBidders}</span>
{state.highestBidNight > 0 && (
<span>最高出价: <strong className="text-zinc-700 dark:text-zinc-300">{state.highestBidNight}</strong> NIGHT</span>
)}
</div>
{state.publicPriceNight > 0 && (
<p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
保留价: <strong className="text-zinc-700 dark:text-zinc-300">{state.publicPriceNight}</strong> NIGHT
</p>
)}
</div>
)}
{/* 卖家操作 */}
{role === 'seller' && (
<div className="space-y-3">
{state?.auctionState === 'OPEN' && (
<button
onClick={handleCloseAuction}
disabled={busy}
className="w-full h-12 rounded-full bg-zinc-900 text-sm font-medium text-white hover:bg-zinc-800 disabled:opacity-50 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
>
{busy ? statusMessage || '处理中…' : '关闭拍卖'}
</button>
)}
{state?.auctionState === 'CLOSED' && (
<>
<label className="flex flex-col gap-1 text-xs text-zinc-500 dark:text-zinc-400">
揭示保留价(NIGHT)——必须与部署值一致
<input
type="number"
min={0.000001}
step={0.01}
value={reservePriceNight}
onChange={(e) => setReservePriceNight(e.target.value)}
className="h-10 rounded-lg border border-zinc-200 bg-white px-3 text-sm dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100"
/>
</label>
<button
onClick={handleRevealPrice}
disabled={busy}
className="w-full h-12 rounded-full bg-zinc-900 text-sm font-medium text-white hover:bg-zinc-800 disabled:opacity-50 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
>
{busy ? statusMessage || '处理中…' : '揭示保留价'}
</button>
</>
)}
{state?.auctionState === 'SETTLED' && (
<button
onClick={handleClaimProceeds}
disabled={busy}
className="w-full h-12 rounded-full bg-zinc-900 text-sm font-medium text-white hover:bg-zinc-800 disabled:opacity-50 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
>
{busy ? statusMessage || '处理中…' : '申领收款'}
</button>
)}
</div>
)}
{/* 出价者操作 */}
{role === 'bidder' && (
<div className="space-y-3">
{state?.auctionState === 'OPEN' && (
<>
<label className="flex flex-col gap-1 text-xs text-zinc-500 dark:text-zinc-400">
你的出价(NIGHT)
<input
type="number"
min={0.000001}
step={0.01}
value={bidAmountNight}
onChange={(e) => setBidAmountNight(e.target.value)}
className="h-10 rounded-lg border border-zinc-200 bg-white px-3 text-sm dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100"
/>
</label>
<button
onClick={handlePlaceBid}
disabled={busy}
className="w-full h-12 rounded-full bg-zinc-900 text-sm font-medium text-white hover:bg-zinc-800 disabled:opacity-50 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
>
{busy ? statusMessage || '处理中…' : '提交出价'}
</button>
</>
)}
{state?.auctionState === 'SETTLED' && state.highestBidNight >= state.publicPriceNight && (
<button
onClick={handleClaimItem}
disabled={busy}
className="w-full h-12 rounded-full bg-zinc-900 text-sm font-medium text-white hover:bg-zinc-800 disabled:opacity-50 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
>
{busy ? statusMessage || '处理中…' : '申领物品(支付保留价 → 身份公开)'}
</button>
)}
</div>
)}
<div className="rounded-lg border border-zinc-200 bg-zinc-50 p-3 text-xs dark:border-zinc-800 dark:bg-zinc-900/50">
<p className="text-zinc-500">
<span className="text-zinc-400">合约地址: </span>
<span className="font-mono text-zinc-700 dark:text-zinc-300 break-all">{contractAddress}</span>
</p>
</div>
<div className="flex justify-center gap-3">
<button
onClick={() => void refresh()}
disabled={busy}
className="text-xs text-zinc-400 underline underline-offset-2 hover:text-zinc-600 disabled:opacity-40 dark:hover:text-zinc-300"
>
刷新
</button>
<button
onClick={reset}
disabled={busy}
className="text-xs text-zinc-400 underline underline-offset-2 hover:text-zinc-600 disabled:opacity-40 dark:hover:text-zinc-300"
>
新建合约
</button>
</div>
</div>
)}
{busy && !contractAddress && (
<div className="mt-6 text-center">
<div className="inline-flex items-center gap-2 rounded-full bg-zinc-100 px-4 py-2 text-xs text-zinc-600 dark:bg-zinc-900 dark:text-zinc-400">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-zinc-400 animate-pulse" />
{statusMessage}
</div>
</div>
)}
{error && (
<div className="mt-6 rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700 dark:border-red-900 dark:bg-red-950 dark:text-red-400">
{error}
</div>
)}
</div>
);
}withLoading
pattern
withLoadingwithLoading
模式
withLoadingEvery circuit call is wrapped in — a reusable helper that centralizes busy/error/status state:
withLoading(message, fn)- Sets and
busy = truebefore executionstatusMessage = message - Passes a callback into
setStatusso the async operation can update the status mid-flight (e.g. "Waiting for indexer…")fn - Catches errors and surfaces in the
e.messagestateerror - Resets and
busyinstatusMessagefinally - Uses to avoid setting state on unmounted components
mountedRef
This pattern is worth reusing for any Midnight dApp UI — it eliminates repetitive try/catch/setError blocks across every handler.
每个电路调用都包装在中 — 这是一个可复用的工具函数,集中处理忙碌/错误/状态信息:
withLoading(message, fn)- 执行前设置和
busy = truestatusMessage = message - 向传入
fn回调,以便异步操作中途更新状态(例如“等待索引器同步…”)setStatus - 捕获错误并在状态中显示
errore.message - 在中重置
finally和busystatusMessage - 使用避免在已卸载组件上设置状态
mountedRef
这种模式适用于任何Midnight DApp UI — 可消除每个处理函数中重复的try/catch/setError代码块。
Bidder auto-generate secret
出价者自动生成密钥
In , the bidder secret is loaded from . If none exists (first bid), a new one is generated and saved immediately. This means bidders never need to manually manage secrets — the UI handles it transparently. The seller path does not auto-generate; the secret is created once during deploy and must be recovered from for close/reveal/proceeds.
handlePlaceBidlocalStoragelocalStorage在中,出价者密钥从加载。如果不存在(首次出价),会立即生成并保存新密钥。这意味着出价者无需手动管理密钥 — UI会透明处理。卖家路径不自动生成;密钥在部署时创建一次,必须从恢复才能执行关闭/揭示/收款操作。
handlePlaceBidlocalStoragelocalStorage12) 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-reserve-auction');
const dest = join(root, 'public/zk/private-reserve-auction');
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-reserve-auction/keys/bid.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-reserve-auction');
const dest = join(root, 'public/zk/private-reserve-auction');
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-reserve-auction/keys/bid.prover13) End-to-End Browser Flow
13) 端到端浏览器流程
1. npm install && npm run compact && npm run sync:assets
2. npm run dev
3. Seller: Connect 1AM → Select "I'm a Seller" → Deploy (0.01 NIGHT reserve, 5 max bidders) → copy contract address
4. Bidder (other browser/wallet): Connect → Select "I'm a Bidder" → Paste address → Place bid (0.02 NIGHT)
5. Seller: Close auction (or auto-closes when 5 bids received)
6. Seller: Reveal reserve price (must match deploy value: 0.01 NIGHT)
7. Bidder: Claim item (pays 0.01 NIGHT unshielded — address now public on ledger)
8. Seller: Claim proceeds (0.01 NIGHT to unshielded address)
9. Poll indexer — verify bidCount, highestBid, publicPrice, auctionState transitions1. npm install && npm run compact && npm run sync:assets
2. npm run dev
3. 卖家:连接1AM → 选择“我是卖家” → 部署(0.01 NIGHT保留价,最多5人出价)→ 复制合约地址
4. 出价者(其他浏览器/钱包):连接 → 选择“我是出价者” → 粘贴地址 → 提交出价(0.02 NIGHT)
5. 卖家:关闭拍卖(或收到5次出价后自动关闭)
6. 卖家:揭示保留价(必须与部署值一致:0.01 NIGHT)
7. 出价者:申领物品(支付0.01 NIGHT非屏蔽代币 — 地址现在在账本上公开)
8. 卖家:申领收款(0.01 NIGHT到非屏蔽地址)
9. 轮询索引器 — 验证bidCount、highestBid、publicPrice、auctionState的状态转换14) Troubleshooting
14) 故障排除
| Symptom | Cause | Fix |
|---|---|---|
| Seller secret used for bid | Use separate bidder secret |
| Bid amount <= previous bid | Increase bid amount |
| Bid after close | Bid only in OPEN state |
| Wrong seller secret | Reload from |
| Wrong price in revealPrice | Use same reserve price as deploy |
| Claiming with non-winning secret | Use the secret of the highest bidder |
| Bech32 passed as bytes | Use |
| Deploy hangs 30–120s | Used | Use |
GraphQL | Default indexer provider | Use patched |
| ZK 404 | Assets not synced | |
| Lost seller secret | No recovery on-chain | Redeploy contract; store secret in localStorage |
| | Import |
| Dual | Delete |
| Passed | Wrap in |
| Confusing it with | Raw |
| Field name mismatch ( | Access |
| Counter doesn't support assignment | Don't initialize in constructor; use |
| Network mismatch | | Use |
| | Add |
| Wallet not fully initialized or user rejected prompt | Wrap |
| Static | Use dynamic |
| 症状 | 原因 | 修复方案 |
|---|---|---|
| 使用卖家密钥出价 | 使用独立的出价者密钥 |
| 出价金额≤之前的出价 | 提高出价金额 |
| 拍卖关闭后出价 | 仅在OPEN状态出价 |
| 卖家密钥错误 | 从 |
| 揭示保留价时输入错误 | 使用与部署时相同的保留价 |
| 使用非获胜者密钥申领 | 使用最高出价者的密钥 |
| 将Bech32字符串作为字节传入 | 使用 |
| 部署挂起30–120秒 | 使用了 | 使用 |
GraphQL | 使用默认索引器提供者 | 使用修补后的 |
| ZK资源404 | 资产未同步 | 运行 |
| 卖家密钥丢失 | 链上无恢复机制 | 重新部署合约;将密钥存储在localStorage中 |
| | 从 |
| 存在双重 | 删除 |
构造函数参数 | 在需要 | 对 |
| 与 | |
| 字段名称不匹配( | 访问 |
| Counter不支持赋值 | 不要在构造函数中初始化;使用 |
| 网络不匹配 | | 使用 |
| | 添加 |
连接时 | 钱包未完全初始化或用户拒绝连接提示 | 将 |
| 在模块顶部静态 | 在函数体内使用动态 |
15) Agent Checklist
15) 开发检查清单
When generating this dApp for a user:
- Write with all five exported circuits + helper circuits
private-reserve-auction.compact - Compile; sync ZK assets to
public/zk/private-reserve-auction/ - Use from
CompiledContract.withVacantWitnesses(not@midnight-ntwrk/compact-js)compact-runtime - Lazy /
getCompiledContract()singleton pattern (avoid dual WASM instance bug)getLedger() - Wire with patched indexer
createConnectedSession - Constructor args:
[BigInt(reservePriceStars), BigInt(maxBidders), rawUint8Array] - Decode unshielded Bech32 via
wallet-sdk-address-format - Store seller/bidder secrets in per contract address
localStorage - UI explains privacy boundary before claim button
- Next.js: +
asyncWebAssembly: truein webpack config;topLevelAwait: trueforresolve.fallback,fs,net,tls; aliaschild_processisomorphic-ws - Delete if present — root-level
contract/node_modulesmust be the solenode_modulescopycompact-runtime - Tailwind: counter dapp pattern — Geist fonts, buttons,
rounded-fulldark modeprefers-color-scheme - Document: reserve price in NIGHT (UI) / Stars (contract); 1 NIGHT = 1,000,000 Stars
为用户生成此DApp时:
- 编写包含所有5个导出电路 + 辅助电路的
private-reserve-auction.compact - 编译;将ZK资产同步到
public/zk/private-reserve-auction/ - 使用来自的
@midnight-ntwrk/compact-js(而非CompiledContract.withVacantWitnesses)compact-runtime - 使用延迟/
getCompiledContract()单例模式(避免双重WASM实例bug)getLedger() - 连接带有修补后索引器的
createConnectedSession - 构造函数参数:
[BigInt(reservePriceStars), BigInt(maxBidders), rawUint8Array] - 通过解码非屏蔽Bech32地址
wallet-sdk-address-format - 按合约地址将卖家/出价者密钥存储在中
localStorage - UI在申领按钮前解释隐私边界
- Next.js:webpack配置中设置+
asyncWebAssembly: true;为topLevelAwait: true、fs、net、tls设置child_process;别名resolve.fallbackisomorphic-ws - 删除(如果存在)——根目录
contract/node_modules必须是唯一的node_modules副本compact-runtime - Tailwind:采用DApp通用样式 — Geist字体、按钮、
rounded-full深色模式prefers-color-scheme - 文档说明:UI中使用NIGHT(保留价)/合约中使用Stars;1 NIGHT = 1,000,000 Stars
16) Related Skills
16) 相关技能
| Next step | Skill |
|---|---|
| Wallet connect only | |
| Unshielded token flows | |
| Payment vault pattern | |
| Privacy audit | |
| Compact language reference | |
| Local vitest harness | |
| 下一步 | 技能 |
|---|---|
| 仅钱包连接 | |
| 非屏蔽代币流程 | |
| 支付金库模式 | |
| 隐私审计 | |
| Compact语言参考 | |
| 本地vitest测试框架 | |