midnight-js
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMidnight.js Skill
Midnight.js 开发技能
Midnight.js is the TypeScript SDK for building DApps on Midnight Network — analogous to Web3.js for Ethereum. The official SDK page is "coming soon"; this skill is grounded in the official Counter CLI tutorial and the 1AM starter template.
Primary references:
- — official full implementation
docs.midnight.network/tutorials/counter/counter-cli - — reference repo (Node.js / headless wallet)
github.com/midnightntwrk/example-counter - — browser wallet variant
github.com/webisoftSoftware/1AM-starter-template
Midnight.js是用于在Midnight Network上构建DApp的TypeScript SDK —— 类似于以太坊的Web3.js。官方SDK页面“即将推出”;本技能基于官方Counter CLI教程和1AM启动模板编写。
主要参考资料:
- —— 官方完整实现
docs.midnight.network/tutorials/counter/counter-cli - —— 参考仓库(Node.js / 无头钱包)
github.com/midnightntwrk/example-counter - —— 浏览器钱包变体
github.com/webisoftSoftware/1AM-starter-template
1) Package Map
1) 包映射表
| Package | Purpose |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 包 | 用途 |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
2) Network Configuration
2) 网络配置
Always call before any SDK operation. It sets a global that all libraries use automatically.
setNetworkIdtypescript
import { setNetworkId, getNetworkId } from '@midnight-ntwrk/midnight-js-network-id';
// Call once at startup, in the config constructor or before building providers
setNetworkId('preprod'); // 'preprod' | 'preview' | 'mainnet' | 'undeployed'Network endpoints:
| Network | Indexer HTTP | Indexer WS | RPC |
|---|---|---|---|
| | | |
| | | |
| | | |
| | | |
在执行任何SDK操作前,务必先调用。 它会设置一个全局变量,供所有库自动使用。
setNetworkIdtypescript
import { setNetworkId, getNetworkId } from '@midnight-ntwrk/midnight-js-network-id';
// 在启动时调用一次,可在配置构造函数中或构建provider之前调用
setNetworkId('preprod'); // 'preprod' | 'preview' | 'mainnet' | 'undeployed'网络端点:
| 网络 | 索引器HTTP | 索引器WS | RPC |
|---|---|---|---|
| | | |
| | | |
| | | |
| | | |
3) Compiled Contract Setup
3) 编译合约设置
Pre-compile the contract once at module load — not on every deploy/call.
typescript
import { CompiledContract } from '@midnight-ntwrk/compact-js';
import { Contract, witnesses } from './managed/counter'; // generated by compact compiler
// Node.js: load ZK assets from filesystem
import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider';
const zkConfigPath = path.resolve('contract/src/managed/counter');
const compiledContract = CompiledContract.make('counter', Contract).pipe(
CompiledContract.withVacantWitnesses,
CompiledContract.withCompiledFileAssets(zkConfigPath),
);
// Browser / CDN: load ZK assets via fetch
import { FetchZkConfigProvider } from '@midnight-ntwrk/midnight-js-fetch-zk-config-provider';
const compiledContract = CompiledContract.make('YourContract', Contract).pipe(
CompiledContract.withVacantWitnesses,
CompiledContract.withCompiledFileAssets('/zk/your-contract'),
);在模块加载时预编译合约一次 —— 不要在每次部署/调用时都编译。
typescript
import { CompiledContract } from '@midnight-ntwrk/compact-js';
import { Contract, witnesses } from './managed/counter'; // 由compact编译器生成
// Node.js:从文件系统加载ZK资产
import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider';
const zkConfigPath = path.resolve('contract/src/managed/counter');
const compiledContract = CompiledContract.make('counter', Contract).pipe(
CompiledContract.withVacantWitnesses,
CompiledContract.withCompiledFileAssets(zkConfigPath),
);
// 浏览器 / CDN:通过fetch加载ZK资产
import { FetchZkConfigProvider } from '@midnight-ntwrk/midnight-js-fetch-zk-config-provider';
const compiledContract = CompiledContract.make('YourContract', Contract).pipe(
CompiledContract.withVacantWitnesses,
CompiledContract.withCompiledFileAssets('/zk/your-contract'),
);4) Type Definitions (Important Pattern)
4) 类型定义(重要模式)
Define typed aliases for your contract's providers — this is the Midnight.js pattern:
typescript
import type { MidnightProviders } from '@midnight-ntwrk/midnight-js-types';
import type { DeployedContract, FoundContract } from '@midnight-ntwrk/midnight-js-contracts';
import type { ImpureCircuitId } from '@midnight-ntwrk/compact-js';
import { Counter, type CounterPrivateState } from './managed/counter';
// Extract circuit IDs from the contract type
export type CounterCircuits = ImpureCircuitId<Counter.Contract<CounterPrivateState>>;
// Private state key (string literal type)
export const CounterPrivateStateId = 'counterPrivateState' as const;
export type CounterPrivateStateId = typeof CounterPrivateStateId;
// Full providers type for this contract
export type CounterProviders = MidnightProviders<
CounterCircuits,
CounterPrivateStateId,
CounterPrivateState
>;
// Contract instance types
export type CounterContract = Counter.Contract<CounterPrivateState>;
export type DeployedCounterContract = DeployedContract<CounterContract> | FoundContract<CounterContract>;为合约的provider定义类型别名 —— 这是Midnight.js的标准模式:
typescript
import type { MidnightProviders } from '@midnight-ntwrk/midnight-js-types';
import type { DeployedContract, FoundContract } from '@midnight-ntwrk/midnight-js-contracts';
import type { ImpureCircuitId } from '@midnight-ntwrk/compact-js';
import { Counter, type CounterPrivateState } from './managed/counter';
// 从合约类型中提取电路ID
export type CounterCircuits = ImpureCircuitId<Counter.Contract<CounterPrivateState>>;
// 私有状态键(字符串字面量类型)
export const CounterPrivateStateId = 'counterPrivateState' as const;
export type CounterPrivateStateId = typeof CounterPrivateStateId;
// 此合约的完整provider类型
export type CounterProviders = MidnightProviders<
CounterCircuits,
CounterPrivateStateId,
CounterPrivateState
>;
// 合约实例类型
export type CounterContract = Counter.Contract<CounterPrivateState>;
export type DeployedCounterContract = DeployedContract<CounterContract> | FoundContract<CounterContract>;5) Wallet Setup (Node.js / Headless)
5) 钱包设置(Node.js / 无头)
HD Key Derivation
HD密钥派生
typescript
import { HDWallet, generateRandomSeed, Roles } from '@midnight-ntwrk/wallet-sdk-hd';
import * as ledger from '@midnight-ntwrk/ledger-v8';
import { Buffer } from 'buffer';
// Generate a fresh random seed (returns Uint8Array)
const seed = generateRandomSeed();
const seedHex = Buffer.from(seed).toString('hex'); // save this
// Or restore from existing hex seed
const seedHex = '...'; // from user input
const hdWallet = HDWallet.fromSeed(Buffer.from(seedHex, 'hex'));
if (hdWallet.type !== 'seedOk') throw new Error('Invalid seed');
const derivationResult = hdWallet.hdWallet
.selectAccount(0)
.selectRoles([Roles.Zswap, Roles.NightExternal, Roles.Dust])
.deriveKeysAt(0);
if (derivationResult.type !== 'keysDerived') throw new Error('Key derivation failed');
hdWallet.hdWallet.clear(); // wipe secret material from memory
const keys = derivationResult.keys;
const shieldedSecretKeys = ledger.ZswapSecretKeys.fromSeed(keys[Roles.Zswap]);
const dustSecretKey = ledger.DustSecretKey.fromSeed(keys[Roles.Dust]);typescript
import { HDWallet, generateRandomSeed, Roles } from '@midnight-ntwrk/wallet-sdk-hd';
import * as ledger from '@midnight-ntwrk/ledger-v8';
import { Buffer } from 'buffer';
// 生成新的随机种子(返回Uint8Array)
const seed = generateRandomSeed();
const seedHex = Buffer.from(seed).toString('hex'); // 保存此值
// 或从现有十六进制种子恢复
const seedHex = '...'; // 来自用户输入
const hdWallet = HDWallet.fromSeed(Buffer.from(seedHex, 'hex'));
if (hdWallet.type !== 'seedOk') throw new Error('无效种子');
const derivationResult = hdWallet.hdWallet
.selectAccount(0)
.selectRoles([Roles.Zswap, Roles.NightExternal, Roles.Dust])
.deriveKeysAt(0);
if (derivationResult.type !== 'keysDerived') throw new Error('密钥派生失败');
hdWallet.hdWallet.clear(); // 从内存中清除机密材料
const keys = derivationResult.keys;
const shieldedSecretKeys = ledger.ZswapSecretKeys.fromSeed(keys[Roles.Zswap]);
const dustSecretKey = ledger.DustSecretKey.fromSeed(keys[Roles.Dust]);Wallet Construction
钱包构建
typescript
import { ShieldedWallet } from '@midnight-ntwrk/wallet-sdk-shielded';
import { UnshieldedWallet, createKeystore, PublicKey, InMemoryTransactionHistoryStorage } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
import { DustWallet } from '@midnight-ntwrk/wallet-sdk-dust-wallet';
import { WalletFacade } from '@midnight-ntwrk/wallet-sdk-facade';
const unshieldedKeystore = createKeystore(keys[Roles.NightExternal], getNetworkId());
const shieldedWallet = ShieldedWallet({
networkId: getNetworkId(),
indexerClientConnection: { indexerHttpUrl: indexer, indexerWsUrl: indexerWS },
provingServerUrl: new URL(proofServer),
relayURL: new URL(node.replace(/^http/, 'ws')), // convert http → ws
}).startWithSecretKeys(shieldedSecretKeys);
const unshieldedWallet = UnshieldedWallet({
networkId: getNetworkId(),
indexerClientConnection: { indexerHttpUrl: indexer, indexerWsUrl: indexerWS },
txHistoryStorage: new InMemoryTransactionHistoryStorage(),
}).startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore));
const dustWallet = DustWallet({
networkId: getNetworkId(),
costParameters: {
additionalFeeOverhead: 300_000_000_000_000n,
feeBlocksMargin: 5,
},
indexerClientConnection: { indexerHttpUrl: indexer, indexerWsUrl: indexerWS },
provingServerUrl: new URL(proofServer),
relayURL: new URL(node.replace(/^http/, 'ws')),
}).startWithSecretKey(dustSecretKey, ledger.LedgerParameters.initialParameters().dust);
const wallet = new WalletFacade(shieldedWallet, unshieldedWallet, dustWallet);
await wallet.start(shieldedSecretKeys, dustSecretKey);typescript
import { ShieldedWallet } from '@midnight-ntwrk/wallet-sdk-shielded';
import { UnshieldedWallet, createKeystore, PublicKey, InMemoryTransactionHistoryStorage } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
import { DustWallet } from '@midnight-ntwrk/wallet-sdk-dust-wallet';
import { WalletFacade } from '@midnight-ntwrk/wallet-sdk-facade';
const unshieldedKeystore = createKeystore(keys[Roles.NightExternal], getNetworkId());
const shieldedWallet = ShieldedWallet({
networkId: getNetworkId(),
indexerClientConnection: { indexerHttpUrl: indexer, indexerWsUrl: indexerWS },
provingServerUrl: new URL(proofServer),
relayURL: new URL(node.replace(/^http/, 'ws')), // 将http转换为ws
}).startWithSecretKeys(shieldedSecretKeys);
const unshieldedWallet = UnshieldedWallet({
networkId: getNetworkId(),
indexerClientConnection: { indexerHttpUrl: indexer, indexerWsUrl: indexerWS },
txHistoryStorage: new InMemoryTransactionHistoryStorage(),
}).startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore));
const dustWallet = DustWallet({
networkId: getNetworkId(),
costParameters: {
additionalFeeOverhead: 300_000_000_000_000n,
feeBlocksMargin: 5,
},
indexerClientConnection: { indexerHttpUrl: indexer, indexerWsUrl: indexerWS },
provingServerUrl: new URL(proofServer),
relayURL: new URL(node.replace(/^http/, 'ws')),
}).startWithSecretKey(dustSecretKey, ledger.LedgerParameters.initialParameters().dust);
const wallet = new WalletFacade(shieldedWallet, unshieldedWallet, dustWallet);
await wallet.start(shieldedSecretKeys, dustSecretKey);Wallet Sync (RxJS pattern — always used with WalletFacade)
钱包同步(RxJS模式 —— 始终与WalletFacade配合使用)
typescript
import * as Rx from 'rxjs';
// Wait until wallet is fully synced
const syncedState = await Rx.firstValueFrom(
wallet.state().pipe(
Rx.throttleTime(5_000),
Rx.filter((state) => state.isSynced),
),
);
// Wait until wallet has non-zero unshielded balance
import { unshieldedToken } from '@midnight-ntwrk/ledger-v8';
const balance = await Rx.firstValueFrom(
wallet.state().pipe(
Rx.throttleTime(10_000),
Rx.filter((s) => s.isSynced),
Rx.map((s) => s.unshielded.balances[unshieldedToken().raw] ?? 0n),
Rx.filter((balance) => balance > 0n),
),
);Node.js WebSocket requirement:
typescript
import { WebSocket } from 'ws';
// Required for GraphQL subscriptions (wallet sync) to work in Node.js
globalThis.WebSocket = WebSocket as unknown as typeof globalThis.WebSocket;
// Put this at the very top of your entry file, before any wallet importstypescript
import * as Rx from 'rxjs';
// 等待钱包完全同步
const syncedState = await Rx.firstValueFrom(
wallet.state().pipe(
Rx.throttleTime(5_000),
Rx.filter((state) => state.isSynced),
),
);
// 等待钱包有非零的未屏蔽余额
import { unshieldedToken } from '@midnight-ntwrk/ledger-v8';
const balance = await Rx.firstValueFrom(
wallet.state().pipe(
Rx.throttleTime(10_000),
Rx.filter((s) => s.isSynced),
Rx.map((s) => s.unshielded.balances[unshieldedToken().raw] ?? 0n),
Rx.filter((balance) => balance > 0n),
),
);Node.js WebSocket要求:
typescript
import { WebSocket } from 'ws';
// 为了让GraphQL订阅(钱包同步)在Node.js中正常工作,这是必需的
globalThis.WebSocket = WebSocket as unknown as typeof globalThis.WebSocket;
// 将此代码放在入口文件的最顶部,在任何钱包导入之前6) DUST Generation (Preprod / Undeployed Only)
6) DUST生成(仅Preprod / Undeployed环境)
NIGHT tokens generate DUST over time, but only after UTXOs are explicitly registered on-chain. DUST is the non-transferable fee resource for all transactions.
typescript
// 1. Wait for sync
const state = await Rx.firstValueFrom(wallet.state().pipe(Rx.filter((s) => s.isSynced)));
// 2. Check if DUST already available
if (state.dust.availableCoins.length > 0) {
console.log('DUST already available:', state.dust.walletBalance(new Date()));
return;
}
// 3. Register unregistered NIGHT UTXOs
const nightUtxos = state.unshielded.availableCoins.filter(
(coin: any) => coin.meta?.registeredForDustGeneration !== true,
);
if (nightUtxos.length > 0) {
const recipe = await wallet.registerNightUtxosForDustGeneration(
nightUtxos,
unshieldedKeystore.getPublicKey(),
(payload) => unshieldedKeystore.signData(payload),
);
const finalized = await wallet.finalizeRecipe(recipe);
await wallet.submitTransaction(finalized);
}
// 4. Wait for DUST balance > 0 (may take a few minutes)
await Rx.firstValueFrom(
wallet.state().pipe(
Rx.throttleTime(5_000),
Rx.filter((s) => s.isSynced),
Rx.filter((s) => s.dust.walletBalance(new Date()) > 0n),
),
);DUST troubleshooting:
- DUST balance drops to 0 after failed deploy → restart DApp to release locked DUST coins
- → DUST locked by a pending/failed transaction
pendingCoins > 0 && availableCoins === 0 - NIGHT registered but DUST = 0 → still accruing, wait a few minutes
NIGHT代币会随时间生成DUST,但只有在UTXO在链上显式注册后才会生成。DUST是所有交易的不可转让手续费资源。
typescript
// 1. 等待同步完成
const state = await Rx.firstValueFrom(wallet.state().pipe(Rx.filter((s) => s.isSynced)));
// 2. 检查是否已有可用DUST
if (state.dust.availableCoins.length > 0) {
console.log('已有可用DUST:', state.dust.walletBalance(new Date()));
return;
}
// 3. 注册未注册的NIGHT UTXO
const nightUtxos = state.unshielded.availableCoins.filter(
(coin: any) => coin.meta?.registeredForDustGeneration !== true,
);
if (nightUtxos.length > 0) {
const recipe = await wallet.registerNightUtxosForDustGeneration(
nightUtxos,
unshieldedKeystore.getPublicKey(),
(payload) => unshieldedKeystore.signData(payload),
);
const finalized = await wallet.finalizeRecipe(recipe);
await wallet.submitTransaction(finalized);
}
// 4. 等待DUST余额大于0(可能需要几分钟)
await Rx.firstValueFrom(
wallet.state().pipe(
Rx.throttleTime(5_000),
Rx.filter((s) => s.isSynced),
Rx.filter((s) => s.dust.walletBalance(new Date()) > 0n),
),
);DUST故障排除:
- 部署失败后DUST余额降至0 → 重启DApp以释放锁定的DUST代币
- → DUST被待处理/失败的交易锁定
pendingCoins > 0 && availableCoins === 0 - NIGHT已注册但DUST = 0 → 仍在累积,请等待几分钟
7) Provider Configuration
7) Provider配置
Node.js (self-hosted proof server)
Node.js(自托管证明服务器)
typescript
import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client-proof-provider';
import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider';
import { levelPrivateStateProvider } from '@midnight-ntwrk/midnight-js-level-private-state-provider';
const zkConfigProvider = new NodeZkConfigProvider<CounterCircuits>(zkConfigPath);
const providers: CounterProviders = {
privateStateProvider: levelPrivateStateProvider<CounterPrivateStateId>({
privateStateStoreName: 'counter-private-state', // LevelDB store name
walletProvider: walletAndMidnightProvider,
}),
publicDataProvider: indexerPublicDataProvider(indexerHttp, indexerWs),
zkConfigProvider,
proofProvider: httpClientProofProvider(proofServerUrl, zkConfigProvider),
walletProvider: walletAndMidnightProvider,
midnightProvider: walletAndMidnightProvider,
};typescript
import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client-proof-provider';
import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider';
import { levelPrivateStateProvider } from '@midnight-ntwrk/midnight-js-level-private-state-provider';
const zkConfigProvider = new NodeZkConfigProvider<CounterCircuits>(zkConfigPath);
const providers: CounterProviders = {
privateStateProvider: levelPrivateStateProvider<CounterPrivateStateId>({
privateStateStoreName: 'counter-private-state', // LevelDB存储名称
walletProvider: walletAndMidnightProvider,
}),
publicDataProvider: indexerPublicDataProvider(indexerHttp, indexerWs),
zkConfigProvider,
proofProvider: httpClientProofProvider(proofServerUrl, zkConfigProvider),
walletProvider: walletAndMidnightProvider,
midnightProvider: walletAndMidnightProvider,
};Browser (1AM wallet / CDN ZK assets)
浏览器(1AM钱包 / CDN ZK资产)
typescript
import { FetchZkConfigProvider } from '@midnight-ntwrk/midnight-js-fetch-zk-config-provider';
import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
import { createProofProvider } from '@midnight-ntwrk/midnight-js-types';
const config = await connectedAPI.getConfiguration();
setNetworkId(config.networkId);
const zkConfigProvider = new FetchZkConfigProvider(
new URL(zkAssetBasePath, window.location.origin).toString(),
window.fetch.bind(window),
);
const provingProvider = await connectedAPI.getProvingProvider(zkConfigProvider);
const providers = {
publicDataProvider: indexerPublicDataProvider(config.indexerUri, config.indexerWsUri),
zkConfigProvider,
proofProvider: createProofProvider(provingProvider), // wraps 1AM proving provider
walletProvider: { /* see 1AM skill */ },
midnightProvider: { /* see 1AM skill */ },
privateStateProvider: createPrivateStateProvider(), // in-memory for browser
};typescript
import { FetchZkConfigProvider } from '@midnight-ntwrk/midnight-js-fetch-zk-config-provider';
import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
import { createProofProvider } from '@midnight-ntwrk/midnight-js-types';
const config = await connectedAPI.getConfiguration();
setNetworkId(config.networkId);
const zkConfigProvider = new FetchZkConfigProvider(
new URL(zkAssetBasePath, window.location.origin).toString(),
window.fetch.bind(window),
);
const provingProvider = await connectedAPI.getProvingProvider(zkConfigProvider);
const providers = {
publicDataProvider: indexerPublicDataProvider(config.indexerUri, config.indexerWsUri),
zkConfigProvider,
proofProvider: createProofProvider(provingProvider), // 包装1AM证明provider
walletProvider: { /* 请查看1AM技能 */ },
midnightProvider: { /* 请查看1AM技能 */ },
privateStateProvider: createPrivateStateProvider(), // 浏览器中使用内存存储
};8) WalletProvider & MidnightProvider (Node.js headless)
8) WalletProvider & MidnightProvider(Node.js无头)
This is the bridge between WalletFacade and the midnight-js contracts API:
typescript
import type { WalletProvider, MidnightProvider } from '@midnight-ntwrk/midnight-js-types';
import * as ledger from '@midnight-ntwrk/ledger-v8';
const state = await Rx.firstValueFrom(wallet.state().pipe(Rx.filter((s) => s.isSynced)));
const walletAndMidnightProvider: WalletProvider & MidnightProvider = {
getCoinPublicKey() {
return state.shielded.coinPublicKey.toHexString();
},
getEncryptionPublicKey() {
return state.shielded.encryptionPublicKey.toHexString();
},
async balanceTx(tx, ttl?) {
const recipe = await wallet.balanceUnboundTransaction(
tx,
{ shieldedSecretKeys, dustSecretKey },
{ ttl: ttl ?? new Date(Date.now() + 30 * 60 * 1000) },
);
// ⚠️ KNOWN BUG WORKAROUND: wallet SDK signRecipe hardcodes 'pre-proof' but
// proven (UnboundTransaction) intents contain 'proof' data → "Failed to clone intent"
// Sign intents manually with the correct proof marker (see signTransactionIntents below)
signTransactionIntents(recipe.baseTransaction, signFn, 'proof');
if (recipe.balancingTransaction) {
signTransactionIntents(recipe.balancingTransaction, signFn, 'pre-proof');
}
return wallet.finalizeRecipe(recipe);
},
submitTx(tx) {
return wallet.submitTransaction(tx) as any;
},
};这是WalletFacade与midnight-js合约API之间的桥梁:
typescript
import type { WalletProvider, MidnightProvider } from '@midnight-ntwrk/midnight-js-types';
import * as ledger from '@midnight-ntwrk/ledger-v8';
const state = await Rx.firstValueFrom(wallet.state().pipe(Rx.filter((s) => s.isSynced)));
const walletAndMidnightProvider: WalletProvider & MidnightProvider = {
getCoinPublicKey() {
return state.shielded.coinPublicKey.toHexString();
},
getEncryptionPublicKey() {
return state.shielded.encryptionPublicKey.toHexString();
},
async balanceTx(tx, ttl?) {
const recipe = await wallet.balanceUnboundTransaction(
tx,
{ shieldedSecretKeys, dustSecretKey },
{ ttl: ttl ?? new Date(Date.now() + 30 * 60 * 1000) },
);
// ⚠️ 已知BUG解决方法:wallet SDK的signRecipe硬编码了'pre-proof',但
// 已验证的(UnboundTransaction)意图包含'proof'数据 → "Failed to clone intent"
// 使用正确的proof标记手动签署意图(请参见下面的signTransactionIntents)
signTransactionIntents(recipe.baseTransaction, signFn, 'proof');
if (recipe.balancingTransaction) {
signTransactionIntents(recipe.balancingTransaction, signFn, 'pre-proof');
}
return wallet.finalizeRecipe(recipe);
},
submitTx(tx) {
return wallet.submitTransaction(tx) as any;
},
};The "Failed to clone intent" Bug Fix
“Failed to clone intent”错误修复
typescript
/**
* Workaround for wallet SDK bug where signRecipe hardcodes 'pre-proof',
* causing failures when signing proven (UnboundTransaction) intents.
* Call with proofMarker='proof' for baseTransaction, 'pre-proof' for balancingTransaction.
*/
const signTransactionIntents = (
tx: { intents?: Map<number, any> },
signFn: (payload: Uint8Array) => ledger.Signature,
proofMarker: 'proof' | 'pre-proof',
): void => {
if (!tx.intents || tx.intents.size === 0) return;
for (const segment of tx.intents.keys()) {
const intent = tx.intents.get(segment);
if (!intent) continue;
const cloned = ledger.Intent.deserialize<
ledger.SignatureEnabled, ledger.Proofish, ledger.PreBinding
>('signature', proofMarker, 'pre-binding', intent.serialize());
const signature = signFn(cloned.signatureData(segment));
if (cloned.fallibleUnshieldedOffer) {
const sigs = cloned.fallibleUnshieldedOffer.inputs.map(
(_: ledger.UtxoSpend, i: number) =>
cloned.fallibleUnshieldedOffer!.signatures.at(i) ?? signature,
);
cloned.fallibleUnshieldedOffer = cloned.fallibleUnshieldedOffer.addSignatures(sigs);
}
if (cloned.guaranteedUnshieldedOffer) {
const sigs = cloned.guaranteedUnshieldedOffer.inputs.map(
(_: ledger.UtxoSpend, i: number) =>
cloned.guaranteedUnshieldedOffer!.signatures.at(i) ?? signature,
);
cloned.guaranteedUnshieldedOffer = cloned.guaranteedUnshieldedOffer.addSignatures(sigs);
}
tx.intents.set(segment, cloned);
}
};typescript
/**
* wallet SDK错误的解决方法:signRecipe硬编码了'pre-proof',
* 导致签署已验证的(UnboundTransaction)意图时失败。
* 对baseTransaction使用proofMarker='proof',对balancingTransaction使用'pre-proof'。
*/
const signTransactionIntents = (
tx: { intents?: Map<number, any> },
signFn: (payload: Uint8Array) => ledger.Signature,
proofMarker: 'proof' | 'pre-proof',
): void => {
if (!tx.intents || tx.intents.size === 0) return;
for (const segment of tx.intents.keys()) {
const intent = tx.intents.get(segment);
if (!intent) continue;
const cloned = ledger.Intent.deserialize<
ledger.SignatureEnabled, ledger.Proofish, ledger.PreBinding
>('signature', proofMarker, 'pre-binding', intent.serialize());
const signature = signFn(cloned.signatureData(segment));
if (cloned.fallibleUnshieldedOffer) {
const sigs = cloned.fallibleUnshieldedOffer.inputs.map(
(_: ledger.UtxoSpend, i: number) =>
cloned.fallibleUnshieldedOffer!.signatures.at(i) ?? signature,
);
cloned.fallibleUnshieldedOffer = cloned.fallibleUnshieldedOffer.addSignatures(sigs);
}
if (cloned.guaranteedUnshieldedOffer) {
const sigs = cloned.guaranteedUnshieldedOffer.inputs.map(
(_: ledger.UtxoSpend, i: number) =>
cloned.guaranteedUnshieldedOffer!.signatures.at(i) ?? signature,
);
cloned.guaranteedUnshieldedOffer = cloned.guaranteedUnshieldedOffer.addSignatures(sigs);
}
tx.intents.set(segment, cloned);
}
};9) Deploy & Call Contracts
9) 部署与调用合约
Deploy
部署
typescript
import { deployContract } from '@midnight-ntwrk/midnight-js-contracts';
const deployed = await deployContract(providers, {
compiledContract,
privateStateId: CounterPrivateStateId,
initialPrivateState: { privateCounter: 0 },
});
console.log('Contract address:', deployed.deployTxData.public.contractAddress);typescript
import { deployContract } from '@midnight-ntwrk/midnight-js-contracts';
const deployed = await deployContract(providers, {
compiledContract,
privateStateId: CounterPrivateStateId,
initialPrivateState: { privateCounter: 0 },
});
console.log('合约地址:', deployed.deployTxData.public.contractAddress);Call a Circuit
调用电路
typescript
// Using callTx (recommended — automatically builds, proves, balances, submits)
const result = await deployed.callTx.increment();
console.log('txId:', result.public.txId);
console.log('blockHeight:', result.public.blockHeight);typescript
// 使用callTx(推荐 —— 自动构建、验证、平衡、提交)
const result = await deployed.callTx.increment();
console.log('txId:', result.public.txId);
console.log('区块高度:', result.public.blockHeight);Join an Existing Contract
加入现有合约
typescript
import { findDeployedContract } from '@midnight-ntwrk/midnight-js-contracts';
const contract = await findDeployedContract(providers, {
contractAddress: '09dbe05f...', // hex string
compiledContract,
privateStateId: CounterPrivateStateId,
initialPrivateState: { privateCounter: 0 },
});
// Now call circuits on it
await contract.callTx.increment();typescript
import { findDeployedContract } from '@midnight-ntwrk/midnight-js-contracts';
const contract = await findDeployedContract(providers, {
contractAddress: '09dbe05f...', // 十六进制字符串
compiledContract,
privateStateId: CounterPrivateStateId,
initialPrivateState: { privateCounter: 0 },
});
// 现在可以在其上调用电路
await contract.callTx.increment();Read Ledger State
读取账本状态
typescript
import { ContractState } from '@midnight-ntwrk/compact-runtime';
import { Counter } from './managed/counter';
const contractState = await providers.publicDataProvider.queryContractState(contractAddress);
if (contractState === null) {
console.log('Contract not found');
} else {
// Apply generated ledger() function to deserialize typed state
const ledgerState = Counter.ledger(contractState.data);
console.log('Counter value:', ledgerState.round);
}typescript
import { ContractState } from '@midnight-ntwrk/compact-runtime';
import { Counter } from './managed/counter';
const contractState = await providers.publicDataProvider.queryContractState(contractAddress);
if (contractState === null) {
console.log('未找到合约');
} else {
// 应用生成的ledger()函数来反序列化类型化状态
const ledgerState = Counter.ledger(contractState.data);
console.log('计数器值:', ledgerState.round);
}10) Private State Provider (In-Memory — Browser / Minimal)
10) 私有状态Provider(内存存储 —— 浏览器 / 轻量场景)
typescript
import type {
PrivateStateProvider, PrivateStateId, PrivateStateExport,
SigningKeyExport,
} from '@midnight-ntwrk/midnight-js-types';
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<PrivateStateExport> { throw new Error('Not implemented'); },
async importPrivateStates() { throw new Error('Not implemented'); },
async exportSigningKeys(): Promise<SigningKeyExport> { throw new Error('Not implemented'); },
async importSigningKeys() { throw new Error('Not implemented'); },
};
}For persistent Node.js private state, use instead.
levelPrivateStateProvidertypescript
import type {
PrivateStateProvider, PrivateStateId, PrivateStateExport,
SigningKeyExport,
} from '@midnight-ntwrk/midnight-js-types';
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<PrivateStateExport> { throw new Error('未实现'); },
async importPrivateStates() { throw new Error('未实现'); },
async exportSigningKeys(): Promise<SigningKeyExport> { throw new Error('未实现'); },
async importSigningKeys() { throw new Error('未实现'); },
};
}对于持久化的Node.js私有状态,请改用。
levelPrivateStateProvider11) Testkit (Integration Testing)
11) 测试工具(集成测试)
typescript
import { getTestEnvironment } from '@midnight-ntwrk/testkit-js';
import pino from 'pino';
const logger = pino({ level: 'info' });
let testEnvironment: Awaited<ReturnType<typeof getTestEnvironment>>;
beforeAll(async () => {
testEnvironment = getTestEnvironment(logger);
const environmentConfig = await testEnvironment.start();
// environmentConfig contains indexer, node, proofServer URLs
});
afterAll(async () => {
await testEnvironment.shutdown();
});
// Get wallet providers for testing
const walletProvider = await testEnvironment.getMidnightWalletProvider();
// or multiple wallets (max 4 on local):
const [wallet1, wallet2] = await testEnvironment.startMidnightWalletProviders(2);Environment selection via env var:
bash
undefinedtypescript
import { getTestEnvironment } from '@midnight-ntwrk/testkit-js';
import pino from 'pino';
const logger = pino({ level: 'info' });
let testEnvironment: Awaited<ReturnType<typeof getTestEnvironment>>;
beforeAll(async () => {
testEnvironment = getTestEnvironment(logger);
const environmentConfig = await testEnvironment.start();
// environmentConfig包含索引器、节点、证明服务器的URL
});
afterAll(async () => {
await testEnvironment.shutdown();
});
// 获取用于测试的钱包provider
const walletProvider = await testEnvironment.getMidnightWalletProvider();
// 或多个钱包(本地环境最多4个):
const [wallet1, wallet2] = await testEnvironment.startMidnightWalletProviders(2);通过环境变量选择环境:
bash
undefinedLocal Docker (default)
本地Docker(默认)
MN_TEST_ENVIRONMENT=undeployed yarn test
MN_TEST_ENVIRONMENT=undeployed yarn test
Against preprod
针对preprod环境
MN_TEST_ENVIRONMENT=devnet yarn test
MN_TEST_ENVIRONMENT=devnet yarn test
Custom endpoints
自定义端点
MN_TEST_ENVIRONMENT=env-var-remote
MN_TEST_NETWORK_ID=undeployed
MN_TEST_INDEXER=http://localhost:8088/api/
MN_TEST_INDEXER_WS=ws://localhost:8088/ws/
MN_TEST_NODE=http://localhost:9944
yarn test
MN_TEST_NETWORK_ID=undeployed
MN_TEST_INDEXER=http://localhost:8088/api/
MN_TEST_INDEXER_WS=ws://localhost:8088/ws/
MN_TEST_NODE=http://localhost:9944
yarn test
---MN_TEST_ENVIRONMENT=env-var-remote
MN_TEST_NETWORK_ID=undeployed
MN_TEST_INDEXER=http://localhost:8088/api/
MN_TEST_INDEXER_WS=ws://localhost:8088/ws/
MN_TEST_NODE=http://localhost:9944
yarn test
MN_TEST_NETWORK_ID=undeployed
MN_TEST_INDEXER=http://localhost:8088/api/
MN_TEST_INDEXER_WS=ws://localhost:8088/ws/
MN_TEST_NODE=http://localhost:9944
yarn test
---12) Proof Server
12) 证明服务器
For Node.js / headless use, run the proof server locally via Docker:
bash
undefined对于Node.js / 无头场景,通过Docker本地运行证明服务器:
bash
undefinedAuto-started proof server (preprod, pulls image automatically)
自动启动证明服务器(preprod环境,自动拉取镜像)
cd counter-cli && npm run preprod-ps
cd counter-cli && npm run preprod-ps
Or manually
或手动启动
docker compose -f proof-server.yml up
docker compose -f proof-server.yml up
Wait for: "starting service... listening on: 0.0.0.0:6300"
等待输出:"starting service... listening on: 0.0.0.0:6300"
Direct Docker run
直接Docker运行
docker run -p 6300:6300 midnightntwrk/proof-server:latest midnight-proof-server -v
Proof server URL: `http://127.0.0.1:6300`
**Mac ARM (Apple Silicon) fix:** If the proof server hangs, go to Docker Desktop → Settings → General → Virtual Machine Options → select **Docker VMM** → restart Docker.
---docker run -p 6300:6300 midnightntwrk/proof-server:latest midnight-proof-server -v
证明服务器URL:`http://127.0.0.1:6300`
**Mac ARM(Apple Silicon)修复:** 如果证明服务器挂起,请进入Docker Desktop → 设置 → 通用 → 虚拟机选项 → 选择 **Docker VMM** → 重启Docker。
---13) Common Pitfalls
13) 常见陷阱
setNetworkIdWebSocket not set globally in Node.js → wallet sync GraphQL subscriptions silently fail. Put at the top of your entry file.
globalThis.WebSocket = WebSocketZK assets not found → double-check is absolute, points to the compiled managed folder, and contains and subdirectories. Run the npm script before dev.
zkConfigPathkeys/zkir/sync:assets"Failed to clone intent" → wallet SDK bug, use the workaround. Occurs when a proven is balanced — the SDK hardcodes but the intents contain data.
signTransactionIntentsUnboundTransaction'pre-proof''proof'DUST = 0 after failed deploy → known wallet SDK issue. Restart the DApp to release the locked DUST coins. Use the DUST monitor to verify status before deploying.
Wallet 0 balance after faucet → wait for sync to complete first. If still 0, verify you used the unshielded address (), not the shielded address.
mn_addr_preprod1...relayURLhttps://wss://http://ws://CompiledContract.makelevelPrivateStateProvidercreatePrivateStateProviderOld contract address after recompile → verifier keys change when the Compact contract changes. Old addresses will fail proof verification. Always redeploy after any contract change.
未调用 → SDK内部出现模糊的类型不匹配错误。务必在启动时立即调用,在任何SDK操作之前。
setNetworkIdNode.js中未全局设置WebSocket → 钱包同步的GraphQL订阅静默失败。将放在入口文件的最顶部。
globalThis.WebSocket = WebSocket未找到ZK资产 → 仔细检查是否为绝对路径,指向编译后的managed文件夹,且包含和子目录。在开发前运行 npm脚本。
zkConfigPathkeys/zkir/sync:assets"Failed to clone intent" → wallet SDK的bug,使用解决方法。当已验证的被平衡时会出现此问题 —— SDK硬编码了,但意图包含数据。
signTransactionIntentsUnboundTransaction'pre-proof''proof'部署失败后DUST = 0 → 已知的wallet SDK问题。重启DApp以释放锁定的DUST代币。在部署前使用DUST监控器验证状态。
水龙头领取后钱包余额为0 → 先等待同步完成。如果仍为0,请确认你使用的是未屏蔽地址(),而非屏蔽地址。
mn_addr_preprod1...relayURLhttps://wss://http://ws://每次部署时调用 → 在模块加载时调用一次,不要在部署函数内部调用。每次调用都加载ZK密钥会很慢。
CompiledContract.make浏览器中使用 → LevelDB仅适用于Node.js。浏览器环境中使用内存存储的。
levelPrivateStateProvidercreatePrivateStateProvider重新编译后使用旧合约地址 → 当Compact合约更改时,验证密钥会变化。旧地址会验证失败。合约更改后务必重新部署。
14) Dependency Versions (Pinned — Official Counter Example)
14) 依赖版本(固定版本 —— 官方Counter示例)
json
{
"@midnight-ntwrk/compact-runtime": "0.15.0",
"@midnight-ntwrk/ledger-v8": "8.0.3",
"@midnight-ntwrk/midnight-js-contracts": "4.0.2",
"@midnight-ntwrk/midnight-js-http-client-proof-provider": "4.0.2",
"@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.0.2",
"@midnight-ntwrk/midnight-js-level-private-state-provider": "4.0.2",
"@midnight-ntwrk/midnight-js-network-id": "4.0.2",
"@midnight-ntwrk/midnight-js-node-zk-config-provider": "4.0.2",
"@midnight-ntwrk/midnight-js-types": "4.0.2",
"@midnight-ntwrk/midnight-js-utils": "4.0.2",
"@midnight-ntwrk/wallet-sdk-address-format": "3.1.0",
"@midnight-ntwrk/wallet-sdk-dust-wallet": "3.0.0",
"@midnight-ntwrk/wallet-sdk-facade": "3.0.0",
"@midnight-ntwrk/wallet-sdk-hd": "3.0.1",
"@midnight-ntwrk/wallet-sdk-shielded": "2.1.0",
"@midnight-ntwrk/wallet-sdk-unshielded-wallet": "2.1.0",
"@midnight-ntwrk/compact-js": "2.5.0"
}Node.js version: 22+ (required)
json
{
"@midnight-ntwrk/compact-runtime": "0.15.0",
"@midnight-ntwrk/ledger-v8": "8.0.3",
"@midnight-ntwrk/midnight-js-contracts": "4.0.2",
"@midnight-ntwrk/midnight-js-http-client-proof-provider": "4.0.2",
"@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.0.2",
"@midnight-ntwrk/midnight-js-level-private-state-provider": "4.0.2",
"@midnight-ntwrk/midnight-js-network-id": "4.0.2",
"@midnight-ntwrk/midnight-js-node-zk-config-provider": "4.0.2",
"@midnight-ntwrk/midnight-js-types": "4.0.2",
"@midnight-ntwrk/midnight-js-utils": "4.0.2",
"@midnight-ntwrk/wallet-sdk-address-format": "3.1.0",
"@midnight-ntwrk/wallet-sdk-dust-wallet": "3.0.0",
"@midnight-ntwrk/wallet-sdk-facade": "3.0.0",
"@midnight-ntwrk/wallet-sdk-hd": "3.0.1",
"@midnight-ntwrk/wallet-sdk-shielded": "2.1.0",
"@midnight-ntwrk/wallet-sdk-unshielded-wallet": "2.1.0",
"@midnight-ntwrk/compact-js": "2.5.0"
}Node.js版本:22+(必需)