multinetwork

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Multi-Network Midnight Skill

多网络Midnight开发指南

This skill covers building a Midnight DApp that works across all four network targets from a single codebase. The key insight: networks differ in three ways — endpoints, proof server (local Docker vs ProofStation), and DUST flow (manual registration vs sponsored). Everything else is identical.
Primary references:
  • github.com/midnightntwrk/midnight-local-dev
    — official local Docker stack + funding CLI
  • github.com/midnightntwrk/example-counter
    — official reference DApp (standalone + preprod modes)

本指南介绍如何从单一代码库构建可适配全部四个目标网络的Midnight DApp。核心要点:各网络仅在三个方面存在差异——端点证明服务器(本地Docker vs ProofStation)、DUST流程(手动注册 vs 赞助模式),其余所有内容完全一致。
主要参考资料:
  • github.com/midnightntwrk/midnight-local-dev
    — 官方本地Docker栈 + 资金管理CLI
  • github.com/midnightntwrk/example-counter
    — 官方参考DApp(独立模式 + 预生产模式)

1) The Four Networks at a Glance

1) 四大网络概览

Network
networkId
Use forProof serverDUST flow
undeployed
'undeployed'
Local dev, CILocal Docker
:6300
Manual registration (genesis wallet seeds funded)
preview
'preview'
Active development on testnet1AM ProofStation (
api-preview.1am.xyz
)
Sponsored — user pays 0 fees
preprod
'preprod'
Pre-release / integration testingLocal Docker
:6300
OR ProofStation
Manual registration (faucet → NIGHT → DUST)
mainnet
'mainnet'
Production1AM ProofStation (
api.1am.xyz
)
Sponsored — user pays 0 fees
Critical distinction: On
preview
and
mainnet
, the 1AM wallet sponsors all fees via ProofStation — users need zero NIGHT or DUST. On
undeployed
and
preprod
, you must run your own proof server and manually register NIGHT UTXOs for DUST generation before transactions work.

网络
networkId
适用场景证明服务器DUST流程
undeployed
'undeployed'
本地开发、CI本地Docker
:6300
手动注册(创世钱包种子已充值)
preview
'preview'
测试网活跃开发1AM ProofStation (
api-preview.1am.xyz
)
赞助模式 — 用户无需支付任何费用
preprod
'preprod'
预发布/集成测试本地Docker
:6300
或 ProofStation
手动注册(水龙头获取NIGHT → 转换为DUST)
mainnet
'mainnet'
生产环境1AM ProofStation (
api.1am.xyz
)
赞助模式 — 用户无需支付任何费用
关键区别:
preview
mainnet
中,1AM钱包通过ProofStation赞助所有费用——用户无需持有NIGHT或DUST。在
undeployed
preprod
中,你必须自行运行证明服务器,并在交易生效前手动注册NIGHT UTXO以生成DUST。

2) Local Docker Stack (
undeployed
)

2) 本地Docker栈(
undeployed
网络)

Running the
undeployed
network locally requires three Docker containers: a Midnight node, an indexer, and a proof server. You cannot just run the proof server — you need all three.
在本地运行
undeployed
网络需要三个Docker容器:Midnight节点、索引器和证明服务器。不能仅运行证明服务器——三者缺一不可。

Option A: Use
midnight-local-dev
(recommended)

选项A:使用
midnight-local-dev
(推荐)

The official tool at
github.com/midnightntwrk/midnight-local-dev
manages the full stack and handles genesis wallet funding:
bash

git clone https://github.com/midnightntwrk/midnight-local-dev
cd midnight-local-dev
npm install
npm start
npm start
will:
  1. Detect if containers are already running (prompts to reuse or restart)
  2. Pull and start all three containers via Docker Compose
  3. Initialize the genesis master wallet (seed
    0x00...01
    ) and register its DUST
  4. Present a funding menu to transfer NIGHT and register DUST for your test accounts
Once running, connect your DApp to:
networkId:  undeployed
indexer:    http://localhost:8088/api/v3/graphql
indexerWS:  ws://localhost:8088/api/v3/graphql/ws
node:       http://localhost:9944
proofServer: http://localhost:6300
Note: The local indexer uses
/api/v3/graphql
(not v4 like testnet). Use the correct path.
github.com/midnightntwrk/midnight-local-dev
官方工具可管理完整栈并处理创世钱包充值:
bash

git clone https://github.com/midnightntwrk/midnight-local-dev
cd midnight-local-dev
npm install
npm start
npm start
将执行以下操作:
  1. 检测容器是否已运行(提示复用或重启)
  2. 通过Docker Compose拉取并启动所有三个容器
  3. 初始化创世主钱包(种子
    0x00...01
    )并注册其DUST
  4. 显示资金管理菜单,用于为测试账户转账NIGHT并注册DUST
运行后,将你的DApp连接至:
networkId:  undeployed
indexer:    http://localhost:8088/api/v3/graphql
indexerWS:  ws://localhost:8088/api/v3/graphql/ws
node:       http://localhost:9944
proofServer: http://localhost:6300
注意: 本地索引器使用
/api/v3/graphql
(而非测试网的v4版本),请使用正确路径。

Option B: Docker Compose directly (no funding CLI)

选项B:直接使用Docker Compose(无资金管理CLI)

bash
undefined
bash
undefined

From midnight-local-dev repo root:

从midnight-local-dev仓库根目录执行:

docker compose -f standalone.yml up -d
docker compose -f standalone.yml up -d

Check all three services are healthy

检查三个服务是否均处于健康状态

docker compose -f standalone.yml ps
docker compose -f standalone.yml ps

View logs

查看日志

docker compose -f standalone.yml logs -f
docker compose -f standalone.yml logs -f

Tear down

停止服务

docker compose -f standalone.yml down

In this mode, you manage genesis wallet initialization and DUST registration yourself.
docker compose -f standalone.yml down

在此模式下,你需要自行管理创世钱包初始化和DUST注册。

Docker Image Versions (as of 2026)

Docker镜像版本(截至2026年)

ServiceImageVersion
Node
midnightntwrk/midnight-node
0.21.0
Indexer
midnightntwrk/indexer-standalone
3.1.0
Proof Server
midnightntwrk/proof-server
7.0.0
服务镜像版本
Node
midnightntwrk/midnight-node
0.21.0
Indexer
midnightntwrk/indexer-standalone
3.1.0
Proof Server
midnightntwrk/proof-server
7.0.0

Lace wallet on
undeployed

Lace钱包适配
undeployed
网络

The Lace browser wallet auto-connects to the local stack when you select "Undeployed" in its network settings — it hardcodes
localhost:9944
,
localhost:8088
, and
localhost:6300
. No custom endpoint config needed.
当你在Lace浏览器钱包的网络设置中选择“Undeployed”时,它会自动连接到本地栈——已硬编码
localhost:9944
localhost:8088
localhost:6300
,无需自定义端点配置。

Funding test accounts locally

本地测试账户充值

bash
undefined
bash
undefined

Option 1: Fund from a JSON file (gets NIGHT + DUST registered)

选项1:通过JSON文件充值(获取NIGHT + 注册DUST)

accounts.json: { "accounts": [{ "name": "Alice", "mnemonic": "..." }] }

accounts.json: { "accounts": [{ "name": "Alice", "mnemonic": "..." }] }

(max 10 accounts, 50,000 NIGHT each)

(最多10个账户,每个账户最多50,000 NIGHT)

Option 2: Fund by Bech32 address (NIGHT only — DUST not registered)

选项2:通过Bech32地址充值(仅NIGHT — 不注册DUST)

Use when you have a Lace address or DApp-generated address

适用于你已有Lace地址或DApp生成的地址


Genesis master wallet seed: `0x0000000000000000000000000000000000000000000000000000000000000001`

---

创世主钱包种子:`0x0000000000000000000000000000000000000000000000000000000000000001`

---

3) Network Configuration

3) 网络配置

typescript
// src/config/networks.ts
import { setNetworkId } from '@midnight-ntwrk/midnight-js-network-id';

export type NetworkId = 'undeployed' | 'preview' | 'preprod' | 'mainnet';

// 'sponsored' = 1AM ProofStation pays fees (preview, mainnet)
// 'manual'    = you run a proof server, user registers NIGHT→DUST (undeployed, preprod)
export type DustMode = 'sponsored' | 'manual';

export interface NetworkConfig {
  networkId: NetworkId;
  indexerHttp: string;
  indexerWs: string;
  rpc: string;
  proofServerUrl: string;
  dustMode: DustMode;
  faucetUrl?: string;
}

export const NETWORKS: Record<NetworkId, NetworkConfig> = {
  undeployed: {
    networkId: 'undeployed',
    indexerHttp: 'http://localhost:8088/api/v3/graphql',  // NOTE: v3, not v4
    indexerWs:   'ws://localhost:8088/api/v3/graphql/ws',
    rpc:         'ws://localhost:9944',
    proofServerUrl: 'http://127.0.0.1:6300',
    dustMode: 'manual',
  },
  preview: {
    networkId: 'preview',
    indexerHttp: 'https://indexer.preview.midnight.network/api/v4/graphql',
    indexerWs:   'wss://indexer.preview.midnight.network/api/v4/graphql/ws',
    rpc:         'wss://rpc.preview.midnight.network',
    proofServerUrl: 'https://api-preview.1am.xyz',  // 1AM ProofStation
    dustMode: 'sponsored',
  },
  preprod: {
    networkId: 'preprod',
    indexerHttp: 'https://indexer.preprod.midnight.network/api/v4/graphql',
    indexerWs:   'wss://indexer.preprod.midnight.network/api/v4/graphql/ws',
    rpc:         'https://rpc.preprod.midnight.network',
    proofServerUrl: 'http://127.0.0.1:6300',  // local proof server (run via Docker)
    dustMode: 'manual',
    faucetUrl: 'https://faucet.preprod.midnight.network',
  },
  mainnet: {
    networkId: 'mainnet',
    indexerHttp: 'https://indexer.mainnet.midnight.network/api/v4/graphql',
    indexerWs:   'wss://indexer.mainnet.midnight.network/api/v4/graphql/ws',
    rpc:         'https://rpc.mainnet.midnight.network',
    proofServerUrl: 'https://api.1am.xyz',  // 1AM ProofStation
    dustMode: 'sponsored',
  },
};

export function getActiveNetwork(): NetworkConfig {
  const id = (
    process.env.NETWORK ||
    process.env.VITE_NETWORK ||
    'undeployed'
  ) as NetworkId;
  const config = NETWORKS[id];
  if (!config) throw new Error(`Unknown network: "${id}". Valid: ${Object.keys(NETWORKS).join(', ')}`);
  return config;
}

// Call this once at startup before any SDK operations
export function applyNetworkId(config: NetworkConfig) {
  setNetworkId(config.networkId);
}

typescript
// src/config/networks.ts
import { setNetworkId } from '@midnight-ntwrk/midnight-js-network-id';

export type NetworkId = 'undeployed' | 'preview' | 'preprod' | 'mainnet';

// 'sponsored' = 1AM ProofStation支付费用(preview、mainnet)
// 'manual'    = 自行运行证明服务器,用户需注册NIGHT→DUST(undeployed、preprod)
export type DustMode = 'sponsored' | 'manual';

export interface NetworkConfig {
  networkId: NetworkId;
  indexerHttp: string;
  indexerWs: string;
  rpc: string;
  proofServerUrl: string;
  dustMode: DustMode;
  faucetUrl?: string;
}

export const NETWORKS: Record<NetworkId, NetworkConfig> = {
  undeployed: {
    networkId: 'undeployed',
    indexerHttp: 'http://localhost:8088/api/v3/graphql',  // 注意:是v3,不是v4
    indexerWs:   'ws://localhost:8088/api/v3/graphql/ws',
    rpc:         'ws://localhost:9944',
    proofServerUrl: 'http://127.0.0.1:6300',
    dustMode: 'manual',
  },
  preview: {
    networkId: 'preview',
    indexerHttp: 'https://indexer.preview.midnight.network/api/v4/graphql',
    indexerWs:   'wss://indexer.preview.midnight.network/api/v4/graphql/ws',
    rpc:         'wss://rpc.preview.midnight.network',
    proofServerUrl: 'https://api-preview.1am.xyz',  // 1AM ProofStation
    dustMode: 'sponsored',
  },
  preprod: {
    networkId: 'preprod',
    indexerHttp: 'https://indexer.preprod.midnight.network/api/v4/graphql',
    indexerWs:   'wss://indexer.preprod.midnight.network/api/v4/graphql/ws',
    rpc:         'https://rpc.preprod.midnight.network',
    proofServerUrl: 'http://127.0.0.1:6300',  // 本地证明服务器(通过Docker运行)
    dustMode: 'manual',
    faucetUrl: 'https://faucet.preprod.midnight.network',
  },
  mainnet: {
    networkId: 'mainnet',
    indexerHttp: 'https://indexer.mainnet.midnight.network/api/v4/graphql',
    indexerWs:   'wss://indexer.mainnet.midnight.network/api/v4/graphql/ws',
    rpc:         'https://rpc.mainnet.midnight.network',
    proofServerUrl: 'https://api.1am.xyz',  // 1AM ProofStation
    dustMode: 'sponsored',
  },
};

export function getActiveNetwork(): NetworkConfig {
  const id = (
    process.env.NETWORK ||
    process.env.VITE_NETWORK ||
    'undeployed'
  ) as NetworkId;
  const config = NETWORKS[id];
  if (!config) throw new Error(`Unknown network: "${id}". Valid: ${Object.keys(NETWORKS).join(', ')}`);
  return config;
}

// 在任何SDK操作前调用一次此函数
export function applyNetworkId(config: NetworkConfig) {
  setNetworkId(config.networkId);
}

4) Unified Provider Builder

4) 统一Provider构建器

typescript
// src/config/providers.ts
import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
import type { NetworkConfig } from './networks';

// ── ZK Config Provider ──────────────────────────────────────────────────────

// Node.js: load ZK assets from filesystem
export function makeNodeZkConfigProvider(zkConfigPath: string) {
  const { NodeZkConfigProvider } = require('@midnight-ntwrk/midnight-js-node-zk-config-provider');
  const path = require('path');
  return new NodeZkConfigProvider(path.resolve(zkConfigPath));
}

// Browser: load ZK assets via fetch from CDN or public/
export function makeFetchZkConfigProvider(zkAssetBasePath: string) {
  const { FetchZkConfigProvider } = require('@midnight-ntwrk/midnight-js-fetch-zk-config-provider');
  return new FetchZkConfigProvider(
    new URL(zkAssetBasePath, window.location.origin).toString(),
    window.fetch.bind(window),
  );
}

// ── Proof Provider ───────────────────────────────────────────────────────────

// Node.js / self-hosted: points at local Docker proof server or ProofStation URL
// For ProofStation (preview/mainnet headless), pass the 1AM endpoint + API key
export function makeHttpProofProvider(proofServerUrl: string, zkConfigProvider: any) {
  const { httpClientProofProvider } = require('@midnight-ntwrk/midnight-js-http-client-proof-provider');
  return httpClientProofProvider(new URL(proofServerUrl), zkConfigProvider);
}

// Browser + 1AM wallet: proving handled by the wallet extension (no proof server needed)
export function make1AMProofProvider(provingProvider: any) {
  const { createProofProvider } = require('@midnight-ntwrk/midnight-js-types');
  return createProofProvider(provingProvider);
}

// ── Public Data Provider ─────────────────────────────────────────────────────

export function makePublicDataProvider(network: NetworkConfig) {
  return indexerPublicDataProvider(network.indexerHttp, network.indexerWs);
}

// ── Private State Provider ───────────────────────────────────────────────────

// Node.js: persistent LevelDB store (survives restarts)
export function makeLevelPrivateStateProvider(storeName: string, walletProvider: any) {
  const { levelPrivateStateProvider } = require('@midnight-ntwrk/midnight-js-level-private-state-provider');
  return levelPrivateStateProvider({ privateStateStoreName: storeName, walletProvider });
}

// Browser: in-memory (lost on page refresh — acceptable for browser DApps)
export function makeInMemoryPrivateStateProvider() {
  let scope = '';
  const stateStore = new Map<string, unknown>();
  const signingKeyStore = new Map<string, unknown>();
  const key = (id: string) => `${scope}:${id}`;
  return {
    setContractAddress(address: string) { scope = address; },
    async set(id: string, state: unknown) { stateStore.set(key(id), state); },
    async get(id: string) { return stateStore.get(key(id)) ?? null; },
    async remove(id: string) { stateStore.delete(key(id)); },
    async clear() { stateStore.clear(); },
    async setSigningKey(addr: string, k: unknown) { signingKeyStore.set(addr, k); },
    async getSigningKey(addr: string) { return signingKeyStore.get(addr) ?? null; },
    async removeSigningKey(addr: string) { signingKeyStore.delete(addr); },
    async clearSigningKeys() { signingKeyStore.clear(); },
    async exportPrivateStates(): Promise<never> { throw new Error('Not implemented'); },
    async importPrivateStates(): Promise<never> { throw new Error('Not implemented'); },
    async exportSigningKeys(): Promise<never> { throw new Error('Not implemented'); },
    async importSigningKeys(): Promise<never> { throw new Error('Not implemented'); },
  };
}

typescript
// src/config/providers.ts
import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
import type { NetworkConfig } from './networks';

// ── ZK配置Provider ──────────────────────────────────────────────────────

// Node.js: 从文件系统加载ZK资产
export function makeNodeZkConfigProvider(zkConfigPath: string) {
  const { NodeZkConfigProvider } = require('@midnight-ntwrk/midnight-js-node-zk-config-provider');
  const path = require('path');
  return new NodeZkConfigProvider(path.resolve(zkConfigPath));
}

// 浏览器: 通过fetch从CDN或public/目录加载ZK资产
export function makeFetchZkConfigProvider(zkAssetBasePath: string) {
  const { FetchZkConfigProvider } = require('@midnight-ntwrk/midnight-js-fetch-zk-config-provider');
  return new FetchZkConfigProvider(
    new URL(zkAssetBasePath, window.location.origin).toString(),
    window.fetch.bind(window),
  );
}

// ── 证明Provider ───────────────────────────────────────────────────────────

// Node.js / 自托管: 指向本地Docker证明服务器或ProofStation URL
// 对于ProofStation(preview/mainnet无头模式),传入1AM端点 + API密钥
export function makeHttpProofProvider(proofServerUrl: string, zkConfigProvider: any) {
  const { httpClientProofProvider } = require('@midnight-ntwrk/midnight-js-http-client-proof-provider');
  return httpClientProofProvider(new URL(proofServerUrl), zkConfigProvider);
}

// 浏览器 + 1AM钱包: 证明由钱包扩展处理(无需证明服务器)
export function make1AMProofProvider(provingProvider: any) {
  const { createProofProvider } = require('@midnight-ntwrk/midnight-js-types');
  return createProofProvider(provingProvider);
}

// ── 公共数据Provider ─────────────────────────────────────────────────────

export function makePublicDataProvider(network: NetworkConfig) {
  return indexerPublicDataProvider(network.indexerHttp, network.indexerWs);
}

// ── 私有状态Provider ───────────────────────────────────────────────────

// Node.js: 持久化LevelDB存储(重启后保留)
export function makeLevelPrivateStateProvider(storeName: string, walletProvider: any) {
  const { levelPrivateStateProvider } = require('@midnight-ntwrk/midnight-js-level-private-state-provider');
  return levelPrivateStateProvider({ privateStateStoreName: storeName, walletProvider });
}

// 浏览器: 内存存储(页面刷新后丢失 — 适用于浏览器DApp)
export function makeInMemoryPrivateStateProvider() {
  let scope = '';
  const stateStore = new Map<string, unknown>();
  const signingKeyStore = new Map<string, unknown>();
  const key = (id: string) => `${scope}:${id}`;
  return {
    setContractAddress(address: string) { scope = address; },
    async set(id: string, state: unknown) { stateStore.set(key(id), state); },
    async get(id: string) { return stateStore.get(key(id)) ?? null; },
    async remove(id: string) { stateStore.delete(key(id)); },
    async clear() { stateStore.clear(); },
    async setSigningKey(addr: string, k: unknown) { signingKeyStore.set(addr, k); },
    async getSigningKey(addr: string) { return signingKeyStore.get(addr) ?? null; },
    async removeSigningKey(addr: string) { signingKeyStore.delete(addr); },
    async clearSigningKeys() { signingKeyStore.clear(); },
    async exportPrivateStates(): Promise<never> { throw new Error('Not implemented'); },
    async importPrivateStates(): Promise<never> { throw new Error('Not implemented'); },
    async exportSigningKeys(): Promise<never> { throw new Error('Not implemented'); },
    async importSigningKeys(): Promise<never> { throw new Error('Not implemented'); },
  };
}

5) WalletProvider & MidnightProvider (Node.js headless, all networks)

5) WalletProvider & MidnightProvider(Node.js无头模式,全网络适配)

This bridge is identical across all networks. The
signTransactionIntents
workaround is required on all networks — it's a wallet SDK bug, not network-specific.
typescript
// src/config/wallet-provider.ts
import * as Rx from 'rxjs';
import * as ledger from '@midnight-ntwrk/ledger-v8';
import type { WalletProvider, MidnightProvider } from '@midnight-ntwrk/midnight-js-types';

/**
 * Workaround for wallet SDK bug: signRecipe hardcodes 'pre-proof' marker,
 * but proven (UnboundTransaction) intents contain 'proof' data → "Failed to clone intent".
 * Call with proofMarker='proof' for baseTransaction, 'pre-proof' for balancingTransaction.
 */
function 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);
  }
}

export async function makeWalletAndMidnightProvider(
  walletFacade: any,
  shieldedSecretKeys: ledger.ZswapSecretKeys,
  dustSecretKey: ledger.DustSecretKey,
  unshieldedKeystore: any,
): Promise<WalletProvider & MidnightProvider> {
  // state() is an Observable — must subscribe, not access as property
  const state = await Rx.firstValueFrom(
    walletFacade.state().pipe(Rx.filter((s: any) => s.isSynced)),
  );

  const signFn = (payload: Uint8Array) => unshieldedKeystore.signData(payload);

  return {
    getCoinPublicKey() {
      return state.shielded.coinPublicKey.toHexString();
    },
    getEncryptionPublicKey() {
      return state.shielded.encryptionPublicKey.toHexString();
    },
    async balanceTx(tx: any, ttl?: Date) {
      const recipe = await walletFacade.balanceUnboundTransaction(
        tx,
        { shieldedSecretKeys, dustSecretKey },
        { ttl: ttl ?? new Date(Date.now() + 30 * 60 * 1000) },
      );

      // Apply signTransactionIntents workaround on both tx parts
      signTransactionIntents(recipe.baseTransaction, signFn, 'proof');
      if (recipe.balancingTransaction) {
        signTransactionIntents(recipe.balancingTransaction, signFn, 'pre-proof');
      }

      return walletFacade.finalizeRecipe(recipe);
    },
    async submitTx(tx: any) {
      return walletFacade.submitTransaction(tx) as any;
    },
  };
}

此桥接层在所有网络中完全一致。
signTransactionIntents
临时解决方案在所有网络中都是必需的——这是钱包SDK的Bug,与网络无关。
typescript
// src/config/wallet-provider.ts
import * as Rx from 'rxjs';
import * as ledger from '@midnight-ntwrk/ledger-v8';
import type { WalletProvider, MidnightProvider } from '@midnight-ntwrk/midnight-js-types';

/**
 * 钱包SDK Bug临时解决方案:signRecipe硬编码了'pre-proof'标记,
 * 但已完成证明的(UnboundTransaction)意图包含'proof'数据 → 抛出"Failed to clone intent"错误。
 * 对于baseTransaction,使用proofMarker='proof';对于balancingTransaction,使用'pre-proof'。
 */
function 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);
  }
}

export async function makeWalletAndMidnightProvider(
  walletFacade: any,
  shieldedSecretKeys: ledger.ZswapSecretKeys,
  dustSecretKey: ledger.DustSecretKey,
  unshieldedKeystore: any,
): Promise<WalletProvider & MidnightProvider> {
  // state()是Observable — 必须订阅,不能直接作为属性访问
  const state = await Rx.firstValueFrom(
    walletFacade.state().pipe(Rx.filter((s: any) => s.isSynced)),
  );

  const signFn = (payload: Uint8Array) => unshieldedKeystore.signData(payload);

  return {
    getCoinPublicKey() {
      return state.shielded.coinPublicKey.toHexString();
    },
    getEncryptionPublicKey() {
      return state.shielded.encryptionPublicKey.toHexString();
    },
    async balanceTx(tx: any, ttl?: Date) {
      const recipe = await walletFacade.balanceUnboundTransaction(
        tx,
        { shieldedSecretKeys, dustSecretKey },
        { ttl: ttl ?? new Date(Date.now() + 30 * 60 * 1000) },
      );

      // 对交易的两部分应用signTransactionIntents临时解决方案
      signTransactionIntents(recipe.baseTransaction, signFn, 'proof');
      if (recipe.balancingTransaction) {
        signTransactionIntents(recipe.balancingTransaction, signFn, 'pre-proof');
      }

      return walletFacade.finalizeRecipe(recipe);
    },
    async submitTx(tx: any) {
      return walletFacade.submitTransaction(tx) as any;
    },
  };
}

6) DUST Flow Per Network

6) 各网络的DUST流程

undeployed  →  manual:  register NIGHT UTXOs → wait for DUST to accrue
preview     →  sponsored: 1AM ProofStation pays all fees → user needs nothing
preprod     →  manual:  faucet NIGHT → register UTXOs → wait for DUST
mainnet     →  sponsored: 1AM ProofStation pays all fees → user needs nothing
typescript
// src/config/dust.ts
import * as Rx from 'rxjs';
import type { NetworkConfig } from './networks';

export async function ensureDust(
  walletFacade: any,
  unshieldedKeystore: any,
  network: NetworkConfig,
): Promise<void> {
  // sponsored networks: 1AM pays fees — nothing to do
  if (network.dustMode === 'sponsored') return;

  const state = await Rx.firstValueFrom(
    walletFacade.state().pipe(Rx.filter((s: any) => s.isSynced)),
  );

  // already have DUST
  if (state.dust.availableCoins.length > 0) return;

  // find NIGHT UTXOs not yet registered for DUST generation
  const unregistered = state.unshielded.availableCoins.filter(
    (coin: any) => coin.meta?.registeredForDustGeneration !== true,
  );

  if (unregistered.length === 0) {
    // all UTXOs registered — just wait for DUST to accrue
    console.log('Waiting for DUST to generate (all UTXOs already registered)...');
  } else {
    // submit registration tx
    console.log(`Registering ${unregistered.length} NIGHT UTXO(s) for DUST generation...`);
    const recipe = await walletFacade.registerNightUtxosForDustGeneration(
      unregistered,
      unshieldedKeystore.getPublicKey(),
      (payload: Uint8Array) => unshieldedKeystore.signData(payload),
    );
    const finalized = await walletFacade.finalizeRecipe(recipe);
    await walletFacade.submitTransaction(finalized);
  }

  // wait for DUST balance > 0 (can take a few minutes on preprod)
  await Rx.firstValueFrom(
    walletFacade.state().pipe(
      Rx.throttleTime(5_000),
      Rx.filter((s: any) => s.isSynced),
      Rx.filter((s: any) => s.dust.walletBalance(new Date()) > 0n),
    ),
  );

  console.log('DUST available.');
}

undeployed  →  手动模式: 注册NIGHT UTXO → 等待DUST生成
preview     →  赞助模式: 1AM ProofStation支付所有费用 → 用户无需任何操作
preprod     →  手动模式: 水龙头获取NIGHT → 注册UTXO → 等待DUST生成
mainnet     →  赞助模式: 1AM ProofStation支付所有费用 → 用户无需任何操作
typescript
// src/config/dust.ts
import * as Rx from 'rxjs';
import type { NetworkConfig } from './networks';

export async function ensureDust(
  walletFacade: any,
  unshieldedKeystore: any,
  network: NetworkConfig,
): Promise<void> {
  // 赞助模式网络:1AM支付费用 — 无需任何操作
  if (network.dustMode === 'sponsored') return;

  const state = await Rx.firstValueFrom(
    walletFacade.state().pipe(Rx.filter((s: any) => s.isSynced)),
  );

  // 已拥有DUST
  if (state.dust.availableCoins.length > 0) return;

  // 查找尚未注册用于生成DUST的NIGHT UTXO
  const unregistered = state.unshielded.availableCoins.filter(
    (coin: any) => coin.meta?.registeredForDustGeneration !== true,
  );

  if (unregistered.length === 0) {
    // 所有UTXO已注册 — 只需等待DUST生成
    console.log('等待DUST生成(所有UTXO已注册)...');
  } else {
    // 提交注册交易
    console.log(`正在注册${unregistered.length}个NIGHT UTXO用于生成DUST...`);
    const recipe = await walletFacade.registerNightUtxosForDustGeneration(
      unregistered,
      unshieldedKeystore.getPublicKey(),
      (payload: Uint8Array) => unshieldedKeystore.signData(payload),
    );
    const finalized = await walletFacade.finalizeRecipe(recipe);
    await walletFacade.submitTransaction(finalized);
  }

  // 等待DUST余额>0(预生产网可能需要几分钟)
  await Rx.firstValueFrom(
    walletFacade.state().pipe(
      Rx.throttleTime(5_000),
      Rx.filter((s: any) => s.isSynced),
      Rx.filter((s: any) => s.dust.walletBalance(new Date()) > 0n),
    ),
  );

  console.log('DUST已可用。');
}

7) Wallet Initialization (Node.js, all networks)

7) 钱包初始化(Node.js,全网络适配)

typescript
// src/config/wallet.ts
import * as Rx from 'rxjs';
import * as ledger from '@midnight-ntwrk/ledger-v8';
import { HDWallet, generateRandomSeed, Roles } from '@midnight-ntwrk/wallet-sdk-hd';
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';
import { WebSocket } from 'ws';
import { Buffer } from 'buffer';
import { applyNetworkId, type NetworkConfig } from './networks';
import { ensureDust } from './dust';
import { makeWalletAndMidnightProvider } from './wallet-provider';

// Required: set WebSocket global before any wallet SDK imports
globalThis.WebSocket = WebSocket as unknown as typeof globalThis.WebSocket;

export async function buildHeadlessWallet(network: NetworkConfig, seedHex?: string) {
  applyNetworkId(network);

  const seed = seedHex
    ? Buffer.from(seedHex, 'hex')
    : Buffer.from(generateRandomSeed());

  const hdWallet = HDWallet.fromSeed(seed);
  if (hdWallet.type !== 'seedOk') throw new Error('Invalid wallet 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]);
  const unshieldedKeystore = createKeystore(keys[Roles.NightExternal], network.networkId);

  // relayURL must be WebSocket — convert http→ws, https→wss
  const relayURL = new URL(network.rpc.replace(/^https/, 'wss').replace(/^http(?!s)/, 'ws'));

  const shieldedWallet = ShieldedWallet({
    networkId: network.networkId,
    indexerClientConnection: { indexerHttpUrl: network.indexerHttp, indexerWsUrl: network.indexerWs },
    provingServerUrl: new URL(network.proofServerUrl),
    relayURL,
  }).startWithSecretKeys(shieldedSecretKeys);

  const unshieldedWallet = UnshieldedWallet({
    networkId: network.networkId,
    indexerClientConnection: { indexerHttpUrl: network.indexerHttp, indexerWsUrl: network.indexerWs },
    txHistoryStorage: new InMemoryTransactionHistoryStorage(),
  }).startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore));

  const dustWallet = DustWallet({
    networkId: network.networkId,
    costParameters: {
      additionalFeeOverhead: 300_000_000_000_000n,
      feeBlocksMargin: 5,
    },
    indexerClientConnection: { indexerHttpUrl: network.indexerHttp, indexerWsUrl: network.indexerWs },
    provingServerUrl: new URL(network.proofServerUrl),
    relayURL,
  }).startWithSecretKey(dustSecretKey, ledger.LedgerParameters.initialParameters().dust);

  const walletFacade = new WalletFacade(shieldedWallet, unshieldedWallet, dustWallet);
  await walletFacade.start(shieldedSecretKeys, dustSecretKey);

  // Wait for sync
  await Rx.firstValueFrom(
    walletFacade.state().pipe(
      Rx.throttleTime(5_000),
      Rx.filter((s: any) => s.isSynced),
    ),
  );

  // Handle DUST based on network's dustMode
  await ensureDust(walletFacade, unshieldedKeystore, network);

  const walletProvider = await makeWalletAndMidnightProvider(
    walletFacade, shieldedSecretKeys, dustSecretKey, unshieldedKeystore,
  );

  return {
    walletFacade,
    shieldedSecretKeys,
    dustSecretKey,
    unshieldedKeystore,
    walletProvider,
    seedHex: Buffer.from(seed).toString('hex'),
    unshieldedAddress: unshieldedKeystore.getBech32Address(),
  };
}

typescript
// src/config/wallet.ts
import * as Rx from 'rxjs';
import * as ledger from '@midnight-ntwrk/ledger-v8';
import { HDWallet, generateRandomSeed, Roles } from '@midnight-ntwrk/wallet-sdk-hd';
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';
import { WebSocket } from 'ws';
import { Buffer } from 'buffer';
import { applyNetworkId, type NetworkConfig } from './networks';
import { ensureDust } from './dust';
import { makeWalletAndMidnightProvider } from './wallet-provider';

// 必需:在导入任何钱包SDK前设置WebSocket全局变量
globalThis.WebSocket = WebSocket as unknown as typeof globalThis.WebSocket;

export async function buildHeadlessWallet(network: NetworkConfig, seedHex?: string) {
  applyNetworkId(network);

  const seed = seedHex
    ? Buffer.from(seedHex, 'hex')
    : Buffer.from(generateRandomSeed());

  const hdWallet = HDWallet.fromSeed(seed);
  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]);
  const unshieldedKeystore = createKeystore(keys[Roles.NightExternal], network.networkId);

  // relayURL必须是WebSocket — 将http转为ws,https转为wss
  const relayURL = new URL(network.rpc.replace(/^https/, 'wss').replace(/^http(?!s)/, 'ws'));

  const shieldedWallet = ShieldedWallet({
    networkId: network.networkId,
    indexerClientConnection: { indexerHttpUrl: network.indexerHttp, indexerWsUrl: network.indexerWs },
    provingServerUrl: new URL(network.proofServerUrl),
    relayURL,
  }).startWithSecretKeys(shieldedSecretKeys);

  const unshieldedWallet = UnshieldedWallet({
    networkId: network.networkId,
    indexerClientConnection: { indexerHttpUrl: network.indexerHttp, indexerWsUrl: network.indexerWs },
    txHistoryStorage: new InMemoryTransactionHistoryStorage(),
  }).startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore));

  const dustWallet = DustWallet({
    networkId: network.networkId,
    costParameters: {
      additionalFeeOverhead: 300_000_000_000_000n,
      feeBlocksMargin: 5,
    },
    indexerClientConnection: { indexerHttpUrl: network.indexerHttp, indexerWsUrl: network.indexerWs },
    provingServerUrl: new URL(network.proofServerUrl),
    relayURL,
  }).startWithSecretKey(dustSecretKey, ledger.LedgerParameters.initialParameters().dust);

  const walletFacade = new WalletFacade(shieldedWallet, unshieldedWallet, dustWallet);
  await walletFacade.start(shieldedSecretKeys, dustSecretKey);

  // 等待同步完成
  await Rx.firstValueFrom(
    walletFacade.state().pipe(
      Rx.throttleTime(5_000),
      Rx.filter((s: any) => s.isSynced),
    ),
  );

  // 根据网络的dustMode处理DUST
  await ensureDust(walletFacade, unshieldedKeystore, network);

  const walletProvider = await makeWalletAndMidnightProvider(
    walletFacade, shieldedSecretKeys, dustSecretKey, unshieldedKeystore,
  );

  return {
    walletFacade,
    shieldedSecretKeys,
    dustSecretKey,
    unshieldedKeystore,
    walletProvider,
    seedHex: Buffer.from(seed).toString('hex'),
    unshieldedAddress: unshieldedKeystore.getBech32Address(),
  };
}

8) Full Provider Assembly

8) 完整Provider组装

typescript
// src/config/providers-full.ts
import type { NetworkConfig } from './networks';
import {
  makeNodeZkConfigProvider, makeFetchZkConfigProvider,
  makeHttpProofProvider, make1AMProofProvider,
  makePublicDataProvider,
  makeLevelPrivateStateProvider, makeInMemoryPrivateStateProvider,
} from './providers';

export async function assembleProviders(
  network: NetworkConfig,
  zkConfigPath: string, // filesystem path (Node.js) or URL path (browser)
  walletContext: {
    walletProvider: any;
    midnightProvider?: any;
  },
  privateStateStoreName: string,
  proofProviderOverride?: any, // pass 1AM provingProvider for browser
) {
  const isBrowser = typeof window !== 'undefined';

  const zkConfigProvider = isBrowser
    ? makeFetchZkConfigProvider(zkConfigPath)
    : makeNodeZkConfigProvider(zkConfigPath);

  const proofProvider = proofProviderOverride
    ? make1AMProofProvider(proofProviderOverride)           // browser + 1AM
    : makeHttpProofProvider(network.proofServerUrl, zkConfigProvider); // Node.js

  const privateStateProvider = isBrowser
    ? makeInMemoryPrivateStateProvider()
    : makeLevelPrivateStateProvider(privateStateStoreName, walletContext.walletProvider);

  return {
    zkConfigProvider,
    publicDataProvider: makePublicDataProvider(network),
    proofProvider,
    privateStateProvider,
    walletProvider: walletContext.walletProvider,
    midnightProvider: walletContext.midnightProvider ?? walletContext.walletProvider,
  };
}

typescript
// src/config/providers-full.ts
import type { NetworkConfig } from './networks';
import {
  makeNodeZkConfigProvider, makeFetchZkConfigProvider,
  makeHttpProofProvider, make1AMProofProvider,
  makePublicDataProvider,
  makeLevelPrivateStateProvider, makeInMemoryPrivateStateProvider,
} from './providers';

export async function assembleProviders(
  network: NetworkConfig,
  zkConfigPath: string, // 文件系统路径(Node.js)或URL路径(浏览器)
  walletContext: {
    walletProvider: any;
    midnightProvider?: any;
  },
  privateStateStoreName: string,
  proofProviderOverride?: any, // 为浏览器传入1AM provingProvider
) {
  const isBrowser = typeof window !== 'undefined';

  const zkConfigProvider = isBrowser
    ? makeFetchZkConfigProvider(zkConfigPath)
    : makeNodeZkConfigProvider(zkConfigPath);

  const proofProvider = proofProviderOverride
    ? make1AMProofProvider(proofProviderOverride)           // 浏览器 + 1AM
    : makeHttpProofProvider(network.proofServerUrl, zkConfigProvider); // Node.js

  const privateStateProvider = isBrowser
    ? makeInMemoryPrivateStateProvider()
    : makeLevelPrivateStateProvider(privateStateStoreName, walletContext.walletProvider);

  return {
    zkConfigProvider,
    publicDataProvider: makePublicDataProvider(network),
    proofProvider,
    privateStateProvider,
    walletProvider: walletContext.walletProvider,
    midnightProvider: walletContext.midnightProvider ?? walletContext.walletProvider,
  };
}

9) Contract Registry

9) 合约注册表

typescript
// src/config/registry.ts

// { contractName: { networkId: contractAddress } }
export type ContractRegistry = Record<string, Partial<Record<string, string>>>;

export const registry: ContractRegistry = {
  counter: {
    undeployed: '',      // fill after local deploy
    preview:    '',      // fill after preview deploy
    preprod:    '09dbe05f...',
    mainnet:    '',
  },
};

export function resolveAddress(
  contractName: string,
  networkId: string,
): string {
  const addr = registry[contractName]?.[networkId];
  if (!addr) throw new Error(
    `No address for contract "${contractName}" on network "${networkId}". Deploy first.`,
  );
  return addr;
}

export function saveAddress(contractName: string, networkId: string, address: string): void {
  if (!registry[contractName]) registry[contractName] = {};
  registry[contractName][networkId] = address;
  // In a real app, persist this to a JSON file or env variable
}

typescript
// src/config/registry.ts

// { 合约名称: { networkId: 合约地址 } }
export type ContractRegistry = Record<string, Partial<Record<string, string>>>;

export const registry: ContractRegistry = {
  counter: {
    undeployed: '',      // 本地部署后填写
    preview:    '',      // 预览网部署后填写
    preprod:    '09dbe05f...',
    mainnet:    '',
  },
};

export function resolveAddress(
  contractName: string,
  networkId: string,
): string {
  const addr = registry[contractName]?.[networkId];
  if (!addr) throw new Error(
    `网络"${networkId}"上没有合约"${contractName}"的地址,请先部署。`,
  );
  return addr;
}

export function saveAddress(contractName: string, networkId: string, address: string): void {
  if (!registry[contractName]) registry[contractName] = {};
  registry[contractName][networkId] = address;
  // 在实际应用中,将此信息持久化到JSON文件或环境变量
}

10) npm Scripts

10) npm脚本

json
{
  "scripts": {
    "dev":              "VITE_NETWORK=undeployed vite",
    "dev:preview":      "VITE_NETWORK=preview vite",
    "dev:preprod":      "VITE_NETWORK=preprod vite",

    "deploy:local":     "NETWORK=undeployed npx tsx scripts/deploy.ts",
    "deploy:preview":   "NETWORK=preview npx tsx scripts/deploy.ts",
    "deploy:preprod":   "NETWORK=preprod npx tsx scripts/deploy.ts",
    "deploy:mainnet":   "NETWORK=mainnet npx tsx scripts/deploy.ts",

    "local:start":      "docker compose -f standalone.yml up -d",
    "local:stop":       "docker compose -f standalone.yml down",
    "local:logs":       "docker compose -f standalone.yml logs -f",
    "local:proof":      "docker run -d -p 6300:6300 midnightntwrk/proof-server:latest midnight-proof-server -v",

    "preprod:proof":    "docker compose -f proof-server.yml up"
  }
}

json
{
  "scripts": {
    "dev":              "VITE_NETWORK=undeployed vite",
    "dev:preview":      "VITE_NETWORK=preview vite",
    "dev:preprod":      "VITE_NETWORK=preprod vite",

    "deploy:local":     "NETWORK=undeployed npx tsx scripts/deploy.ts",
    "deploy:preview":   "NETWORK=preview npx tsx scripts/deploy.ts",
    "deploy:preprod":   "NETWORK=preprod npx tsx scripts/deploy.ts",
    "deploy:mainnet":   "NETWORK=mainnet npx tsx scripts/deploy.ts",

    "local:start":      "docker compose -f standalone.yml up -d",
    "local:stop":       "docker compose -f standalone.yml down",
    "local:logs":       "docker compose -f standalone.yml logs -f",
    "local:proof":      "docker run -d -p 6300:6300 midnightntwrk/proof-server:latest midnight-proof-server -v",

    "preprod:proof":    "docker compose -f proof-server.yml up"
  }
}

11) Vite Config

11) Vite配置

typescript
// vite.config.ts
import { defineConfig } from 'vite';
import wasm from 'vite-plugin-wasm';
import topLevelAwait from 'vite-plugin-top-level-await';

export default defineConfig({
  plugins: [wasm(), topLevelAwait()],
  build: {
    target: 'esnext',
  },
  server: {            // proxy lives inside 'server', not at the top level
    allowedHosts: true,
    proxy: process.env.VITE_NETWORK === 'undeployed' ? {
      '/api': {
        target: 'http://localhost:8088',
        changeOrigin: true,
      },
    } : undefined,
  },
});

typescript
// vite.config.ts
import { defineConfig } from 'vite';
import wasm from 'vite-plugin-wasm';
import topLevelAwait from 'vite-plugin-top-level-await';

export default defineConfig({
  plugins: [wasm(), topLevelAwait()],
  build: {
    target: 'esnext',
  },
  server: {            // proxy配置在'server'内部,而非顶层
    allowedHosts: true,
    proxy: process.env.VITE_NETWORK === 'undeployed' ? {
      '/api': {
        target: 'http://localhost:8088',
        changeOrigin: true,
      },
    } : undefined,
  },
});

12) Common Pitfalls

12) 常见陷阱

undeployed
indexer uses
/api/v3/graphql
, not
/api/v4/
— the local Docker indexer ships with v3. Using v4 will 404. All live networks (preview, preprod, mainnet) use v4.
Three containers required for local dev, not just the proof server — the node and indexer must also be running. Use
midnight-local-dev
or
docker compose -f standalone.yml up
.
walletFacade.state
is a method, not a property
walletFacade.state()
returns an Observable. Always use
Rx.firstValueFrom(walletFacade.state().pipe(...))
to read it. Accessing
.state
directly returns the function, not the state.
relayURL
must be WebSocket
— convert
https://
wss://
,
http://
ws://
. The wallet SDK rejects non-WS relay URLs silently or with a confusing error.
globalThis.WebSocket = WebSocket
must be at the top of the entry file
— before any
@midnight-ntwrk/wallet-sdk-*
imports. GraphQL subscriptions (wallet sync) will silently fail without it in Node.js.
signTransactionIntents
workaround required on all networks
— this is a wallet SDK bug, not environment-specific. Without it,
balanceTx
throws "Failed to clone intent" on headless wallets.
DUST locked after a failed deploy — restart the DApp to release locked DUST coins. Affects
undeployed
and
preprod
only; sponsored networks aren't affected.
ProofStation API key for direct headless preprod access — if calling
api-preprod.1am.xyz
directly (not via browser 1AM extension), you need
X-API-Key: pk_live_xxx
in headers. The
httpClientProofProvider
does not add this automatically — you'd need to wrap or fork it.
Old contract addresses after recompile — verifier keys change with every Compact contract change. Update the registry and redeploy for the affected network. Addresses from other networks are unaffected.
setNetworkId
called after SDK operations
— call it before building any providers or wallets. Use
applyNetworkId(network)
at the very top of your setup flow.
Wallet seed in browser build — never pass
WALLET_SEED
to Vite's env (it exposes it in the bundle). Headless wallet is Node.js only. Browser always uses the 1AM extension.
undeployed
索引器使用
/api/v3/graphql
,而非
/api/v4/
——本地Docker索引器自带v3版本,使用v4会返回404。所有在线网络(preview、preprod、mainnet)均使用v4。
本地开发需要三个容器,而非仅证明服务器——节点和索引器也必须运行。使用
midnight-local-dev
docker compose -f standalone.yml up
walletFacade.state
是方法,而非属性
——
walletFacade.state()
返回Observable。请始终使用
Rx.firstValueFrom(walletFacade.state().pipe(...))
读取状态,直接访问
.state
会返回函数而非状态。
relayURL
必须是WebSocket
——将
https://
转为
wss://
http://
转为
ws://
。钱包SDK会静默拒绝非WS的relayURL,或抛出令人困惑的错误。
globalThis.WebSocket = WebSocket
必须放在入口文件顶部
——在导入任何
@midnight-ntwrk/wallet-sdk-*
之前。在Node.js中,没有此配置的话GraphQL订阅(钱包同步)会静默失败。
所有网络都需要
signTransactionIntents
临时解决方案
——这是钱包SDK的Bug,与环境无关。没有此方案的话,无头钱包的
balanceTx
会抛出"Failed to clone intent"错误。
部署失败后DUST被锁定——重启DApp以释放锁定的DUST代币。仅影响
undeployed
preprod
网络;赞助模式网络不受影响。
无头模式直接访问preprod需要ProofStation API密钥——如果直接调用
api-preprod.1am.xyz
(而非通过浏览器1AM扩展),需要在请求头中添加
X-API-Key: pk_live_xxx
httpClientProofProvider
不会自动添加此头——你需要包装或fork该Provider。
重新编译后合约地址失效——每次Compact合约变更都会改变验证密钥。请更新注册表并重新部署受影响的网络,其他网络的地址不受影响。
setNetworkId
在SDK操作后调用
——请在构建任何Provider或钱包之前调用它。在设置流程的最开始使用
applyNetworkId(network)
浏览器构建中包含钱包种子——切勿将
WALLET_SEED
传入Vite环境变量(会在打包产物中暴露)。无头钱包仅适用于Node.js,浏览器始终使用1AM扩展。