example-hello-world
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseExample Hello World DApp Skill
Hello World DApp示例技能
This skill generates a complete, runnable Midnight DApp. It covers every file in the project: the Compact contract, TypeScript utilities, test script, Docker configs, and package setup. All code matches the official reference implementation.
midnightntwrk/example-hello-worldPrimary references:
- — official reference repo
github.com/midnightntwrk/example-hello-world - — official hello-world guide
docs.midnight.network/getting-started/hello-world - — authoritative version compatibility matrix
docs.midnight.network/relnotes/support-matrix
Package versions in this skill match the official compatibility matrix (May 2026). Always cross-check against the support matrix before pinning versions — Midnight packages update frequently and version mismatches cause errors at install time.
ETARGETKey architecture notes:
- Uses
@midnight-ntwrk/testkit-jsfor wallet creationFluentWalletBuilder - Uses
@midnight-ntwrk/midnight-js-contractsanddeployContractsubmitCallTx - Compact pragma is
>= 0.23 - Indexer API paths use for local devnet
/api/v4/graphql - class wraps
MidnightWalletProviderto implementWalletFacadeandMidnightProviderinterfacesWalletProvider - function provides detailed progress logging during wallet sync
syncWallet - Test uses static seed () for reproducible testing
ALICE_SEED - uses
privateStateStoreNamefor test isolationDate.now() - required for vitest with ESM
NODE_OPTIONS='--experimental-vm-modules'
本技能可生成一个完整、可运行的Midnight DApp,涵盖项目中的所有文件:Compact智能合约、TypeScript工具类、测试脚本、Docker配置以及包管理配置。所有代码均与官方参考实现一致。
midnightntwrk/example-hello-world主要参考资料:
- — 官方参考仓库
github.com/midnightntwrk/example-hello-world - — 官方Hello World指南
docs.midnight.network/getting-started/hello-world - — 权威版本兼容性矩阵
docs.midnight.network/relnotes/support-matrix
本技能中的包版本符合官方兼容性矩阵(2026年5月)。在固定版本前请务必对照兼容性矩阵交叉检查——Midnight包更新频繁,版本不匹配会导致安装时出现错误。
ETARGET核心架构说明:
- 使用的
@midnight-ntwrk/testkit-js创建钱包FluentWalletBuilder - 使用的
@midnight-ntwrk/midnight-js-contracts和deployContract方法submitCallTx - Compact编译指令为
>= 0.23 - 索引器API路径在本地开发网使用
/api/v4/graphql - 类封装
MidnightWalletProvider,实现WalletFacade和MidnightProvider接口WalletProvider - 函数在钱包同步期间提供详细进度日志
syncWallet - 测试使用静态种子()确保测试可复现
ALICE_SEED - 使用
privateStateStoreName实现测试隔离Date.now() - 使用vitest配合ESM时需要
NODE_OPTIONS='--experimental-vm-modules'
1) Project Structure
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.tsThe directory is created by the Compact compiler — do not create or edit files inside it manually.
contracts/managed/hello-world-local/
├── contracts/
│ ├── hello-world.compact # Compact智能合约(需手动创建)
│ ├── index.ts # 编译合约导出的桶文件(需手动创建)
│ └── managed/
│ └── hello-world/ # 编译器输出(自动生成,请勿编辑)
│ ├── compiler/
│ ├── contract/
│ ├── keys/
│ └── zkir/
├── src/
│ ├── config.ts # 网络配置
│ ├── wallet.ts # MidnightWalletProvider + syncWallet
│ ├── providers.ts # HelloWorldProviders构建器
│ └── test/
│ └── hw.test.ts # vitest测试(部署 + storeMessage)
├── compose.yml # Docker:完整本地栈
├── package.json
├── tsconfig.json
└── vitest.config.tscontracts/managed/2) Prerequisites
2) 前置条件
bash
undefinedbash
undefinedNode.js 22+ required
需要Node.js 22+版本
node --version
node --version
Docker required (local stack)
需要Docker(本地栈依赖)
docker --version
docker --version
Install Compact compiler
安装Compact编译器
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
https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh source $HOME/.local/bin/env compact --version
---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
https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh source $HOME/.local/bin/env compact --version
---3) package.json
package.json3) package.json
package.jsonjson
{
"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": "yarn@1.22.22"
}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": "yarn@1.22.22"
}4) tsconfig.json
tsconfig.json4) tsconfig.json
tsconfig.jsonjson
{
"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"]
}⚠️is singular — this matches the repo exactly and covers the compiler output path. Using"contract/**/*.ts"(plural) will silently break TypeScript resolution of the compiled contract types."contracts/**/*.ts"
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"会导致TypeScript无法解析编译后的合约类型,且不会给出明确提示。"contracts/**/*.ts"
5) vitest.config.ts
vitest.config.ts5) vitest.config.ts
vitest.config.tstypescript
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 },
},
});typescript
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
contracts/hello-world.compact6) contracts/hello-world.compact
contracts/hello-world.compactCreate this file manually — the compiler output in is generated from it.
managed/compact
pragma language_version >= 0.23;
export ledger message: Opaque<"string">;
export circuit storeMessage(newMessage: Opaque<"string">): [] {
message = disclose(newMessage);
}Compile from the directory:
contracts/bash
cd contracts
compact compile hello-world.compact managed/hello-world
cd ..Expected output:
Compiling 1 circuits:
circuit "storeMessage" (k=6, rows=26)This generates (and keys, zkir, compiler dirs). Always compile before running tests.
contracts/managed/hello-world/contract/index.js手动创建此文件——目录下的编译输出将由此文件生成。
managed/compact
pragma language_version >= 0.23;
export ledger message: Opaque<"string">;
export circuit storeMessage(newMessage: Opaque<"string">): [] {
message = disclose(newMessage);
}从目录执行编译:
contracts/bash
cd contracts
compact compile hello-world.compact managed/hello-world
cd ..预期输出:
Compiling 1 circuits:
circuit "storeMessage" (k=6, rows=26)此命令会生成(以及keys、zkir、compiler目录)。运行测试前请务必先执行编译。
contracts/managed/hello-world/contract/index.js7) contracts/index.ts
(barrel file — create manually)
contracts/index.ts7) contracts/index.ts
(桶文件——需手动创建)
contracts/index.tsCreate this file manually — it imports from the compiled contract output. The compiled contract module must exist (from running ) for the imports to resolve.
compact compiletypescript
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),
);手动创建此文件——它从编译后的合约输出中导入内容。编译后的合约模块必须存在(执行生成),导入才能生效。
compact compiletypescript
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
src/config.ts8) src/config.ts
src/config.tstypescript
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;
}typescript
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(
`未知网络: ${network}。此测试工具仅支持'local'网络。`,
);
}
return LOCAL_CONFIG;
}9) src/wallet.ts
src/wallet.ts9) src/wallet.ts
src/wallet.tstypescript
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);
}),
),
);
}typescript
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('启动钱包...');
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() 返回 { wallet, seeds } — seeds包含派生的密钥材料。
// 此处类型转换符合testkit-js文档中的使用模式;若出现类型错误,请对照已安装版本验证。
const buildResult = await builder.withSeed(seed).buildWithoutStarting();
const { wallet, seeds } = buildResult as {
wallet: WalletFacade;
seeds: {
masterSeed: string;
shielded: Uint8Array;
dust: Uint8Array;
};
};
logger.info(`从种子构建钱包: ${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('同步钱包...');
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(
`钱包同步 [${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(`钱包同步完成,共接收${emissionCount}次状态更新`)),
Rx.timeout({
each: timeout,
with: () =>
Rx.throwError(
() => new Error(`钱包同步超时,已等待${timeout}ms(共接收${emissionCount}次状态更新)`),
),
}),
Rx.catchError((err) => {
logger.error(`钱包同步错误: ${err}`);
return Rx.throwError(() => err);
}),
),
);
}10) src/providers.ts
src/providers.ts10) src/providers.ts
src/providers.tstypescript
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,
};
}typescript
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
src/test/hw.test.ts11) src/test/hw.test.ts
src/test/hw.test.tstypescript
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);
});
});typescript
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';
// Node.js环境下GraphQL订阅所需
// @ts-expect-error 为apollo全局赋值WebSocket
globalThis.WebSocket = WebSocket;
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的Promise拒绝:', reason);
console.error('Promise:', promise);
});
process.on('uncaughtException', (err) => {
console.error('未捕获的异常:', 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合约', () => {
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初始化完成,准备测试!');
});
afterAll(async () => {
if (aliceWallet) {
logger.info('停止Alice钱包...');
await aliceWallet.stop();
}
});
it('部署合约', async () => {
logger.info('创建私有状态...');
const deployed: any = await (deployContract as any)(aliceProviders, {
compiledContract: CompiledHelloWorldContract,
privateStateId: ALICE_PRIVATE_STATE_ID,
initialPrivateState: {},
args: [],
});
logger.info('设置合约地址...');
contractAddress = deployed.deployTxData.public.contractAddress;
logger.info(`合约部署地址: ${contractAddress}`);
expect(contractAddress).toBeDefined();
expect(contractAddress.length).toBeGreaterThan(0);
const state = await queryLedger(aliceProviders);
expect(state.message).toEqual('');
});
it('存储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
compose.yml12) compose.yml
compose.ymlyaml
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'yaml
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) First Run — Local Devnet
13) 首次运行——本地开发网
bash
undefinedbash
undefined1. Install deps
1. 安装依赖
yarn install
yarn install
2. Compile contract (must run from contracts/ directory)
2. 编译合约(必须从contracts/目录执行)
cd contracts
compact compile hello-world.compact managed/hello-world
cd ..
cd contracts
compact compile hello-world.compact managed/hello-world
cd ..
3. Start local stack
3. 启动本地栈
yarn env:up
yarn env:up
wait ~30 seconds for all services to become healthy
等待约30秒,直到所有服务进入健康状态
4. Run tests (new terminal)
4. 运行测试(新终端窗口)
yarn test:local
Expected output:[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
---yarn test:local
预期输出:[12:46:12.694] INFO (22064): 钱包同步完成,共接收23次状态更新
[12:46:12.703] INFO (22064): Providers初始化完成,准备测试
[12:46:12.707] INFO (22064): 创建私有状态...
[12:46:32.347] INFO (22064): 设置合约地址...
[12:46:32.347] INFO (22064): 合约部署地址: bba6579743ae23b44301d4a9f8df30dbd5244d63a59d8fbc2c9fc7ea521a04f8
✓ src/test/hw.test.ts (2 tests) 39112ms
✓ Hello World合约 > 部署合约 19649ms
✓ Hello World合约 > 存储Hello World! 18184ms
---14) Troubleshooting
14) 故障排除
| Error | Cause | Fix |
|---|---|---|
| PATH not set | |
| Contract not compiled yet | Run |
| Wrong package version | Use versions from this skill's |
| Proof server not running | |
| Missing experimental flag | Ensure |
| Docker services not healthy | Verify |
| Old indexer URL | Local devnet uses |
| Wrong testkit-js import | Import from |
| Missing required fields | Must pass |
TS: | Wrong | Must be |
| Wrong import | Import from |
| Old contract address fails after recompile | Verifier key changed | Redeploy — compiled keys change when contract source changes |
| 错误 | 原因 | 修复方案 |
|---|---|---|
| PATH未配置 | 执行 |
| 合约未编译 | 先从 |
| 包版本错误 | 使用本技能 |
| 证明服务器未运行 | 执行 |
测试中出现 | 缺少实验性标志 | 确保测试脚本中包含 |
| Docker服务未进入健康状态 | 确认 |
| 索引器URL过时 | 本地开发网使用 |
| testkit-js导入错误 | 从 |
| 缺少必填字段 | 必须传入 |
TS报错: | tsconfig.json的include配置错误 | 必须使用 |
| 导入错误 | 从 |
| 重新编译后旧合约地址失效 | 验证密钥已变更 | 重新部署——合约源码变更会导致编译后的密钥改变 |
15) Adapting This Template
15) 适配此模板
- Replace with your contract
contracts/hello-world.compact - Compile:
cd contracts && compact compile your-contract.compact managed/your-contract - Update :
contracts/index.ts- Change import paths to
./managed/your-contract/contract/index.js - Update to
zkConfigPathmanaged/your-contract - Update name and
CompiledContract.make()referenceContract
- Change import paths to
- In :
src/providers.ts- Update type to your circuit names
HelloWorldCircuits - Update prefix to avoid LevelDB collisions
privateStateStoreName
- Update
- In :
src/test/hw.test.ts- Replace args with your circuit calls
submitCallTx - Replace with your contract's
ledger()functionledger() - Update and
ALICE_PRIVATE_STATE_IDshapeinitialPrivateState
- Replace
- Update if targeting non-local networks
src/config.ts
- 将替换为你的合约
contracts/hello-world.compact - 编译:
cd contracts && compact compile your-contract.compact managed/your-contract - 更新:
contracts/index.ts- 将导入路径改为
./managed/your-contract/contract/index.js - 更新为
zkConfigPathmanaged/your-contract - 更新的名称和
CompiledContract.make()引用Contract
- 将导入路径改为
- 在中:
src/providers.ts- 更新类型为你的电路名称
HelloWorldCircuits - 更新前缀,避免LevelDB冲突
privateStateStoreName
- 更新
- 在中:
src/test/hw.test.ts- 将的参数替换为你的电路调用
submitCallTx - 将替换为你的合约的
ledger()函数ledger() - 更新和
ALICE_PRIVATE_STATE_ID的结构initialPrivateState
- 将
- 若目标为非本地网络,更新
src/config.ts