关于 example-hello-world
为 Midnight Network 开发者提供生成完整、可运行 Hello World DApp 的技能,涵盖 Compact 合约、TypeScript、测试和本地环境配置。内容遵循官方参考实现,并提示按兼容矩阵选择版本。
主要功能
- 01生成完整 DApp 项目文件
- 02提供 Compact 合约与 TypeScript 代码
- 03包含测试和 Docker 配置
- 04说明合约编译与本地运行
- 05提示检查版本兼容矩阵
适用场景
- 01搭建 Midnight Hello World 示例
- 02配置本地开发网与测试
- 03将模板改造为自定义合约
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ kali-decoder/ midnight-skills --skill example-hello-world安装后在 Claude Code 中输入/example-hello-world确认已加载。
详细介绍
示例 Hello World DApp 技能
此技能可生成完整且可运行的 Midnight DApp,涵盖项目中的每个文件:Compact 合约、TypeScript 工具、测试脚本、Docker 配置和软件包设置。所有代码均与官方 midnightntwrk/example-hello-world 参考实现一致。
主要参考资料:
github.com/midnightntwrk/example-hello-world— 官方参考仓库docs.midnight.network/getting-started/hello-world— 官方 Hello World 指南docs.midnight.network/relnotes/support-matrix— 权威版本兼容性矩阵
以下软件包版本是截至 2026 年 5 月的快照。 固定版本前,请从支持矩阵中确认当前经过测试的版本组合,另请参阅 references/volatile.md。切勿安装 @latest。
关键架构说明:
- 使用
@midnight-ntwrk/testkit-js的FluentWalletBuilder创建钱包 - 使用
@midnight-ntwrk/midnight-js-contracts的deployContract和submitCallTx - Compact pragma 为
>= 0.23 - 本地 devnet 的索引器 API 路径使用
/api/v4/graphql MidnightWalletProvider类封装WalletFacade,以实现MidnightProvider和WalletProvider接口syncWallet函数会在钱包同步期间提供详细的进度日志- 测试使用静态种子(
ALICE_SEED),以实现可复现的测试 privateStateStoreName使用Date.now()实现测试隔离- 使用 ESM 的 vitest 需要
NODE_OPTIONS='--experimental-vm-modules'
1) 项目结构
hello-world-local/
├── contracts/
│ ├── hello-world.compact # Compact smart contract (you create this)
│ ├── index.ts # Barrel file for compiled contract exports (create manually)
│ └── managed/
│ └── hello-world/ # Compiler output (auto-generated, do not edit)
│ ├── compiler/
│ ├── contract/
│ ├── keys/
│ └── zkir/
├── src/
│ ├── config.ts # Network configuration
│ ├── wallet.ts # MidnightWalletProvider + syncWallet
│ ├── providers.ts # HelloWorldProviders builder
│ └── test/
│ └── hw.test.ts # vitest tests (deploy + storeMessage)
├── compose.yml # Docker: full local stack
├── package.json
├── tsconfig.json
└── vitest.config.ts
contracts/managed/ 目录由 Compact 编译器创建 — 请勿手动创建或编辑其中的文件。
2) 先决条件
# Node.js 22+ required
node --version
# Docker required (local stack)
docker --version
# Install Compact compiler
curl --proto '=https' --tlsv1.2 -sSf \
https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh
source $HOME/.local/bin/env
compact --version
3) package.json
{
"name": "hello-world-local",
"version": "0.1.0",
"private": true,
"type": "module",
"engines": {
"node": ">=22.0.0"
},
"scripts": {
"compile": "compact compile contracts/hello-world.compact managed/hello-world",
"test": "NODE_OPTIONS='--experimental-vm-modules' vitest run",
"test:local": "MIDNIGHT_NETWORK=local yarn test",
"env:up": "docker compose up -d --wait",
"env:down": "docker compose down",
"validate": "yarn env:up && yarn test:local; yarn env:down"
},
"dependencies": {
"@midnight-ntwrk/compact-runtime": "0.16.0",
"@midnight-ntwrk/ledger-v8": "8.0.3",
"@midnight-ntwrk/midnight-js-contracts": "4.0.4",
"@midnight-ntwrk/midnight-js-http-client-proof-provider": "4.0.4",
"@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.0.4",
"@midnight-ntwrk/midnight-js-level-private-state-provider": "4.0.4",
"@midnight-ntwrk/midnight-js-network-id": "4.0.4",
"@midnight-ntwrk/midnight-js-node-zk-config-provider": "4.0.4",
"@midnight-ntwrk/midnight-js-types": "4.0.4",
"@midnight-ntwrk/midnight-js-utils": "4.0.4",
"@midnight-ntwrk/testkit-js": "4.0.4",
"@midnight-ntwrk/wallet-sdk-facade": "3.0.0",
"axios": "^1.15.0",
"pino": "^9.0.0",
"pino-pretty": "^13.0.0",
"rxjs": "^7.8.2",
"testcontainers": "^11.13.0",
"ws": "^8.14.2"
},
"devDependencies": {
"@midnight-ntwrk/midnight-js-compact": "4.0.4",
"@types/node": "^22.0.0",
"@types/ws": "^8.5.9",
"typescript": "^5.7.0",
"vitest": "^3.0.0"
},
"resolutions": {
"@midnight-ntwrk/ledger-v8": "8.0.3",
"@midnight-ntwrk/midnight-js-network-id": "4.0.4",
"@midnight-ntwrk/compact-runtime": "0.16.0"
},
"packageManager": "[email protected]"
}
4) tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"outDir": "dist",
"rootDir": "."
},
"include": ["src/**/*.ts", "contract/**/*.ts"]
}
⚠️
"contract/**/*.ts"使用单数形式 — 这与仓库完全一致,并涵盖编译器的输出路径。使用复数形式的"contracts/**/*.ts"会导致 TypeScript 无提示地无法解析编译后的合约类型。
5) vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
globals: true,
testTimeout: 10 * 60_000,
hookTimeout: 15 * 60_000,
include: ['src/**/*.test.ts'],
reporters: ['default'],
sequence: { concurrent: false },
},
});
6) contracts/hello-world.compact
请手动创建此文件 — managed/ 中的编译器输出由此文件生成。
pragma language_version >= 0.23;
export ledger message: Opaque<"string">;
export circuit storeMessage(newMessage: Opaque<"string">): [] {
message = disclose(newMessage);
}
请从 contracts/ 目录进行编译:
cd contracts
compact compile hello-world.compact managed/hello-world
cd ..
预期输出:
Compiling 1 circuits:
circuit "storeMessage" (k=6, rows=26)
这会生成 contracts/managed/hello-world/contract/index.js(以及密钥、zkir 和编译器目录)。运行测试前请务必先进行编译。
7) contracts/index.ts(barrel 文件 — 手动创建)
请手动创建此文件 — 它会从编译后的合约输出中导入内容。必须先运行 compact compile,生成已编译的合约模块,导入才能成功解析。
import { CompiledContract } from '@midnight-ntwrk/compact-runtime';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
export {
Contract,
ledger,
pureCircuits,
type Ledger,
type ImpureCircuits,
type PureCircuits,
} from './managed/hello-world/contract/index.js';
import { Contract } from './managed/hello-world/contract/index.js';
const currentDir = path.dirname(fileURLToPath(import.meta.url));
export const zkConfigPath = path.resolve(currentDir, 'managed', 'hello-world');
export const CompiledHelloWorldContract = CompiledContract.make(
'HelloWorldContract',
Contract,
).pipe(
CompiledContract.withVacantWitnesses,
CompiledContract.withCompiledFileAssets(zkConfigPath),
);
8) src/config.ts
export type NetworkConfig = {
networkId: string;
indexer: string;
indexerWS: string;
node: string;
nodeWS: string;
proofServer: string;
faucet: string;
};
export const LOCAL_CONFIG: NetworkConfig = {
networkId: 'undeployed',
indexer: 'http://127.0.0.1:8088/api/v4/graphql',
indexerWS: 'ws://127.0.0.1:8088/api/v4/graphql/ws',
node: 'http://127.0.0.1:9944',
nodeWS: 'ws://127.0.0.1:9944',
proofServer: 'http://127.0.0.1:6300',
faucet: '',
};
export function getConfig(): NetworkConfig {
const network = process.env['MIDNIGHT_NETWORK'] ?? 'local';
if (network !== 'local') {
throw new Error(
`Unknown network: ${network}. This harness only supports 'local'.`,
);
}
return LOCAL_CONFIG;
}
9) src/wallet.ts
import {
type CoinPublicKey,
DustSecretKey,
type EncPublicKey,
type FinalizedTransaction,
LedgerParameters,
ZswapSecretKeys,
} from '@midnight-ntwrk/ledger-v8';
import {
type MidnightProvider,
type UnboundTransaction,
type WalletProvider,
} from '@midnight-ntwrk/midnight-js-types';
import { ttlOneHour } from '@midnight-ntwrk/midnight-js-utils';
import { type WalletFacade, type FacadeState } from '@midnight-ntwrk/wallet-sdk-facade';
import {
type DustWalletOptions,
type EnvironmentConfiguration,
FluentWalletBuilder,
} from '@midnight-ntwrk/testkit-js';
import * as Rx from 'rxjs';
import type { Logger } from 'pino';
export class MidnightWalletProvider implements MidnightProvider, WalletProvider {
readonly wallet: WalletFacade;
private constructor(
private readonly logger: Logger,
wallet: WalletFacade,
private readonly zswapSecretKeys: ZswapSecretKeys,
private readonly dustSecretKey: DustSecretKey,
) {
this.wallet = wallet;
}
getCoinPublicKey(): CoinPublicKey {
return this.zswapSecretKeys.coinPublicKey;
}
getEncryptionPublicKey(): EncPublicKey {
return this.zswapSecretKeys.encryptionPublicKey;
}
async balanceTx(
tx: UnboundTransaction,
ttl: Date = ttlOneHour(),
): Promise<FinalizedTransaction> {
const recipe = await this.wallet.balanceUnboundTransaction(
tx,
{
shieldedSecretKeys: this.zswapSecretKeys,
dustSecretKey: this.dustSecretKey,
},
{ ttl },
);
return await this.wallet.finalizeRecipe(recipe);
}
submitTx(tx: FinalizedTransaction): Promise<string> {
return this.wallet.submitTransaction(tx);
}
async start(): Promise<void> {
this.logger.info('Starting wallet...');
await this.wallet.start(this.zswapSecretKeys, this.dustSecretKey);
}
async stop(): Promise<void> {
return this.wallet.stop();
}
static async build(
logger: Logger,
env: EnvironmentConfiguration,
seed: string,
): Promise<MidnightWalletProvider> {
const dustOptions: DustWalletOptions = {
ledgerParams: LedgerParameters.initialParameters(),
additionalFeeOverhead: 1_000n,
feeBlocksMargin: 5,
};
const builder = FluentWalletBuilder.forEnvironment(env)
.withDustOptions(dustOptions);
// buildWithoutStarting() returns { wallet, seeds } — seeds contains
// the derived key material. The cast reflects the documented testkit-js
// pattern; verify against your installed version if you see type errors.
const buildResult = await builder.withSeed(seed).buildWithoutStarting();
const { wallet, seeds } = buildResult as {
wallet: WalletFacade;
seeds: {
masterSeed: string;
shielded: Uint8Array;
dust: Uint8Array;
};
};
logger.info(`Wallet built from seed: ${seeds.masterSeed.slice(0, 8)}...`);
return new MidnightWalletProvider(
logger,
wallet,
ZswapSecretKeys.fromSeed(seeds.shielded),
DustSecretKey.fromSeed(seeds.dust),
);
}
}
function isProgressStrictlyComplete(progress: unknown): boolean {
if (!progress || typeof progress !== 'object') {
return false;
}
const candidate = progress as { isStrictlyComplete?: unknown };
if (typeof candidate.isStrictlyComplete !== 'function') {
return false;
}
return (candidate.isStrictlyComplete as () => boolean)();
}
export async function syncWallet(
logger: Logger,
wallet: WalletFacade,
timeout = 300_000,
): Promise<FacadeState> {
logger.info('Syncing wallet...');
let emissionCount = 0;
return Rx.firstValueFrom(
wallet.state().pipe(
Rx.tap((state: FacadeState) => {
emissionCount++;
const shielded = isProgressStrictlyComplete(state.shielded.state.progress);
const unshielded = isProgressStrictlyComplete(state.unshielded.progress);
const dust = isProgressStrictlyComplete(state.dust.state.progress);
logger.info(
`Wallet sync [${emissionCount}]: shielded=${shielded}, unshielded=${unshielded}, dust=${dust}`,
);
}),
Rx.filter(
(state: FacadeState) =>
isProgressStrictlyComplete(state.shielded.state.progress) &&
isProgressStrictlyComplete(state.dust.state.progress) &&
isProgressStrictlyComplete(state.unshielded.progress),
),
Rx.tap(() => logger.info(`Wallet sync complete after ${emissionCount} emissions`)),
Rx.timeout({
each: timeout,
with: () =>
Rx.throwError(
() => new Error(`Wallet sync timeout after ${timeout}ms (${emissionCount} emissions received)`),
),
}),
Rx.catchError((err) => {
logger.error(`Wallet sync error: ${err}`);
return Rx.throwError(() => err);
}),
),
);
}
10) src/providers.ts
import { type MidnightProviders } from '@midnight-ntwrk/midnight-js-types';
import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client-proof-provider';
import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider';
import { levelPrivateStateProvider } from '@midnight-ntwrk/midnight-js-level-private-state-provider';
import { type MidnightWalletProvider } from './wallet.js';
import { type NetworkConfig } from './config.js';
export type HelloWorldCircuits = 'storeMessage';
export type HelloWorldProviders = MidnightProviders<any>;
export function buildProviders(
wallet: MidnightWalletProvider,
zkConfigPath: string,
config: NetworkConfig,
): HelloWorldProviders {
const zkConfigProvider = new NodeZkConfigProvider<HelloWorldCircuits>(zkConfigPath);
return {
privateStateProvider: levelPrivateStateProvider({
privateStateStoreName: `hello-world-${Date.now()}`,
walletProvider: wallet,
privateStoragePasswordProvider: () => 'xK9#mQ2$pL8@nR5!vW3*',
accountId: `test-account-${Date.now()}`,
}),
publicDataProvider: indexerPublicDataProvider(
config.indexer,
config.indexerWS,
),
zkConfigProvider,
proofProvider: httpClientProofProvider(
config.proofServer,
zkConfigProvider,
),
walletProvider: wallet,
midnightProvider: wallet,
};
}
11) src/test/hw.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { WebSocket } from 'ws';
import { setNetworkId } from '@midnight-ntwrk/midnight-js-network-id';
import {
deployContract,
submitCallTx,
} from '@midnight-ntwrk/midnight-js-contracts';
import type { ContractAddress } from '@midnight-ntwrk/compact-runtime';
import pino from 'pino';
import { getConfig } from '../config.js';
import { MidnightWalletProvider, syncWallet } from '../wallet.js';
import { buildProviders, type HelloWorldProviders } from '../providers.js';
import {
CompiledHelloWorldContract,
ledger,
zkConfigPath,
} from '../../contracts/index.js';
import type { EnvironmentConfiguration } from '@midnight-ntwrk/testkit-js';
// Required for GraphQL subscriptions in Node.js
// @ts-expect-error WebSocket global assignment for apollo
globalThis.WebSocket = WebSocket;
process.on('unhandledRejection', (reason, promise) => {
console.error('UNHANDLED REJECTION:', reason);
console.error('Promise:', promise);
});
process.on('uncaughtException', (err) => {
console.error('UNCAUGHT EXCEPTION:', err);
});
const ALICE_SEED =
'0000000000000000000000000000000000000000000000000000000000000001';
const ALICE_PRIVATE_STATE_ID = 'AlicePrivateHWState';
const logger = pino({
level: process.env['LOG_LEVEL'] ?? 'info',
transport: { target: 'pino-pretty' },
});
describe('Hello World Contract', () => {
let aliceWallet: MidnightWalletProvider;
let aliceProviders: HelloWorldProviders;
let contractAddress: ContractAddress;
const config = getConfig();
async function queryLedger(providers: HelloWorldProviders) {
const state =
await providers.publicDataProvider.queryContractState(contractAddress);
expect(state).not.toBeNull();
return ledger(state!.data);
}
beforeAll(async () => {
setNetworkId(config.networkId);
const envConfig: EnvironmentConfiguration = {
walletNetworkId: config.networkId,
networkId: config.networkId,
indexer: config.indexer,
indexerWS: config.indexerWS,
node: config.node,
nodeWS: config.nodeWS,
faucet: config.faucet,
proofServer: config.proofServer,
};
aliceWallet = await MidnightWalletProvider.build(logger, envConfig, ALICE_SEED);
await aliceWallet.start();
await syncWallet(logger, aliceWallet.wallet, 600_000);
aliceProviders = buildProviders(aliceWallet, zkConfigPath, config);
logger.info('Providers initialized. Ready to test!');
});
afterAll(async () => {
if (aliceWallet) {
logger.info('Stopping Alice wallet...');
await aliceWallet.stop();
}
});
it('Deploys the contract', async () => {
logger.info('Creating private state...');
const deployed: any = await (deployContract as any)(aliceProviders, {
compiledContract: CompiledHelloWorldContract,
privateStateId: ALICE_PRIVATE_STATE_ID,
initialPrivateState: {},
args: [],
});
logger.info('Setting the contract address...');
contractAddress = deployed.deployTxData.public.contractAddress;
logger.info(`Contract deployed at: ${contractAddress}`);
expect(contractAddress).toBeDefined();
expect(contractAddress.length).toBeGreaterThan(0);
const state = await queryLedger(aliceProviders);
expect(state.message).toEqual('');
});
it('Stores Hello World!', async () => {
const message = 'Hello World!';
await (submitCallTx as any)(aliceProviders, {
compiledContract: CompiledHelloWorldContract,
contractAddress,
privateStateId: ALICE_PRIVATE_STATE_ID,
circuitId: 'storeMessage',
args: [message],
});
const state = await queryLedger(aliceProviders);
expect(state.message).toEqual(message);
});
});
12) compose.yml
services:
proof-server:
image: 'midnightntwrk/proof-server:8.0.3'
command: ['midnight-proof-server', '-v']
ports:
- '127.0.0.1:6300:6300'
environment:
RUST_BACKTRACE: 'full'
healthcheck:
test: ['CMD-SHELL', 'echo > /dev/tcp/127.0.0.1/6300']
interval: 10s
timeout: 5s
retries: 20
start_period: 10s
indexer:
image: 'midnightntwrk/indexer-standalone:4.0.0'
ports:
- '127.0.0.1:8088:8088'
environment:
RUST_LOG: 'indexer=info,chain_indexer=info,indexer_api=info,wallet_indexer=info,indexer_common=info,fastrace_opentelemetry=off,info'
APP__INFRA__NODE__URL: 'ws://node:9944'
APP__APPLICATION__NETWORK_ID: 'undeployed'
APP__INFRA__STORAGE__PASSWORD: 'indexer'
APP__INFRA__PUB_SUB__PASSWORD: 'indexer'
APP__INFRA__LEDGER_STATE_STORAGE__PASSWORD: 'indexer'
APP__INFRA__SECRET: '303132333435363738393031323334353637383930313233343536373839303132'
healthcheck:
test: ['CMD-SHELL', 'cat /var/run/indexer-standalone/running']
interval: 10s
timeout: 5s
retries: 20
start_period: 10s
depends_on:
node:
condition: service_healthy
node:
image: 'midnightntwrk/midnight-node:0.22.3'
ports:
- '127.0.0.1:9944:9944'
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:9944/health']
interval: 2s
timeout: 5s
retries: 20
start_period: 5s
environment:
CFG_PRESET: 'dev'
SIDECHAIN_BLOCK_BENEFICIARY: '04bcf7ad3be7a5c790460be82a713af570f22e0f801f6659ab8e84a52be6969e'
13) 首次运行 — 本地 Devnet
# 1. Install deps
yarn install
# 2. Compile contract (must run from contracts/ directory)
cd contracts
compact compile hello-world.compact managed/hello-world
cd ..
# 3. Start local stack
yarn env:up
# wait ~30 seconds for all services to become healthy
# 4. Run tests (new terminal)
yarn test:local
预期输出:
[12:46:12.694] INFO (22064): Wallet sync complete after 23 emissions
[12:46:12.703] INFO (22064): Providers initialized. Ready to test
[12:46:12.707] INFO (22064): Creating private state...
[12:46:32.347] INFO (22064): Setting the contract address...
[12:46:32.347] INFO (22064): Contract deployed at: bba6579743ae23b44301d4a9f8df30dbd5244d63a59d8fbc2c9fc7ea521a04f8
✓ src/test/hw.test.ts (2 tests) 39112ms
✓ Hello World Contract > Deploys the contract 19649ms
✓ Hello World Contract > Stores Hello World! 18184ms
14) 故障排查
| 错误 | 原因 | 修复方法 |
|---|---|---|
compact: command not found | 未设置 PATH | source $HOME/.local/bin/env |
Cannot find module '../../contracts/...' | 尚未编译合约 | 先在 contracts/ 目录中运行 compact compile |
ETARGET No matching version found | 版本不在经过测试的集合中 | 根据支持矩阵确定当前版本 — 参见 references/volatile.md。不要安装 @latest。 |
ECONNREFUSED 127.0.0.1:6300 | 证明服务未运行 | yarn env:up |
测试中出现 NODE_OPTIONS 错误 | 缺少实验性标志 | 确保在测试脚本中设置了 NODE_OPTIONS='--experimental-vm-modules' |
Wallet sync timeout | Docker 服务未正常运行 | 确认 yarn env:up 已完成 — 所有 3 个容器都必须处于正常状态 |
v4/graphql 返回 404 | 索引器 URL 已过时 | 本地 devnet 使用 /api/v4/graphql — 在 config.ts 中修正 |
FluentWalletBuilder is not a function | 导入的 testkit-js 不正确 | 从 @midnight-ntwrk/testkit-js 导入 |
levelPrivateStateProvider 选项错误 | 缺少必需字段 | 必须传入 privateStateStoreName、walletProvider、privateStoragePasswordProvider(强密码,至少 16 个字符,且不能包含连续字符模式)以及 accountId |
TS:合约的 Cannot find module | tsconfig.json 的 include 不正确 | 必须使用 "contract/**/*.ts"(单数),而不是 "contracts/**/*.ts" |
找不到 submitCallTx | 导入错误 | 从 @midnight-ntwrk/midnight-js-contracts 导入 |
| 重新编译后旧合约地址失效 | 验证密钥已更改 | 重新部署 — 合约源码更改时,编译后的密钥也会改变 |
15) 改造此模板
- 将
contracts/hello-world.compact替换为你的合约 - 编译:
cd contracts && compact compile your-contract.compact managed/your-contract - 更新
contracts/index.ts:- 将导入路径改为
./managed/your-contract/contract/index.js - 将
zkConfigPath更新为managed/your-contract - 更新
CompiledContract.make()名称和Contract引用
- 将导入路径改为
- 在
src/providers.ts中:- 将
HelloWorldCircuits类型更新为你的电路名称 - 更新
privateStateStoreName前缀,避免 LevelDB 冲突
- 将
- 在
src/test/hw.test.ts中:- 将
submitCallTx参数替换为你的电路调用 - 将
ledger()替换为合约的ledger()函数 - 更新
ALICE_PRIVATE_STATE_ID和initialPrivateState的结构
- 将
- 如果目标网络不是本地网络,则更新
src/config.ts