Help us improve
Share bugs, ideas, or general feedback.
From midnight-cq
This skill should be used when the user asks to write ledger tests, test transaction construction, test proof staging, test ZswapLocalState, test DustLocalState, test cost model, test coinCommitment, test ledger-v8, test onchain-runtime, or test well-formedness. Also triggered by requests to write tests for code that uses @midnight-ntwrk/ledger-v8 or @midnight-ntwrk/onchain-runtime directly, test proof staging lifecycle, test token type functions, test crypto fixtures, test serialization round-trips for ledger types, test fee estimation, test SyntheticCost, or test coinNullifier.
npx claudepluginhub devrelaicom/midnight-expert --plugin midnight-cqHow this skill is triggered — by the user, by Claude, or both
Slash command
/midnight-cq:ledger-testingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Write tests for code that uses `@midnight-ntwrk/ledger-v8` and
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
Write tests for code that uses @midnight-ntwrk/ledger-v8 and
@midnight-ntwrk/onchain-runtime directly.
| Question | Skill |
|---|---|
| Am I testing code that calls ledger-v8 or onchain-runtime APIs? | ledger-testing (this skill) |
| Am I testing Compact contract logic? | midnight-cq:compact-testing |
| Am I building a custom wallet variant or capability? | midnight-cq:wallet-testing |
| Am I integrating with the wallet via the DApp Connector API? | midnight-cq:dapp-connector-testing |
| Am I testing DApp UI flows? | midnight-cq:dapp-testing |
You are testing code that uses the ledger packages directly:
midnight-cq:compact-testing)midnight-cq:dapp-testing)midnight-cq:wallet-testing)midnight-cq:dapp-connector-testing)Transaction<S, P, B> has three type parameters controlling what stage the
transaction is in. Tests need to construct transactions at the right stage and
transition them through the pipeline in order.
import {
UnprovenTransaction,
WellFormedStrictness,
} from '@midnight-ntwrk/ledger-v8';
// Stage 1 — Unproven: Transaction<SignatureEnabled, PreProof, PreBinding>
const unproven: UnprovenTransaction = buildUnprovenTransaction();
// Stage 2 — Proved: Transaction<SignatureEnabled, Proof, PreBinding>
const proved = await unproven.prove(provingParams);
// Stage 3 — Bound: Transaction<SignatureEnabled, Proof, FiatShamirPedersen>
const bound = proved.bind(bindingParams);
// Stage 4 — Erased proofs (for storage)
const erased = bound.eraseProofs();
Each stage has different methods available. TypeScript enforces the transitions —
calling bind() on an unproven transaction is a compile error.
See references/transaction-construction-patterns.md for complete lifecycle examples.
Many types (CoinPublicKey, ContractAddress, TokenType, Nullifier, etc.) are hex-encoded strings with specific lengths and encoding rules. Arbitrary strings will fail validation at runtime.
import {
sampleCoinPublicKey,
sampleContractAddress,
sampleRawTokenType,
} from '@midnight-ntwrk/ledger-v8';
// GOOD: Use sample functions for valid test fixtures
const pk = sampleCoinPublicKey(); // Valid 64-char hex
const addr = sampleContractAddress(); // Valid contract address hex
const rawType = sampleRawTokenType(); // Valid raw token type
// BAD: Arbitrary strings will fail validation
const pk = '0xdeadbeef'; // Wrong length, wrong format
const addr = 'test-contract'; // Not a valid hex string
See references/crypto-fixture-patterns.md for all sample* functions and
encode/decode patterns.
ZswapLocalState and DustLocalState return new instances on every mutation.
The original is unchanged. Tests that assert on the original after calling
spend(), apply(), or revertTransaction() will always pass — even if the
operation is broken.
it('should remove coin after spend', () => {
const original = zswapState;
const updated = original.spend(coinInfo);
// GOOD: Assert on the returned state
expect(updated.coins.length).toBe(original.coins.length - 1);
// BAD: Asserting on original — this always passes, proves nothing
// expect(original.coins.length).toBe(original.coins.length);
});
See references/ledger-state-patterns.md for complete state mutation patterns.
DustLocalState.walletBalance() takes a Date parameter. Dust balances
change over time as TTLs expire. Tests that use new Date() or Date.now()
are non-deterministic.
it('should calculate balance at fixed time', () => {
// GOOD: Fixed time for deterministic results
const fixedTime = new Date('2026-01-01T00:00:00Z');
const balance = dustState.walletBalance(fixedTime);
expect(balance).toBeDefined();
expect(balance.available).toBe(expectedAmount);
});
// BAD: Non-deterministic — result changes as real time passes
// const balance = dustState.walletBalance(new Date());
See references/ledger-state-patterns.md for time control patterns and TTL
testing.
SyntheticCost has 5 dimensions: read_time, compute_time, block_usage,
bytes_written, and bytes_churned. Asserting only that cost is defined
proves nothing about the fee structure.
import { CostModel } from '@midnight-ntwrk/ledger-v8';
it('should calculate cost with expected dimensions', () => {
const cost = CostModel.calculate(transaction);
// GOOD: Assert specific dimensions
expect(cost.block_usage).toBeGreaterThan(0n);
expect(cost.bytes_written).toBeGreaterThanOrEqual(0n);
expect(cost.compute_time).toBeGreaterThan(0n);
// BAD: Proves nothing
// expect(cost).toBeDefined();
});
See references/transaction-construction-patterns.md for cost model patterns.
| Anti-Pattern | Why It's Wrong | Fix |
|---|---|---|
| Arbitrary strings for hex types | Fails validation at runtime; wrong length or encoding | Use sample* functions (sampleCoinPublicKey, sampleContractAddress, etc.) |
| Asserting on original state after mutation | Returns always pass; the original is immutable and never changes | Assert on the value returned by spend()/apply()/revertTransaction() |
Using new Date() in Dust balance tests | Non-deterministic; test results change as real time passes | Pass a fixed new Date('...') to walletBalance() |
| Asserting only that cost is non-zero | Proves nothing about SyntheticCost dimensions | Assert specific dimension values (block_usage, bytes_written, etc.) |
| Skipping proof staging transitions | Tests bypass type safety; miss stage-dependent bugs | Test each transition: Unproven → prove() → bind() → eraseProofs() |
| Not testing well-formedness rejection | Only happy-path testing; misses constraint violations | Build invalid transactions and assert wellFormed() returns false |
| Sharing state instances across tests | State bleeds between tests; order-dependent failures | Construct fresh state objects in beforeEach |
| Topic | Reference |
|---|---|
| Proof staging lifecycle, Intent construction, well-formedness, transaction merging, fee calculation | references/transaction-construction-patterns.md |
| ZswapLocalState, DustLocalState, time control, serialization, LedgerState.apply() | references/ledger-state-patterns.md |
| sample* functions, coinCommitment/coinNullifier, token types, encode/decode, signData/verifySignature | references/crypto-fixture-patterns.md |