| name | midnight-compact |
| description | Write Midnight Compact smart contracts with correct syntax and patterns. Use when writing, reviewing, or debugging .compact files, implementing AMM/DEX/token contracts, or asking about Compact language features. Covers type system, ledger ADTs, circuits, witnesses, privacy model, token standards, math workarounds, and known bugs. |
| version | 1.0.0 |
| scope | public |
Midnight Compact Contract Development
Write correct Compact smart contracts for the Midnight blockchain. Compact compiles to ZK circuits (ZKIR) + JS runtime.
Critical: Before Writing Any Contract
REQUIRED — include these steps in every contract-writing response:
- Always call
midnight-mcp tool get-latest-syntax to verify current syntax before generating any Compact code
- Always check
references/known-bugs.md for runtime bugs that affect your design (especially shielded tokens, Map operations)
- Always call
midnight-mcp tool compile-contract to validate the output before claiming success — do not present unvalidated code as correct
Do not skip these steps even for "simple" contracts. The Compact compiler and runtime have non-obvious behaviors and ongoing bugs. If midnight-mcp is unavailable, explicitly state that and flag the output as unvalidated.
Contract Structure
pragma language_version >= 0.21.0;
import CompactStandardLibrary;
import "./path/to/Module" prefix Module_;
// Ledger state
export ledger myField: Uint<64>;
export sealed ledger immutable: Bytes<32>; // set once in constructor
// Constructor
constructor(param: Type) {
myField = disclose(param);
}
// Public circuits (entry points)
export circuit myFunction(): [] { }
// Internal circuits
circuit helper(): [] { }
// Pure circuits (no state)
export pure circuit constant(): Uint<64> { return 42; }
// Witnesses (off-chain, declaration only — NO body)
witness mySecret(): Bytes<32>;
Type System Quick Reference
Primitives
| Type | Notes |
|---|
Uint<N> | Max native: Uint<128>. No Uint<256> |
Field | ZK scalar. Use for intermediate casting |
Boolean | true / false |
Bytes<N> | Fixed-size. Bytes<32> for hashes/keys |
Opaque<"string"> | Token names, metadata |
Compound Types
| Type | Usage |
|---|
Either<A, B> | Sum type. left<A,B>(v) / right<A,B>(v). Check .is_left |
Maybe<T> | Optional. some<T>(v) / none<T>(). Check .is_some, access .value |
Vector<N, T> | Fixed-size array. Constant index only (v[0] OK, v[i] NOT OK) |
Counter | .increment(n), .decrement(n), .read(), .lessThan(n) |
Map<K, V> | .insert(k,v), .lookup(k), .member(k), .remove(k), .size() |
Set<T> | .insert(v), .member(v), .remove(v), .size() |
Type Casting Chain
Uint<64> -> Field (always safe: `x as Field`)
Field -> Uint<64> (checked: `f as Uint<64>`, can fail)
Field -> Bytes<32> (conversion: `f as Bytes<32>`)
Uint<64> -> Bytes<32> (TWO casts: `(x as Field) as Bytes<32>`)
Boolean -> Uint<0..1> (conversion: `b as Uint<0..1>`)
Compact -> TypeScript Mapping
| Compact | TypeScript |
|---|
Uint<N> / Counter | bigint |
Bytes<32> | Uint8Array |
Boolean | boolean |
Opaque<"string"> | string |
Either<A, B> | { is_left: boolean, left: A, right: B } |
Ledger Modifiers
| Modifier | Meaning |
|---|
export ledger | Publicly queryable on-chain |
sealed ledger | Set only in constructor |
export sealed ledger | Public + immutable |
ledger (no export) | Internal only |
Privacy Model
disclose() Rule
Values from witnesses written to ledger MUST use disclose():
owner = disclose(publicKey(localSecretKey()));
tokenAPrice = disclose(newPrice);
_balances.insert(disclose(account), disclose(newBalance));
Compiler enforces this. disclose() is a compile-time annotation (no-op at runtime).
Conditionals on Witness Values
// CORRECT
if (disclose(guess == secret)) { ... }
// WRONG — implicit disclosure error
if (guess == secret) { ... }
Common Patterns
Identity (Anti-Linkability)
circuit publicKey(sk: Bytes<32>, round: Bytes<32>): Bytes<32> {
return persistentHash<Vector<3, Bytes<32>>>([pad(32, "myapp:pk:"), round, sk]);
}
Witness: Compute Off-Chain, Verify On-Chain
witness wit_div(a: Uint<64>, b: Uint<64>): DivResult;
circuit div(a: Uint<64>, b: Uint<64>): DivResult {
assert(b != 0, "division by zero");
const result = wit_div(a, b);
assert((result.quotient * b + result.remainder) as Uint<64> == a, "invalid");
return result;
}
AMM Math: Cross-Multiplication (Avoids Division Entirely)
// Instead of: fee/input >= feeBps/10000
assert(fee * 10000 >= input * feeBps, "fee too low");
// Instead of: lpOut/totalLP <= xIn/xReserves
assert(lpOut * xReserves <= xIn * lpTotal, "ratio exceeded");
// Use Uint<248> for intermediate products
circuit calcK(x: Uint<128>, y: Uint<128>): Uint<248> {
return ((x as Uint<124>) * (y as Uint<124>)) as Uint<248>;
}
Multiplication Overflow
// Uint<64> * Uint<64> overflows. Use Field:
pure circuit mul(a: Uint<64>, b: Uint<64>): Uint<64> {
return ((a as Field) * (b as Field)) as Uint<64>;
}
Nested Map Init
if (!_allowances.member(disclose(owner))) {
_allowances.insert(disclose(owner), default<Map<...>>);
}
_allowances.lookup(owner).insert(disclose(spender), disclose(value));
Token Patterns
Unshielded (ERC-20 Style) — RECOMMENDED
export ledger _balances: Map<Either<ZswapCoinPublicKey, ContractAddress>, Uint<128>>;
export ledger _totalSupply: Uint<128>;
circuit _mint(account: Either<...>, value: Uint<128>): [] {
const MAX = 340282366920938463463374607431768211455;
assert(MAX - _totalSupply >= value, "overflow");
// ...
}
Shielded (UTXO) — CURRENTLY BROKEN
See references/known-bugs.md. Use for LP tokens only when runtime is fixed.
_counter.increment(1);
const nonce = evolveNonce(_counter, _nonce);
_nonce = nonce;
return mintShieldedToken(domain, disclose(amount), nonce, disclose(recipient));
LP Tokens as Native Coins
circuit getLPTokenColor(): Bytes<32> {
return tokenType(pad(32, "LP_TOKEN"), kernel.self());
}
// LP color is deterministic per contract address
Constraints (What You Can't Do)
| Constraint | Workaround |
|---|
No Uint<256> | struct U128 { low: Uint<64>, high: Uint<64> } |
| No division in circuits | Witness + verify pattern, or cross-multiplication |
| No dynamic loops | Unroll fixed iterations |
| No variable array index | Map<Uint<64>, T> instead |
| No contract-to-contract calls | Module imports (compile-time composition) |
| No events | Poll indexer contractActions subscription |
| No recursion | Flatten data structures |
Common Mistakes
| Wrong | Right |
|---|
ledger { field: Type; } | export ledger field: Type; |
circuit fn(): Void | circuit fn(): [] |
pragma >= 0.21.0 | pragma language_version >= 0.21.0; |
counter.value() | counter.read() |
Choice::rock | Choice.rock |
witness fn(): T { ... } | witness fn(): T; (no body) |
pure function helper() | pure circuit helper() |
For Detailed Reference
references/known-bugs.md — Runtime bugs, version compatibility, workarounds
references/amm-patterns.md — Pulse Finance AMM, LunarSwap math, OpenZeppelin patterns