mcpField
M
multinetwork

multinetwork

github.com/kali-decoder開発18121 回インストール1 日前に更新
リポジトリ

multinetwork について

Midnight DApp を単一のコードベースで undeployed、preview、preprod、mainnet に対応させる開発スキルです。ネットワーク設定、Provider、ウォレット、証明サービス、DUST の各フローを解説します。

主な機能

  1. 014 種類の 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 スキル

このスキルでは、単一のコードベースで4つすべてのネットワークターゲットに対応する Midnight DApp の構築方法を説明します。重要なポイントは、ネットワーク間の違いが エンドポイント、証明サーバー(ローカルの Docker と ProofStation)、DUST のフロー(手動登録とスポンサー提供)の3つであることです。それ以外はすべて同じです。

主な参考資料:

  • github.com/midnightntwrk/midnight-local-dev — 公式のローカル Docker スタックと資金供給 CLI
  • github.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 を実行すると、次の処理が行われます。

  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 ウォレット

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 拡張機能を使用してください。

コメント