mcpField
M
multinetwork

multinetwork

github.com/kali-decoder开发18121 次安装1 天前更新
代码仓库

关于 multinetwork

面向 Midnight DApp 开发的技能,介绍如何用同一代码库支持 undeployed、preview、preprod 和 mainnet。内容涵盖网络配置、Provider、钱包、证明服务及不同的 DUST 流程。

主要功能

  1. 01配置四种 Midnight 网络
  2. 02搭建本地 Docker 服务栈
  3. 03设置各网络的证明 Provider
  4. 04组装钱包与 Provider
  5. 05手动及赞助式 DUST 流程

适用场景

  1. 01搭建本地 Midnight DApp 环境
  2. 02为测试网和主网配置 DApp
  3. 03排查特定网络的部署问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/kali-decoder/midnight-skills --skill multinetwork

安装后在 Claude Code 中输入/multinetwork确认已加载。

详细介绍

多网络 Midnight 技能

本技能介绍如何通过单一代码库构建适用于全部四个网络目标的 Midnight DApp。关键在于:网络有三处不同——端点、证明服务(本地 Docker 或 ProofStation)以及 DUST 流程(手动注册或赞助)。其他所有内容都相同。

主要参考资料:

  • github.com/midnightntwrk/midnight-local-dev — 官方本地 Docker 技术栈 + 资金发放 CLI
  • github.com/midnightntwrk/example-counter — 官方参考 DApp(独立模式 + preprod 模式)

1) 四种网络一览

网络networkId用途证明服务DUST 流程
undeployed'undeployed'本地开发、CI本地 Docker :6300手动注册(创世钱包预先注资)
preview'preview'在测试网上积极开发1AM ProofStation(api-preview.1am.xyz)赞助 — 用户无需支付任何费用
preprod'preprod'发布前 / 集成测试本地 Docker :6300 或 ProofStation手动注册(faucet → NIGHT → DUST)
mainnet'mainnet'生产环境1AM ProofStation(api.1am.xyz)赞助 — 用户无需支付任何费用

关键区别: 在 preview 和 mainnet 上,1AM 钱包通过 ProofStation 赞助所有费用——用户无需 NIGHT 或 DUST。在 undeployed 和 preprod 上,必须运行自己的证明服务,并在交易生效前手动注册 NIGHT UTXO 以生成 DUST。


2) 本地 Docker 技术栈(undeployed)

在本地运行 undeployed 网络需要三个 Docker 容器:Midnight 节点、索引器和证明服务。不能只运行证明服务——三个都需要运行。

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

github.com/midnightntwrk/midnight-local-dev 中的官方工具负责管理完整技术栈并处理创世钱包注资:


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)。请使用正确的路径。

选项 B:直接使用 Docker Compose(不使用资金发放 CLI)
# From midnight-local-dev repo root:
docker compose -f standalone.yml up -d

# Check all three services are healthy
docker compose -f standalone.yml ps

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

# Tear down
docker compose -f standalone.yml down

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

Docker 镜像版本(截至 2026 年)
服务镜像版本
节点midnightntwrk/midnight-node0.21.0
索引器midnightntwrk/indexer-standalone3.1.0
证明服务midnightntwrk/proof-server7.0.0
在 undeployed 上使用 Lace 钱包

在网络设置中选择“Undeployed”后,Lace 浏览器钱包会自动连接到本地技术栈——它将 localhost:9944、localhost:8088 和 localhost:6300 硬编码在其中。无需配置自定义端点。

在本地为测试账户注资
# Option 1: Fund from a JSON file (gets NIGHT + DUST registered)
# accounts.json: { "accounts": [{ "name": "Alice", "mnemonic": "..." }] }
# (max 10 accounts, 50,000 NIGHT each)

# Option 2: Fund by Bech32 address (NIGHT only — DUST not registered)
# Use when you have a Lace address or DApp-generated address

创世主钱包种子:0x0000000000000000000000000000000000000000000000000000000000000001


3) 网络配置

// 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);
}

4) 统一 Provider 构建器

// 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'); },
  };
}

5) WalletProvider 和 MidnightProvider(Node.js 无头模式,适用于所有网络)

此桥接在所有网络上都相同。所有网络都需要使用 signTransactionIntents 变通方案——这是钱包 SDK 的缺陷,并非特定于某个网络。

// 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;
    },
  };
}

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
// 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.');
}

7) 钱包初始化(Node.js,适用于所有网络)

// 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(),
  };
}

8) 完整 Provider 组装

// 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,
  };
}

9) 合约注册表

// 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
}

10) npm 脚本

{
  "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 配置

// 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,
  },
});

12)常见陷阱

undeployed 索引器使用 /api/v3/graphql,而不是 /api/v4/ — 本地 Docker 索引器随附的是 v3。使用 v4 会返回 404。所有线上网络(preview、preprod、mainnet)都使用 v4。

本地开发需要运行 3 个容器,而不仅仅是证明服务 — 节点和索引器也必须运行。使用 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 中继 URL,或返回令人困惑的错误。

globalThis.WebSocket = WebSocket 必须放在入口文件顶部 — 位于任何 @midnight-ntwrk/wallet-sdk-* 导入之前。若未设置,Node.js 中的 GraphQL 订阅(钱包同步)会静默失败。

所有网络都需要使用 signTransactionIntents 变通方案 — 这是钱包 SDK 的错误,与环境无关。若不使用此方案,无头钱包中的 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 不会自动添加此项,你需要对其进行包装或派生分支。

重新编译后合约地址会过期 — verifier 密钥会随 Compact 合约的每次更改而变化。更新注册表并在受影响的网络上重新部署。其他网络的地址不受影响。

在 SDK 操作之后才调用 setNetworkId — 应在构建任何 provider 或钱包之前调用它。在设置流程最开始使用 applyNetworkId(network)。

浏览器构建中包含钱包助记词 — 切勿将 WALLET_SEED 传入 Vite 的环境变量(它会暴露在打包文件中)。无头钱包仅支持 Node.js。浏览器始终使用 1AM 扩展。

评论

支持 Markdown