For the complete documentation index, see llms.txt
Preprod에서 프로그래밍 방식으로 DUST 생성하기
빈 프로젝트 설정부터 지갑 생성 또는 복원, tNIGHT 충전, DUST 생성을 위한 토큰 등록까지 전체 과정을 안내합니다. 완료하면 지갑을 설정하고 DUST 생성을 시작하는 TypeScript 스크립트를 갖게 됩니다.
사전 요구사항
Midnight 개발은 **macOS, Linux, Windows(WSL 사용)**에서 지원됩니다.
다음이 설치되어 있는지 확인하세요:
- Node.js (v18 이상)
- Docker Desktop — proof server 실행에 필요합니다
설치를 확인합니다:
node --version # should print v18.x.x or higher
npm --version # should print 9.x.x or higher
docker --version # should print Docker version 2x.x.x or higher
Docker가 필요한 이유
Midnight는 데이터 프라이버시를 위해 영지식 증명을 사용합니다. Proof server는 이 증명을 생성하는 서비스로, 로컬 머신에서 실행됩니다. 프라이빗 데이터가 컴퓨터 밖으 로 나가지 않습니다. Docker를 사용하면 하나의 명령어로 이미지를 풀하고 시작할 수 있어 간편합니다.
Step 1: 프로젝트 생성
프로젝트용 빈 디렉토리를 만듭니다. 이 튜토리얼에서 생성하는 모든 파일이 이 안에 들어갑니다.
mkdir midnight-dust-tutorial
cd midnight-dust-tutorial
Step 2: package.json 생성 및 의존성 설치
모든 Node.js 프로젝트에는 의존성과 스크립트를 선언하는 package.json 파일이 필요합니다.
touch package.json
열어서 아래 내용을 붙여넣습니다.
{
"type": "module",
"scripts": {
"start": "tsx src/index.ts"
},
"dependencies": {
"@midnight-ntwrk/ledger-v8": "^8.0.0",
"@midnight-ntwrk/midnight-js-network-id": "^4.0.0",
"@midnight-ntwrk/midnight-js-utils": "^4.0.0",
"@midnight-ntwrk/wallet-sdk-address-format": "^3.0.0",
"@midnight-ntwrk/wallet-sdk-dust-wallet": "^3.0.0",
"@midnight-ntwrk/wallet-sdk-facade": "^3.0.0",
"@midnight-ntwrk/wallet-sdk-hd": "^3.0.0",
"@midnight-ntwrk/wallet-sdk-shielded": "^2.0.0",
"@midnight-ntwrk/wallet-sdk-unshielded-wallet": "^2.0.0",
"rxjs": "^7.8.1",
"ws": "^8.19.0"
},
"devDependencies": {
"@types/ws": "^8.18.1",
"tsx": "^4.19.0"
}
}
각 항목의 역할은 다음과 같습니다:
"type": "module": Node.js가 최신 ES module import(import/export)를 사용하도록 설정합니다"scripts" > "start":npm start의 동작을 정의합니다.tsx src/index.ts를 실행하여 TypeScript 파일을 한 번에 컴파일하고 실행합니다"dependencies": 스크립트가 런타임에 import하는 Midnight wallet SDK 패키지입니다"devDependencies":tsx는 별도의 컴파일 단계 없이 TypeScript 파일을 직접 실행하며,@types/ws는 WebSocket 라이브러리의 타입 정의를 제공합니다
Midnight wallet SDK 패키지는 호환성 매트릭스에 따라 버전을 맞추어야 합니다.
이제 모든 것을 설치합니다:
npm install
Midnight SDK 패키지와 의존성이 다운로드됩니다.
Step 3: proof server 설정 파일 생성
Proof server는 Docker에서 실행됩니다. 나중에 한 명령어로 시작할 수 있도록 작은 Compose 파일에 실행 방법을 정의합니다.
touch proof-server.yml
proof-server.yml을 열고 아래 내용을 붙여넣습니다:
services:
proof-server:
image: 'midnightntwrk/proof-server:8.0.3'
command: ['midnight-proof-server -v']
ports:
- '6300:6300'
environment:
RUST_BACKTRACE: 'full'
Docker에 proof server 실행 방법을 알려주는 설정입니다. 공식 Midnight proof server 이미지를 다운로드하고 포트 6300으로 노출하여 스크립트가 http://localhost:6300으로 증명을 요청할 수 있게 합니다.
Step 4: src/index.ts 생성
모든 스크립트 로직은 src/index.ts에 들어갑니다. 디렉토리와 파일을 생성한 뒤, 이 단계의 나머지에서 섹션별로 채웁니다.
mkdir src
touch src/index.ts
src/index.ts를 열고 아래 코드 블록을 모두 붙여넣거나, 페이지 하 단의 전체 스크립트를 사용하세요. 각 섹션에 역할을 설명하는 주석이 달려 있습니다:
Import
스크립트에는 여러 패키지가 필요합니다: WebSocket polyfill(Node.js에 내장되어 있지 않음), shielded/unshielded/dust 지갑용 Midnight wallet SDK 패키지, HD 키 파생 라이브러리, 주소 포매팅 유틸리티 등입니다. 먼저 전역 WebSocket을 설정하여 wallet SDK가 indexer에 연결할 수 있게 합니다.
// The Midnight wallet SDK communicates with the indexer over WebSockets.
// Node.js doesn't have a built-in WebSocket like browsers do, so we import
// one and set it globally before any wallet code runs.
import { WebSocket } from 'ws';
(globalThis as any).WebSocket = WebSocket;
// Buffer: converts hex strings to/from binary data (used for seeds and keys)
// readline: reads user input from the terminal (for the wallet create/restore prompt)
// rxjs: reactive streams library — the wallet SDK emits state updates as observables
import { Buffer } from 'buffer';
import * as readline from 'readline';
import * as Rx from 'rxjs';
// HD wallet — derives multiple key pairs from a single seed phrase
import { HDWallet, Roles, generateRandomSeed } from '@midnight-ntwrk/wallet-sdk-hd';
// Utility to convert binary data to hex strings
import { toHex } from '@midnight-ntwrk/midnight-js-utils';
// Ledger types — defines tokens, keys, and on-chain parameters
import * as ledger from '@midnight-ntwrk/ledger-v8';
import { unshieldedToken } from '@midnight-ntwrk/ledger-v8';
// WalletFacade — combines the three sub-wallets into a single interface
import { WalletFacade } from '@midnight-ntwrk/wallet-sdk-facade';
// ShieldedWallet — handles private (zero-knowledge) transactions
import { ShieldedWallet } from '@midnight-ntwrk/wallet-sdk-shielded';
// DustWallet — generates and spends DUST for transaction fees
import { DustWallet } from '@midnight-ntwrk/wallet-sdk-dust-wallet';
// UnshieldedWallet — handles transparent (public) transactions like receiving tNight
import {
createKeystore,
InMemoryTransactionHistoryStorage,
PublicKey,
UnshieldedWallet,
} from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
import type { UnshieldedKeystore } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
// Network ID — tells the SDK which network to connect to (preprod, preview, etc.)
import { setNetworkId, getNetworkId } from '@midnight-ntwrk/midnight-js-network-id';
// Address formatting — encodes wallet keys into human-readable bech32m addresses
import {
DustAddress,
MidnightBech32m,
ShieldedAddress,
ShieldedCoinPublicKey,
ShieldedEncryptionPublicKey,
} from '@midnight-ntwrk/wallet-sdk-address-format';
설정
네트워크 엔드포인트를 한 곳에 정의합니다.
// Preprod network endpoints. The indexer and RPC node are public services hosted
// by Midnight. The proof server runs locally on your machine (via Docker) so that
// your private data never leaves your computer.
const CONFIG = {
networkId: 'preprod' as const,
indexerHttpUrl: 'https://indexer.preprod.midnight.network/api/v4/graphql',
indexerWsUrl: 'wss://indexer.preprod.midnight.network/api/v4/graphql/ws',
node: 'https://rpc.preprod.midnight.network',
proofServer: 'http://localhost:6300',
faucetUrl: 'https://faucet.preprod.midnight.network',
};
잔액 포매팅
Midnight 토큰 잔액은 bigint 원자 단위로 저장됩니다. 사람이 읽기 쉬운 형태로 표시하기 위해, NIGHT와 DUST 각각에 대해 정수를 소수 문자열로 변환하는 간단한 헬퍼 함수 두 개를 사용합니다.
// NIGHT is divided into 10^6 STAR. A raw value of 1,000,000,000 equals 1,000.000000 tNight.
// DUST is divided into 10^15 SPECK, so it uses a more granular decimal representation.
const formatNight = (raw: bigint): string => {
const whole = raw / 1_000_000n;
const fraction = (raw % 1_000_000n).toString().padStart(6, '0');
return `${whole.toLocaleString()}.${fraction}`;
};
const formatDust = (raw: bigint): string => {
const whole = raw / 1_000_000_000_000_000n;
const fraction = (raw % 1_000_000_000_000_000n).toString().padStart(15, '0');
return `${whole.toLocaleString()}.${fraction}`;
};
시계 스피너
지갑 빌드, 네트워크 동기화, NIGHT 등록 등 이 스크립트의 여러 작업은 완료까지 몇 초가 걸립니다. 사용자에게 시각적 피드백을 주기 위해 장시간 실행되는 각 작업을 스피너 애니메이션으로 감쌉니다.
// Shows a rotating clock animation in the terminal while an async operation runs.
const withStatus = async <T>(message: string, fn: () => Promise<T>): Promise<T> => {
const clocks = ['🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚', '🕛'];
let i = 0;
const interval = setInterval(() => {
process.stdout.write(`\r ${clocks[i++ % clocks.length]} ${message}`);
}, 150);
try {
const result = await fn();
clearInterval(interval);
process.stdout.write(`\r ✅ ${message}\n`);
return result;
} catch (e) {
clearInterval(interval);
process.stdout.write(`\r ❌ ${message}\n`);
throw e;
}
};
사용자 입력 프롬프트
스크립트는 사용자에게 몇 가지를 대화형으로 질문합니다(지갑 생성/복원 여부, 지정할 dust 주소 등). Node의 내장 readline 모듈을 Promise로 감싸는 작은 prompt 헬퍼로 사용자 입력을 await할 수 있게 합니다.
const prompt = (question: string): Promise<string> => {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(answer.trim());
});
});
};
Dust 주소 검증 및 프롬프트
DUST 생성을 위해 dust 주소를 지정할 때, 실수로 shielded나 unshielded 주소를 붙여넣지 않았는지 확인해야 합니다. 이 헬퍼는 bech32m 형식을 검증하고, 사용자가 유효한 값을 입력하거나 Enter를 눌러 자신의 dust 주소를 사용할 때까지 반복합니다.
// Dust addresses use the bech32m prefix "mn_dust_". This validates that the
// user hasn't accidentally pasted a shielded or unshielded address.
const isValidDustAddress = (addr: string): boolean => {
if (!addr.startsWith('mn_dust_')) return false;
try {
MidnightBech32m.parse(addr).decode(DustAddress, getNetworkId());
return true;
} catch {
return false;
}
};
const promptForDustAddress = async (ownDustAddress: string): Promise<string> => {
while (true) {
const input = await prompt(` Paste your Dust address to designate (Enter for this wallet's): `);
const target = input || ownDustAddress;
if (isValidDustAddress(target)) {
if (target !== ownDustAddress) {
console.log(`\n Using external dust address: ${target}\n`);
} else {
console.log('');
}
return target;
}
console.log(' ❌ Invalid dust address. Dust addresses start with "mn_dust_" followed by the network.');
console.log(' Make sure you\'re not pasting a shielded or unshielded address.\n');
}
};
지갑 시드 생성 또는 복원
모든 Midnight 지갑은 32바이트 시드에서 파생됩니다. 이 함수는 새 지갑 생성(새로운 랜덤 시드 생성)과 기존 지갑 복원(기존 시드 입력) 중 하나를 선택하게 합니다. 어느 경우든 시드를 hex 문자열로 반환합니다.
const getOrCreateSeed = async (): Promise<string> => {
const choice = await prompt(' Create a new wallet or restore an existing one? (n/r): ');
if (choice.toLowerCase() === 'r') {
const seed = await prompt(' Enter your seed: ');
if (!seed || seed.length < 32) {
throw new Error('Invalid seed. The seed should be a 64-character hex string.');
}
console.log(' Restoring wallet from seed...\n');
return seed;
}
// Generate a brand new seed
const seed = toHex(Buffer.from(generateRandomSeed()));
console.log('\n Created new wallet.');
console.log(' ⚠️ Save this seed — it is the ONLY way to restore your wallet:\n');
console.log(` ${seed}\n`);
return seed;
};
시드에서 키 파생
하나의 시드에서 HD(hierarchical deterministic) 파생을 통해 shielded 트랜잭션용, unshielded 트랜잭션용, DUST용 세 세트의 키를 생성합니다. 이 함수는 HD wallet 라이브러리에 세 키를 한꺼번에 파생하도록 요청한 뒤, 민감한 키 데이터를 메모리에서 삭제합니다.
// The HD wallet derives three sets of keys from a single seed:
// - Zswap: for shielded (private) transactions
// - NightExternal: for unshielded (transparent) transactions
// - Dust: for DUST fee management
const deriveKeys = (seed: string) => {
const hdWallet = HDWallet.fromSeed(Buffer.from(seed, 'hex'));
if (hdWallet.type !== 'seedOk') {
throw new Error('Failed to initialize HDWallet from seed. Is the seed a valid hex string?');
}
const derivationResult = hdWallet.hdWallet
.selectAccount(0)
.selectRoles([Roles.Zswap, Roles.NightExternal, Roles.Dust])
.deriveKeysAt(0);
if (derivationResult.type !== 'keysDerived') {
throw new Error('Failed to derive keys from seed.');
}
// Clear sensitive key material from memory
hdWallet.hdWallet.clear();
return derivationResult.keys;
};
지갑 빌드
세 개의 서브 지갑을 하나로 결합하는 단계입니다. 각 서브 지갑은 고유한 설정으로 생성된 뒤 WalletFacade.init()에 전달되어 하나의 통합 인터페이스가 됩니다. 이 facade가 나머지 스크 립트에서 사용하는 대상이며, 내부적으로 세 지갑이 함께 동작하는 것을 추상화합니다.
// Midnight uses three sub-wallets, each handling a different type of transaction.
// The WalletFacade ties them together into a single interface.
const buildWallet = async (keys: ReturnType<typeof deriveKeys>) => {
setNetworkId(CONFIG.networkId);
const shieldedSecretKeys = ledger.ZswapSecretKeys.fromSeed(keys[Roles.Zswap]);
const dustSecretKey = ledger.DustSecretKey.fromSeed(keys[Roles.Dust]);
const unshieldedKeystore = createKeystore(keys[Roles.NightExternal], getNetworkId());
// Build the three sub-wallet configs. WalletFacade.init() takes a single
// configuration object — we spread the per-wallet configs into one and let
// the factory functions receive the relevant subset.
const shieldedConfig = {
networkId: getNetworkId(),
indexerClientConnection: {
indexerHttpUrl: CONFIG.indexerHttpUrl,
indexerWsUrl: CONFIG.indexerWsUrl,
},
provingServerUrl: new URL(CONFIG.proofServer),
relayURL: new URL(CONFIG.node.replace(/^http/, 'ws')),
};
const unshieldedConfig = {
networkId: getNetworkId(),
indexerClientConnection: {
indexerHttpUrl: CONFIG.indexerHttpUrl,
indexerWsUrl: CONFIG.indexerWsUrl,
},
txHistoryStorage: new InMemoryTransactionHistoryStorage(),
};
const dustConfig = {
...shieldedConfig,
costParameters: {
additionalFeeOverhead: 300_000_000_000_000n,
feeBlocksMargin: 5,
},
};
// ── Create and start the unified facade via static factory ──
const wallet = await WalletFacade.init({
configuration: { ...shieldedConfig, ...unshieldedConfig, ...dustConfig },
shielded: (cfg) => ShieldedWallet(cfg).startWithSecretKeys(shieldedSecretKeys),
unshielded: (cfg) => UnshieldedWallet(cfg).startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore)),
dust: (cfg) =>
DustWallet(cfg).startWithSecretKey(dustSecretKey, ledger.LedgerParameters.initialParameters().dust),
});
await wallet.start(shieldedSecretKeys, dustSecretKey);
return { wallet, shieldedSecretKeys, dustSecretKey, unshieldedKeystore };
};
지갑 동기화 대기
지갑을 빌드한 후 사용하기 전에 네트워크와 동기화해야 합니다. 지갑은 RxJS observable로 상태 업데이트를 방출하므로, isSynced가 true인 첫 번째 상태를 기다립니다.
const waitForSync = (wallet: WalletFacade) =>
Rx.firstValueFrom(
wallet.state().pipe(
Rx.throttleTime(5_000),
Rx.filter((state) => state.isSynced),
),
);