multinetwork について
Midnight DApp を単一のコードベースで undeployed、preview、preprod、mainnet に対応させる開発スキルです。ネットワーク設定、Provider、ウォレット、証明サービス、DUST の各フローを解説します。
主な機能
- 014 種類の Midnight ネットワーク設定
- 02ローカル Docker スタックの構築
- 03ネットワーク別の証明 Provider 設定
- 04ウォレットと Provider の組み立て
- 05手動およびスポンサー型 DUST フロー
活用シーン
- 01ローカル Midnight DApp 環境の構築
- 02テストネットとメインネット向けの DApp 設定
- 03ネットワーク固有のデプロイ問題の調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ kali-decoder/ midnight-skills --skill multinetworkインストール後、Claude Code で/multinetworkを入力して読み込まれたか確認します。
詳細
マルチネットワーク Midnight スキル
このスキルでは、単一のコードベースで4つすべてのネットワークターゲットに対応する Midnight DApp の構築方法を説明します。重要なポイントは、ネットワーク間の違いが エンドポイント、証明サーバー(ローカルの Docker と ProofStation)、DUST のフロー(手動登録とスポンサー提供)の3つであることです。それ以外はすべて同じです。
主な参考資料:
github.com/midnightntwrk/midnight-local-dev— 公式のローカル Docker スタックと資金供給 CLIgithub.com/midnightntwrk/example-counter— 公式リファレンス DApp(スタンドアロンモードと preprod モード)
1) 4つのネットワークの概要
| ネットワーク | 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 では、自分で証明サーバーを実行し、トランザクションを実行する前に、DUST 生成用の NIGHT UTXO を手動で登録する必要があります。
2) ローカル Docker スタック(undeployed)
undeployed ネットワークをローカルで実行するには、Midnight ノード、インデクサー、証明サーバーの3つの Docker コンテナが必要です。証明サーバーだけを実行することはできません。3つすべてが必要です。
オプション 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 を実行すると、次の処理が行われます。
- コンテナがすでに実行中かどうかを検出します(再利用するか再起動するかを尋ねます)
- Docker Compose 経由ですべてのコンテナを取得して起動します
- ジェネシスのマスターウォレット(シード
0x00...01)を初期化し、DUST を登録します - テストアカウントに 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-node | 0.21.0 |
| インデクサー | midnightntwrk/indexer-standalone | 3.1.0 |
| 証明サーバー | midnightntwrk/proof-server | 7.0.0 |
undeployed での Lace ウォレット
Lace ブラウザーウォレットは、ネットワーク設定で「Undeployed」を選択すると、ローカルスタックに自動接続します。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) 統合プロバイダーのビルダー
// 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) プロバイダーの完全な組み立て
// 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 つのコンテナが必要で、proof サーバーだけではありません — node とインデクサーも起動している必要があります。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 は、WebSocket 以外のリレー URL を黙って拒否するか、わかりにくいエラーを表示します。
globalThis.WebSocket = WebSocket はエントリーファイルの先頭に記述する必要があります — @midnight-ntwrk/wallet-sdk-* のインポートより前に記述してください。これがないと、Node.js で GraphQL サブスクリプション(ウォレット同期)が黙って失敗します。
すべてのネットワークで signTransactionIntents の回避策が必要です — これは環境固有ではなく、ウォレット SDK のバグです。回避策を適用しない場合、ヘッドレスウォレットで balanceTx が「Failed to clone intent」エラーをスローします。
デプロイ失敗後に DUST がロックされます — ロックされた DUST コインを解放するには、DApp を再起動してください。影響するのは undeployed と preprod のみで、スポンサー付きネットワークには影響しません。
ヘッドレスから preprod に直接アクセスするには ProofStation API キーが必要です — (ブラウザーの 1AM 拡張機能経由ではなく)api-preprod.1am.xyz を直接呼び出す場合は、ヘッダーに X-API-Key: pk_live_xxx が必要です。httpClientProofProvider はこれを自動で追加しないため、ラップするかフォークする必要があります。
再コンパイル後はコントラクトアドレスが古くなります — verifier キーは Compact コントラクトを変更するたびに変わります。対象ネットワークのレジストリを更新して再デプロイしてください。他のネットワークのアドレスには影響しません。
SDK の操作後に setNetworkId を呼び出しています — プロバイダーやウォレットを構築する前に呼び出してください。セットアップフローの最初に applyNetworkId(network) を使用してください。
ブラウザービルドでのウォレットシード — WALLET_SEED を Vite の環境変数に渡さないでください(バンドル内に公開されます)。ヘッドレスウォレットは Node.js 専用です。ブラウザーでは常に 1AM 拡張機能を使用してください。