multinetwork
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMulti-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:
- — official local Docker stack + funding CLI
github.com/midnightntwrk/midnight-local-dev - — official reference DApp (standalone + preprod modes)
github.com/midnightntwrk/example-counter
本指南介绍如何从单一代码库构建可适配全部四个目标网络的Midnight DApp。核心要点:各网络仅在三个方面存在差异——端点、证明服务器(本地Docker vs ProofStation)、DUST流程(手动注册 vs 赞助模式),其余所有内容完全一致。
主要参考资料:
- — 官方本地Docker栈 + 资金管理CLI
github.com/midnightntwrk/midnight-local-dev - — 官方参考DApp(独立模式 + 预生产模式)
github.com/midnightntwrk/example-counter
1) The Four Networks at a Glance
1) 四大网络概览
| Network | | Use for | Proof server | DUST flow |
|---|---|---|---|---|
| | Local dev, CI | Local Docker | Manual registration (genesis wallet seeds funded) |
| | Active development on testnet | 1AM ProofStation ( | Sponsored — user pays 0 fees |
| | Pre-release / integration testing | Local Docker | Manual registration (faucet → NIGHT → DUST) |
| | Production | 1AM ProofStation ( | Sponsored — user pays 0 fees |
Critical distinction: On and , the 1AM wallet sponsors all fees via ProofStation — users need zero NIGHT or DUST. On and , you must run your own proof server and manually register NIGHT UTXOs for DUST generation before transactions work.
previewmainnetundeployedpreprod| 网络 | | 适用场景 | 证明服务器 | DUST流程 |
|---|---|---|---|---|
| | 本地开发、CI | 本地Docker | 手动注册(创世钱包种子已充值) |
| | 测试网活跃开发 | 1AM ProofStation ( | 赞助模式 — 用户无需支付任何费用 |
| | 预发布/集成测试 | 本地Docker | 手动注册(水龙头获取NIGHT → 转换为DUST) |
| | 生产环境 | 1AM ProofStation ( | 赞助模式 — 用户无需支付任何费用 |
关键区别: 在和中,1AM钱包通过ProofStation赞助所有费用——用户无需持有NIGHT或DUST。在和中,你必须自行运行证明服务器,并在交易生效前手动注册NIGHT UTXO以生成DUST。
previewmainnetundeployedpreprod2) Local Docker Stack (undeployed
)
undeployed2) 本地Docker栈(undeployed
网络)
undeployedRunning the 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节点、索引器和证明服务器。不能仅运行证明服务器——三者缺一不可。
undeployedOption A: Use midnight-local-dev
(recommended)
midnight-local-dev选项A:使用midnight-local-dev
(推荐)
midnight-local-devThe official tool at manages the full stack and handles genesis wallet funding:
github.com/midnightntwrk/midnight-local-devbash
git clone https://github.com/midnightntwrk/midnight-local-dev
cd midnight-local-dev
npm install
npm start
npm start- Detect if containers are already running (prompts to reuse or restart)
- Pull and start all three containers via Docker Compose
- Initialize the genesis master wallet (seed ) and register its DUST
0x00...01 - 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:6300Note: The local indexer uses (not v4 like testnet). Use the correct path.
/api/v3/graphqlgithub.com/midnightntwrk/midnight-local-devbash
git clone https://github.com/midnightntwrk/midnight-local-dev
cd midnight-local-dev
npm install
npm start
npm start- 检测容器是否已运行(提示复用或重启)
- 通过Docker Compose拉取并启动所有三个容器
- 初始化创世主钱包(种子)并注册其DUST
0x00...01 - 显示资金管理菜单,用于为测试账户转账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注意: 本地索引器使用(而非测试网的v4版本),请使用正确路径。
/api/v3/graphqlOption B: Docker Compose directly (no funding CLI)
选项B:直接使用Docker Compose(无资金管理CLI)
bash
undefinedbash
undefinedFrom 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年)
| Service | Image | Version |
|---|---|---|
| Node | | |
| Indexer | | |
| Proof Server | | |
| 服务 | 镜像 | 版本 |
|---|---|---|
| Node | | |
| Indexer | | |
| Proof Server | | |
Lace wallet on undeployed
undeployedLace钱包适配undeployed
网络
undeployedThe Lace browser wallet auto-connects to the local stack when you select "Undeployed" in its network settings — it hardcodes , , and . No custom endpoint config needed.
localhost:9944localhost:8088localhost:6300当你在Lace浏览器钱包的网络设置中选择“Undeployed”时,它会自动连接到本地栈——已硬编码、和,无需自定义端点配置。
localhost:9944localhost:8088localhost:6300Funding test accounts locally
本地测试账户充值
bash
undefinedbash
undefinedOption 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 workaround is required on all networks — it's a wallet SDK bug, not network-specific.
signTransactionIntentstypescript
// 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;
},
};
}此桥接层在所有网络中完全一致。临时解决方案在所有网络中都是必需的——这是钱包SDK的Bug,与网络无关。
signTransactionIntentstypescript
// 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 nothingtypescript
// 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/api/v3/graphql/api/v4/Three containers required for local dev, not just the proof server — the node and indexer must also be running. Use or .
midnight-local-devdocker compose -f standalone.yml upwalletFacade.statewalletFacade.state()Rx.firstValueFrom(walletFacade.state().pipe(...)).staterelayURLhttps://wss://http://ws://globalThis.WebSocket = WebSocket@midnight-ntwrk/wallet-sdk-*signTransactionIntentsbalanceTxDUST locked after a failed deploy — restart the DApp to release locked DUST coins. Affects and only; sponsored networks aren't affected.
undeployedpreprodProofStation API key for direct headless preprod access — if calling directly (not via browser 1AM extension), you need in headers. The does not add this automatically — you'd need to wrap or fork it.
api-preprod.1am.xyzX-API-Key: pk_live_xxxhttpClientProofProviderOld 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.
setNetworkIdapplyNetworkId(network)Wallet seed in browser build — never pass to Vite's env (it exposes it in the bundle). Headless wallet is Node.js only. Browser always uses the 1AM extension.
WALLET_SEEDundeployed/api/v3/graphql/api/v4/本地开发需要三个容器,而非仅证明服务器——节点和索引器也必须运行。使用或。
midnight-local-devdocker compose -f standalone.yml upwalletFacade.statewalletFacade.state()Rx.firstValueFrom(walletFacade.state().pipe(...)).staterelayURLhttps://wss://http://ws://globalThis.WebSocket = WebSocket@midnight-ntwrk/wallet-sdk-*所有网络都需要临时解决方案——这是钱包SDK的Bug,与环境无关。没有此方案的话,无头钱包的会抛出"Failed to clone intent"错误。
signTransactionIntentsbalanceTx部署失败后DUST被锁定——重启DApp以释放锁定的DUST代币。仅影响和网络;赞助模式网络不受影响。
undeployedpreprod无头模式直接访问preprod需要ProofStation API密钥——如果直接调用(而非通过浏览器1AM扩展),需要在请求头中添加。不会自动添加此头——你需要包装或fork该Provider。
api-preprod.1am.xyzX-API-Key: pk_live_xxxhttpClientProofProvider重新编译后合约地址失效——每次Compact合约变更都会改变验证密钥。请更新注册表并重新部署受影响的网络,其他网络的地址不受影响。
setNetworkIdapplyNetworkId(network)浏览器构建中包含钱包种子——切勿将传入Vite环境变量(会在打包产物中暴露)。无头钱包仅适用于Node.js,浏览器始终使用1AM扩展。
WALLET_SEED