example-locker-dapp
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMidnight Network Locker DApp
Midnight Network 锁仓dApp
A locker dApp holds unshielded NIGHT in a Compact contract until (Unix seconds, ). No one — including the deployer — can withdraw early. After the deadline, only the beneficiary (proven via witness) can call and send tokens to a recipient address.
unlockTimeUint<64>releaseRunnable template: Copy for a complete Next.js project (contract + UI). Run after installing the 1AM wallet.
templates/locker-dapp/npm install && npm run compact && npm run devWhat this skill produces:
- — Compact locker vault + TypeScript witnesses + compile scripts
contract/ - — Next.js client UI (connect, deploy, lock, release, status)
app/locker/ - — wallet session + patched indexer provider (copy from
lib/midnight.tsorreferences/midnight-session.md)templates/locker-dapp/lib/midnight.ts - — deploy,
lib/locker.ts,lockTokens, ledger decoderelease - — ZK proving assets synced from contract build
public/zk/locker/
Shared references (canonical provider + troubleshooting — do not duplicate in prompts):
- —
references/midnight-session.md, indexer patch, deploy/call helperscreateConnectedSession - — preprod deploy hangs, GraphQL
references/gotchas.md, ZK asset pathsoffset: null - — pinned
references/versions.jsonversions@midnight-ntwrk/*
Primary references:
- — provider wiring, low-level deploy/call, indexer patch
example-payment-dapp/ - — 1AM browser extension, dust-free flow
1am-wallet/ - —
compact//blockTimeGte,blockTimeLt, ledger ADTsdisclose() - — unshielded NIGHT units (Stars),
token-transfers/recipient patternUserAddress - — poll contract state after transactions
indexer/
Key architecture notes:
- Use — never compare raw block timestamps on ledger (privacy + API design)
blockTimeGte(unlockTime) - Always use for Unix timestamps —
Uint<64>cannot hold current epoch valuesUint<16> - Use +
createUnprovenDeployTx— notsubmitTxAsync(hangs on preprod)deployContract() - Wrap with the patched
indexerPublicDataProvider(GraphQLqueryContractStatebug)offset: null - Token amounts are in Stars (1 NIGHT = 1_000_000 Stars); use everywhere
BigInt - Beneficiary public key bytes are stored on-chain at lock time; witness proves release authority
beneficiaryKey
一款锁仓dApp将未屏蔽的NIGHT代币存放在Compact合约中,直至(Unix时间戳,类型为)。包括部署者在内的任何人都无法提前提取。截止时间过后,只有受益人(通过见证证明身份)可调用方法,将代币发送至指定接收地址。
unlockTimeUint<64>release可运行模板: 复制目录获取完整的Next.js项目(合约+UI)。安装1AM钱包后,运行即可启动。
templates/locker-dapp/npm install && npm run compact && npm run dev本技能生成的内容:
- — Compact锁仓金库合约 + TypeScript见证脚本 + 编译脚本
contract/ - — Next.js客户端UI(连接钱包、部署合约、锁仓、提取、状态展示)
app/locker/ - — 钱包会话 + 修补后的索引器提供者(从
lib/midnight.ts或references/midnight-session.md复制)templates/locker-dapp/lib/midnight.ts - — 部署、
lib/locker.ts、lockTokens、账本解码逻辑release - — 从合约构建同步而来的ZK证明资产
public/zk/locker/
共享参考资料(标准提供者+故障排查——请勿在提示中重复):
- —
references/midnight-session.md、索引器补丁、部署/调用工具createConnectedSession - — 预生产环境部署卡顿、GraphQL
references/gotchas.md问题、ZK资产路径offset: null - — 固定版本的
references/versions.json依赖@midnight-ntwrk/*
主要参考资料:
- — 提供者配置、底层部署/调用、索引器补丁
example-payment-dapp/ - — 1AM浏览器扩展、无小额资产流程
1am-wallet/ - —
compact//blockTimeGte、blockTimeLt、账本ADTsdisclose() - — 未屏蔽NIGHT单位(Stars)、
token-transfers/接收者模式UserAddress - — 交易完成后轮询合约状态
indexer/
关键架构说明:
- 使用——切勿在账本上直接比较原始区块时间戳(隐私+API设计考量)
blockTimeGte(unlockTime) - 始终使用存储Unix时间戳——
Uint<64>无法容纳当前时间戳数值Uint<16> - 使用+
createUnprovenDeployTx——而非submitTxAsync(预生产环境下会卡顿)deployContract() - 使用修补后的包装
queryContractState(解决GraphQLindexerPublicDataProviderbug)offset: null - 代币数量以Stars为单位(1 NIGHT = 1_000_000 Stars);所有场景均使用处理
BigInt - 受益人公钥字节在锁仓时存储在链上;见证用于验证提取权限
beneficiaryKey
Workflow
工作流程
When helping the user, follow this sequence:
- Contract — compile + witnesses
locker.compact - Providers — (from
createConnectedSession)references/midnight-session.md - Deploy — low-level deploy, persist private state + contract address
- Lock —
lockTokens(amount, releaseTime, beneficiaryPkBytes) - Poll indexer — read ,
balance,unlockTimefrom ledgerlockActive - Release — after , beneficiary calls
blockTimeGte(unlockTime)release(recipient) - UI — show countdown, disable release until unlock time (UX only — contract enforces)
协助用户时,请遵循以下步骤:
- 合约 — 编译合约 + 生成见证脚本
locker.compact - 提供者 — 配置(来自
createConnectedSession)references/midnight-session.md - 部署 — 底层部署合约,持久化私有状态 + 合约地址
- 锁仓 — 调用
lockTokens(amount, releaseTime, beneficiaryPkBytes) - 轮询索引器 — 从账本读取、
balance、unlockTime状态lockActive - 提取 — 当后,受益人调用
blockTimeGte(unlockTime)release(recipient) - UI — 显示倒计时,在解锁时间前禁用提取按钮(仅UX层面限制——合约会强制验证)
1) Project Structure
1) 项目结构
locker-dapp/
├── package.json
├── next.config.mjs
├── lib/
│ ├── isomorphic-ws-fix.mjs
│ ├── midnight.ts # session, patched provider, hex helpers
│ └── locker.ts # deploy, lock, release, decode state
├── app/
│ ├── layout.tsx
│ └── locker/
│ └── LockerClient.tsx # all wallet + contract UI (client component)
├── contract/
│ ├── package.json
│ ├── src/
│ │ ├── locker.compact
│ │ ├── witnesses.ts
│ │ ├── index.ts # barrel → managed/locker
│ │ └── managed/locker/ # compiler output (gitignored)
│ └── tsconfig.json
└── public/zk/locker/ # synced keys + zkir (gitignored in dev, committed for deploy)locker-dapp/
├── package.json
├── next.config.mjs
├── lib/
│ ├── isomorphic-ws-fix.mjs
│ ├── midnight.ts # 会话、修补后的提供者、十六进制工具
│ └── locker.ts # 部署、锁仓、提取、状态解码
├── app/
│ ├── layout.tsx
│ └── locker/
│ └── LockerClient.tsx # 所有钱包+合约UI(客户端组件)
├── contract/
│ ├── package.json
│ ├── src/
│ │ ├── locker.compact
│ │ ├── witnesses.ts
│ │ ├── index.ts # 导出入口 → managed/locker
│ │ └── managed/locker/ # 编译器输出(Git忽略)
│ └── tsconfig.json
└── public/zk/locker/ # 同步的密钥+zkir(开发环境Git忽略,部署时提交)2) Prerequisites
2) 前置条件
bash
node --version # 20+ for Next.js; 22+ if adding vitest contract tests
docker --version # optional local stackbash
node --version # Next.js需要20+版本;若添加vitest合约测试则需要22+版本
docker --version # 可选本地栈Compact compiler
Compact编译器
curl --proto '=https' --tlsv1.2 -sSf
https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh source $HOME/.local/bin/env
https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh source $HOME/.local/bin/env
Browser: **1AM wallet** extension on `preprod` (or Lace fallback).
---curl --proto '=https' --tlsv1.2 -sSf
https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh source $HOME/.local/bin/env
https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh source $HOME/.local/bin/env
浏览器:安装`preprod`环境的**1AM wallet**扩展(或使用Lace作为备选)。
---3) Root package.json
package.json3) 根目录package.json
package.jsonjson
{
"name": "locker-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/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"
}
}json
{
"name": "locker-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/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"
}
}4) contract/package.json
contract/package.json4) contract/package.json
contract/package.jsonjson
{
"name": "@locker/contract",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"compact": "compact compile src/locker.compact src/managed/locker"
},
"dependencies": {
"@midnight-ntwrk/compact-runtime": "0.16.0"
}
}Compile:
bash
npm run compactjson
{
"name": "@locker/contract",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"compact": "compact compile src/locker.compact src/managed/locker"
},
"dependencies": {
"@midnight-ntwrk/compact-runtime": "0.16.0"
}
}编译:
bash
npm run compact→ contract/src/managed/locker/{contract,keys,zkir}/
→ contract/src/managed/locker/{contract,keys,zkir}/
npm run sync:assets
npm run sync:assets
→ public/zk/locker/
→ public/zk/locker/
---
---5) contract/src/locker.compact
contract/src/locker.compact5) contract/src/locker.compact
contract/src/locker.compactSingle active lock per contract instance (simple vault). Deploy multiple contract addresses for parallel locks, or extend with (see §18).
Map<Bytes<32>, LockRecord>compact
pragma language_version >= 0.23;
import CompactStandardLibrary;
export ledger balance: Uint<128>;
export ledger unlockTime: Uint<64>;
export ledger beneficiary: Bytes<32>;
export ledger lockActive: Boolean;
export ledger totalLocked: Uint<128>;
export ledger totalReleased: Uint<128>;
witness beneficiaryKey(): Bytes<32>;
constructor() {
balance = 0;
unlockTime = 0;
beneficiary = default<Bytes<32>>;
lockActive = false;
totalLocked = 0;
totalReleased = 0;
}
// Deposit unshielded NIGHT and schedule release for beneficiaryPk at releaseTime (Unix seconds).
export circuit lockTokens(
amount: Uint<128>,
releaseTime: Uint<64>,
beneficiaryPk: Bytes<32>
): [] {
assert(!disclose(lockActive), "Vault already has an active lock");
assert(disclose(amount) > 0, "Amount must be positive");
receiveUnshielded(default<Bytes<32>>, disclose(amount));
beneficiary = disclose(beneficiaryPk);
unlockTime = disclose(releaseTime);
balance = disclose(amount);
lockActive = disclose(true);
totalLocked = disclose((totalLocked + amount) as Uint<128>);
}
// Beneficiary releases locked tokens after unlockTime to recipient (unshielded UserAddress).
export circuit release(recipient: UserAddress): [] {
assert(disclose(lockActive), "No active lock");
assert(blockTimeGte(disclose(unlockTime)), "Lock period not finished");
assert(deriveKey(beneficiaryKey()) == beneficiary, "Only beneficiary can release");
const amount = disclose(balance);
assert(amount > 0, "Nothing to release");
sendUnshielded(
default<Bytes<32>>,
amount,
right<ContractAddress, UserAddress>(disclose(recipient))
);
totalReleased = disclose((totalReleased + amount) as Uint<128>);
balance = 0;
lockActive = false;
}
pure circuit deriveKey(sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([pad(32, "locker:beneficiary:v1"), sk]);
}Time API: returns true when . Use Unix seconds. Do not store raw on ledger.
blockTimeGte(t)block_time >= tUint<64>block_time每个合约实例仅支持一个活跃锁仓(简易金库)。若需并行锁仓,可部署多个合约地址,或扩展为使用(见第18节)。
Map<Bytes<32>, LockRecord>compact
pragma language_version >= 0.23;
import CompactStandardLibrary;
export ledger balance: Uint<128>;
export ledger unlockTime: Uint<64>;
export ledger beneficiary: Bytes<32>;
export ledger lockActive: Boolean;
export ledger totalLocked: Uint<128>;
export ledger totalReleased: Uint<128>;
witness beneficiaryKey(): Bytes<32>;
constructor() {
balance = 0;
unlockTime = 0;
beneficiary = default<Bytes<32>>;
lockActive = false;
totalLocked = 0;
totalReleased = 0;
}
// 存入未屏蔽的NIGHT代币,并安排在releaseTime(Unix时间戳)由beneficiaryPk对应的受益人提取。
export circuit lockTokens(
amount: Uint<128>,
releaseTime: Uint<64>,
beneficiaryPk: Bytes<32>
): [] {
assert(!disclose(lockActive), "Vault already has an active lock");
assert(disclose(amount) > 0, "Amount must be positive");
receiveUnshielded(default<Bytes<32>>, disclose(amount));
beneficiary = disclose(beneficiaryPk);
unlockTime = disclose(releaseTime);
balance = disclose(amount);
lockActive = disclose(true);
totalLocked = disclose((totalLocked + amount) as Uint<128>);
}
// 受益人在unlockTime之后将锁定的代币提取至接收者(未屏蔽的UserAddress)。
export circuit release(recipient: UserAddress): [] {
assert(disclose(lockActive), "No active lock");
assert(blockTimeGte(disclose(unlockTime)), "Lock period not finished");
assert(deriveKey(beneficiaryKey()) == beneficiary, "Only beneficiary can release");
const amount = disclose(balance);
assert(amount > 0, "Nothing to release");
sendUnshielded(
default<Bytes<32>>,
amount,
right<ContractAddress, UserAddress>(disclose(recipient))
);
totalReleased = disclose((totalReleased + amount) as Uint<128>);
balance = 0;
lockActive = false;
}
pure circuit deriveKey(sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([pad(32, "locker:beneficiary:v1"), sk]);
}时间API: 在时返回true。使用类型的Unix时间戳。请勿在账本上存储原始。
blockTimeGte(t)block_time >= tUint<64>block_time6) contract/src/witnesses.ts
contract/src/witnesses.ts6) contract/src/witnesses.ts
contract/src/witnesses.tstypescript
import type { WitnessContext } from '@midnight-ntwrk/compact-runtime';
export type LockerPrivateState = {
beneficiarySecretKey: Uint8Array;
};
export const witnesses = {
beneficiaryKey: (context: WitnessContext<LockerPrivateState>) =>
[context.privateState, context.privateState.beneficiarySecretKey] as const,
};The beneficiary's Zswap secret seed (32 bytes) must live in private state. When locking for yourself, initialize private state with your shielded coin secret. When locking for someone else, they must deploy their own locker or use a multi-party flow — the witness must match the on-chain field at release.
beneficiarytypescript
import type { WitnessContext } from '@midnight-ntwrk/compact-runtime';
export type LockerPrivateState = {
beneficiarySecretKey: Uint8Array;
};
export const witnesses = {
beneficiaryKey: (context: WitnessContext<LockerPrivateState>) =>
[context.privateState, context.privateState.beneficiarySecretKey] as const,
};受益人的Zswap密钥种子(32字节)必须存储在私有状态中。若为自己锁仓,需使用你的屏蔽币密钥初始化私有状态。若为他人锁仓,对方必须部署自己的锁仓合约或使用多方流程——提取时见证必须与链上存储的字段匹配。
beneficiary7) contract/src/index.ts
contract/src/index.ts7) contract/src/index.ts
contract/src/index.tstypescript
import { CompiledContract } from '@midnight-ntwrk/compact-runtime';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import { witnesses } from './witnesses.js';
export {
Contract,
ledger,
pureCircuits,
type Ledger,
} from './managed/locker/contract/index.js';
import { Contract } from './managed/locker/contract/index.js';
const currentDir = path.dirname(fileURLToPath(import.meta.url));
export const zkConfigPath = path.resolve(currentDir, 'managed', 'locker');
export const CompiledLockerContract = CompiledContract.make('locker', Contract).pipe(
CompiledContract.withWitnesses(witnesses),
CompiledContract.withCompiledFileAssets(zkConfigPath),
);typescript
import { CompiledContract } from '@midnight-ntwrk/compact-runtime';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import { witnesses } from './witnesses.js';
export {
Contract,
ledger,
pureCircuits,
type Ledger,
} from './managed/locker/contract/index.js';
import { Contract } from './managed/locker/contract/index.js';
const currentDir = path.dirname(fileURLToPath(import.meta.url));
export const zkConfigPath = path.resolve(currentDir, 'managed', 'locker');
export const CompiledLockerContract = CompiledContract.make('locker', Contract).pipe(
CompiledContract.withWitnesses(witnesses),
CompiledContract.withCompiledFileAssets(zkConfigPath),
);8) Provider Setup
8) 提供者配置
Copy the full provider block from § "Provider Setup":
example-payment-dapp/SKILL.md- type
ConnectedSession - (ZK path →
createConnectedSession)/zk/locker/ coinPublicKeyToBytescreatePatchedPublicDataProvidercreatePrivateStateProvider
Change only:
- /
ZK_ASSET_PATHbase →FetchZkConfigProvider/zk/locker - after
setNetworkId(config.networkId)api.getConfiguration()
Wallet detection (1AM first, Lace fallback):
typescript
const wallet =
(window as any).midnight?.['1am'] ??
Object.values((window as any).midnight ?? {})[0];Prefer enumerating over hardcoded Lace keys — see .
Object.values(window.midnight)react-wallet-connector/从的“Provider Setup”部分复制完整的提供者代码块:
example-payment-dapp/SKILL.md- 类型
ConnectedSession - (ZK路径 →
createConnectedSession)/zk/locker/ coinPublicKeyToBytescreatePatchedPublicDataProvidercreatePrivateStateProvider
仅修改以下内容:
- /
ZK_ASSET_PATH基础路径 →FetchZkConfigProvider/zk/locker - 在之后调用
api.getConfiguration()setNetworkId(config.networkId)
钱包检测(优先1AM,备选Lace):
typescript
const wallet =
(window as any).midnight?.['1am'] ??
Object.values((window as any).midnight ?? {})[0];优先使用枚举,而非硬编码Lace密钥——详见。
Object.values(window.midnight)react-wallet-connector/9) lib/locker.ts
lib/locker.ts9) lib/locker.ts
lib/locker.tstypescript
import { createUnprovenDeployTx, submitCallTxAsync } from '@midnight-ntwrk/midnight-js-contracts';
import { ContractState, sampleSigningKey } from '@midnight-ntwrk/compact-runtime';
import { CompiledLockerContract, Contract, ledger } from '../contract/src/index.js';
import type { ConnectedSession } from './midnight.js';
import { fromHex, pollForState } from './midnight.js';
const PRIVATE_STATE_ID = 'LockerPrivateState';
const ZK_ASSET_PATH = '/zk/locker';
function makeCompiledContract() {
return CompiledLockerContract as any;
}
export async function deployLocker(
session: ConnectedSession,
beneficiarySecretKey: Uint8Array,
): Promise<string> {
const initialPrivateState = { beneficiarySecretKey };
const deployTxData = await (createUnprovenDeployTx as any)(
{
zkConfigProvider: session.providers.zkConfigProvider,
walletProvider: session.providers.walletProvider,
},
{
compiledContract: makeCompiledContract(),
args: [],
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, initialPrivateState);
await session.providers.privateStateProvider.setSigningKey(
contractAddress,
deployTxData.private.signingKey,
);
return contractAddress;
}
export async function lockTokens(
session: ConnectedSession,
contractAddress: string,
amount: bigint,
releaseTimeUnix: bigint,
beneficiaryPkBytes: Uint8Array,
) {
await (submitCallTxAsync as any)(session.providers, {
compiledContract: makeCompiledContract(),
contractAddress,
circuitId: 'lockTokens',
args: [amount, releaseTimeUnix, { bytes: beneficiaryPkBytes }],
privateStateId: PRIVATE_STATE_ID,
});
}
export async function releaseTokens(
session: ConnectedSession,
contractAddress: string,
recipientBytes: Uint8Array,
) {
await (submitCallTxAsync as any)(session.providers, {
compiledContract: makeCompiledContract(),
contractAddress,
circuitId: 'release',
args: [{ bytes: recipientBytes }],
privateStateId: PRIVATE_STATE_ID,
});
}
export function decodeLockerState(stateHex: string) {
const contractState = ContractState.deserialize(fromHex(stateHex));
const l = ledger(contractState.data);
return {
balance: l.balance as unknown as bigint,
unlockTime: l.unlockTime as unknown as bigint,
beneficiary: l.beneficiary as unknown as Uint8Array,
lockActive: l.lockActive as unknown as boolean,
totalLocked: l.totalLocked as unknown as bigint,
totalReleased: l.totalReleased as unknown as bigint,
};
}
export async function fetchLockerState(queryUrl: string, contractAddress: string) {
const hex = await pollForState(queryUrl, contractAddress);
return decodeLockerState(hex);
}typescript
import { createUnprovenDeployTx, submitCallTxAsync } from '@midnight-ntwrk/midnight-js-contracts';
import { ContractState, sampleSigningKey } from '@midnight-ntwrk/compact-runtime';
import { CompiledLockerContract, Contract, ledger } from '../contract/src/index.js';
import type { ConnectedSession } from './midnight.js';
import { fromHex, pollForState } from './midnight.js';
const PRIVATE_STATE_ID = 'LockerPrivateState';
const ZK_ASSET_PATH = '/zk/locker';
function makeCompiledContract() {
return CompiledLockerContract as any;
}
export async function deployLocker(
session: ConnectedSession,
beneficiarySecretKey: Uint8Array,
): Promise<string> {
const initialPrivateState = { beneficiarySecretKey };
const deployTxData = await (createUnprovenDeployTx as any)(
{
zkConfigProvider: session.providers.zkConfigProvider,
walletProvider: session.providers.walletProvider,
},
{
compiledContract: makeCompiledContract(),
args: [],
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, initialPrivateState);
await session.providers.privateStateProvider.setSigningKey(
contractAddress,
deployTxData.private.signingKey,
);
return contractAddress;
}
export async function lockTokens(
session: ConnectedSession,
contractAddress: string,
amount: bigint,
releaseTimeUnix: bigint,
beneficiaryPkBytes: Uint8Array,
) {
await (submitCallTxAsync as any)(session.providers, {
compiledContract: makeCompiledContract(),
contractAddress,
circuitId: 'lockTokens',
args: [amount, releaseTimeUnix, { bytes: beneficiaryPkBytes }],
privateStateId: PRIVATE_STATE_ID,
});
}
export async function releaseTokens(
session: ConnectedSession,
contractAddress: string,
recipientBytes: Uint8Array,
) {
await (submitCallTxAsync as any)(session.providers, {
compiledContract: makeCompiledContract(),
contractAddress,
circuitId: 'release',
args: [{ bytes: recipientBytes }],
privateStateId: PRIVATE_STATE_ID,
});
}
export function decodeLockerState(stateHex: string) {
const contractState = ContractState.deserialize(fromHex(stateHex));
const l = ledger(contractState.data);
return {
balance: l.balance as unknown as bigint,
unlockTime: l.unlockTime as unknown as bigint,
beneficiary: l.beneficiary as unknown as Uint8Array,
lockActive: l.lockActive as unknown as boolean,
totalLocked: l.totalLocked as unknown as bigint,
totalReleased: l.totalReleased as unknown as bigint,
};
}
export async function fetchLockerState(queryUrl: string, contractAddress: string) {
const hex = await pollForState(queryUrl, contractAddress);
return decodeLockerState(hex);
}10) Frontend — app/locker/LockerClient.tsx
app/locker/LockerClient.tsx10) 前端 — app/locker/LockerClient.tsx
app/locker/LockerClient.tsxClient component pattern (same as payment dapp). Core UI states:
| State | UI |
|---|---|
| Wallet disconnected | Connect button |
| Connected, no contract | Deploy locker |
| Deployed, no lock | Lock form (amount, unlock datetime, beneficiary = self) |
| Active lock, before unlock | Show balance, unlock date, countdown, disabled Release |
| Active lock, after unlock | Enabled Release → recipient = own unshielded address bytes |
| Released | Show totals, offer new lock (deploy new contract or extend contract) |
tsx
'use client';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
deployLocker,
fetchLockerState,
lockTokens,
releaseTokens,
} from '@/lib/locker';
const STARS_PER_NIGHT = 1_000_000n;
export default function LockerClient() {
const [session, setSession] = useState<ConnectedSession | null>(null);
const [contractAddress, setContractAddress] = useState<string | null>(null);
const [ledger, setLedger] = useState<LockerLedgerView | null>(null);
const [amountNight, setAmountNight] = useState('1');
const [unlockAt, setUnlockAt] = useState(''); // datetime-local input
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const nowUnix = BigInt(Math.floor(Date.now() / 1000));
const canRelease = ledger?.lockActive && ledger.unlockTime <= nowUnix;
const refresh = useCallback(async () => {
if (!session || !contractAddress) return;
const state = await fetchLockerState(session.config.indexerUri, contractAddress);
setLedger(state);
}, [session, contractAddress]);
useEffect(() => { void refresh(); }, [refresh]);
async function onConnect() {
setError(null);
const wallet =
(window as any).midnight?.['1am'] ??
Object.values((window as any).midnight ?? {})[0];
if (!wallet) throw new Error('Install a Midnight wallet extension');
const api = await wallet.connect('preprod');
const { createConnectedSession } = await import('@/lib/midnight');
setSession(await createConnectedSession(api, '/zk/locker'));
}
async function onDeploy() {
if (!session) return;
setBusy(true);
setError(null);
try {
const beneficiarySecretKey = session.coinPublicKeyBytes; // lock to self
const addr = await deployLocker(session, beneficiarySecretKey);
setContractAddress(addr);
await refresh();
} catch (e) {
setError(String(e));
} finally {
setBusy(false);
}
}
async function onLock() {
if (!session || !contractAddress) return;
setBusy(true);
setError(null);
try {
const amount = BigInt(amountNight) * STARS_PER_NIGHT;
const releaseTime = BigInt(Math.floor(new Date(unlockAt).getTime() / 1000));
await lockTokens(
session,
contractAddress,
amount,
releaseTime,
session.coinPublicKeyBytes,
);
await refresh();
} catch (e) {
setError(String(e));
} finally {
setBusy(false);
}
}
async function onRelease() {
if (!session || !contractAddress) return;
setBusy(true);
setError(null);
try {
const recipientBytes = unshieldedAddressToBytes(session.unshieldedAddress);
await releaseTokens(session, contractAddress, recipientBytes);
await refresh();
} catch (e) {
setError(String(e));
} finally {
setBusy(false);
}
}
return (
<div>
<h1>Token Locker</h1>
{!session ? (
<button type="button" onClick={onConnect} disabled={busy}>Connect Wallet</button>
) : (
<>
<p>Connected: {session.unshieldedAddress}</p>
{!contractAddress ? (
<button type="button" onClick={onDeploy} disabled={busy}>Deploy Locker</button>
) : (
<>
<p>Contract: {contractAddress}</p>
{ledger?.lockActive ? (
<>
<p>Locked: {formatStars(ledger.balance)} Stars</p>
<p>Unlocks: {new Date(Number(ledger.unlockTime) * 1000).toLocaleString()}</p>
<button type="button" onClick={onRelease} disabled={busy || !canRelease}>
Release Tokens
</button>
</>
) : (
<>
<label>
Amount (NIGHT)
<input value={amountNight} onChange={(e) => setAmountNight(e.target.value)} />
</label>
<label>
Unlock at
<input type="datetime-local" value={unlockAt} onChange={(e) => setUnlockAt(e.target.value)} />
</label>
<button type="button" onClick={onLock} disabled={busy}>Lock Tokens</button>
</>
)}
</>
)}
</>
)}
{error ? <p role="alert">{error}</p> : null}
</div>
);
}
function formatStars(n: bigint) {
return n.toString();
}
// Implement: unshieldedAddressToBytes — decode mn_addr_* to 32-byte recipient for circuits
// Never use encodeUserAddress with raw coin pk; use bytes helper from 1am-wallet skillAdd styling (Tailwind/CSS) as needed — logic above is intentionally unstyled like .
react-wallet-connector/客户端组件模式(与支付dApp相同)。核心UI状态:
| 状态 | UI展示 |
|---|---|
| 钱包未连接 | 显示“连接钱包”按钮 |
| 已连接,无合约 | 显示“部署锁仓合约”按钮 |
| 已部署,无活跃锁仓 | 显示锁仓表单(金额、解锁时间、受益人为自己) |
| 活跃锁仓,未到解锁时间 | 显示锁定余额、解锁日期、倒计时、禁用的“提取”按钮 |
| 活跃锁仓,已到解锁时间 | 启用“提取”按钮 → 接收地址为自己的未屏蔽地址字节 |
| 已提取 | 显示总计金额,提供新锁仓选项(部署新合约或扩展现有合约) |
tsx
'use client';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
deployLocker,
fetchLockerState,
lockTokens,
releaseTokens,
} from '@/lib/locker';
const STARS_PER_NIGHT = 1_000_000n;
export default function LockerClient() {
const [session, setSession] = useState<ConnectedSession | null>(null);
const [contractAddress, setContractAddress] = useState<string | null>(null);
const [ledger, setLedger] = useState<LockerLedgerView | null>(null);
const [amountNight, setAmountNight] = useState('1');
const [unlockAt, setUnlockAt] = useState(''); // datetime-local输入框
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const nowUnix = BigInt(Math.floor(Date.now() / 1000));
const canRelease = ledger?.lockActive && ledger.unlockTime <= nowUnix;
const refresh = useCallback(async () => {
if (!session || !contractAddress) return;
const state = await fetchLockerState(session.config.indexerUri, contractAddress);
setLedger(state);
}, [session, contractAddress]);
useEffect(() => { void refresh(); }, [refresh]);
async function onConnect() {
setError(null);
const wallet =
(window as any).midnight?.['1am'] ??
Object.values((window as any).midnight ?? {})[0];
if (!wallet) throw new Error('请安装Midnight钱包扩展');
const api = await wallet.connect('preprod');
const { createConnectedSession } = await import('@/lib/midnight');
setSession(await createConnectedSession(api, '/zk/locker'));
}
async function onDeploy() {
if (!session) return;
setBusy(true);
setError(null);
try {
const beneficiarySecretKey = session.coinPublicKeyBytes; // 锁仓给自己
const addr = await deployLocker(session, beneficiarySecretKey);
setContractAddress(addr);
await refresh();
} catch (e) {
setError(String(e));
} finally {
setBusy(false);
}
}
async function onLock() {
if (!session || !contractAddress) return;
setBusy(true);
setError(null);
try {
const amount = BigInt(amountNight) * STARS_PER_NIGHT;
const releaseTime = BigInt(Math.floor(new Date(unlockAt).getTime() / 1000));
await lockTokens(
session,
contractAddress,
amount,
releaseTime,
session.coinPublicKeyBytes,
);
await refresh();
} catch (e) {
setError(String(e));
} finally {
setBusy(false);
}
}
async function onRelease() {
if (!session || !contractAddress) return;
setBusy(true);
setError(null);
try {
const recipientBytes = unshieldedAddressToBytes(session.unshieldedAddress);
await releaseTokens(session, contractAddress, recipientBytes);
await refresh();
} catch (e) {
setError(String(e));
} finally {
setBusy(false);
}
}
return (
<div>
<h1>代币锁仓</h1>
{!session ? (
<button type="button" onClick={onConnect} disabled={busy}>连接钱包</button>
) : (
<>
<p>已连接:{session.unshieldedAddress}</p>
{!contractAddress ? (
<button type="button" onClick={onDeploy} disabled={busy}>部署锁仓合约</button>
) : (
<>
<p>合约地址:{contractAddress}</p>
{ledger?.lockActive ? (
<>
<p>锁定金额:{formatStars(ledger.balance)} Stars</p>
<p>解锁时间:{new Date(Number(ledger.unlockTime) * 1000).toLocaleString()}</p>
<button type="button" onClick={onRelease} disabled={busy || !canRelease}>
提取代币
</button>
</>
) : (
<>
<label>
金额(NIGHT)
<input value={amountNight} onChange={(e) => setAmountNight(e.target.value)} />
</label>
<label>
解锁时间
<input type="datetime-local" value={unlockAt} onChange={(e) => setUnlockAt(e.target.value)} />
</label>
<button type="button" onClick={onLock} disabled={busy}>锁定代币</button>
</>
)}
</>
)}
</>
)}
{error ? <p role="alert">{error}</p> : null}
</div>
);
}
function formatStars(n: bigint) {
return n.toString();
}
// 实现:unshieldedAddressToBytes — 将mn_addr_*解码为32字节的接收者地址,用于合约调用
// 切勿使用encodeUserAddress处理原始币公钥;请使用1am-wallet技能中的字节工具可根据需要添加样式(Tailwind/CSS)——上述逻辑故意保持无样式,与一致。
react-wallet-connector/11) ZK Asset Sync — scripts/sync-zk-assets.mjs
scripts/sync-zk-assets.mjs11) 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/locker');
const dest = join(root, 'public/zk/locker');
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 before debugging SDK errors:
must return 200.
http://localhost:3000/zk/locker/keys/lockTokens.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/locker');
const dest = join(root, 'public/zk/locker');
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 });
}调试SDK错误前请验证:
必须返回200状态码。
http://localhost:3000/zk/locker/keys/lockTokens.prover12) Next.js Config
12) Next.js配置
Same as § Next.js:
example-payment-dapp/SKILL.md- WebSocket shim
lib/isomorphic-ws-fix.mjs - with
next.config.mjs,asyncWebAssembly,topLevelAwaitaliasisomorphic-ws - Scripts must use and
next dev --webpacknext build --webpack
与中的Next.js配置相同:
example-payment-dapp/SKILL.md- WebSocket垫片
lib/isomorphic-ws-fix.mjs - 启用
next.config.mjs、asyncWebAssembly,并添加topLevelAwait别名isomorphic-ws - 脚本必须使用和
next dev --webpacknext build --webpack
13) End-to-End Flow
13) 端到端流程
1. npm install && npm run compact && npm run sync:assets
2. npm run dev
3. Connect 1AM wallet (preprod, funded with tNIGHT)
4. Deploy locker contract
5. Lock 1 NIGHT until chosen datetime
6. UI shows locked balance + unlock time (poll indexer every 2s after tx)
7. Wait until unlock (or advance clock on local undeployed only)
8. Beneficiary clicks Release → tokens sent to unshielded address1. npm install && npm run compact && npm run sync:assets
2. npm run dev
3. 连接1AM钱包(preprod环境,已充值tNIGHT)
4. 部署锁仓合约
5. 锁定1 NIGHT至指定时间
6. UI显示锁定余额+解锁时间(交易完成后每2秒轮询索引器)
7. 等待至解锁时间(或仅在本地未部署环境中调整时钟)
8. 受益人点击“提取”→ 代币发送至未屏蔽地址14) Troubleshooting
14) 故障排查
| Symptom | Cause | Fix |
|---|---|---|
| | Wait until on-chain time ≥ |
| Witness key ≠ ledger | Private state |
| Single-lock contract | Release first, or deploy another contract instance |
| Wrong integer width | Use |
| Deploy hangs 30–120s | Used | Use |
GraphQL | Default indexer provider | Use patched |
| ZK 404 | Assets not synced | |
| Wrong address encoding for recipient | Pass |
| Release button works before time in UI | UI-only check | Contract still rejects — fix UX but trust on-chain |
| 症状 | 原因 | 修复方案 |
|---|---|---|
| | 等待链上时间≥ |
| 见证密钥≠账本 | 私有状态中的 |
| 合约仅支持单锁仓 | 先提取代币,或部署新的合约实例 |
时间戳出现 | 整数宽度错误 | 所有Unix时间戳均使用 |
| 部署卡顿30–120秒 | 使用了 | 使用 |
GraphQL | 默认索引器提供者问题 | 使用修补后的 |
| ZK资源404 | 资产未同步 | 运行 |
| 接收者地址编码错误 | 向合约传递 |
| UI中提取按钮在解锁时间前可用 | 仅UX层面限制 | 合约仍会拒绝调用——修复UX但信任链上的 |
15) Agent Checklist
15) 代理检查清单
When generating this dApp for a user:
- Create with
locker.compact+lockTokens+releaseblockTimeGte - Compile contract; sync ZK assets to
public/zk/locker/ - Wire with patched indexer (from payment dapp)
createConnectedSession - Use low-level deploy + for lock/release
submitCallTxAsync - Store in private state; pass
beneficiarySecretKeyat lockcoinPublicKeyBytes - Decode ledger with — not raw
ledger(contractState.data)ContractState - Use for amounts and Unix timestamps
BigInt - Next.js flag for WASM
--webpack - Document: 1 NIGHT = 1_000_000 Stars
为用户生成该dApp时,请确保:
- 创建包含+
lockTokens+release的blockTimeGte合约locker.compact - 编译合约;将ZK资产同步至
public/zk/locker/ - 配置带有修补后索引器的(来自支付dApp)
createConnectedSession - 使用底层部署+处理锁仓/提取
submitCallTxAsync - 将存储在私有状态;锁仓时传递
beneficiarySecretKeycoinPublicKeyBytes - 使用解码账本——而非原始
ledger(contractState.data)ContractState - 使用处理金额和Unix时间戳
BigInt - Next.js使用标志以支持WASM
--webpack - 文档说明:1 NIGHT = 1_000_000 Stars
16) Use Case Mapping
16) 用例映射
| Use case | How this template maps |
|---|---|
| Fixed deposit | User locks until |
| Token vesting | Deploy one locker per tranche, or extend with |
| Liquidity lock | Lock LP amount until |
| Escrow | Set beneficiary to counterparty pk; release after deadline when conditions met off-chain |
| NFT lock | Not covered here — use |
| 用例 | 本模板的适配方式 |
|---|---|
| 定期存款 | 用户锁定代币至 |
| 代币归属 | 为每个 tranche 部署一个锁仓合约,或扩展为使用 |
| 流动性锁定 | 锁定LP代币至 |
| 托管 | 将受益人设置为交易对手公钥;截止时间后在链下条件满足时提取 |
| NFT锁仓 | 本文未覆盖——使用 |
17) Reading Lock Status (Indexer)
17) 读取锁仓状态(索引器)
typescript
// After lockTokens tx — poll until balance > 0
const state = await fetchLockerState(indexerUrl, contractAddress);
console.log({
locked: state.lockActive,
amount: state.balance,
unlockAt: new Date(Number(state.unlockTime) * 1000).toISOString(),
});typescript
// lockTokens交易完成后——轮询直至balance>0
const state = await fetchLockerState(indexerUrl, contractAddress);
console.log({
locked: state.lockActive,
amount: state.balance,
unlockAt: new Date(Number(state.unlockTime) * 1000).toISOString(),
});18) Extensions
18) 扩展功能
Multi-lock (Map
)
Map多锁仓(Map
)
MapReplace single cell with:
lockActivecompact
struct LockRecord {
amount: Uint<128>,
unlockTime: Uint<64>,
beneficiary: Bytes<32>,
active: Boolean,
}
export ledger locks: Map<Bytes<32>, LockRecord>;
export ledger nextLockId: Counter;Add parameter to ; increment counter on each .
lockIdreleaselockTokens将单个字段替换为:
lockActivecompact
struct LockRecord {
amount: Uint<128>,
unlockTime: Uint<64>,
beneficiary: Bytes<32>,
active: Boolean,
}
export ledger locks: Map<Bytes<32>, LockRecord>;
export ledger nextLockId: Counter;为方法添加参数;每次调用时递增计数器。
releaselockIdlockTokensLinear vesting
线性归属
Deploy multiple locker contracts with staggered values, or add a with and separate circuit using .
unlockTimevesting.compactList<VestTranche>claimTranche(index)blockTimeGte(tranche.unlockTime)部署多个带有交错的锁仓合约,或添加合约,使用结构,并添加单独的合约方法,通过验证。
unlockTimevesting.compactList<VestTranche>claimTranche(index)blockTimeGte(tranche.unlockTime)React-only wallet shell
纯React钱包外壳
If the user only needs connect UI first, scaffold with then add provider wiring from .
react-wallet-connector/references/midnight-session.md若用户仅需先实现连接UI,可使用搭建基础,再从添加提供者配置。
react-wallet-connector/references/midnight-session.md20) Troubleshooting
20) 故障排查
See for the full table. Common locker-specific issues:
references/gotchas.md| Symptom | Fix |
|---|---|
| Use |
| Release before deadline | |
| Wrong beneficiary on release | Witness |
| Deploy hangs on preprod | Use |
完整表格请见。锁仓相关常见问题:
references/gotchas.md| 症状 | 修复方案 |
|---|---|
| 使用 |
| 截止时间前可提取 | 必须等待 |
| 提取时受益人错误 | 见证 |
| 预生产环境部署卡顿 | 使用 |
21) Related Skills
21) 相关技能
| Next step | Skill |
|---|---|
| Wallet connect only | |
| Payment vault (no timelock) | |
| Full CLI + tests reference | |
| NFT locking | |
| Privacy audit | |
| 下一步 | 技能 |
|---|---|
| 仅钱包连接 | |
| 支付金库(无时间锁) | |
| 完整CLI+测试参考 | |
| NFT锁仓 | |
| 隐私审计 | |