midnight-js

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Midnight.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:
  • docs.midnight.network/tutorials/counter/counter-cli
    — official full implementation
  • github.com/midnightntwrk/example-counter
    — reference repo (Node.js / headless wallet)
  • github.com/webisoftSoftware/1AM-starter-template
    — browser wallet variant

Midnight.js是用于在Midnight Network上构建DApp的TypeScript SDK —— 类似于以太坊的Web3.js。官方SDK页面“即将推出”;本技能基于官方Counter CLI教程和1AM启动模板编写。
主要参考资料:
  • docs.midnight.network/tutorials/counter/counter-cli
    —— 官方完整实现
  • github.com/midnightntwrk/example-counter
    —— 参考仓库(Node.js / 无头钱包)
  • github.com/webisoftSoftware/1AM-starter-template
    —— 浏览器钱包变体

1) Package Map

1) 包映射表

PackagePurpose
@midnight-ntwrk/midnight-js-contracts
deployContract
,
findDeployedContract
,
submitCallTx
@midnight-ntwrk/midnight-js-types
MidnightProviders
,
WalletProvider
,
MidnightProvider
,
createProofProvider
@midnight-ntwrk/midnight-js-indexer-public-data-provider
indexerPublicDataProvider
(GraphQL indexer client)
@midnight-ntwrk/midnight-js-fetch-zk-config-provider
FetchZkConfigProvider
(browser / CDN)
@midnight-ntwrk/midnight-js-node-zk-config-provider
NodeZkConfigProvider
(Node.js / filesystem)
@midnight-ntwrk/midnight-js-http-client-proof-provider
httpClientProofProvider
(self-hosted proof server)
@midnight-ntwrk/midnight-js-level-private-state-provider
levelPrivateStateProvider
(persistent LevelDB private state)
@midnight-ntwrk/midnight-js-network-id
setNetworkId
,
getNetworkId
@midnight-ntwrk/compact-runtime
ContractState
(indexer deserialization)
@midnight-ntwrk/ledger-v8
Transaction
,
LedgerParameters
,
ZswapSecretKeys
,
DustSecretKey
@midnight-ntwrk/compact-js
CompiledContract
,
ImpureCircuitId
@midnight-ntwrk/wallet-sdk-facade
WalletFacade
(unified wallet)
@midnight-ntwrk/wallet-sdk-hd
HDWallet
,
generateRandomSeed
,
Roles
@midnight-ntwrk/wallet-sdk-shielded
ShieldedWallet
@midnight-ntwrk/wallet-sdk-unshielded-wallet
UnshieldedWallet
,
createKeystore
,
PublicKey
@midnight-ntwrk/wallet-sdk-dust-wallet
DustWallet
@midnight-ntwrk/wallet-sdk-address-format
MidnightBech32m
,
ShieldedAddress
, etc.

用途
@midnight-ntwrk/midnight-js-contracts
deployContract
,
findDeployedContract
,
submitCallTx
@midnight-ntwrk/midnight-js-types
MidnightProviders
,
WalletProvider
,
MidnightProvider
,
createProofProvider
@midnight-ntwrk/midnight-js-indexer-public-data-provider
indexerPublicDataProvider
(GraphQL索引器客户端)
@midnight-ntwrk/midnight-js-fetch-zk-config-provider
FetchZkConfigProvider
(浏览器 / CDN)
@midnight-ntwrk/midnight-js-node-zk-config-provider
NodeZkConfigProvider
(Node.js / 文件系统)
@midnight-ntwrk/midnight-js-http-client-proof-provider
httpClientProofProvider
(自托管证明服务器)
@midnight-ntwrk/midnight-js-level-private-state-provider
levelPrivateStateProvider
(持久化LevelDB私有状态)
@midnight-ntwrk/midnight-js-network-id
setNetworkId
,
getNetworkId
@midnight-ntwrk/compact-runtime
ContractState
(索引器反序列化)
@midnight-ntwrk/ledger-v8
Transaction
,
LedgerParameters
,
ZswapSecretKeys
,
DustSecretKey
@midnight-ntwrk/compact-js
CompiledContract
,
ImpureCircuitId
@midnight-ntwrk/wallet-sdk-facade
WalletFacade
(统一钱包)
@midnight-ntwrk/wallet-sdk-hd
HDWallet
,
generateRandomSeed
,
Roles
@midnight-ntwrk/wallet-sdk-shielded
ShieldedWallet
@midnight-ntwrk/wallet-sdk-unshielded-wallet
UnshieldedWallet
,
createKeystore
,
PublicKey
@midnight-ntwrk/wallet-sdk-dust-wallet
DustWallet
@midnight-ntwrk/wallet-sdk-address-format
MidnightBech32m
,
ShieldedAddress

2) Network Configuration

2) 网络配置

Always call
setNetworkId
before any SDK operation.
It sets a global that all libraries use automatically.
typescript
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:
NetworkIndexer HTTPIndexer WSRPC
preprod
https://indexer.preprod.midnight.network/api/v4/graphql
wss://indexer.preprod.midnight.network/api/v4/graphql/ws
https://rpc.preprod.midnight.network
preview
https://indexer.preview.midnight.network/api/v4/graphql
wss://indexer.preview.midnight.network/api/v4/graphql/ws
wss://rpc.preview.midnight.network
mainnet
https://indexer.mainnet.midnight.network/api/v4/graphql
wss://indexer.mainnet.midnight.network/api/v4/graphql/ws
https://rpc.mainnet.midnight.network
undeployed
(local)
http://localhost:8088/api/v4/graphql
ws://localhost:8088/api/v4/graphql/ws
ws://localhost:9944

在执行任何SDK操作前,务必先调用
setNetworkId
它会设置一个全局变量,供所有库自动使用。
typescript
import { setNetworkId, getNetworkId } from '@midnight-ntwrk/midnight-js-network-id';

// 在启动时调用一次,可在配置构造函数中或构建provider之前调用
setNetworkId('preprod'); // 'preprod' | 'preview' | 'mainnet' | 'undeployed'
网络端点:
网络索引器HTTP索引器WSRPC
preprod
https://indexer.preprod.midnight.network/api/v4/graphql
wss://indexer.preprod.midnight.network/api/v4/graphql/ws
https://rpc.preprod.midnight.network
preview
https://indexer.preview.midnight.network/api/v4/graphql
wss://indexer.preview.midnight.network/api/v4/graphql/ws
wss://rpc.preview.midnight.network
mainnet
https://indexer.mainnet.midnight.network/api/v4/graphql
wss://indexer.mainnet.midnight.network/api/v4/graphql/ws
https://rpc.mainnet.midnight.network
undeployed
(本地)
http://localhost:8088/api/v4/graphql
ws://localhost:8088/api/v4/graphql/ws
ws://localhost:9944

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 imports

typescript
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
  • pendingCoins > 0 && availableCoins === 0
    → DUST locked by a pending/failed transaction
  • 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代币
  • pendingCoins > 0 && availableCoins === 0
    → DUST被待处理/失败的交易锁定
  • 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
levelPrivateStateProvider
instead.

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('未实现'); },
    async importPrivateStates() { throw new Error('未实现'); },
    async exportSigningKeys(): Promise<SigningKeyExport> { throw new Error('未实现'); },
    async importSigningKeys() { throw new Error('未实现'); },
  };
}
对于持久化的Node.js私有状态,请改用
levelPrivateStateProvider

11) 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
undefined
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包含索引器、节点、证明服务器的URL
});

afterAll(async () => {
  await testEnvironment.shutdown();
});

// 获取用于测试的钱包provider
const walletProvider = await testEnvironment.getMidnightWalletProvider();
// 或多个钱包(本地环境最多4个):
const [wallet1, wallet2] = await testEnvironment.startMidnightWalletProviders(2);
通过环境变量选择环境:
bash
undefined

Local 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_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

---

12) Proof Server

12) 证明服务器

For Node.js / headless use, run the proof server locally via Docker:
bash
undefined
对于Node.js / 无头场景,通过Docker本地运行证明服务器:
bash
undefined

Auto-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) 常见陷阱

setNetworkId
not called
→ cryptic type mismatch errors deep in SDK. Call it immediately at startup, before any SDK operations.
WebSocket not set globally in Node.js → wallet sync GraphQL subscriptions silently fail. Put
globalThis.WebSocket = WebSocket
at the top of your entry file.
ZK assets not found → double-check
zkConfigPath
is absolute, points to the compiled managed folder, and contains
keys/
and
zkir/
subdirectories. Run the
sync:assets
npm script before dev.
"Failed to clone intent" → wallet SDK bug, use the
signTransactionIntents
workaround. Occurs when a proven
UnboundTransaction
is balanced — the SDK hardcodes
'pre-proof'
but the intents contain
'proof'
data.
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 (
mn_addr_preprod1...
), not the shielded address.
relayURL
must be WebSocket
→ convert
https://
wss://
,
http://
ws://
. The wallet SDK expects a WebSocket URL for the relay.
CompiledContract.make
called on every deploy
→ call it once at module load, not inside the deploy function. Loading ZK keys on every call is slow.
levelPrivateStateProvider
in browser
→ LevelDB is Node.js only. Use the in-memory
createPrivateStateProvider
for browser contexts.
Old contract address after recompile → verifier keys change when the Compact contract changes. Old addresses will fail proof verification. Always redeploy after any contract change.

未调用
setNetworkId
→ SDK内部出现模糊的类型不匹配错误。务必在启动时立即调用,在任何SDK操作之前。
Node.js中未全局设置WebSocket → 钱包同步的GraphQL订阅静默失败。将
globalThis.WebSocket = WebSocket
放在入口文件的最顶部。
未找到ZK资产 → 仔细检查
zkConfigPath
是否为绝对路径,指向编译后的managed文件夹,且包含
keys/
zkir/
子目录。在开发前运行
sync:assets
npm脚本。
"Failed to clone intent" → wallet SDK的bug,使用
signTransactionIntents
解决方法。当已验证的
UnboundTransaction
被平衡时会出现此问题 —— SDK硬编码了
'pre-proof'
,但意图包含
'proof'
数据。
部署失败后DUST = 0 → 已知的wallet SDK问题。重启DApp以释放锁定的DUST代币。在部署前使用DUST监控器验证状态。
水龙头领取后钱包余额为0 → 先等待同步完成。如果仍为0,请确认你使用的是未屏蔽地址(
mn_addr_preprod1...
),而非屏蔽地址。
relayURL
必须是WebSocket
→ 将
https://
转换为
wss://
http://
转换为
ws://
。wallet SDK期望relay使用WebSocket URL。
每次部署时调用
CompiledContract.make
→ 在模块加载时调用一次,不要在部署函数内部调用。每次调用都加载ZK密钥会很慢。
浏览器中使用
levelPrivateStateProvider
→ LevelDB仅适用于Node.js。浏览器环境中使用内存存储的
createPrivateStateProvider
重新编译后使用旧合约地址 → 当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+(必需)