token-transfers
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseToken Transfers Skill
代币转账技能
Midnight has two distinct token systems that operate independently: ledger tokens (NIGHT, UTXO-based, native to the chain) and contract tokens (account-based, ERC-20-style, implemented in Compact). They work differently, transfer differently, and serve different purposes.
Primary references:
- — ledger vs contract token model
docs.midnight.network/concepts/ledgers - — UTXO mechanics and nullifier set
docs.midnight.network/concepts/utxo - — DUST generation lifecycle
docs.midnight.network/concepts/dust-architecture - — atomic swap and shielded transfer protocol
docs.midnight.network/concepts/zswap - — reference FungibleToken implementation
github.com/OpenZeppelin/compact-contracts
Midnight 拥有两个独立运行的代币系统:账本代币(NIGHT,基于UTXO,为链原生代币)和合约代币(基于账户,ERC-20风格,在Compact中实现)。它们的工作方式、转账机制和用途各不相同。
主要参考资料:
- — 账本代币与合约代币模型对比
docs.midnight.network/concepts/ledgers - — UTXO机制与零知识证明集合
docs.midnight.network/concepts/utxo - — DUST生成生命周期
docs.midnight.network/concepts/dust-architecture - — 原子交换与加密转账协议
docs.midnight.network/concepts/zswap - — FungibleToken参考实现
github.com/OpenZeppelin/compact-contracts
1) Two Token Systems — Which One You Need
1) 两种代币系统——如何选择
| Ledger Tokens (NIGHT) | Contract Tokens | |
|---|---|---|
| Where they live | Chain ledger, UTXO-based | Inside a Compact contract, account-based |
| Transfer mechanism | Zswap (ZK atomic swap) | Circuit call ( |
| Privacy | Shielded or unshielded at UTXO level | Private state (balances can be private) |
| Fee resource | NIGHT generates DUST (transaction fees) | No fee role — just application logic |
| Wallet SDK method | | |
| Who manages it | Protocol + wallet SDK | Your Compact contract |
| Analogy | Native ETH / BTC | ERC-20 |
Decision rule: If you are moving NIGHT tokens (the chain's native token), use the wallet SDK transfer. If you are building an application token (governance, game currency, stablecoin, NFT), implement it in Compact using the account/map model.
| 账本代币(NIGHT) | 合约代币 | |
|---|---|---|
| 存储位置 | 链账本,基于UTXO | Compact合约内部,基于账户 |
| 转账机制 | Zswap(零知识原子交换) | 电路调用( |
| 隐私性 | UTXO层面支持加密或未加密 | 私有状态(余额可设为私有) |
| 费用资源 | NIGHT生成DUST(交易手续费) | 无费用角色——仅遵循应用逻辑 |
| 钱包SDK方法 | | |
| 管理方 | 协议 + 钱包SDK | 你的Compact合约 |
| 类比 | 原生ETH / BTC | ERC-20 |
决策规则: 若转移的是NIGHT代币(链原生代币),使用钱包SDK转账;若构建应用代币(治理代币、游戏货币、稳定币、NFT),则在Compact中使用账户/映射模型实现。
2) NIGHT Token — Shielded vs Unshielded
2) NIGHT代币——加密与未加密
Every NIGHT UTXO is either shielded (private, hidden from observers) or unshielded (public, visible on-chain).
| Shielded | Unshielded | |
|---|---|---|
| Address prefix | | |
| Amount visible | No | Yes |
| Sender/receiver visible | No | Yes |
| DUST generation | Yes (via Zswap registration) | Yes (via registration table) |
| Faucet/bridge sends to | No | Yes — always unshielded first |
| Required for | Privacy-sensitive transfers | Interop, faucet, bridge, contracts |
Critical: Faucets and bridges always send to the unshielded address. Never give a shielded address to a faucet.
每个NIGHT UTXO要么是加密(私有,对观察者隐藏),要么是未加密(公开,链上可见)。
| 加密 | 未加密 | |
|---|---|---|
| 地址前缀 | | |
| 金额可见性 | 否 | 是 |
| 发送方/接收方可见性 | 否 | 是 |
| DUST生成 | 是(通过Zswap注册) | 是(通过注册表) |
| 水龙头/桥接器转账目标 | 否 | 是——始终先转到未加密地址 |
| 适用场景 | 隐私敏感型转账 | 互操作、水龙头、桥接器、合约交互 |
关键提示: 水龙头和桥接器始终转账到未加密地址,切勿向水龙头提供加密地址。
Address Types from Wallet SDK
钱包SDK中的地址类型
typescript
import * as Rx from 'rxjs';
const state = await Rx.firstValueFrom(
wallet.state().pipe(Rx.filter((s: any) => s.isSynced)),
);
// Unshielded address — use for faucets, bridges, contract interactions
const unshieldedAddress = state.unshielded.address;
// → "mn_addr_preprod1qxy..."
// Shielded coin public key — used as recipient in Zswap transfers
const shieldedCoinPublicKey = state.shielded.coinPublicKey.toHexString();
// → "0x3a7f..."
// DUST address — for DUST registration queries
const dustAddress = state.dust.address;typescript
import * as Rx from 'rxjs';
const state = await Rx.firstValueFrom(
wallet.state().pipe(Rx.filter((s: any) => s.isSynced)),
);
// 未加密地址——用于水龙头、桥接器、合约交互
const unshieldedAddress = state.unshielded.address;
// → "mn_addr_preprod1qxy..."
// 加密代币公钥——作为Zswap转账的接收方
const shieldedCoinPublicKey = state.shielded.coinPublicKey.toHexString();
// → "0x3a7f..."
// DUST地址——用于DUST注册查询
const dustAddress = state.dust.address;3) Sending NIGHT Tokens (Wallet SDK)
3) 发送NIGHT代币(钱包SDK)
Use to transfer NIGHT. The wallet handles UTXO selection, Zswap proving, and DUST fee payment.
wallet.makeTransfer(outputs)typescript
import * as Rx from 'rxjs';
import { unshieldedToken } from '@midnight-ntwrk/ledger-v8';
// Get current state
const state = await Rx.firstValueFrom(
wallet.state().pipe(Rx.filter((s: any) => s.isSynced)),
);
// Check unshielded NIGHT balance before sending
const nightTokenType = unshieldedToken().raw; // hex token type identifier
const nightBalance = state.unshielded.balances[nightTokenType] ?? 0n;
console.log('NIGHT balance (Stars):', nightBalance);
// 1 NIGHT = 1_000_000 Stars
// Send unshielded NIGHT to an unshielded address
const transferRecipe = await wallet.makeTransfer([
{
value: 1_000_000n, // 1 NIGHT in Stars
tokenType: nightTokenType,
receiverAddress: 'mn_addr_preprod1...', // recipient's unshielded address
},
]);
const finalized = await wallet.finalizeRecipe(transferRecipe);
const txId = await wallet.submitTransaction(finalized);
console.log('Transfer submitted:', txId);使用转账NIGHT,钱包会自动处理UTXO选择、Zswap证明生成和DUST手续费支付。
wallet.makeTransfer(outputs)typescript
import * as Rx from 'rxjs';
import { unshieldedToken } from '@midnight-ntwrk/ledger-v8';
// 获取当前状态
const state = await Rx.firstValueFrom(
wallet.state().pipe(Rx.filter((s: any) => s.isSynced)),
);
// 转账前检查未加密NIGHT余额
const nightTokenType = unshieldedToken().raw; // 十六进制代币类型标识符
const nightBalance = state.unshielded.balances[nightTokenType] ?? 0n;
console.log('NIGHT余额(Stars):', nightBalance);
// 1 NIGHT = 1_000_000 Stars
// 向未加密地址发送未加密NIGHT
const transferRecipe = await wallet.makeTransfer([
{
value: 1_000_000n, // 1 NIGHT(以Stars为单位)
tokenType: nightTokenType,
receiverAddress: 'mn_addr_preprod1...', // 接收方的未加密地址
},
]);
const finalized = await wallet.finalizeRecipe(transferRecipe);
const txId = await wallet.submitTransaction(finalized);
console.log('转账已提交:', txId);Multi-Output Transfer (Batch)
多输出转账(批量)
typescript
// Send to multiple recipients in one atomic transaction
const transferRecipe = await wallet.makeTransfer([
{
value: 500_000n,
tokenType: nightTokenType,
receiverAddress: 'mn_addr_preprod1_alice...',
},
{
value: 250_000n,
tokenType: nightTokenType,
receiverAddress: 'mn_addr_preprod1_bob...',
},
]);typescript
// 在一笔原子交易中向多个接收方转账
const transferRecipe = await wallet.makeTransfer([
{
value: 500_000n,
tokenType: nightTokenType,
receiverAddress: 'mn_addr_preprod1_alice...',
},
{
value: 250_000n,
tokenType: nightTokenType,
receiverAddress: 'mn_addr_preprod1_bob...',
},
]);Querying Unshielded NIGHT Balance (Without Wallet)
查询未加密NIGHT余额(无需钱包)
You can also query the indexer directly for unshielded balances at a contract or address:
typescript
async function getUnshieldedBalance(
indexerUrl: string,
contractAddress: string,
): Promise<Map<string, bigint>> {
const res = await fetch(indexerUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
query: `
query($address: HexEncoded!) {
contractAction(address: $address) {
... on ContractCall { unshieldedBalances { tokenType amount } }
... on ContractUpdate { unshieldedBalances { tokenType amount } }
}
}
`,
variables: { address: contractAddress },
}),
});
const payload = await res.json();
const balances: Array<{ tokenType: string; amount: string }> =
payload.data?.contractAction?.unshieldedBalances ?? [];
return new Map(balances.map(b => [b.tokenType, BigInt(b.amount)]));
}也可直接通过索引器查询合约或地址的未加密余额:
typescript
async function getUnshieldedBalance(
indexerUrl: string,
contractAddress: string,
): Promise<Map<string, bigint>> {
const res = await fetch(indexerUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
query: `
query($address: HexEncoded!) {
contractAction(address: $address) {
... on ContractCall { unshieldedBalances { tokenType amount } }
... on ContractUpdate { unshieldedBalances { tokenType amount } }
}
}
`,
variables: { address: contractAddress },
}),
});
const payload = await res.json();
const balances: Array<{ tokenType: string; amount: string }> =
payload.data?.contractAction?.unshieldedBalances ?? [];
return new Map(balances.map(b => [b.tokenType, BigInt(b.amount)]));
}4) DUST — Mechanics and Developer Implications
4) DUST——机制与开发者注意事项
DUST is the non-transferable fee resource generated by holding NIGHT. It is not a token you send — it is a capacity resource consumed automatically when you submit transactions.
DUST是持有NIGHT生成的不可转账费用资源,它不是可发送的代币,而是提交交易时自动消耗的容量资源。
Mental model
心智模型
NIGHT → generates → DUST → consumed by → transactions
(Solar Panel) (Electricity) (Appliances)NIGHT → 生成 → DUST → 被消耗于 → 交易
(太阳能板) (电力) (电器)Key properties
核心属性
- 1 NIGHT = 5 DUST maximum capacity (at full generation: )
night_dust_ratio = 5_000_000_000 - 1 DUST = 10^15 Specks (unit used internally)
- Generation time to capacity: ~1 week ()
generation_decay_rate = 8267 - Grace period: 3 hours (timestamp window for proof acceptance)
- DUST is shielded and non-transferable — you cannot send DUST to another user
- DUST starts decaying immediately when its backing NIGHT UTXO is spent
- 1 NIGHT = 最大5 DUST容量(满额生成时:)
night_dust_ratio = 5_000_000_000 - 1 DUST = 10^15 Specks(内部使用单位)
- 满额生成时间:约1周()
generation_decay_rate = 8267 - 宽限期:3小时(证明接受的时间窗口)
- DUST是加密且不可转账的——无法向其他用户发送DUST
- 当支撑DUST的NIGHT UTXO被花费时,DUST会立即开始衰减
DUST lifecycle
DUST生命周期
NIGHT UTXO created
↓
Registration: DustRegistration links NIGHT public key → DUST public key
↓
DUST UTXO starts generating value (grows toward cap over ~1 week)
↓
Transaction submitted: DUST UTXO consumed → new DUST UTXO created (value - fees)
↓
NIGHT UTXO spent → DUST UTXO immediately begins decaying to zeroNIGHT UTXO创建
↓
注册:DustRegistration将NIGHT公钥关联到DUST公钥
↓
DUST UTXO开始生成价值(约1周内增长至上限)
↓
提交交易:消耗DUST UTXO → 创建新的DUST UTXO(价值扣除手续费)
↓
NIGHT UTXO被花费 → DUST UTXO立即开始衰减至零Registering NIGHT for DUST generation
注册NIGHT以生成DUST
typescript
const state = await Rx.firstValueFrom(
wallet.state().pipe(Rx.filter((s: any) => s.isSynced)),
);
// Find unregistered NIGHT UTXOs
const unregistered = state.unshielded.availableCoins.filter(
(coin: any) => coin.meta?.registeredForDustGeneration !== true,
);
if (unregistered.length > 0) {
const recipe = await wallet.registerNightUtxosForDustGeneration(
unregistered,
unshieldedKeystore.getPublicKey(),
(payload: Uint8Array) => unshieldedKeystore.signData(payload),
);
const finalized = await wallet.finalizeRecipe(recipe);
await wallet.submitTransaction(finalized);
}
// Wait for DUST to become available
await Rx.firstValueFrom(
wallet.state().pipe(
Rx.throttleTime(5_000),
Rx.filter((s: any) => s.isSynced),
Rx.filter((s: any) => s.dust.walletBalance(new Date()) > 0n),
),
);typescript
const state = await Rx.firstValueFrom(
wallet.state().pipe(Rx.filter((s: any) => s.isSynced)),
);
// 查找未注册的NIGHT UTXO
const unregistered = state.unshielded.availableCoins.filter(
(coin: any) => coin.meta?.registeredForDustGeneration !== true,
);
if (unregistered.length > 0) {
const recipe = await wallet.registerNightUtxosForDustGeneration(
unregistered,
unshieldedKeystore.getPublicKey(),
(payload: Uint8Array) => unshieldedKeystore.signData(payload),
);
const finalized = await wallet.finalizeRecipe(recipe);
await wallet.submitTransaction(finalized);
}
// 等待DUST可用
await Rx.firstValueFrom(
wallet.state().pipe(
Rx.throttleTime(5_000),
Rx.filter((s: any) => s.isSynced),
Rx.filter((s: any) => s.dust.walletBalance(new Date()) > 0n),
),
);Reading DUST balance
查询DUST余额
typescript
const state = await Rx.firstValueFrom(
wallet.state().pipe(Rx.filter((s: any) => s.isSynced)),
);
const dustBalance = state.dust.walletBalance(new Date()); // Specks
const dustCoins = state.dust.availableCoins.length;
const dustPending = state.dust.pendingCoins.length;
console.log(`DUST: ${dustBalance.toLocaleString()} Specks`);
console.log(`Coins: ${dustCoins} available, ${dustPending} pending`);pendingCoins > 0 && availableCoins === 0typescript
const state = await Rx.firstValueFrom(
wallet.state().pipe(Rx.filter((s: any) => s.isSynced)),
);
const dustBalance = state.dust.walletBalance(new Date()); // Specks单位
const dustCoins = state.dust.availableCoins.length;
const dustPending = state.dust.pendingCoins.length;
console.log(`DUST: ${dustBalance.toLocaleString()} Specks`);
console.log(`代币数量: ${dustCoins}可用, ${dustPending}待处理`);pendingCoins > 0 && availableCoins === 0DUST on sponsored networks (preview, mainnet)
赞助网络中的DUST(预览网、主网)
On and , 1AM ProofStation sponsors all fees. Users need zero NIGHT and zero DUST. The call to the 1AM wallet handles fee payment server-side. Do not attempt DUST registration flows on these networks — they are unnecessary.
previewmainnetbalanceUnsealedTransaction在和上,1AM ProofStation会赞助所有手续费。用户无需持有NIGHT或DUST,调用1AM钱包的接口即可由服务器端支付手续费。请勿在这些网络上尝试DUST注册流程——完全不必要。
previewmainnetbalanceUnsealedTransaction5) Contract Tokens (FungibleToken Pattern)
5) 合约代币(FungibleToken模式)
For application tokens, implement them in Compact. The standard pattern follows the OpenZeppelin Contracts for Compact library.
对于应用代币,需在Compact中实现,标准模式遵循OpenZeppelin Contracts for Compact库。
Minimal FungibleToken Contract
极简FungibleToken合约
compact
pragma language_version >= 0.22;
import CompactStandardLibrary;
// Either<ZswapCoinPublicKey, ContractAddress> = shielded wallet OR another contract
// This is the standard recipient type for contract tokens
export ledger name: Opaque<"string">;
export ledger symbol: Opaque<"string">;
export ledger decimals: Uint<8>;
export ledger totalSupply: Uint<128>;
export ledger balances: Map<Bytes<32>, Uint<128>>;
witness callerAddress(): Bytes<32>;
constructor(
_name: Opaque<"string">,
_symbol: Opaque<"string">,
_decimals: Uint<8>,
) {
name = disclose(_name);
symbol = disclose(_symbol);
decimals = disclose(_decimals);
totalSupply = 0;
}
export circuit mint(to: Bytes<32>, amount: Uint<128>): [] {
const recipient = disclose(to);
const current = balances.member(recipient)
? balances.lookup(recipient)
: 0;
balances.insert(recipient, disclose((current + amount) as Uint<128>));
totalSupply = disclose((totalSupply + amount) as Uint<128>);
}
export circuit transfer(to: Bytes<32>, amount: Uint<128>): Boolean {
const sender = disclose(callerAddress());
assert(balances.member(sender), "sender has no balance");
const senderBal = balances.lookup(sender);
assert(senderBal >= amount, "insufficient balance");
balances.insert(sender, disclose((senderBal - amount) as Uint<128>));
const recipientBal = balances.member(disclose(to))
? balances.lookup(disclose(to))
: 0;
balances.insert(disclose(to), disclose((recipientBal + amount) as Uint<128>));
return true;
}
export circuit balanceOf(account: Bytes<32>): Uint<128> {
if (!balances.member(account)) { return 0; }
return balances.lookup(account);
}compact
pragma language_version >= 0.22;
import CompactStandardLibrary;
// Either<ZswapCoinPublicKey, ContractAddress> = 加密钱包 OR 其他合约
// 这是合约代币的标准接收方类型
export ledger name: Opaque<"string">;
export ledger symbol: Opaque<"string">;
export ledger decimals: Uint<8>;
export ledger totalSupply: Uint<128>;
export ledger balances: Map<Bytes<32>, Uint<128>>;
witness callerAddress(): Bytes<32>;
constructor(
_name: Opaque<"string">,
_symbol: Opaque<"string">,
_decimals: Uint<8>,
) {
name = disclose(_name);
symbol = disclose(_symbol);
decimals = disclose(_decimals);
totalSupply = 0;
}
export circuit mint(to: Bytes<32>, amount: Uint<128>): [] {
const recipient = disclose(to);
const current = balances.member(recipient)
? balances.lookup(recipient)
: 0;
balances.insert(recipient, disclose((current + amount) as Uint<128>));
totalSupply = disclose((totalSupply + amount) as Uint<128>);
}
export circuit transfer(to: Bytes<32>, amount: Uint<128>): Boolean {
const sender = disclose(callerAddress());
assert(balances.member(sender), "sender has no balance");
const senderBal = balances.lookup(sender);
assert(senderBal >= amount, "insufficient balance");
balances.insert(sender, disclose((senderBal - amount) as Uint<128>));
const recipientBal = balances.member(disclose(to))
? balances.lookup(disclose(to))
: 0;
balances.insert(disclose(to), disclose((recipientBal + amount) as Uint<128>));
return true;
}
export circuit balanceOf(account: Bytes<32>): Uint<128> {
if (!balances.member(account)) { return 0; }
return balances.lookup(account);
}OpenZeppelin FungibleToken (Full-Featured)
OpenZeppelin FungibleToken(全功能版)
The OpenZeppelin library provides a production-grade implementation with , , and modules:
OwnablePausableFungibleTokenbash
undefinedOpenZeppelin库提供了生产级实现,包含、和模块:
OwnablePausableFungibleTokenbash
undefinedInstall as a git submodule
作为git子模块安装
git init && git submodule add https://github.com/OpenZeppelin/compact-contracts.git
cd compact-contracts && nvm install && yarn && SKIP_ZK=true yarn compact
```compact
pragma language_version >= 0.22;
import CompactStandardLibrary;
import "./compact-contracts/node_modules/@openzeppelin/compact-contracts/src/access/Ownable"
prefix Ownable_;
import "./compact-contracts/node_modules/@openzeppelin/compact-contracts/src/security/Pausable"
prefix Pausable_;
import "./compact-contracts/node_modules/@openzeppelin/compact-contracts/src/token/FungibleToken"
prefix FungibleToken_;
constructor(
_name: Opaque<"string">,
_symbol: Opaque<"string">,
_decimals: Uint<8>,
_recipient: Either<ZswapCoinPublicKey, ContractAddress>,
_amount: Uint<128>,
_initOwner: Either<ZswapCoinPublicKey, ContractAddress>,
) {
Ownable_initialize(_initOwner);
FungibleToken_initialize(_name, _symbol, _decimals);
FungibleToken__mint(_recipient, _amount);
}
export circuit transfer(
to: Either<ZswapCoinPublicKey, ContractAddress>,
value: Uint<128>,
): Boolean {
Pausable_assertNotPaused();
return FungibleToken_transfer(to, value);
}
export circuit pause(): [] {
Ownable_assertOnlyOwner();
Pausable__pause();
}
export circuit unpause(): [] {
Ownable_assertOnlyOwner();
Pausable__unpause();
}Compile output example:
circuit "pause" (k=10, rows=125)
circuit "transfer" (k=11, rows=1180)
circuit "unpause" (k=10, rows=121)git init && git submodule add https://github.com/OpenZeppelin/compact-contracts.git
cd compact-contracts && nvm install && yarn && SKIP_ZK=true yarn compact
```compact
pragma language_version >= 0.22;
import CompactStandardLibrary;
import "./compact-contracts/node_modules/@openzeppelin/compact-contracts/src/access/Ownable"
prefix Ownable_;
import "./compact-contracts/node_modules/@openzeppelin/compact-contracts/src/security/Pausable"
prefix Pausable_;
import "./compact-contracts/node_modules/@openzeppelin/compact-contracts/src/token/FungibleToken"
prefix FungibleToken_;
constructor(
_name: Opaque<"string">,
_symbol: Opaque<"string">,
_decimals: Uint<8>,
_recipient: Either<ZswapCoinPublicKey, ContractAddress>,
_amount: Uint<128>,
_initOwner: Either<ZswapCoinPublicKey, ContractAddress>,
) {
Ownable_initialize(_initOwner);
FungibleToken_initialize(_name, _symbol, _decimals);
FungibleToken__mint(_recipient, _amount);
}
export circuit transfer(
to: Either<ZswapCoinPublicKey, ContractAddress>,
value: Uint<128>,
): Boolean {
Pausable_assertNotPaused();
return FungibleToken_transfer(to, value);
}
export circuit pause(): [] {
Ownable_assertOnlyOwner();
Pausable__pause();
}
export circuit unpause(): [] {
Ownable_assertOnlyOwner();
Pausable__unpause();
}编译输出示例:
circuit "pause" (k=10, rows=125)
circuit "transfer" (k=11, rows=1180)
circuit "unpause" (k=10, rows=121)6) Calling Token Circuits from TypeScript
6) 从TypeScript调用代币电路
typescript
import { findDeployedContract } from '@midnight-ntwrk/midnight-js-contracts';
const contract = await findDeployedContract(providers, {
contractAddress: '09dbe05f...',
compiledContract,
privateStateId: 'tokenState',
initialPrivateState: {},
});
// Transfer tokens
const result = await contract.callTx.transfer(recipientAddressBytes, 100n);
console.log('txId:', result.public.txId);
console.log('blockHeight:', result.public.blockHeight);
// Read balance (no transaction — direct state query)
import { ContractState } from '@midnight-ntwrk/compact-runtime';
import { YourToken } from './managed/your-token';
const stateRaw = await providers.publicDataProvider.queryContractState(contractAddress);
if (stateRaw) {
const ledgerState = YourToken.ledger(stateRaw.data);
const balance = ledgerState.balances.lookup(callerAddressBytes);
console.log('Balance:', balance);
}typescript
import { findDeployedContract } from '@midnight-ntwrk/midnight-js-contracts';
const contract = await findDeployedContract(providers, {
contractAddress: '09dbe05f...',
compiledContract,
privateStateId: 'tokenState',
initialPrivateState: {},
});
// 转账代币
const result = await contract.callTx.transfer(recipientAddressBytes, 100n);
console.log('txId:', result.public.txId);
console.log('blockHeight:', result.public.blockHeight);
// 查询余额(无需交易——直接状态查询)
import { ContractState } from '@midnight-ntwrk/compact-runtime';
import { YourToken } from './managed/your-token';
const stateRaw = await providers.publicDataProvider.queryContractState(contractAddress);
if (stateRaw) {
const ledgerState = YourToken.ledger(stateRaw.data);
const balance = ledgerState.balances.lookup(callerAddressBytes);
console.log('余额:', balance);
}7) Reading Token Balances via Indexer
7) 通过索引器查询代币余额
Contract token balances live in — readable from the indexer like any other contract state:
export ledgertypescript
import { ContractState } from '@midnight-ntwrk/compact-runtime';
async function getTokenBalance(
indexerUrl: string,
contractAddress: string,
holderAddress: Uint8Array,
ledgerFn: (data: any) => any,
): Promise<bigint> {
const res = await fetch(indexerUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
query: `
query($address: HexEncoded!) {
contractAction(address: $address) { state }
}
`,
variables: { address: contractAddress },
}),
});
const payload = await res.json();
const stateHex = payload.data?.contractAction?.state;
if (!stateHex) return 0n;
const normalized = stateHex.startsWith('0x') ? stateHex.slice(2) : stateHex;
const bytes = new Uint8Array(normalized.length / 2);
for (let i = 0; i < normalized.length; i += 2) {
bytes[i / 2] = parseInt(normalized.slice(i, i + 2), 16);
}
const contractState = ContractState.deserialize(bytes);
const ledgerState = ledgerFn(contractState.data);
return ledgerState.balances.member(holderAddress)
? ledgerState.balances.lookup(holderAddress)
: 0n;
}合约代币余额存储在中,可像其他合约状态一样通过索引器查询:
export ledgertypescript
import { ContractState } from '@midnight-ntwrk/compact-runtime';
async function getTokenBalance(
indexerUrl: string,
contractAddress: string,
holderAddress: Uint8Array,
ledgerFn: (data: any) => any,
): Promise<bigint> {
const res = await fetch(indexerUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
query: `
query($address: HexEncoded!) {
contractAction(address: $address) { state }
}
`,
variables: { address: contractAddress },
}),
});
const payload = await res.json();
const stateHex = payload.data?.contractAction?.state;
if (!stateHex) return 0n;
const normalized = stateHex.startsWith('0x') ? stateHex.slice(2) : stateHex;
const bytes = new Uint8Array(normalized.length / 2);
for (let i = 0; i < normalized.length; i += 2) {
bytes[i / 2] = parseInt(normalized.slice(i, i + 2), 16);
}
const contractState = ContractState.deserialize(bytes);
const ledgerState = ledgerFn(contractState.data);
return ledgerState.balances.member(holderAddress)
? ledgerState.balances.lookup(holderAddress)
: 0n;
}8) Zswap — Atomic Swaps Between Parties
8) Zswap——多方原子交换
Zswap is Midnight's multi-asset atomic swap protocol built on ZK-SNARK proofs. It enables non-interactive atomic swaps with transaction merging.
Zswap是Midnight基于ZK-SNARK证明构建的多资产原子交换协议,支持非交互式原子交换与交易合并。
What Zswap enables
Zswap的能力
- Atomic exchange: Alice's NIGHT ↔ Bob's contract token in one transaction
- Shielded swaps: Neither sender, receiver, nor amounts need to be publicly visible
- Non-interactive merging: Parties can merge offers off-chain before on-chain submission
- Front-running resistance: Shielded asset transfers hide pre-trade information
- 原子兑换:Alice的NIGHT ↔ Bob的合约代币,在一笔交易中完成
- 加密交换:发送方、接收方和金额均无需公开可见
- 非交互式合并:各方可在链下合并报价后再提交链上交易
- 抗抢先交易:加密资产转账可隐藏交易前信息
Developer-facing Zswap surface
面向开发者的Zswap接口
As a DApp developer, you interact with Zswap indirectly:
- — triggers a Zswap transaction for NIGHT
wallet.makeTransfer(outputs) - — the balancing step uses Zswap internally to add DUST fees and change outputs
walletProvider.balanceTx(tx) - — returned by indexer queries, needed by some SDK provider methods
ZswapChainState - — derived from HD wallet seed (
ZswapSecretKeys), used byRoles.ZswapShieldedWallet
Zswap does not expose a direct "swap two contracts" API at the DApp layer yet — that is exchange infrastructure. At the current SDK level, Zswap is primarily surfaced as the mechanism for NIGHT transfers and DUST fee payment.
作为DApp开发者,你将间接与Zswap交互:
- — 触发NIGHT的Zswap交易
wallet.makeTransfer(outputs) - — 余额平衡步骤内部使用Zswap添加DUST手续费和找零输出
walletProvider.balanceTx(tx) - — 索引器查询返回结果,部分SDK提供方法需要此数据
ZswapChainState - — 从HD钱包种子派生(
ZswapSecretKeys),供Roles.Zswap使用ShieldedWallet
目前Zswap在DApp层尚未暴露直接的“交换两个合约”API——这属于交易所基础设施范畴。在当前SDK层面,Zswap主要作为NIGHT转账和DUST手续费支付的机制。
9) Token Units Reference
9) 代币单位参考
| Token | Unit | Conversion |
|---|---|---|
| NIGHT | Star | 1 NIGHT = 1,000,000 Stars |
| DUST | Speck | 1 DUST = 10^15 Specks |
| Contract tokens | Defined by | Typically 18 decimals (1 token = 10^18 base units) |
Always use for amounts — NIGHT Stars and DUST Specks overflow JavaScript's type at realistic balances.
BigIntnumbertypescript
const ONE_NIGHT = 1_000_000n; // Stars
const ONE_DUST = 1_000_000_000_000_000n; // Specks
const DUST_PER_NIGHT_MAX = 5_000_000_000n; // Specks per Star at full cap| 代币 | 单位 | 换算关系 |
|---|---|---|
| NIGHT | Star | 1 NIGHT = 1,000,000 Stars |
| DUST | Speck | 1 DUST = 10^15 Specks |
| 合约代币 | 由 | 通常为18位小数(1代币 = 10^18基础单位) |
金额请始终使用——NIGHT Stars和DUST Specks的实际余额会超出JavaScript 类型的安全范围。
BigIntnumbertypescript
const ONE_NIGHT = 1_000_000n; // Stars
const ONE_DUST = 1_000_000_000_000_000n; // Specks
const DUST_PER_NIGHT_MAX = 5_000_000_000n; // 满额时每Star对应的Specks数10) Common Pitfalls
10) 常见陷阱
Sending to shielded address from faucet — faucets only support unshielded () addresses. Giving a shielded address gets zero tokens.
mn_addr_preprod1...Spending NIGHT before DUST is available — spending a NIGHT UTXO causes its associated DUST to start decaying immediately. If you spend NIGHT before DUST reaches useful levels, you lose the generation time invested. Wait until DUST is available before spending NIGHT.
DUST locked after failed transaction — known wallet SDK issue. means DUST is stuck. Restart the wallet/DApp process to release the lock.
pendingCoins > 0 && availableCoins === 0amountnumberBigIntNumber.MAX_SAFE_INTEGERBigInt1_000_000nbalances.lookup(k)balances.member(k)lookupmemberOverflow on arithmetic — addition that exceeds wraps silently or errors depending on context. Cast explicitly: and add guards.
Uint<128>2^128 - 1(a + b) as Uint<128>assertunshieldedToken().rawunshieldedToken()OZ contract import path changed between versions — the import path changed from to between library versions. Check the README of the exact version you are using.
@openzeppelin-compact/contracts@openzeppelin/compact-contractsContract tokens do not generate DUST — only NIGHT (the native ledger token) generates DUST. Your custom has no fee role.
FungibleToken向水龙头提供加密地址——水龙头仅支持未加密()地址,提供加密地址将无法收到代币。
mn_addr_preprod1...DUST可用前花费NIGHT——花费NIGHT UTXO会导致关联的DUST立即开始衰减。若在DUST达到可用水平前花费NIGHT,将浪费已投入的生成时间,需等待DUST可用后再花费NIGHT。
交易失败后DUST被锁定——钱包SDK的已知问题。表示DUST被卡住,重启钱包/DApp进程即可释放锁定。
pendingCoins > 0 && availableCoins === 0使用JS 而非表示金额——1 NIGHT = 1,000,000 Stars,实际余额会超过,请始终使用字面量(如)。
numberBigIntNumber.MAX_SAFE_INTEGERBigInt1_000_000n未检查就调用——在Compact中,对Map中不存在的键调用会导致运行时错误,务必先检查。
balances.member(k)balances.lookup(k)lookupmemberUint<128>2^128 - 1(a + b) as Uint<128>assertunshieldedToken().rawunshieldedToken()OZ合约导入路径版本变更——库版本更新后,导入路径从改为,请检查你使用的具体版本的README。
@openzeppelin-compact/contracts@openzeppelin/compact-contracts合约代币不生成DUST——只有NIGHT(链原生账本代币)会生成DUST,自定义不具备费用角色。
FungibleToken