name: compact-core:compact-tokens description: This skill should be used when the user asks about Midnight tokens, token types (NIGHT, DUST, shielded, unshielded), minting and burning tokens, token transfers, token colors and domain separators, the zswap protocol, ShieldedCoinInfo, QualifiedShieldedCoinInfo, Kernel mint operations, contract token patterns (FungibleToken, NonFungibleToken, MultiToken), the account model vs UTXO model for tokens, sendShielded, receiveShielded, sendUnshielded, mintShieldedToken, mintUnshieldedToken, unshieldedBalance, OpenZeppelin Compact token contracts, or choosing between shielded and unshielded token approaches. version: 0.1.0
Compact Tokens
This skill covers tokens on Midnight: choosing between shielded and unshielded approaches, using the standard library mint/send/receive functions, understanding token colors and domain separators, and the NIGHT/DUST token model. It does not cover ledger ADT types or state design -- those belong in compact-ledger. It does not cover overall contract anatomy or circuit/witness design -- those belong in compact-structure.
Token Decision Tree
| Need | Approach | Key Functions |
|---|---|---|
| Private balances/transfers | Shielded ledger tokens (zswap UTXO) | mintShieldedToken, sendShielded, receiveShielded |
| Transparent balances/transfers | Unshielded ledger tokens | mintUnshieldedToken, sendUnshielded, unshieldedBalance |
| Programmable fungible token (ERC-20 style) | Contract token with Map state |
OpenZeppelin FungibleToken pattern |
| NFTs / multi-token collections | Contract token with ownership Maps |
OpenZeppelin NonFungibleToken / MultiToken |
| Gas fees | DUST (generated from NIGHT) | Not contract-programmable |
Token Types Quick Reference
| Type | Location | Privacy | Model | Key Traits |
|---|---|---|---|---|
| Shielded ledger | Blockchain ledger | Private | UTXO | Native privacy, maximum efficiency, hidden sender/recipient/value |
| Unshielded ledger | Blockchain ledger | Transparent | UTXO | Full transparency, high performance, visible balances |
| Shielded contract | Contract state | Private | Account (via Map) |
Private balances via ZK proofs, but no post-issuance spend enforcement — contract cannot freeze, pause, or claw back coins once received (see Known Limitations in references/token-patterns.md). OpenZeppelin ShieldedERC20 is archived; use unshielded contract tokens for custom logic. |
| Unshielded contract | Contract state | Transparent | Account (via Map) |
Full programmability, visible operations |
Shielded Token Operations
Key types:
| Type | Fields | Purpose |
|---|---|---|
ShieldedCoinInfo |
nonce: Bytes<32>, color: Bytes<32>, value: Uint<128> |
Newly created coin (this transaction) |
QualifiedShieldedCoinInfo |
nonce, color, value, mt_index: Uint<64> |
Existing coin on ledger, ready to spend |
ShieldedSendResult |
change: Maybe<ShieldedCoinInfo>, sent: ShieldedCoinInfo |
Result of send operations |
ZswapCoinPublicKey |
bytes: Bytes<32> |
User public key for coin output |
Key functions:
| Function | Signature | Returns |
|---|---|---|
mintShieldedToken |
(domainSep: Bytes<32>, value: Uint<64>, nonce: Bytes<32>, recipient: Either<ZswapCoinPublicKey, ContractAddress>) |
ShieldedCoinInfo |
receiveShielded |
(coin: ShieldedCoinInfo) |
[] |
sendShielded |
(input: QualifiedShieldedCoinInfo, recipient: Either<ZswapCoinPublicKey, ContractAddress>, value: Uint<128>) |
ShieldedSendResult |
sendImmediateShielded |
(input: ShieldedCoinInfo, target: Either<ZswapCoinPublicKey, ContractAddress>, value: Uint<128>) |
ShieldedSendResult |
mergeCoin |
(a: QualifiedShieldedCoinInfo, b: QualifiedShieldedCoinInfo) |
ShieldedCoinInfo |
mergeCoinImmediate |
(a: QualifiedShieldedCoinInfo, b: ShieldedCoinInfo) |
ShieldedCoinInfo |
evolveNonce |
(index: Uint<128>, nonce: Bytes<32>) |
Bytes<32> |
shieldedBurnAddress |
() |
Either<ZswapCoinPublicKey, ContractAddress> |
ownPublicKey |
() |
ZswapCoinPublicKey |
export circuit mint(amount: Uint<64>): ShieldedCoinInfo {
counter.increment(1);
const newNonce = evolveNonce(counter.read() as Uint<128>, nonce);
nonce = newNonce;
return mintShieldedToken(
domain, disclose(amount), nonce,
left<ZswapCoinPublicKey, ContractAddress>(ownPublicKey())
);
}
export circuit send(coin: ShieldedCoinInfo, to: ZswapCoinPublicKey, amount: Uint<128>): ShieldedSendResult {
receiveShielded(disclose(coin));
return sendImmediateShielded(disclose(coin), left<ZswapCoinPublicKey, ContractAddress>(disclose(to)), disclose(amount));
}
Unshielded Token Operations
| Function | Signature | Returns |
|---|---|---|
mintUnshieldedToken |
(domainSep: Bytes<32>, value: Uint<64>, recipient: Either<ContractAddress, UserAddress>) |
Bytes<32> (color) |
sendUnshielded |
(color: Bytes<32>, amount: Uint<128>, recipient: Either<ContractAddress, UserAddress>) |
[] |
receiveUnshielded |
(color: Bytes<32>, amount: Uint<128>) |
[] |
unshieldedBalance |
(color: Bytes<32>) |
Uint<128> |
unshieldedBalanceLt/Gte/Gt/Lte |
(color: Bytes<32>, amount: Uint<128>) |
Boolean |
Caveat:
unshieldedBalance()returns the balance at transaction construction time, not at application time. If the balance changes between construction and application, the transaction fails. Prefer the comparison functions (unshieldedBalanceLt,unshieldedBalanceGte, etc.) unless you specifically need an exact-match constraint.
export circuit mintToSelf(domainSep: Bytes<32>, amount: Uint<64>): Bytes<32> {
const color = mintUnshieldedToken(
disclose(domainSep), disclose(amount),
left<ContractAddress, UserAddress>(kernel.self())
);
receiveUnshielded(color, disclose(amount) as Uint<128>);
return color;
}
Token Colors & Identification
A token color (type) is a Bytes<32> value derived from the contract address and a domain separator:
| Function | Signature | Purpose |
|---|---|---|
tokenType |
(domainSep: Bytes<32>, contract: ContractAddress): Bytes<32> |
Compute color for another contract's token |
nativeToken |
(): Bytes<32> |
Returns the zero value (NIGHT token color) |
Colors are deterministic: the same (domainSep, contractAddress) pair always yields the same color. A contract can issue multiple token types by using different domain separators. The color field in ShieldedCoinInfo identifies which token a coin represents.
NIGHT & DUST
NIGHT is Midnight's native utility token. It exists as UTXOs on the ledger with the zero token color (nativeToken()). NIGHT is used for staking, governance, and generating DUST.
DUST is a shielded network resource, not a token. It is generated from NIGHT over time when NIGHT UTXOs are registered for dust generation. Key properties:
- Non-transferable: cannot be sent to other users
- Used exclusively for transaction fees
- Proportional to NIGHT balance; decays when disconnected from NIGHT
- Provides operational predictability: no volatile gas prices
On testnet, these are called tNIGHT and tDUST. Contracts cannot mint, send, or manipulate NIGHT or DUST directly through standard library functions.
Common Mistakes
| Wrong | Correct | Why |
|---|---|---|
kernel.mintShielded(dom, amt) |
mintShieldedToken(dom, amt, nonce, recipient) |
kernel.mintShielded is the low-level Kernel op; use the standard library wrapper which also returns ShieldedCoinInfo |
unshieldedBalance(color) for conditional logic |
unshieldedBalanceGte(color, amount) |
unshieldedBalance locks the exact balance at construction time; comparison functions are more robust |
Omitting disclose() on token params |
mintShieldedToken(disclose(dom), ...) |
Witness-derived values passed to token functions must be disclosed |
Sending shielded to ContractAddress without receiveShielded |
Call receiveShielded(coin) in the receiving contract |
The receiving contract must explicitly accept the coin |
Uint<64> for shielded amounts in send/receive |
Uint<128> |
ShieldedCoinInfo.value, sendShielded, and sendImmediateShielded use Uint<128> |
Uint<128> for mintShieldedToken value |
Uint<64> |
mintShieldedToken accepts Uint<64> for the value parameter, not Uint<128> |
| Minting unshielded to self without receiving | Call receiveUnshielded(color, amount) after mintUnshieldedToken |
The contract must receive its own minted unshielded tokens to update its balance |
Reference Routing
| Topic | Reference File |
|---|---|
| Token architecture, shielded vs unshielded deep dive, UTXO vs account model | references/token-architecture.md |
| Complete function signatures, detailed parameters, nonce management, merge strategies | references/token-operations.md |
| OpenZeppelin FungibleToken, NonFungibleToken, MultiToken patterns and examples | references/token-patterns.md |
| Example | File |
|---|---|
| ERC-20 style fungible token (non-compilable — requires OpenZeppelin compact-contracts) | examples/FungibleToken.compact |
| Non-fungible token with ownership tracking (non-compilable — requires OpenZeppelin compact-contracts) | examples/NonFungibleToken.compact |
| Multi-token collection with mint/burn per ID (non-compilable — requires OpenZeppelin compact-contracts) | examples/MultiToken.compact |
| Shielded fungible token using zswap coin infrastructure (non-compilable — requires OpenZeppelin midnight-apps) | examples/ShieldedFungibleToken.compact |