=> {
+ const updated = { ...(await this.getPrivateState()), ...partial };
+ this.setPrivateState(updated);
+ return updated;
+ },
+ };
+
+ public setInputNote(note: Note) {
+ return this.privateState.set({ inputNote: note });
+ }
+}
diff --git a/contracts/src/token/test/witnesses/ConfidentialNoteTokenAuditWitnesses.ts b/contracts/src/token/test/witnesses/ConfidentialNoteTokenAuditWitnesses.ts
new file mode 100644
index 000000000..6855f518c
--- /dev/null
+++ b/contracts/src/token/test/witnesses/ConfidentialNoteTokenAuditWitnesses.ts
@@ -0,0 +1,37 @@
+// TEST-ONLY WITNESS. NOT FOR PRODUCTION USE.
+// Drives ConfidentialNoteTokenAudit (auditor viewing) circuits in off-chain
+// tests.
+
+import { getRandomValues } from 'node:crypto';
+import type { WitnessContext } from '@midnight-ntwrk/compact-runtime';
+import type { Ledger } from '../../../../artifacts/MockConfidentialNoteTokenAudit/contract/index.js';
+
+export type ConfidentialNoteTokenAuditPrivateState = {
+ /**
+ * Optional fixed randomness seed. Leave undefined for the production-correct
+ * behavior (a fresh secret seed per witness call); set it only in tests that
+ * need deterministic ephemerals.
+ */
+ randomnessSeed?: Uint8Array;
+};
+
+export const ConfidentialNoteTokenAuditPrivateState = {
+ generate: (): ConfidentialNoteTokenAuditPrivateState => ({}),
+};
+
+export interface IConfidentialNoteTokenAuditWitnesses {
+ wit_AuditRandomness(context: WitnessContext): [P, Uint8Array];
+}
+
+export const ConfidentialNoteTokenAuditWitnesses =
+ (): IConfidentialNoteTokenAuditWitnesses => ({
+ // Fresh + secret per call, as the extension requires; a fixed seed is only
+ // honored when a test explicitly plants one.
+ wit_AuditRandomness(context) {
+ return [
+ context.privateState,
+ context.privateState.randomnessSeed ??
+ new Uint8Array(getRandomValues(Buffer.alloc(32))),
+ ];
+ },
+ });
diff --git a/contracts/src/token/test/witnesses/ConfidentialNoteTokenDeliveryWitnesses.ts b/contracts/src/token/test/witnesses/ConfidentialNoteTokenDeliveryWitnesses.ts
new file mode 100644
index 000000000..59e24fb5a
--- /dev/null
+++ b/contracts/src/token/test/witnesses/ConfidentialNoteTokenDeliveryWitnesses.ts
@@ -0,0 +1,36 @@
+// TEST-ONLY WITNESS. NOT FOR PRODUCTION USE.
+// Drives ConfidentialNoteTokenDelivery (note delivery) circuits in off-chain
+// tests.
+
+import { getRandomValues } from 'node:crypto';
+import type { WitnessContext } from '@midnight-ntwrk/compact-runtime';
+import type { Ledger } from '../../../../artifacts/MockConfidentialNoteTokenDelivery/contract/index.js';
+
+export type ConfidentialNoteTokenDeliveryPrivateState = {
+ /**
+ * Optional fixed randomness seed. Leave undefined for the production-correct
+ * behavior (a fresh secret seed per witness call).
+ */
+ randomnessSeed?: Uint8Array;
+};
+
+export const ConfidentialNoteTokenDeliveryPrivateState = {
+ generate: (): ConfidentialNoteTokenDeliveryPrivateState => ({}),
+};
+
+export interface IConfidentialNoteTokenDeliveryWitnesses {
+ wit_DeliveryRandomness(context: WitnessContext): [P, Uint8Array];
+}
+
+export const ConfidentialNoteTokenDeliveryWitnesses =
+ (): IConfidentialNoteTokenDeliveryWitnesses => ({
+ // Fresh + secret per call, as the extension requires; a fixed seed is only
+ // honored when a test explicitly plants one.
+ wit_DeliveryRandomness(context) {
+ return [
+ context.privateState,
+ context.privateState.randomnessSeed ??
+ new Uint8Array(getRandomValues(Buffer.alloc(32))),
+ ];
+ },
+ });
diff --git a/contracts/src/token/test/witnesses/ConfidentialNoteTokenSupplyWitnesses.ts b/contracts/src/token/test/witnesses/ConfidentialNoteTokenSupplyWitnesses.ts
new file mode 100644
index 000000000..2933c60c8
--- /dev/null
+++ b/contracts/src/token/test/witnesses/ConfidentialNoteTokenSupplyWitnesses.ts
@@ -0,0 +1,44 @@
+// TEST-ONLY WITNESS. NOT FOR PRODUCTION USE.
+// Drives ConfidentialNoteTokenSupply (confidential supply) circuits in
+// off-chain tests.
+
+import { getRandomValues } from 'node:crypto';
+import type { WitnessContext } from '@midnight-ntwrk/compact-runtime';
+import type { Ledger } from '../../../../artifacts/MockConfidentialNoteTokenSupply/contract/index.js';
+
+export type ConfidentialNoteTokenSupplyPrivateState = {
+ /** Supply-key secret (supplyKey = derivePk(secret)); consumed by attestSupply. */
+ supplyKeySecret: Uint8Array;
+ /**
+ * Optional fixed randomness seed. Leave undefined for the production-correct
+ * behavior (a fresh secret seed per witness call).
+ */
+ randomnessSeed?: Uint8Array;
+};
+
+export const ConfidentialNoteTokenSupplyPrivateState = {
+ generate: (): ConfidentialNoteTokenSupplyPrivateState => ({
+ supplyKeySecret: new Uint8Array(getRandomValues(Buffer.alloc(32))),
+ }),
+};
+
+export interface IConfidentialNoteTokenSupplyWitnesses {
+ wit_SupplyRandomness(context: WitnessContext): [P, Uint8Array];
+ wit_SupplyKeySecret(context: WitnessContext): [P, Uint8Array];
+}
+
+export const ConfidentialNoteTokenSupplyWitnesses =
+ (): IConfidentialNoteTokenSupplyWitnesses => ({
+ // Fresh + secret per call, as the extension requires; a fixed seed is only
+ // honored when a test explicitly plants one.
+ wit_SupplyRandomness(context) {
+ return [
+ context.privateState,
+ context.privateState.randomnessSeed ??
+ new Uint8Array(getRandomValues(Buffer.alloc(32))),
+ ];
+ },
+ wit_SupplyKeySecret(context) {
+ return [context.privateState, context.privateState.supplyKeySecret];
+ },
+ });
diff --git a/contracts/src/token/test/witnesses/ConfidentialNoteTokenWitnesses.ts b/contracts/src/token/test/witnesses/ConfidentialNoteTokenWitnesses.ts
new file mode 100644
index 000000000..656a9ac25
--- /dev/null
+++ b/contracts/src/token/test/witnesses/ConfidentialNoteTokenWitnesses.ts
@@ -0,0 +1,76 @@
+// TEST-ONLY WITNESS. NOT FOR PRODUCTION USE.
+// Drives ConfidentialNoteToken (core) circuits in off-chain tests.
+
+import { getRandomValues } from 'node:crypto';
+import type {
+ MerkleTreePath,
+ WitnessContext,
+} from '@midnight-ntwrk/compact-runtime';
+import type { Ledger } from '../../../../artifacts/MockConfidentialNoteToken/contract/index.js';
+
+/** A note as the circuits see it: value + field-typed nonce. */
+export type Note = { value: bigint; nonce: bigint };
+
+export type ConfidentialNoteTokenPrivateState = {
+ /** Owner spend secret; pk = Hf(sk). */
+ secretKey: Uint8Array;
+ /** Issuer secret (issuerPk = Hf(issuerSecret)). */
+ issuerSecret: Uint8Array;
+ /** The input note being spent in a transfer/burn (or consume target). */
+ inputNote: Note;
+ /**
+ * Optional fixed nonce-randomness seed. Leave undefined for the
+ * production-correct behavior (a fresh secret seed per witness call).
+ */
+ nonceSeed?: Uint8Array;
+};
+
+export const ConfidentialNoteTokenPrivateState = {
+ generate: (): ConfidentialNoteTokenPrivateState => ({
+ secretKey: new Uint8Array(getRandomValues(Buffer.alloc(32))),
+ issuerSecret: new Uint8Array(getRandomValues(Buffer.alloc(32))),
+ inputNote: { value: 0n, nonce: 0n },
+ }),
+};
+
+export interface IConfidentialNoteTokenWitnesses {
+ wit_SecretKey(context: WitnessContext): [P, Uint8Array];
+ wit_IssuerSecret(context: WitnessContext): [P, Uint8Array];
+ wit_InputNote(context: WitnessContext): [P, Note];
+ wit_Path(
+ context: WitnessContext,
+ cm: Uint8Array,
+ ): [P, MerkleTreePath];
+ wit_NonceRandomness(context: WitnessContext): [P, Uint8Array];
+}
+
+export const ConfidentialNoteTokenWitnesses =
+ (): IConfidentialNoteTokenWitnesses => ({
+ wit_SecretKey(context) {
+ return [context.privateState, context.privateState.secretKey];
+ },
+ wit_IssuerSecret(context) {
+ return [context.privateState, context.privateState.issuerSecret];
+ },
+ wit_InputNote(context) {
+ return [context.privateState, context.privateState.inputNote];
+ },
+ // The circuit passes the input commitment; we return its Merkle path by
+ // reading the live commitment tree from the ledger.
+ wit_Path(context, cm) {
+ const path = context.ledger.CNT__commitments.findPathForLeaf(cm);
+ if (path === undefined) {
+ throw new Error('wit_Path: commitment not found in tree');
+ }
+ return [context.privateState, path];
+ },
+ // Fresh + secret per call, as the module requires; a fixed seed is only
+ // honored when a test explicitly plants one.
+ wit_NonceRandomness(context) {
+ return [
+ context.privateState,
+ context.privateState.nonceSeed ??
+ new Uint8Array(getRandomValues(Buffer.alloc(32))),
+ ];
+ },
+ });
diff --git a/contracts/src/token/test/witnesses/RegulatedConfidentialNoteTokenWitnesses.ts b/contracts/src/token/test/witnesses/RegulatedConfidentialNoteTokenWitnesses.ts
new file mode 100644
index 000000000..deafbd8cf
--- /dev/null
+++ b/contracts/src/token/test/witnesses/RegulatedConfidentialNoteTokenWitnesses.ts
@@ -0,0 +1,93 @@
+// TEST-ONLY WITNESS. NOT FOR PRODUCTION USE.
+// Drives the RegulatedConfidentialNoteToken preset contract in off-chain
+// tests: the union of the composed modules' witnesses.
+
+import { getRandomValues } from 'node:crypto';
+import type {
+ MerkleTreePath,
+ WitnessContext,
+} from '@midnight-ntwrk/compact-runtime';
+import type { Ledger } from '../../../../artifacts/RegulatedConfidentialNoteToken/contract/index.js';
+
+/** A note as the circuits see it: value + field-typed nonce. */
+export type Note = { value: bigint; nonce: bigint };
+
+export type RegulatedConfidentialNoteTokenPrivateState = {
+ /** Owner spend secret; pk = Hf(sk). */
+ secretKey: Uint8Array;
+ /** Issuer secret (issuerPk = Hf(issuerSecret)). */
+ issuerSecret: Uint8Array;
+ /** Seizure authority secret (authorityPk = Hf(authoritySecret)). */
+ authoritySecret: Uint8Array;
+ /** Supply-key secret (supplyKey = derivePk(secret)); consumed by attestSupply. */
+ supplyKeySecret: Uint8Array;
+ /** The input note being spent in a transfer/burn (or seize target). */
+ inputNote: Note;
+};
+
+export const RegulatedConfidentialNoteTokenPrivateState = {
+ generate: (): RegulatedConfidentialNoteTokenPrivateState => ({
+ secretKey: new Uint8Array(getRandomValues(Buffer.alloc(32))),
+ issuerSecret: new Uint8Array(getRandomValues(Buffer.alloc(32))),
+ authoritySecret: new Uint8Array(getRandomValues(Buffer.alloc(32))),
+ supplyKeySecret: new Uint8Array(getRandomValues(Buffer.alloc(32))),
+ inputNote: { value: 0n, nonce: 0n },
+ }),
+};
+
+const freshSeed = (): Uint8Array =>
+ new Uint8Array(getRandomValues(Buffer.alloc(32)));
+
+export interface IRegulatedConfidentialNoteTokenWitnesses {
+ wit_SecretKey(context: WitnessContext): [P, Uint8Array];
+ wit_IssuerSecret(context: WitnessContext): [P, Uint8Array];
+ wit_AuthoritySecret(context: WitnessContext): [P, Uint8Array];
+ wit_SupplyKeySecret(context: WitnessContext): [P, Uint8Array];
+ wit_InputNote(context: WitnessContext): [P, Note];
+ wit_Path(
+ context: WitnessContext,
+ cm: Uint8Array,
+ ): [P, MerkleTreePath];
+ wit_AuditRandomness(context: WitnessContext): [P, Uint8Array];
+ wit_DeliveryRandomness(context: WitnessContext): [P, Uint8Array];
+ wit_SupplyRandomness(context: WitnessContext): [P, Uint8Array];
+}
+
+export const RegulatedConfidentialNoteTokenWitnesses =
+ (): IRegulatedConfidentialNoteTokenWitnesses => ({
+ wit_SecretKey(context) {
+ return [context.privateState, context.privateState.secretKey];
+ },
+ wit_IssuerSecret(context) {
+ return [context.privateState, context.privateState.issuerSecret];
+ },
+ wit_AuthoritySecret(context) {
+ return [context.privateState, context.privateState.authoritySecret];
+ },
+ wit_SupplyKeySecret(context) {
+ return [context.privateState, context.privateState.supplyKeySecret];
+ },
+ wit_InputNote(context) {
+ return [context.privateState, context.privateState.inputNote];
+ },
+ // The circuit passes the input commitment; we return its Merkle path by
+ // reading the live commitment tree from the ledger.
+ wit_Path(context, cm) {
+ const path = context.ledger._commitments.findPathForLeaf(cm);
+ if (path === undefined) {
+ throw new Error('wit_Path: commitment not found in tree');
+ }
+ return [context.privateState, path];
+ },
+ // All randomness seeds are fresh + secret per call, as the modules
+ // require.
+ wit_AuditRandomness(context) {
+ return [context.privateState, freshSeed()];
+ },
+ wit_DeliveryRandomness(context) {
+ return [context.privateState, freshSeed()];
+ },
+ wit_SupplyRandomness(context) {
+ return [context.privateState, freshSeed()];
+ },
+ });
From b3a9a5f1488aa1330f0f62ac1f5dd5eaf5730058 Mon Sep 17 00:00:00 2001
From: 0xisk <0xisk@proton.me>
Date: Thu, 23 Jul 2026 17:04:54 +0200
Subject: [PATCH 2/4] feat(token): rework confidential note token family
* rename the family: ConfidentialNoteToken becomes
ConfidentialNoteFungibleToken (asset class in the name, keeping the
FungibleToken suffix family); the supply extension becomes
ConfidentialNoteFungibleTokenPrivateSupply
* split the issuer role out of the core into
extensions/ConfidentialNoteFungibleTokenIssuer; the core is now
role-free and initialization-free (pure note machinery) with two
ungated value-creation blocks: _mint (core-derived fresh nonce) and
_mintNote (policy-built note, e.g. audit-derived nonces)
* convert presets/RegulatedConfidentialNoteFungibleToken from a
top-level contract into a ready-to-use module: a one-shot initialize
replaces the constructor, and the composed modules' observable
ledgers and artifact types re-export under stable bare names; the
new MockRegulatedConfidentialNoteFungibleToken carries the
deployable shape for the simulators
* drop the CNT_ import prefix (Core_ for the core internally, Token_
for the preset in consumers)
* add compliance extensions: Freeze (freeze-before-seize via a frozen
nullifier set), Allowlist (ZK Merkle-membership KYC with tombstone
revocation), Review (selective disclosure to approved reviewer
keys), plus self-rotation blocks for the issuer/audit/supply keys
and rotateAuthority in the preset
* mocks, simulators, and witness harnesses updated accordingly;
verified via direct compact compile (skip-zk plus full keygen for
circuit tags), tsc, and biome
NOT audited, NOT production.
---
CHANGELOG.md | 4 +-
... => ConfidentialNoteFungibleToken.compact} | 140 ++---
...identialNoteFungibleTokenAllowlist.compact | 100 ++++
...onfidentialNoteFungibleTokenAudit.compact} | 47 +-
...identialNoteFungibleTokenDelivery.compact} | 10 +-
...onfidentialNoteFungibleTokenFreeze.compact | 79 +++
...onfidentialNoteFungibleTokenIssuer.compact | 99 ++++
...ialNoteFungibleTokenPrivateSupply.compact} | 42 +-
...onfidentialNoteFungibleTokenReview.compact | 170 ++++++
...latedConfidentialNoteFungibleToken.compact | 333 +++++++++++
.../RegulatedConfidentialNoteToken.compact | 249 --------
.../src/token/test/FungibleToken.test.ts | 385 ++++++------
contracts/src/token/test/MultiToken.test.ts | 547 +++++++++---------
.../token/test/NativeShieldedToken.test.ts | 15 +-
.../test/NativeShieldedTokenCore.test.ts | 15 +-
.../test/NativeShieldedTokenFamily.test.ts | 15 +-
.../MockConfidentialNoteFungibleToken.compact | 66 +++
...identialNoteFungibleTokenAllowlist.compact | 26 +
...onfidentialNoteFungibleTokenAudit.compact} | 6 +-
...identialNoteFungibleTokenDelivery.compact} | 2 +-
...onfidentialNoteFungibleTokenFreeze.compact | 22 +
...onfidentialNoteFungibleTokenIssuer.compact | 22 +
...ialNoteFungibleTokenPrivateSupply.compact} | 6 +-
...onfidentialNoteFungibleTokenReview.compact | 38 ++
.../mocks/MockConfidentialNoteToken.compact | 74 ---
...latedConfidentialNoteFungibleToken.compact | 98 ++++
...tialNoteFungibleTokenAllowlistSimulator.ts | 58 ++
...identialNoteFungibleTokenAuditSimulator.ts | 71 +++
...ntialNoteFungibleTokenDeliverySimulator.ts | 56 ++
...dentialNoteFungibleTokenFreezeSimulator.ts | 59 ++
...dentialNoteFungibleTokenIssuerSimulator.ts | 67 +++
...NoteFungibleTokenPrivateSupplySimulator.ts | 77 +++
...dentialNoteFungibleTokenReviewSimulator.ts | 79 +++
.../ConfidentialNoteFungibleTokenSimulator.ts | 72 +++
.../ConfidentialNoteTokenAuditSimulator.ts | 67 ---
.../ConfidentialNoteTokenDeliverySimulator.ts | 54 --
.../ConfidentialNoteTokenSimulator.ts | 73 ---
.../ConfidentialNoteTokenSupplySimulator.ts | 71 ---
...ConfidentialNoteFungibleTokenSimulator.ts} | 62 +-
...tialNoteFungibleTokenAllowlistWitnesses.ts | 38 ++
...identialNoteFungibleTokenAuditWitnesses.ts | 47 ++
...tialNoteFungibleTokenDeliveryWitnesses.ts} | 16 +-
...dentialNoteFungibleTokenIssuerWitnesses.ts | 29 +
...oteFungibleTokenPrivateSupplyWitnesses.ts} | 16 +-
...entialNoteFungibleTokenReviewWitnesses.ts} | 22 +-
...ConfidentialNoteFungibleTokenWitnesses.ts} | 25 +-
...ConfidentialNoteFungibleTokenWitnesses.ts} | 29 +-
47 files changed, 2446 insertions(+), 1252 deletions(-)
rename contracts/src/token/{ConfidentialNoteToken.compact => ConfidentialNoteFungibleToken.compact} (73%)
create mode 100644 contracts/src/token/extensions/ConfidentialNoteFungibleTokenAllowlist.compact
rename contracts/src/token/extensions/{ConfidentialNoteTokenAudit.compact => ConfidentialNoteFungibleTokenAudit.compact} (74%)
rename contracts/src/token/extensions/{ConfidentialNoteTokenDelivery.compact => ConfidentialNoteFungibleTokenDelivery.compact} (89%)
create mode 100644 contracts/src/token/extensions/ConfidentialNoteFungibleTokenFreeze.compact
create mode 100644 contracts/src/token/extensions/ConfidentialNoteFungibleTokenIssuer.compact
rename contracts/src/token/extensions/{ConfidentialNoteTokenSupply.compact => ConfidentialNoteFungibleTokenPrivateSupply.compact} (78%)
create mode 100644 contracts/src/token/extensions/ConfidentialNoteFungibleTokenReview.compact
create mode 100644 contracts/src/token/presets/RegulatedConfidentialNoteFungibleToken.compact
delete mode 100644 contracts/src/token/presets/RegulatedConfidentialNoteToken.compact
create mode 100644 contracts/src/token/test/mocks/MockConfidentialNoteFungibleToken.compact
create mode 100644 contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenAllowlist.compact
rename contracts/src/token/test/mocks/{MockConfidentialNoteTokenAudit.compact => MockConfidentialNoteFungibleTokenAudit.compact} (82%)
rename contracts/src/token/test/mocks/{MockConfidentialNoteTokenDelivery.compact => MockConfidentialNoteFungibleTokenDelivery.compact} (88%)
create mode 100644 contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenFreeze.compact
create mode 100644 contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenIssuer.compact
rename contracts/src/token/test/mocks/{MockConfidentialNoteTokenSupply.compact => MockConfidentialNoteFungibleTokenPrivateSupply.compact} (81%)
create mode 100644 contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenReview.compact
delete mode 100644 contracts/src/token/test/mocks/MockConfidentialNoteToken.compact
create mode 100644 contracts/src/token/test/mocks/MockRegulatedConfidentialNoteFungibleToken.compact
create mode 100644 contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenAllowlistSimulator.ts
create mode 100644 contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenAuditSimulator.ts
create mode 100644 contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenDeliverySimulator.ts
create mode 100644 contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenFreezeSimulator.ts
create mode 100644 contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenIssuerSimulator.ts
create mode 100644 contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenPrivateSupplySimulator.ts
create mode 100644 contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenReviewSimulator.ts
create mode 100644 contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenSimulator.ts
delete mode 100644 contracts/src/token/test/simulators/ConfidentialNoteTokenAuditSimulator.ts
delete mode 100644 contracts/src/token/test/simulators/ConfidentialNoteTokenDeliverySimulator.ts
delete mode 100644 contracts/src/token/test/simulators/ConfidentialNoteTokenSimulator.ts
delete mode 100644 contracts/src/token/test/simulators/ConfidentialNoteTokenSupplySimulator.ts
rename contracts/src/token/test/simulators/{RegulatedConfidentialNoteTokenSimulator.ts => RegulatedConfidentialNoteFungibleTokenSimulator.ts} (52%)
create mode 100644 contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenAllowlistWitnesses.ts
create mode 100644 contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenAuditWitnesses.ts
rename contracts/src/token/test/witnesses/{ConfidentialNoteTokenDeliveryWitnesses.ts => ConfidentialNoteFungibleTokenDeliveryWitnesses.ts} (59%)
create mode 100644 contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenIssuerWitnesses.ts
rename contracts/src/token/test/witnesses/{ConfidentialNoteTokenSupplyWitnesses.ts => ConfidentialNoteFungibleTokenPrivateSupplyWitnesses.ts} (66%)
rename contracts/src/token/test/witnesses/{ConfidentialNoteTokenAuditWitnesses.ts => ConfidentialNoteFungibleTokenReviewWitnesses.ts} (52%)
rename contracts/src/token/test/witnesses/{ConfidentialNoteTokenWitnesses.ts => ConfidentialNoteFungibleTokenWitnesses.ts} (70%)
rename contracts/src/token/test/witnesses/{RegulatedConfidentialNoteTokenWitnesses.ts => RegulatedConfidentialNoteFungibleTokenWitnesses.ts} (73%)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4deb16726..ee11ea57e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,8 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add EcdhMask (#655)
- Add NoteDelivery: an encrypted note-delivery channel with a derived-nonce variant (`deliver`/`recover`) and an explicit-nonce variant (`deliverNote`/`recoverNote`)
-- Add ConfidentialNoteToken (draft): a note-based confidential token with full graph privacy (amounts — including issuance and burns — sender, and recipient hidden), with extensions for mandatory auditor viewing (audit-derived nonces) and on-chain recipient note deliveries, plus the RegulatedConfidentialNoteToken preset wiring issuer-gated mint, burn, and shared-nullifier seizure. See `contracts/src/token/docs/hybrid-confidential-token.md`
-- Add ConfidentialNoteTokenSupply (draft): a standalone extension keeping supply as a homomorphic ElGamal ciphertext updated alongside mint/burn, with `attestSupply` publishing a proof-backed public total at a chosen cadence
+- Add ConfidentialNoteFungibleToken (draft): a note-based confidential token with full graph privacy (amounts — including issuance and burns — sender, and recipient hidden), with extensions for mandatory auditor viewing (audit-derived nonces) and on-chain recipient note deliveries, plus the RegulatedConfidentialNoteFungibleToken preset wiring issuer-gated mint, burn, and shared-nullifier seizure. See `contracts/src/token/docs/hybrid-confidential-token.md`
+- Add ConfidentialNoteFungibleTokenPrivateSupply (draft): a standalone extension keeping supply as a homomorphic ElGamal ciphertext updated alongside mint/burn, with `attestSupply` publishing a proof-backed public total at a chosen cadence
## 0.3.0-alpha (2026-06-30)
diff --git a/contracts/src/token/ConfidentialNoteToken.compact b/contracts/src/token/ConfidentialNoteFungibleToken.compact
similarity index 73%
rename from contracts/src/token/ConfidentialNoteToken.compact
rename to contracts/src/token/ConfidentialNoteFungibleToken.compact
index 7e0eee76b..a5a49c1c4 100644
--- a/contracts/src/token/ConfidentialNoteToken.compact
+++ b/contracts/src/token/ConfidentialNoteFungibleToken.compact
@@ -1,10 +1,10 @@
// SPDX-License-Identifier: MIT
-// OpenZeppelin Compact Contracts (token/ConfidentialNoteToken.compact)
+// OpenZeppelin Compact Contracts (token/ConfidentialNoteFungibleToken.compact)
pragma language_version >= 0.23.0;
/**
- * @module ConfidentialNoteToken
+ * @module ConfidentialNoteFungibleToken
* @description DRAFT tier-4 core: a self-contained note-based confidential
* token with FULL graph privacy (amounts, sender, and recipient hidden).
*
@@ -15,27 +15,33 @@ pragma language_version >= 0.23.0;
* `nf = H(domain, nonce)` marks the note spent. Spending proves membership of
* the note's commitment without revealing which leaf.
*
- * The core is a complete token on its own: `initialize` binds the issuer,
- * `mint` (issuer-gated), `transfer`, and `burn` work out of the box, deriving
- * output nonces from the caller's own randomness witness. Created notes are
- * returned to the caller (a local, private result — nothing extra goes
- * on-chain), who hands them to recipients out of band.
+ * The core is deliberately barebones: the note machinery and nothing else. It
+ * holds NO roles and needs NO initialization — the ledger is just the
+ * commitment tree and the nullifier set. `transfer` and `burn` work out of the
+ * box because they are self-gated (spending requires the owner's secret via
+ * `wit_SecretKey`), deriving output nonces from the caller's own randomness
+ * witness. Created notes are returned to the caller (a local, private result —
+ * nothing extra goes on-chain), who hands them to recipients out of band.
*
- * Compliance and UX are layered on top, not baked in:
+ * Value creation carries no default gate: `_mint` / `_mintNote` are ungated
+ * building blocks, and the composing contract decides who may create value.
+ * Roles, compliance, and UX are layered on top, not baked in:
*
- * - auditor viewing — `extensions/ConfidentialNoteTokenAudit`,
+ * - issuer gating (the single-issuer shape) —
+ * `extensions/ConfidentialNoteFungibleTokenIssuer`,
+ * - auditor viewing — `extensions/ConfidentialNoteFungibleTokenAudit`,
* - on-chain note delivery (recipients discover funds by scanning) —
- * `extensions/ConfidentialNoteTokenDelivery`,
+ * `extensions/ConfidentialNoteFungibleTokenDelivery`,
* - confidential supply + public attestation —
- * `extensions/ConfidentialNoteTokenSupply`,
+ * `extensions/ConfidentialNoteFungibleTokenPrivateSupply`,
* - the wired-together deployable token —
- * `presets/RegulatedConfidentialNoteToken`.
+ * `presets/RegulatedConfidentialNoteFungibleToken`.
*
- * For that wiring, the `_`-prefixed building blocks (`_mint`, `_transfer`,
+ * For that wiring, the `_`-prefixed building blocks (`_mintNote`, `_transfer`,
* `_burn`, `_consumeNote`) accept caller-built notes, so a composing contract
- * can source nonces from its emission policy (e.g. audit-derived) instead of
- * the core default. Like all `_` circuits, they carry no authorization —
- * the composer gates them.
+ * can source nonces from its emission policy (e.g. audit-derived); `_mint`
+ * instead derives the core-default nonce. Like all `_` circuits, they carry
+ * no authorization — the composer gates them.
*
* @dev Nonces are spend-critical: the nullifier preimage is the nonce alone
* (no owner secret), so any party that knows a nonce derives the SAME
@@ -46,7 +52,7 @@ pragma language_version >= 0.23.0;
*
* @dev NOT audited, NOT production.
*/
-module ConfidentialNoteToken {
+module ConfidentialNoteFungibleToken {
import CompactStandardLibrary;
// A note's value and its unique nonce. Ownership is a separate pk.
@@ -67,16 +73,11 @@ module ConfidentialNoteToken {
nonce: Field;
}
- export ledger _isInitialized: Boolean;
- // Mint authorization: `Hf(issuerSecret)`.
- export ledger _issuerPk: Field;
export ledger _commitments: HistoricMerkleTree<32, Bytes<32>>;
export ledger _nullifiers: Set>;
// Owner's spend secret (pk = Hf(sk)).
witness wit_SecretKey(): Bytes<32>;
- // The issuer's secret (issuerPk = Hf(issuerSecret)).
- witness wit_IssuerSecret(): Bytes<32>;
// The note being consumed, and its Merkle path.
witness wit_InputNote(): Note;
witness wit_Path(cm: Bytes<32>): MerkleTreePath<32, Bytes<32>>;
@@ -116,44 +117,6 @@ module ConfidentialNoteToken {
});
}
- /**
- * @description One-shot initialization binding the issuer (the only role
- * the core itself needs).
- *
- * Requirements:
- *
- * - Module is not already initialized.
- *
- * @circuitInfo k=6, rows=31
- */
- export circuit initialize(issuerPk: Field): [] {
- assert(!_isInitialized, "ConfidentialNoteToken: already initialized");
- _issuerPk = disclose(issuerPk);
- _isInitialized = true;
- }
-
- /**
- * @description Mints a note of `value` to `recipientPk` and returns it (a
- * local private result) so the issuer can hand it to the recipient out of
- * band. The amount is NOT written to public state — issuance stays hidden;
- * only the hiding commitment appears on-chain.
- *
- * Requirements:
- *
- * - Module is initialized.
- * - The caller proves the issuer secret (`Hf(secret) == _issuerPk`).
- *
- * @circuitInfo k=14, rows=13217
- */
- export circuit mint(recipientPk: Field, value: Uint<128>): Note {
- _assertIssuer();
- const note = Note { value: value, nonce: freshNonce(pad(32, "OZ:cnt:out")) };
- _mint(note, recipientPk);
- // The note returns only to the local caller; revealing it on-chain would
- // expose the nonce (spend-critical) and the amount.
- return disclose(note);
- }
-
/**
* @description Fully-private transfer: spends the caller's input note and
* creates a recipient note of `value` plus a change note back to the
@@ -172,7 +135,7 @@ module ConfidentialNoteToken {
export circuit transfer(recipientPk: Field, value: Uint<128>): [Note, Note] {
const pk = _spenderPk();
const input = _inputNote();
- assert(input.value >= value, "ConfidentialNoteToken: insufficient note value");
+ assert(input.value >= value, "ConfidentialNoteFungibleToken: insufficient note value");
const outNote = Note { value: value, nonce: freshNonce(pad(32, "OZ:cnt:out")) };
const changeNote = Note {
@@ -199,7 +162,7 @@ module ConfidentialNoteToken {
export circuit burn(value: Uint<128>): Note {
const pk = _spenderPk();
const input = _inputNote();
- assert(input.value >= value, "ConfidentialNoteToken: insufficient note value");
+ assert(input.value >= value, "ConfidentialNoteFungibleToken: insufficient note value");
const changeNote = Note {
value: (input.value - value) as Uint<128>,
@@ -218,22 +181,6 @@ module ConfidentialNoteToken {
return derivePk(wit_SecretKey());
}
- /**
- * @description Building block: asserts the caller proves the issuer secret.
- *
- * Requirements:
- *
- * - Module is initialized.
- * - `Hf(wit_IssuerSecret()) == _issuerPk`.
- *
- * @circuitInfo k=13, rows=2277
- */
- export circuit _assertIssuer(): [] {
- assert(_isInitialized, "ConfidentialNoteToken: contract not initialized");
- assert(derivePk(wit_IssuerSecret()) == _issuerPk,
- "ConfidentialNoteToken: not the issuer");
- }
-
/**
* @description Building block: peek at the input note about to be consumed
* (the same witness `_consumeNote` reads), so a composing contract can size
@@ -252,11 +199,30 @@ module ConfidentialNoteToken {
*
* @circuitInfo k=13, rows=6766
*/
- export circuit _mint(note: Note, ownerPk: Field): [] {
+ export circuit _mintNote(note: Note, ownerPk: Field): [] {
// Only the hiding commitment crosses to public state.
_commitments.insert(disclose(commitOf(note, ownerPk)));
}
+ /**
+ * @description UNGATED building block: mints a note of `value` to
+ * `recipientPk` with a core-derived fresh nonce and returns it (a local
+ * private result) so the caller can hand it to the recipient out of band.
+ * The amount is NOT written to public state — issuance stays hidden; only
+ * the hiding commitment appears on-chain. The composer gates who may call
+ * this (see `extensions/ConfidentialNoteFungibleTokenIssuer` for the
+ * single-issuer shape).
+ *
+ * @circuitInfo k=14, rows=10964
+ */
+ export circuit _mint(recipientPk: Field, value: Uint<128>): Note {
+ const note = Note { value: value, nonce: freshNonce(pad(32, "OZ:cnt:out")) };
+ _mintNote(note, recipientPk);
+ // The note returns only to the local caller; revealing it on-chain would
+ // expose the nonce (spend-critical) and the amount.
+ return disclose(note);
+ }
+
/**
* @description UNGATED building block: consumes the input note owned by
* `spenderPk` and commits `outNote` to `recipientPk` plus `changeNote` back
@@ -272,9 +238,9 @@ module ConfidentialNoteToken {
export circuit _transfer(spenderPk: Field, recipientPk: Field, outNote: Note, changeNote: Note): [] {
const input = _consumeNote(spenderPk);
assert(input.value == outNote.value + changeNote.value,
- "ConfidentialNoteToken: transfer does not conserve value");
- _mint(outNote, recipientPk);
- _mint(changeNote, spenderPk);
+ "ConfidentialNoteFungibleToken: transfer does not conserve value");
+ _mintNote(outNote, recipientPk);
+ _mintNote(changeNote, spenderPk);
}
/**
@@ -292,8 +258,8 @@ module ConfidentialNoteToken {
export circuit _burn(spenderPk: Field, value: Uint<128>, changeNote: Note): [] {
const input = _consumeNote(spenderPk);
assert(input.value == value + changeNote.value,
- "ConfidentialNoteToken: burn does not conserve value");
- _mint(changeNote, spenderPk);
+ "ConfidentialNoteFungibleToken: burn does not conserve value");
+ _mintNote(changeNote, spenderPk);
}
/**
@@ -319,14 +285,14 @@ module ConfidentialNoteToken {
const path = wit_Path(cm);
const root = disclose(merkleTreePathRoot<32, Bytes<32>>(path));
assert(_commitments.checkRoot(root),
- "ConfidentialNoteToken: input root not recognized");
+ "ConfidentialNoteFungibleToken: input root not recognized");
assert(cm == path.leaf,
- "ConfidentialNoteToken: path does not match input commitment");
+ "ConfidentialNoteFungibleToken: path does not match input commitment");
// Single-spend: the shared nullifier can be consumed exactly once.
const nf = nullifierOf(input);
assert(!_nullifiers.member(disclose(nf)),
- "ConfidentialNoteToken: note already spent");
+ "ConfidentialNoteFungibleToken: note already spent");
_nullifiers.insert(disclose(nf));
return input;
diff --git a/contracts/src/token/extensions/ConfidentialNoteFungibleTokenAllowlist.compact b/contracts/src/token/extensions/ConfidentialNoteFungibleTokenAllowlist.compact
new file mode 100644
index 000000000..082f9c19e
--- /dev/null
+++ b/contracts/src/token/extensions/ConfidentialNoteFungibleTokenAllowlist.compact
@@ -0,0 +1,100 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Compact Contracts (token/extensions/ConfidentialNoteFungibleTokenAllowlist.compact)
+
+pragma language_version >= 0.23.0;
+
+/**
+ * @module ConfidentialNoteFungibleTokenAllowlist
+ * @description Optional standalone extension adding a KYC allowlist to a
+ * `ConfidentialNoteFungibleToken` pool. Because spenders are HIDDEN, a plain
+ * `Set` lookup is unusable (it would disclose who is transacting); membership
+ * is instead proven in zero knowledge against a Merkle tree of identity
+ * leaves — an observer learns only "an allowed party", never which one. It
+ * imports no token module; the composing contract gates `_addAllowed` /
+ * `_removeAllowed` (admin) and wires `_assertAllowed` at its chokepoints:
+ *
+ * export circuit transfer(recipientPk: Field, ...): [] {
+ * Allowlist__assertAllowed(Core__spenderPk()); // hidden-spender KYC
+ * Allowlist__assertAllowed(recipientPk); // recipient KYC
+ * // ...
+ * }
+ *
+ * The tree is PLAIN (not historic) on purpose: `checkRoot` accepts only the
+ * CURRENT root, so any update — including a tombstone removal — invalidates
+ * every previously fetched path, making revocation immediate. Wallets must
+ * refetch their path when the tree changes.
+ *
+ * @dev Removal is by leaf index (`_removeAllowed` overwrites the slot with
+ * the default leaf), which trusts the composer's off-chain index
+ * bookkeeping. Fail-closed hardening (prove the leaf at `index` matches the
+ * pk being removed) is a known follow-up.
+ *
+ * @dev `wit_AllowlistPath` supplies the prover's own membership path; the
+ * path stays witness, so which leaf proved membership is never revealed.
+ *
+ * @dev NOT audited, NOT production.
+ */
+module ConfidentialNoteFungibleTokenAllowlist {
+ import CompactStandardLibrary;
+
+ // Allowlist leaf preimage (domain-separated identity commitment).
+ struct AllowLeafPreimage {
+ domain: Bytes<32>;
+ pk: Field;
+ }
+
+ // Identity commitments of allowed parties. Plain tree: current-root proofs
+ // only, so updates revoke stale paths.
+ export ledger _allowed: MerkleTree<16, Bytes<32>>;
+
+ // The prover's own membership path (fetched from the public tree).
+ witness wit_AllowlistPath(leaf: Bytes<32>): MerkleTreePath<16, Bytes<32>>;
+
+ /**
+ * @description Identity leaf: `H(domain, pk)`. Exported so admins and
+ * wallets derive leaves the way `_assertAllowed` does.
+ */
+ export pure circuit leafOf(pk: Field): Bytes<32> {
+ return persistentHash(AllowLeafPreimage {
+ domain: pad(32, "OZ:cnt:allow"),
+ pk: pk
+ });
+ }
+
+ /**
+ * @description UNGATED building block: adds `pk` to the allowlist. The
+ * composer gates who may administer the list.
+ */
+ export circuit _addAllowed(pk: Field): [] {
+ _allowed.insert(disclose(leafOf(pk)));
+ }
+
+ /**
+ * @description UNGATED building block: removes the leaf at `index` by
+ * overwriting it with the default leaf (a tombstone no identity can match).
+ * Every outstanding path proof is invalidated by the root change. The
+ * composer gates who may administer the list; see the module doc for the
+ * index-bookkeeping caveat.
+ */
+ export circuit _removeAllowed(index: Uint<64>): [] {
+ _allowed.insertIndexDefault(disclose(index));
+ }
+
+ /**
+ * @description Building block: proves `pk` is on the allowlist without
+ * revealing which leaf. Place it at the composer's chokepoints (hidden
+ * spender, disclosed-to-circuit recipient).
+ *
+ * Requirements:
+ *
+ * - `leafOf(pk)` is a leaf of the CURRENT tree (stale paths fail).
+ */
+ export circuit _assertAllowed(pk: Field): [] {
+ const leaf = leafOf(pk);
+ const path = wit_AllowlistPath(leaf);
+ assert(_allowed.checkRoot(disclose(merkleTreePathRoot<16, Bytes<32>>(path))),
+ "ConfidentialNoteFungibleTokenAllowlist: not allowed");
+ assert(leaf == path.leaf,
+ "ConfidentialNoteFungibleTokenAllowlist: path does not match identity");
+ }
+}
diff --git a/contracts/src/token/extensions/ConfidentialNoteTokenAudit.compact b/contracts/src/token/extensions/ConfidentialNoteFungibleTokenAudit.compact
similarity index 74%
rename from contracts/src/token/extensions/ConfidentialNoteTokenAudit.compact
rename to contracts/src/token/extensions/ConfidentialNoteFungibleTokenAudit.compact
index e6aec76bc..7c8a18435 100644
--- a/contracts/src/token/extensions/ConfidentialNoteTokenAudit.compact
+++ b/contracts/src/token/extensions/ConfidentialNoteFungibleTokenAudit.compact
@@ -1,12 +1,12 @@
// SPDX-License-Identifier: MIT
-// OpenZeppelin Compact Contracts (token/extensions/ConfidentialNoteTokenAudit.compact)
+// OpenZeppelin Compact Contracts (token/extensions/ConfidentialNoteFungibleTokenAudit.compact)
pragma language_version >= 0.23.0;
/**
- * @module ConfidentialNoteTokenAudit
+ * @module ConfidentialNoteFungibleTokenAudit
* @description Optional standalone extension that gives a designated auditor
- * COMPLETE visibility over a `ConfidentialNoteToken` pool while the public
+ * COMPLETE visibility over a `ConfidentialNoteFungibleToken` pool while the public
* sees nothing. It imports no token module.
*
* For each output note, `_emitAuditedOutput`:
@@ -27,17 +27,17 @@ pragma language_version >= 0.23.0;
* preimage, so in a seizure-enabled preset the audit trail is exactly what
* arms the seizure authority (see the preset).
*
- * @notice Pairs with `ConfidentialNoteToken`. The consuming contract wires
+ * @notice Pairs with `ConfidentialNoteFungibleToken`. The consuming contract wires
* every note-creation path through this extension:
*
* circuit emitOutput(ownerPk: Field, value: Uint<128>, slot: Bytes<32>): [] {
* const nonce = Audit__emitAuditedOutput(ownerPk, value, slot);
- * CNT__createNote(CNT_Note { value: value, nonce: nonce }, ownerPk);
+ * Core__mintNote(Core_Note { value: value, nonce: nonce }, ownerPk);
* }
*
* @warning Audit completeness is a property of the CONSUMER's wiring: a note
* created without `_emitAuditedOutput` is invisible to the auditor. Route
- * every `_createNote` through it.
+ * every `_mintNote` through it.
*
* @dev `wit_AuditRandomness` MUST return a fresh, secret seed per invocation;
* ephemerals expand from it (see `crypto/EcdhMask` freshness rules). `slot`
@@ -45,7 +45,7 @@ pragma language_version >= 0.23.0;
*
* @dev NOT audited, NOT production.
*/
-module ConfidentialNoteTokenAudit {
+module ConfidentialNoteFungibleTokenAudit {
import CompactStandardLibrary;
import "../../crypto/EcdhMask" prefix EcdhMask_;
@@ -76,6 +76,9 @@ module ConfidentialNoteTokenAudit {
// Randomness seed for audit ephemerals. MUST be fresh + secret per
// invocation (see module doc).
witness wit_AuditRandomness(): Bytes<32>;
+ // The audit secret scalar (`_auditKey = g^auditSk`); only `_rotateAuditKey`
+ // consumes it.
+ witness wit_AuditKeySecret(): Field;
/**
* @description One-shot initialization binding the audit key.
@@ -88,9 +91,9 @@ module ConfidentialNoteTokenAudit {
* @circuitInfo k=10, rows=615
*/
export circuit initialize(auditKey: JubjubPoint): [] {
- assert(!_isInitialized, "ConfidentialNoteTokenAudit: already initialized");
+ assert(!_isInitialized, "ConfidentialNoteFungibleTokenAudit: already initialized");
assert(auditKey != ecMulGenerator(0 as Field),
- "ConfidentialNoteTokenAudit: identity audit key");
+ "ConfidentialNoteFungibleTokenAudit: identity audit key");
_auditKey = disclose(auditKey);
_isInitialized = true;
}
@@ -107,14 +110,14 @@ module ConfidentialNoteTokenAudit {
* @circuitInfo k=15, rows=31599
*/
export circuit _emitAuditedOutput(ownerPk: Field, value: Uint<128>, slot: Bytes<32>): Field {
- assert(_isInitialized, "ConfidentialNoteTokenAudit: extension not initialized");
+ assert(_isInitialized, "ConfidentialNoteFungibleTokenAudit: extension not initialized");
const ea = degradeToTransient(persistentHash>>(
[wit_AuditRandomness(), pad(32, "OZ:cnt:ea"), slot]));
const eaPk = ecMulGenerator(ea);
// Point guard subsumes `ea != 0` (see crypto/EcdhMask weak-inputs note).
assert(eaPk != ecMulGenerator(0 as Field),
- "ConfidentialNoteTokenAudit: zero audit ephemeral");
+ "ConfidentialNoteFungibleTokenAudit: zero audit ephemeral");
const shared = ecMul(_auditKey, ea);
// The nonce is DERIVED from the audit shared secret: an output the auditor
@@ -132,6 +135,28 @@ module ConfidentialNoteTokenAudit {
return nonce;
}
+ /**
+ * @description Building block: self-rotation — the current audit-key holder
+ * proves the secret scalar and binds a new audit key. Records already
+ * published stay readable by the OLD key (ciphertexts cannot be clawed
+ * back); outputs emitted after rotation derive their nonces from the new
+ * key. Production deployments gate this behind governance too.
+ *
+ * Requirements:
+ *
+ * - Extension is initialized.
+ * - `newKey` is not the identity point.
+ * - The caller proves the CURRENT audit secret (`g^secret == _auditKey`).
+ */
+ export circuit _rotateAuditKey(newKey: JubjubPoint): [] {
+ assert(_isInitialized, "ConfidentialNoteFungibleTokenAudit: extension not initialized");
+ assert(newKey != ecMulGenerator(0 as Field),
+ "ConfidentialNoteFungibleTokenAudit: identity audit key");
+ assert(ecMulGenerator(wit_AuditKeySecret()) == _auditKey,
+ "ConfidentialNoteFungibleTokenAudit: not the audit key holder");
+ _auditKey = disclose(newKey);
+ }
+
/**
* @description Auditor-side: recover one output's `(value, nonce, ownerPk)`
* from an AuditRecord using the audit secret scalar (`auditKey = g^auditSk`).
diff --git a/contracts/src/token/extensions/ConfidentialNoteTokenDelivery.compact b/contracts/src/token/extensions/ConfidentialNoteFungibleTokenDelivery.compact
similarity index 89%
rename from contracts/src/token/extensions/ConfidentialNoteTokenDelivery.compact
rename to contracts/src/token/extensions/ConfidentialNoteFungibleTokenDelivery.compact
index 77d2b6eba..638e9c064 100644
--- a/contracts/src/token/extensions/ConfidentialNoteTokenDelivery.compact
+++ b/contracts/src/token/extensions/ConfidentialNoteFungibleTokenDelivery.compact
@@ -1,10 +1,10 @@
// SPDX-License-Identifier: MIT
-// OpenZeppelin Compact Contracts (token/extensions/ConfidentialNoteTokenDelivery.compact)
+// OpenZeppelin Compact Contracts (token/extensions/ConfidentialNoteFungibleTokenDelivery.compact)
pragma language_version >= 0.23.0;
/**
- * @module ConfidentialNoteTokenDelivery
+ * @module ConfidentialNoteFungibleTokenDelivery
* @description Optional standalone extension that delivers each output note's
* `(value, nonce)` to its owner's encryption key on-chain, so a recipient
* discovers incoming notes from chain data alone — no out-of-band channel.
@@ -14,8 +14,8 @@ pragma language_version >= 0.23.0;
* encryption secret (`crypto/NoteDelivery.recoverNote`), and keep the notes
* whose recomputed commitment exists in the token core's tree.
*
- * @notice Pairs with `ConfidentialNoteToken`. The consuming contract calls
- * `_deliver` alongside every `_createNote` whose owner should be able to find
+ * @notice Pairs with `ConfidentialNoteFungibleToken`. The consuming contract calls
+ * `_deliver` alongside every `_mintNote` whose owner should be able to find
* the note by scanning (skipping it makes the note reachable only out of
* band — the funds still exist, but only the creator knows the nonce).
*
@@ -32,7 +32,7 @@ pragma language_version >= 0.23.0;
*
* @dev NOT audited, NOT production.
*/
-module ConfidentialNoteTokenDelivery {
+module ConfidentialNoteFungibleTokenDelivery {
import CompactStandardLibrary;
import "../../crypto/NoteDelivery" prefix NoteDelivery_;
diff --git a/contracts/src/token/extensions/ConfidentialNoteFungibleTokenFreeze.compact b/contracts/src/token/extensions/ConfidentialNoteFungibleTokenFreeze.compact
new file mode 100644
index 000000000..c9fb52a91
--- /dev/null
+++ b/contracts/src/token/extensions/ConfidentialNoteFungibleTokenFreeze.compact
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Compact Contracts (token/extensions/ConfidentialNoteFungibleTokenFreeze.compact)
+
+pragma language_version >= 0.23.0;
+
+/**
+ * @module ConfidentialNoteFungibleTokenFreeze
+ * @description Optional standalone extension adding freeze-before-seize to a
+ * `ConfidentialNoteFungibleToken` pool: a frozen-nullifier set checked at the
+ * owner-spend chokepoint. It imports no token module and holds no roles; the
+ * composing contract gates `_freeze` / `_unfreeze` (typically behind the same
+ * authority that may seize) and wires `_assertNotFrozen` into every
+ * owner-spend path.
+ *
+ * A note is frozen by its NULLIFIER: `nf = H(domain, nonce)` depends only on
+ * the nonce, so an authority armed by the audit trail derives the target's
+ * nullifier without consuming the note — freezing is non-destructive and
+ * reversible, unlike seizure. Wiring, at the spend chokepoint:
+ *
+ * export circuit transfer(...): [] {
+ * Freeze__assertNotFrozen(Core_nullifierOf(Core__inputNote()));
+ * // ...the core re-reads the same witness, so the checked note IS the
+ * // spent note...
+ * }
+ *
+ * Do NOT wire the check into `seize`: seizure of a frozen note is the whole
+ * point of freeze-then-seize.
+ *
+ * @dev What freezing publishes: the nullifier itself. Observers learn "some
+ * specific note is frozen" (and can later link its spend or seizure), but not
+ * the note's owner or value — the nullifier preimage stays hidden. The size
+ * of the frozen set is public.
+ *
+ * @dev NOT audited, NOT production.
+ */
+module ConfidentialNoteFungibleTokenFreeze {
+ import CompactStandardLibrary;
+
+ // Frozen nullifiers: notes whose owner-spend is administratively blocked.
+ export ledger _frozen: Set>;
+
+ /**
+ * @description UNGATED building block: freezes the note behind `nf`. The
+ * composer gates who may freeze (typically the seizure authority, armed
+ * with the note's nonce from the audit trail).
+ *
+ * Requirements:
+ *
+ * - `nf` is not already frozen.
+ */
+ export circuit _freeze(nf: Bytes<32>): [] {
+ assert(!_frozen.member(disclose(nf)),
+ "ConfidentialNoteFungibleTokenFreeze: already frozen");
+ _frozen.insert(disclose(nf));
+ }
+
+ /**
+ * @description UNGATED building block: lifts the freeze on `nf`. The
+ * composer gates who may unfreeze.
+ *
+ * Requirements:
+ *
+ * - `nf` is frozen.
+ */
+ export circuit _unfreeze(nf: Bytes<32>): [] {
+ assert(_frozen.member(disclose(nf)),
+ "ConfidentialNoteFungibleTokenFreeze: not frozen");
+ _frozen.remove(disclose(nf));
+ }
+
+ /**
+ * @description Building block: asserts the note behind `nf` is not frozen.
+ * Place it at every owner-spend chokepoint (and nowhere near `seize`).
+ */
+ export circuit _assertNotFrozen(nf: Bytes<32>): [] {
+ assert(!_frozen.member(disclose(nf)),
+ "ConfidentialNoteFungibleTokenFreeze: note is frozen");
+ }
+}
diff --git a/contracts/src/token/extensions/ConfidentialNoteFungibleTokenIssuer.compact b/contracts/src/token/extensions/ConfidentialNoteFungibleTokenIssuer.compact
new file mode 100644
index 000000000..e862a16ec
--- /dev/null
+++ b/contracts/src/token/extensions/ConfidentialNoteFungibleTokenIssuer.compact
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Compact Contracts (token/extensions/ConfidentialNoteFungibleTokenIssuer.compact)
+
+pragma language_version >= 0.23.0;
+
+/**
+ * @module ConfidentialNoteFungibleTokenIssuer
+ * @description Optional standalone extension binding a single ISSUER role for
+ * a `ConfidentialNoteFungibleToken` pool: the issuer is whoever proves the
+ * secret behind `_issuerPk = Hf(issuerSecret)` in-circuit. It imports no
+ * token module.
+ *
+ * The core is deliberately role-free: its `_mint` / `_mintNote` building
+ * blocks are ungated, and the composing contract decides who may create
+ * value. This extension is the out-of-the-box answer for the single-issuer
+ * shape:
+ *
+ * export circuit mint(recipientPk: Field, value: Uint<128>): Core_Note {
+ * Issuer__assertIssuer();
+ * return Core__mint(recipientPk, value);
+ * }
+ *
+ * Alternative issuance policies (multisig-gated, role sets, or none at all
+ * for a fixed-supply genesis mint) compose against the same blocks the same
+ * way.
+ *
+ * @dev Identity derivation matches the token core's `derivePk`
+ * (`pk = Hf(sk)`, field-typed), so one keypair works across both modules and
+ * off-chain code can derive the issuer identity with the core's exported
+ * circuit.
+ *
+ * @dev NOT audited, NOT production.
+ */
+module ConfidentialNoteFungibleTokenIssuer {
+ import CompactStandardLibrary;
+
+ export ledger _isInitialized: Boolean;
+ // Mint authorization: `Hf(issuerSecret)`.
+ export ledger _issuerPk: Field;
+
+ // The issuer's secret (issuerPk = Hf(issuerSecret)).
+ witness wit_IssuerSecret(): Bytes<32>;
+
+ /**
+ * @description One-shot initialization binding the issuer.
+ *
+ * Requirements:
+ *
+ * - Extension is not already initialized.
+ *
+ * @circuitInfo k=6, rows=31
+ */
+ export circuit initialize(issuerPk: Field): [] {
+ assert(!_isInitialized, "ConfidentialNoteFungibleTokenIssuer: already initialized");
+ _issuerPk = disclose(issuerPk);
+ _isInitialized = true;
+ }
+
+ /**
+ * @description Building block: asserts the caller proves the issuer secret.
+ * Place it before any ungated value-creation block the deployment reserves
+ * for the issuer.
+ *
+ * Requirements:
+ *
+ * - Extension is initialized.
+ * - `Hf(wit_IssuerSecret()) == _issuerPk`.
+ *
+ * @circuitInfo k=13, rows=2277
+ */
+ export circuit _assertIssuer(): [] {
+ assert(_isInitialized, "ConfidentialNoteFungibleTokenIssuer: extension not initialized");
+ assert(derivePk(wit_IssuerSecret()) == _issuerPk,
+ "ConfidentialNoteFungibleTokenIssuer: not the issuer");
+ }
+
+ /**
+ * @description Building block: self-rotation — the current issuer proves
+ * their secret and binds a new issuer key. Same semantics as an
+ * `Ownable`-style ownership transfer: a compromised key can rotate itself
+ * away, so production deployments gate this behind governance too.
+ *
+ * Requirements:
+ *
+ * - Extension is initialized.
+ * - The caller proves the CURRENT issuer secret.
+ */
+ export circuit _rotateIssuer(newIssuerPk: Field): [] {
+ _assertIssuer();
+ _issuerPk = disclose(newIssuerPk);
+ }
+
+ // Same construction as the token core's `derivePk`, duplicated so this
+ // extension stays free of token imports (a module import would share the
+ // token's ledger state into any consumer of this extension).
+ circuit derivePk(sk: Bytes<32>): Field {
+ return degradeToTransient(persistentHash>(sk));
+ }
+}
diff --git a/contracts/src/token/extensions/ConfidentialNoteTokenSupply.compact b/contracts/src/token/extensions/ConfidentialNoteFungibleTokenPrivateSupply.compact
similarity index 78%
rename from contracts/src/token/extensions/ConfidentialNoteTokenSupply.compact
rename to contracts/src/token/extensions/ConfidentialNoteFungibleTokenPrivateSupply.compact
index e7d60dcc1..e1fa11de1 100644
--- a/contracts/src/token/extensions/ConfidentialNoteTokenSupply.compact
+++ b/contracts/src/token/extensions/ConfidentialNoteFungibleTokenPrivateSupply.compact
@@ -1,12 +1,12 @@
// SPDX-License-Identifier: MIT
-// OpenZeppelin Compact Contracts (token/extensions/ConfidentialNoteTokenSupply.compact)
+// OpenZeppelin Compact Contracts (token/extensions/ConfidentialNoteFungibleTokenPrivateSupply.compact)
pragma language_version >= 0.23.0;
/**
- * @module ConfidentialNoteTokenSupply
+ * @module ConfidentialNoteFungibleTokenPrivateSupply
* @description Optional standalone extension that adds CONFIDENTIAL supply
- * accounting to a `ConfidentialNoteToken`. It imports no token module.
+ * accounting to a `ConfidentialNoteFungibleToken`. It imports no token module.
*
* The base token deliberately writes no public supply: a disclosed counter
* would leak every mint and burn amount as a public delta. This extension
@@ -24,7 +24,7 @@ pragma language_version >= 0.23.0;
* only that total. Between attestations, individual amounts stay hidden;
* batching gives issuance amounts k-anonymity at the attestation cadence.
*
- * @notice Pairs with `ConfidentialNoteToken`. The consuming contract
+ * @notice Pairs with `ConfidentialNoteFungibleToken`. The consuming contract
* composes the pieces, calling the accounting block alongside the matching
* token op:
*
@@ -58,7 +58,7 @@ pragma language_version >= 0.23.0;
*
* @dev NOT audited, NOT production.
*/
-module ConfidentialNoteTokenSupply {
+module ConfidentialNoteFungibleTokenPrivateSupply {
import CompactStandardLibrary;
import "../../crypto/ElGamal" prefix ElGamal_;
@@ -89,9 +89,9 @@ module ConfidentialNoteTokenSupply {
* @circuitInfo k=11, rows=1167
*/
export circuit initialize(supplyKey: JubjubPoint): [] {
- assert(!_isInitialized, "ConfidentialNoteTokenSupply: already initialized");
+ assert(!_isInitialized, "ConfidentialNoteFungibleTokenPrivateSupply: already initialized");
assert(supplyKey != ecMulGenerator(0 as Field),
- "ConfidentialNoteTokenSupply: identity supply key");
+ "ConfidentialNoteFungibleTokenPrivateSupply: identity supply key");
_supplyKey = disclose(supplyKey);
_encSupply = ElGamal_encryptZero();
_isInitialized = true;
@@ -155,7 +155,33 @@ module ConfidentialNoteTokenSupply {
_attestationCount = disclose(_attestationCount + 1 as Uint<64>);
}
+ /**
+ * @description Building block: self-rotation — the current supply-key
+ * holder proves the secret AND the exact running total, and the encrypted
+ * supply is re-encrypted under the new key in the same proof (a ciphertext
+ * under the old key cannot be updated homomorphically by the new holder).
+ * `total` is a private circuit argument: it is never disclosed, only bound
+ * into the new ciphertext. Production deployments gate this behind
+ * governance too.
+ *
+ * Requirements:
+ *
+ * - Extension is initialized.
+ * - `newKey` is not the identity point.
+ * - The caller proves the CURRENT supply-key secret and `_encSupply`
+ * decrypts to `total`.
+ */
+ export circuit _rotateSupplyKey(newKey: JubjubPoint, total: Uint<128>): [] {
+ assertInitialized();
+ assert(newKey != ecMulGenerator(0 as Field),
+ "ConfidentialNoteFungibleTokenPrivateSupply: identity supply key");
+ ElGamal_assertDecryptsTo(_encSupply, _supplyKey, wit_SupplyKeySecret(), total);
+ const r = ElGamal_expandRandomness(wit_SupplyRandomness(), pad(32, "OZ:cnt:supply:rot"));
+ _encSupply = disclose(ElGamal_encrypt(newKey, total, r));
+ _supplyKey = disclose(newKey);
+ }
+
circuit assertInitialized(): [] {
- assert(_isInitialized, "ConfidentialNoteTokenSupply: extension not initialized");
+ assert(_isInitialized, "ConfidentialNoteFungibleTokenPrivateSupply: extension not initialized");
}
}
diff --git a/contracts/src/token/extensions/ConfidentialNoteFungibleTokenReview.compact b/contracts/src/token/extensions/ConfidentialNoteFungibleTokenReview.compact
new file mode 100644
index 000000000..4317245ca
--- /dev/null
+++ b/contracts/src/token/extensions/ConfidentialNoteFungibleTokenReview.compact
@@ -0,0 +1,170 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Compact Contracts (token/extensions/ConfidentialNoteFungibleTokenReview.compact)
+
+pragma language_version >= 0.23.0;
+
+/**
+ * @module ConfidentialNoteFungibleTokenReview
+ * @description Optional standalone extension adding SELECTIVE disclosure to a
+ * `ConfidentialNoteFungibleToken` pool: per-output encrypted records to an
+ * approved REVIEWER key (a custodian or FIU), alongside — not replacing — the
+ * global audit channel. Where the audit key sees everything, a reviewer sees
+ * only the outputs explicitly addressed to it. It imports no token module.
+ *
+ * The composing contract gates `_addReviewer` / `_removeReviewer` (admin) and
+ * emits a review record next to its emission policy where its deployment
+ * rules require one (a compile-time variant — in ZK an optional emission
+ * still pays its constraints, so it cannot be a runtime flag):
+ *
+ * const nonce = Audit__emitAuditedOutput(ownerPk, value, slot);
+ * Review__emitReviewRecord(reviewerKey, ownerPk, value, nonce, slot);
+ * Core__mintNote(Core_Note { value: value, nonce: nonce }, ownerPk);
+ *
+ * Unlike the audit channel, the note's nonce is NOT derived here (the audit
+ * ECDH owns nonce derivation); it rides an explicit ciphertext, so the
+ * reviewer recovers `(value, ownerPk, nonce)` and can recompute the note's
+ * commitment and nullifier — full visibility over exactly the outputs
+ * addressed to it.
+ *
+ * @dev Disclosure caveat (draft): each record publishes WHICH approved
+ * reviewer can open it (`reviewerKeyHash`), so custodian affiliation is
+ * public per output. Hiding the reviewer behind a Merkle membership proof is
+ * the known Phase-2 refinement, pending the selective-disclosure design
+ * review.
+ *
+ * @dev `wit_ReviewRandomness` MUST return a fresh, secret seed per
+ * invocation; ephemerals expand from it (see `crypto/EcdhMask` freshness
+ * rules). `slot` domain-separates ephemerals when one transaction emits
+ * several outputs.
+ *
+ * @dev NOT audited, NOT production.
+ */
+module ConfidentialNoteFungibleTokenReview {
+ import CompactStandardLibrary;
+ import "../../crypto/EcdhMask" prefix EcdhMask_;
+
+ // Per-output review ciphertext: the note's value, owner, and nonce
+ // encrypted to one approved reviewer key.
+ export struct ReviewRecord {
+ reviewerKeyHash: Bytes<32>;
+ ephemeralPk: JubjubPoint;
+ valueCt: Field;
+ ownerCt: Field;
+ nonceCt: Field;
+ }
+
+ // The reviewer's recovered view of one output (see `recoverReviewRecord`).
+ export struct ReviewView {
+ value: Field;
+ ownerPk: Field;
+ nonce: Field;
+ }
+
+ // Approved reviewer keys, stored as point hashes (see `reviewerKeyHashOf`).
+ export ledger _reviewers: Set>;
+ // Per-output records, for observation: reviewers scan this list filtering
+ // by their own key hash. It is this extension's event substitute.
+ export ledger _reviewTrail: List;
+
+ // Randomness seed for review ephemerals. MUST be fresh + secret per
+ // invocation (see module doc).
+ witness wit_ReviewRandomness(): Bytes<32>;
+
+ /**
+ * @description Registry key for a reviewer: `H(reviewerKey)`. Exported so
+ * admins and reviewers derive registry entries and scan filters the way the
+ * circuits do.
+ */
+ export pure circuit reviewerKeyHashOf(reviewerKey: JubjubPoint): Bytes<32> {
+ return persistentHash(reviewerKey);
+ }
+
+ /**
+ * @description UNGATED building block: approves a reviewer key. The
+ * composer gates who may administer the registry.
+ *
+ * Requirements:
+ *
+ * - `reviewerKey` is not the identity point.
+ * - `reviewerKey` is not already approved.
+ */
+ export circuit _addReviewer(reviewerKey: JubjubPoint): [] {
+ assert(reviewerKey != ecMulGenerator(0 as Field),
+ "ConfidentialNoteFungibleTokenReview: identity reviewer key");
+ const keyHash = reviewerKeyHashOf(reviewerKey);
+ assert(!_reviewers.member(disclose(keyHash)),
+ "ConfidentialNoteFungibleTokenReview: already a reviewer");
+ _reviewers.insert(disclose(keyHash));
+ }
+
+ /**
+ * @description UNGATED building block: revokes a reviewer key. Existing
+ * records stay readable by the revoked key (published ciphertexts cannot be
+ * clawed back); revocation only stops NEW records. The composer gates who
+ * may administer the registry.
+ *
+ * Requirements:
+ *
+ * - `reviewerKey` is an approved reviewer.
+ */
+ export circuit _removeReviewer(reviewerKey: JubjubPoint): [] {
+ const keyHash = reviewerKeyHashOf(reviewerKey);
+ assert(_reviewers.member(disclose(keyHash)),
+ "ConfidentialNoteFungibleTokenReview: not a reviewer");
+ _reviewers.remove(disclose(keyHash));
+ }
+
+ /**
+ * @description Emits one output's review record to `reviewerKey`: value,
+ * owner, and nonce one-time-padded to an ECDH shared secret with the
+ * reviewer key. The record is only accepted for an APPROVED reviewer.
+ *
+ * Requirements:
+ *
+ * - `reviewerKey` is an approved reviewer.
+ */
+ export circuit _emitReviewRecord(
+ reviewerKey: JubjubPoint,
+ ownerPk: Field,
+ value: Uint<128>,
+ nonce: Field,
+ slot: Bytes<32>
+ ): [] {
+ const keyHash = reviewerKeyHashOf(reviewerKey);
+ assert(_reviewers.member(disclose(keyHash)),
+ "ConfidentialNoteFungibleTokenReview: not a reviewer");
+
+ const er = degradeToTransient(persistentHash>>(
+ [wit_ReviewRandomness(), pad(32, "OZ:cnt:er"), slot]));
+ const erPk = ecMulGenerator(er);
+ // Point guard subsumes `er != 0` (see crypto/EcdhMask weak-inputs note);
+ // the registry's identity-key guard covers the other weak input.
+ assert(erPk != ecMulGenerator(0 as Field),
+ "ConfidentialNoteFungibleTokenReview: zero review ephemeral");
+ const shared = ecMul(reviewerKey, er);
+
+ _reviewTrail.pushFront(disclose(ReviewRecord {
+ reviewerKeyHash: keyHash,
+ ephemeralPk: erPk,
+ valueCt: (value as Field) + EcdhMask_kdf(shared, pad(32, "OZ:cnt:r:value")),
+ ownerCt: ownerPk + EcdhMask_kdf(shared, pad(32, "OZ:cnt:r:owner")),
+ nonceCt: nonce + EcdhMask_kdf(shared, pad(32, "OZ:cnt:r:nonce"))
+ }));
+ }
+
+ /**
+ * @description Reviewer-side: recover one output's `(value, ownerPk, nonce)`
+ * from a ReviewRecord using the reviewer secret scalar
+ * (`reviewerKey = g^reviewSk`). Pure and off-chain; feeding the result to
+ * the token core's `commitOf` / `nullifierOf` reconstructs the note's
+ * lifecycle.
+ */
+ export pure circuit recoverReviewRecord(record: ReviewRecord, reviewSk: Field): ReviewView {
+ const shared = ecMul(record.ephemeralPk, reviewSk);
+ return ReviewView {
+ value: record.valueCt - EcdhMask_kdf(shared, pad(32, "OZ:cnt:r:value")),
+ ownerPk: record.ownerCt - EcdhMask_kdf(shared, pad(32, "OZ:cnt:r:owner")),
+ nonce: record.nonceCt - EcdhMask_kdf(shared, pad(32, "OZ:cnt:r:nonce"))
+ };
+ }
+}
diff --git a/contracts/src/token/presets/RegulatedConfidentialNoteFungibleToken.compact b/contracts/src/token/presets/RegulatedConfidentialNoteFungibleToken.compact
new file mode 100644
index 000000000..999038dbb
--- /dev/null
+++ b/contracts/src/token/presets/RegulatedConfidentialNoteFungibleToken.compact
@@ -0,0 +1,333 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Compact Contracts (token/presets/RegulatedConfidentialNoteFungibleToken.compact)
+
+pragma language_version >= 0.23.0;
+
+/**
+ * @module RegulatedConfidentialNoteFungibleToken
+ * @description DRAFT ready-to-use module: the tier-4 confidential note token
+ * wired for a regulated deployment. A consuming contract imports it, binds
+ * every role key via `initialize` (typically from its constructor), and
+ * exposes the circuits it wants public:
+ *
+ * import "./presets/RegulatedConfidentialNoteFungibleToken" prefix Token_;
+ *
+ * constructor(issuerPk: Field, authorityPk: Field, auditKey: JubjubPoint, supplyKey: JubjubPoint) {
+ * Token_initialize(issuerPk, authorityPk, auditKey, supplyKey);
+ * }
+ *
+ * export circuit mint(recipientPk: Field, recipientEncPk: JubjubPoint, value: Uint<128>): [] {
+ * return Token_mint(recipientPk, recipientEncPk, value);
+ * }
+ * // ...and likewise for transfer, burn, seize, attestSupply.
+ *
+ * Guarantees: full graph privacy for users (amounts — including issuance and
+ * burns — senders, and recipients hidden), complete visibility for a
+ * designated auditor, escrow-free seizure for a designated authority, and
+ * confidential-but-attestable supply.
+ *
+ * Composition (each piece is independently reusable):
+ *
+ * - `ConfidentialNoteFungibleToken` — the role-free token core (conservation,
+ * single-spend); this preset drives its `_`-building blocks so output nonces
+ * come from the audit channel instead of the core default,
+ * - `extensions/ConfidentialNoteFungibleTokenIssuer` — the single-issuer gate
+ * in front of value creation,
+ * - `extensions/ConfidentialNoteFungibleTokenAudit` — mandatory auditor viewing; each
+ * output nonce derives from the audit ECDH, so every note this contract
+ * creates is auditor-recoverable BY CONSTRUCTION,
+ * - `extensions/ConfidentialNoteFungibleTokenDelivery` — on-chain note delivery, so
+ * recipients discover funds from chain data alone (no out-of-band channel),
+ * - `extensions/ConfidentialNoteFungibleTokenPrivateSupply` — homomorphic encrypted supply
+ * with proof-backed public attestation.
+ *
+ * Roles: the ISSUER may mint, the AUTHORITY may seize, the AUDIT key reads
+ * everything (never spends), the SUPPLY key attests totals. All four bind in
+ * one `initialize` call, and each can later SELF-ROTATE by proving its
+ * current secret (`rotateIssuer` / `rotateAuthority` / `rotateAuditKey` /
+ * `rotateSupplyKey`); production deployments gate rotation behind governance
+ * too.
+ *
+ * No state-changing circuit is usable before `initialize`: mint asserts the
+ * issuer extension's initialization, every output emission asserts the audit
+ * extension's, supply updates assert the supply extension's, and the zero
+ * authority key has no known preimage.
+ *
+ * Seizure needs no key escrow: the core nullifier depends only on the nonce,
+ * so the owner and the authority derive the SAME nullifier, making owner-spend
+ * and seizure mutually exclusive (first to land wins). The authority learns
+ * the target note from the audit trail and re-mints its value to a recovery
+ * owner, itself audited and delivered.
+ *
+ * What the public sees: commitment inserts, nullifiers, ciphertexts, the
+ * seizure counter, and attested supply totals. Amounts, senders, and
+ * recipients stay hidden.
+ *
+ * See `token/docs/confidential-note-token.md` for the full design and the
+ * auditor/compliance rationale.
+ *
+ * @dev The module re-exports the composed modules' observable ledger state
+ * and artifact types under stable bare names (`Note`, `AuditRecord`,
+ * `AuditView`, `FullDelivery`), so a consuming contract surfaces them with a
+ * single selective import + export.
+ *
+ * @dev The randomness witnesses (`wit_AuditRandomness`,
+ * `wit_DeliveryRandomness`, `wit_SupplyRandomness`) MUST each return a fresh,
+ * secret seed per invocation (see `crypto/EcdhMask` freshness rules).
+ *
+ * @dev NOT audited, NOT production.
+ */
+module RegulatedConfidentialNoteFungibleToken {
+ import CompactStandardLibrary;
+ import "../ConfidentialNoteFungibleToken" prefix Core_;
+ import "../extensions/ConfidentialNoteFungibleTokenIssuer" prefix Issuer_;
+ import "../extensions/ConfidentialNoteFungibleTokenAudit" prefix Audit_;
+ import "../extensions/ConfidentialNoteFungibleTokenDelivery" prefix Delivery_;
+ import "../extensions/ConfidentialNoteFungibleTokenPrivateSupply" prefix Supply_;
+ import "../../crypto/NoteDelivery" prefix NoteDelivery_;
+
+ // Surface the composed modules' observable state and artifact types under
+ // stable bare names, for wallets (commitment tree, deliveries), auditors
+ // (audit trail), and indexers (issuer key, supply): a prefix-only import
+ // would keep them out of a consumer's generated ledger reader.
+ import { Note, _commitments, _nullifiers } from "../ConfidentialNoteFungibleToken";
+ import { _issuerPk } from "../extensions/ConfidentialNoteFungibleTokenIssuer";
+ import {
+ AuditRecord,
+ AuditView,
+ _auditKey,
+ _auditTrail
+ } from "../extensions/ConfidentialNoteFungibleTokenAudit";
+ import { _deliveries } from "../extensions/ConfidentialNoteFungibleTokenDelivery";
+ import {
+ _supplyKey,
+ _encSupply,
+ _attestedSupply,
+ _attestationCount
+ } from "../extensions/ConfidentialNoteFungibleTokenPrivateSupply";
+ import { FullDelivery } from "../../crypto/NoteDelivery";
+ export { Note, AuditRecord, AuditView, FullDelivery };
+ export {
+ _issuerPk,
+ _commitments,
+ _nullifiers,
+ _auditKey,
+ _auditTrail,
+ _deliveries,
+ _supplyKey,
+ _encSupply,
+ _attestedSupply,
+ _attestationCount
+ };
+
+ export ledger _isInitialized: Boolean;
+ // Global seizure authority (`Hf(authoritySecret)`; governance-gated in a real
+ // deployment) and an auditable count of seizures performed.
+ export ledger _authorityPk: Field;
+ export ledger _seizureCount: Uint<64>;
+
+ // The seizure authority's secret (authorityPk = Hf(authoritySecret)).
+ witness wit_AuthoritySecret(): Bytes<32>;
+
+ /**
+ * @description One-shot initialization binding all four roles: the issuer
+ * (may mint), the seizure authority (may claw back), the audit key
+ * (decrypt-only viewing), and the supply key (attestation). A consuming
+ * contract typically calls this from its constructor, so the deployed token
+ * never exists ungoverned.
+ *
+ * Requirements:
+ *
+ * - Module is not already initialized.
+ */
+ export circuit initialize(
+ issuerPk: Field,
+ authorityPk: Field,
+ auditKey: JubjubPoint,
+ supplyKey: JubjubPoint
+ ): [] {
+ assert(!_isInitialized, "RegulatedConfidentialNoteFungibleToken: already initialized");
+ Issuer_initialize(issuerPk);
+ Audit_initialize(auditKey);
+ Supply_initialize(supplyKey);
+ _authorityPk = disclose(authorityPk);
+ _isInitialized = true;
+ }
+
+ /**
+ * @description Mints a note of `value` to `recipientPk`, audited and
+ * delivered. The minted amount is NOT written to public state — issuance
+ * stays hidden (unlike native shielded tokens, whose `shieldedMints` effect
+ * publishes it); the encrypted supply absorbs it homomorphically and the
+ * auditor reads it from the audit record.
+ *
+ * Requirements:
+ *
+ * - The caller proves the issuer secret.
+ *
+ * @circuitInfo k=17, rows=69322
+ */
+ export circuit mint(recipientPk: Field, recipientEncPk: JubjubPoint, value: Uint<128>): [] {
+ Issuer__assertIssuer();
+ const note = emitOutput(recipientPk, recipientEncPk, value, pad(32, "OZ:cnt:out"));
+ Core__mintNote(note, recipientPk);
+ Supply__addMinted(value);
+ }
+
+ /**
+ * @description Fully-private transfer: consumes the caller's input note and
+ * creates a recipient note of `value` plus a change note back to the sender,
+ * conserving value — both audited and delivered. Sender and recipient are
+ * hidden; the public ledger gains one nullifier, two commitments, and their
+ * ciphertexts.
+ *
+ * Requirements:
+ *
+ * - The input note is committed in the tree and unspent.
+ * - `value <= input.value`.
+ *
+ * @circuitInfo k=18, rows=135775
+ */
+ export circuit transfer(
+ recipientPk: Field,
+ recipientEncPk: JubjubPoint,
+ senderEncPk: JubjubPoint,
+ value: Uint<128>
+ ): [] {
+ const pk = Core__spenderPk();
+
+ // Peek at the input to size the change; the core re-reads the same witness
+ // and enforces conservation against it.
+ const input = Core__inputNote();
+ assert(input.value >= value,
+ "RegulatedConfidentialNoteFungibleToken: insufficient note value");
+ const changeValue = (input.value - value) as Uint<128>;
+
+ const outNote = emitOutput(recipientPk, recipientEncPk, value, pad(32, "OZ:cnt:out"));
+ const changeNote = emitOutput(pk, senderEncPk, changeValue, pad(32, "OZ:cnt:chg"));
+ Core__transfer(pk, recipientPk, outNote, changeNote);
+ }
+
+ /**
+ * @description Burns `value` from the caller's input note: consumes the note
+ * and re-issues only the change (audited and delivered), so `value` leaves
+ * circulation. Both the burned amount and the burner stay hidden; publicly a
+ * burn looks like any other spend. The encrypted supply absorbs the decrease.
+ *
+ * Requirements:
+ *
+ * - The input note is committed in the tree and unspent.
+ * - `value <= input.value`.
+ *
+ * @circuitInfo k=17, rows=82803
+ */
+ export circuit burn(senderEncPk: JubjubPoint, value: Uint<128>): [] {
+ const pk = Core__spenderPk();
+
+ const input = Core__inputNote();
+ assert(input.value >= value,
+ "RegulatedConfidentialNoteFungibleToken: insufficient note value");
+ const changeValue = (input.value - value) as Uint<128>;
+
+ const changeNote = emitOutput(pk, senderEncPk, changeValue, pad(32, "OZ:cnt:chg"));
+ Core__burn(pk, value, changeNote);
+ Supply__addBurned(value);
+ }
+
+ /**
+ * @description Regulated clawback: the authority consumes a target note it
+ * learned from the audit trail (supplied as the core's input-note witness)
+ * and re-mints the full value to `recoveryPk`, with the recovery note itself
+ * audited + delivered. Owner-spend and seizure race on the SAME nullifier, so
+ * they are mutually exclusive; the authority never needs the owner's spend
+ * secret. Value is conserved, and `_seizureCount` records the action
+ * publicly.
+ *
+ * In production the authority key is governance-gated (multisig), and
+ * per-user recovery keys would replace this single global key for least
+ * privilege.
+ *
+ * Requirements:
+ *
+ * - The caller proves the authority secret (`Hf(secret) == _authorityPk`).
+ * - The target note (owner pk + value + nonce) is committed and unspent.
+ *
+ * @circuitInfo k=17, rows=75043
+ */
+ export circuit seize(targetOwnerPk: Field, recoveryPk: Field, recoveryEncPk: JubjubPoint): [] {
+ assert(Core_derivePk(wit_AuthoritySecret()) == _authorityPk,
+ "RegulatedConfidentialNoteFungibleToken: not the authority");
+
+ const target = Core__consumeNote(targetOwnerPk);
+ const recoveryNote = emitOutput(recoveryPk, recoveryEncPk, target.value, pad(32, "OZ:cnt:out"));
+ Core__mintNote(recoveryNote, recoveryPk);
+
+ _seizureCount = disclose(_seizureCount + 1 as Uint<64>);
+ }
+
+ /**
+ * @description Publishes a proof-backed public supply total (see
+ * `ConfidentialNoteFungibleTokenPrivateSupply`). Run at a chosen cadence for public,
+ * non-inflatable supply while per-transaction amounts stay hidden.
+ *
+ * Requirements:
+ *
+ * - The caller proves the supply-key secret and the exact total.
+ *
+ * @circuitInfo k=13, rows=4720
+ */
+ export circuit attestSupply(total: Uint<128>): [] {
+ Supply_attestSupply(total);
+ }
+
+ /**
+ * @description Rotates the issuer key: the current issuer proves their
+ * secret and binds `newIssuerPk` (see the Issuer extension).
+ */
+ export circuit rotateIssuer(newIssuerPk: Field): [] {
+ Issuer__rotateIssuer(newIssuerPk);
+ }
+
+ /**
+ * @description Rotates the seizure authority: the current authority proves
+ * their secret and binds `newAuthorityPk`.
+ *
+ * Requirements:
+ *
+ * - The caller proves the CURRENT authority secret.
+ */
+ export circuit rotateAuthority(newAuthorityPk: Field): [] {
+ assert(Core_derivePk(wit_AuthoritySecret()) == _authorityPk,
+ "RegulatedConfidentialNoteFungibleToken: not the authority");
+ _authorityPk = disclose(newAuthorityPk);
+ }
+
+ /**
+ * @description Rotates the audit key: the current holder proves the audit
+ * secret scalar and binds `newKey`. Prior records stay readable by the old
+ * key; later outputs derive nonces from the new one (see the Audit
+ * extension).
+ */
+ export circuit rotateAuditKey(newKey: JubjubPoint): [] {
+ Audit__rotateAuditKey(newKey);
+ }
+
+ /**
+ * @description Rotates the supply key: the current holder proves the secret
+ * and the exact (undisclosed) running total, and `_encSupply` is
+ * re-encrypted under `newKey` in the same proof (see the PrivateSupply
+ * extension).
+ */
+ export circuit rotateSupplyKey(newKey: JubjubPoint, total: Uint<128>): [] {
+ Supply__rotateSupplyKey(newKey, total);
+ }
+
+ // Emission policy for one output note: the audit record derives the nonce,
+ // the delivery makes the note discoverable. Returns the note for the core to
+ // commit.
+ circuit emitOutput(ownerPk: Field, encPk: JubjubPoint, value: Uint<128>, slot: Bytes<32>): Core_Note {
+ const nonce = Audit__emitAuditedOutput(ownerPk, value, slot);
+ Delivery__deliver(encPk, value, nonce, slot);
+ return Core_Note { value: value, nonce: nonce };
+ }
+}
diff --git a/contracts/src/token/presets/RegulatedConfidentialNoteToken.compact b/contracts/src/token/presets/RegulatedConfidentialNoteToken.compact
deleted file mode 100644
index 992ba2b1b..000000000
--- a/contracts/src/token/presets/RegulatedConfidentialNoteToken.compact
+++ /dev/null
@@ -1,249 +0,0 @@
-// SPDX-License-Identifier: MIT
-// OpenZeppelin Compact Contracts (token/presets/RegulatedConfidentialNoteToken.compact)
-
-pragma language_version >= 0.23.0;
-
-/**
- * @contract RegulatedConfidentialNoteToken
- * @description DRAFT plug-and-play contract: the tier-4 confidential note
- * token wired for a regulated deployment. Deploy it as-is — the constructor
- * binds every role key; there is no separate initialization step.
- *
- * Guarantees: full graph privacy for users (amounts — including issuance and
- * burns — senders, and recipients hidden), complete visibility for a
- * designated auditor, escrow-free seizure for a designated authority, and
- * confidential-but-attestable supply.
- *
- * Composition (each piece is independently reusable):
- *
- * - `ConfidentialNoteToken` — the token core (conservation, single-spend,
- * issuer gate); this preset drives its `_`-building blocks so output nonces
- * come from the audit channel instead of the core default,
- * - `extensions/ConfidentialNoteTokenAudit` — mandatory auditor viewing; each
- * output nonce derives from the audit ECDH, so every note this contract
- * creates is auditor-recoverable BY CONSTRUCTION,
- * - `extensions/ConfidentialNoteTokenDelivery` — on-chain note delivery, so
- * recipients discover funds from chain data alone (no out-of-band channel),
- * - `extensions/ConfidentialNoteTokenSupply` — homomorphic encrypted supply
- * with proof-backed public attestation.
- *
- * Roles: the ISSUER may mint, the AUTHORITY may seize, the AUDIT key reads
- * everything (never spends), the SUPPLY key attests totals. All four bind at
- * deploy time.
- *
- * Seizure needs no key escrow: the core nullifier depends only on the nonce,
- * so the owner and the authority derive the SAME nullifier, making owner-spend
- * and seizure mutually exclusive (first to land wins). The authority learns
- * the target note from the audit trail and re-mints its value to a recovery
- * owner, itself audited and delivered.
- *
- * What the public sees: commitment inserts, nullifiers, ciphertexts, the
- * seizure counter, and attested supply totals. Amounts, senders, and
- * recipients stay hidden.
- *
- * See `token/docs/confidential-note-token.md` for the full design and the
- * auditor/compliance rationale.
- *
- * @dev The randomness witnesses (`wit_AuditRandomness`,
- * `wit_DeliveryRandomness`, `wit_SupplyRandomness`) MUST each return a fresh,
- * secret seed per invocation (see `crypto/EcdhMask` freshness rules).
- *
- * @dev NOT audited, NOT production.
- */
-
-import CompactStandardLibrary;
-import "../ConfidentialNoteToken" prefix CNT_;
-import "../extensions/ConfidentialNoteTokenAudit" prefix Audit_;
-import "../extensions/ConfidentialNoteTokenDelivery" prefix Delivery_;
-import "../extensions/ConfidentialNoteTokenSupply" prefix Supply_;
-import "../../crypto/NoteDelivery" prefix NoteDelivery_;
-
-// Surface the composed modules' observable state under stable names, for
-// wallets (commitment tree, deliveries), auditors (audit trail), and
-// indexers (supply): a prefix-only import would keep it out of the generated
-// ledger reader.
-import {
- _commitments,
- _nullifiers
-} from "../ConfidentialNoteToken";
-import { _auditKey, _auditTrail } from "../extensions/ConfidentialNoteTokenAudit";
-import { _deliveries } from "../extensions/ConfidentialNoteTokenDelivery";
-import {
- _supplyKey,
- _encSupply,
- _attestedSupply,
- _attestationCount
-} from "../extensions/ConfidentialNoteTokenSupply";
-export {
- _commitments,
- _nullifiers,
- _auditKey,
- _auditTrail,
- _deliveries,
- _supplyKey,
- _encSupply,
- _attestedSupply,
- _attestationCount
-};
-
-export { CNT_Note, Audit_AuditRecord, Audit_AuditView, NoteDelivery_FullDelivery }
-
-// Global seizure authority (`Hf(authoritySecret)`; governance-gated in a real
-// deployment) and an auditable count of seizures performed.
-export ledger _authorityPk: Field;
-export ledger _seizureCount: Uint<64>;
-
-// The seizure authority's secret (authorityPk = Hf(authoritySecret)).
-witness wit_AuthoritySecret(): Bytes<32>;
-
-/**
- * @description Binds all four roles at deploy time: the issuer (may mint),
- * the seizure authority (may claw back), the audit key (decrypt-only
- * viewing), and the supply key (attestation).
- */
-constructor(
- issuerPk: Field,
- authorityPk: Field,
- auditKey: JubjubPoint,
- supplyKey: JubjubPoint
-) {
- CNT_initialize(issuerPk);
- Audit_initialize(auditKey);
- Supply_initialize(supplyKey);
- _authorityPk = disclose(authorityPk);
-}
-
-/**
- * @description Mints a note of `value` to `recipientPk`, audited and
- * delivered. The minted amount is NOT written to public state — issuance
- * stays hidden (unlike native shielded tokens, whose `shieldedMints` effect
- * publishes it); the encrypted supply absorbs it homomorphically and the
- * auditor reads it from the audit record.
- *
- * Requirements:
- *
- * - The caller proves the issuer secret.
- *
- * @circuitInfo k=17, rows=69322
- */
-export circuit mint(recipientPk: Field, recipientEncPk: JubjubPoint, value: Uint<128>): [] {
- CNT__assertIssuer();
- const note = emitOutput(recipientPk, recipientEncPk, value, pad(32, "OZ:cnt:out"));
- CNT__mint(note, recipientPk);
- Supply__addMinted(value);
-}
-
-/**
- * @description Fully-private transfer: consumes the caller's input note and
- * creates a recipient note of `value` plus a change note back to the sender,
- * conserving value — both audited and delivered. Sender and recipient are
- * hidden; the public ledger gains one nullifier, two commitments, and their
- * ciphertexts.
- *
- * Requirements:
- *
- * - The input note is committed in the tree and unspent.
- * - `value <= input.value`.
- *
- * @circuitInfo k=18, rows=135775
- */
-export circuit transfer(
- recipientPk: Field,
- recipientEncPk: JubjubPoint,
- senderEncPk: JubjubPoint,
- value: Uint<128>
-): [] {
- const pk = CNT__spenderPk();
-
- // Peek at the input to size the change; the core re-reads the same witness
- // and enforces conservation against it.
- const input = CNT__inputNote();
- assert(input.value >= value,
- "RegulatedConfidentialNoteToken: insufficient note value");
- const changeValue = (input.value - value) as Uint<128>;
-
- const outNote = emitOutput(recipientPk, recipientEncPk, value, pad(32, "OZ:cnt:out"));
- const changeNote = emitOutput(pk, senderEncPk, changeValue, pad(32, "OZ:cnt:chg"));
- CNT__transfer(pk, recipientPk, outNote, changeNote);
-}
-
-/**
- * @description Burns `value` from the caller's input note: consumes the note
- * and re-issues only the change (audited and delivered), so `value` leaves
- * circulation. Both the burned amount and the burner stay hidden; publicly a
- * burn looks like any other spend. The encrypted supply absorbs the decrease.
- *
- * Requirements:
- *
- * - The input note is committed in the tree and unspent.
- * - `value <= input.value`.
- *
- * @circuitInfo k=17, rows=82803
- */
-export circuit burn(senderEncPk: JubjubPoint, value: Uint<128>): [] {
- const pk = CNT__spenderPk();
-
- const input = CNT__inputNote();
- assert(input.value >= value,
- "RegulatedConfidentialNoteToken: insufficient note value");
- const changeValue = (input.value - value) as Uint<128>;
-
- const changeNote = emitOutput(pk, senderEncPk, changeValue, pad(32, "OZ:cnt:chg"));
- CNT__burn(pk, value, changeNote);
- Supply__addBurned(value);
-}
-
-/**
- * @description Regulated clawback: the authority consumes a target note it
- * learned from the audit trail (supplied as the core's input-note witness)
- * and re-mints the full value to `recoveryPk`, with the recovery note itself
- * audited + delivered. Owner-spend and seizure race on the SAME nullifier, so
- * they are mutually exclusive; the authority never needs the owner's spend
- * secret. Value is conserved, and `_seizureCount` records the action
- * publicly.
- *
- * In production the authority key is governance-gated (multisig), and
- * per-user recovery keys would replace this single global key for least
- * privilege.
- *
- * Requirements:
- *
- * - The caller proves the authority secret (`Hf(secret) == _authorityPk`).
- * - The target note (owner pk + value + nonce) is committed and unspent.
- *
- * @circuitInfo k=17, rows=75043
- */
-export circuit seize(targetOwnerPk: Field, recoveryPk: Field, recoveryEncPk: JubjubPoint): [] {
- assert(CNT_derivePk(wit_AuthoritySecret()) == _authorityPk,
- "RegulatedConfidentialNoteToken: not the authority");
-
- const target = CNT__consumeNote(targetOwnerPk);
- const recoveryNote = emitOutput(recoveryPk, recoveryEncPk, target.value, pad(32, "OZ:cnt:out"));
- CNT__mint(recoveryNote, recoveryPk);
-
- _seizureCount = disclose(_seizureCount + 1 as Uint<64>);
-}
-
-/**
- * @description Publishes a proof-backed public supply total (see
- * `ConfidentialNoteTokenSupply`). Run at a chosen cadence for public,
- * non-inflatable supply while per-transaction amounts stay hidden.
- *
- * Requirements:
- *
- * - The caller proves the supply-key secret and the exact total.
- *
- * @circuitInfo k=13, rows=4720
- */
-export circuit attestSupply(total: Uint<128>): [] {
- Supply_attestSupply(total);
-}
-
-// Emission policy for one output note: the audit record derives the nonce,
-// the delivery makes the note discoverable. Returns the note for the core to
-// commit.
-circuit emitOutput(ownerPk: Field, encPk: JubjubPoint, value: Uint<128>, slot: Bytes<32>): CNT_Note {
- const nonce = Audit__emitAuditedOutput(ownerPk, value, slot);
- Delivery__deliver(encPk, value, nonce, slot);
- return CNT_Note { value: value, nonce: nonce };
-}
diff --git a/contracts/src/token/test/FungibleToken.test.ts b/contracts/src/token/test/FungibleToken.test.ts
index 108de8b3e..adcbefc2e 100644
--- a/contracts/src/token/test/FungibleToken.test.ts
+++ b/contracts/src/token/test/FungibleToken.test.ts
@@ -354,66 +354,70 @@ describe('FungibleToken', () => {
});
describe('_unsafeTransfer', () => {
- describe.each(
- recipientTypes,
- )('when the recipient is a %s', (_, recipient) => {
- beforeEach(async () => {
- await token._mint(OWNER.either, AMOUNT);
- expect(await token.balanceOf(OWNER.either)).toEqual(AMOUNT);
- expect(await token.balanceOf(recipient)).toEqual(0n);
- });
-
- afterEach(async () => {
- expect(await token.totalSupply()).toEqual(AMOUNT);
- });
-
- it('should transfer partial', async () => {
- await token.privateState.injectSecretKey(OWNER.secretKey);
-
- const partialAmt = AMOUNT - 1n;
- const txSuccess = await token._unsafeTransfer(recipient, partialAmt);
+ describe.each(recipientTypes)(
+ 'when the recipient is a %s',
+ (_, recipient) => {
+ beforeEach(async () => {
+ await token._mint(OWNER.either, AMOUNT);
+ expect(await token.balanceOf(OWNER.either)).toEqual(AMOUNT);
+ expect(await token.balanceOf(recipient)).toEqual(0n);
+ });
+
+ afterEach(async () => {
+ expect(await token.totalSupply()).toEqual(AMOUNT);
+ });
+
+ it('should transfer partial', async () => {
+ await token.privateState.injectSecretKey(OWNER.secretKey);
+
+ const partialAmt = AMOUNT - 1n;
+ const txSuccess = await token._unsafeTransfer(
+ recipient,
+ partialAmt,
+ );
- expect(txSuccess).toBe(true);
- expect(await token.balanceOf(OWNER.either)).toEqual(1n);
- expect(await token.balanceOf(recipient)).toEqual(partialAmt);
- });
+ expect(txSuccess).toBe(true);
+ expect(await token.balanceOf(OWNER.either)).toEqual(1n);
+ expect(await token.balanceOf(recipient)).toEqual(partialAmt);
+ });
- it('should transfer full', async () => {
- await token.privateState.injectSecretKey(OWNER.secretKey);
+ it('should transfer full', async () => {
+ await token.privateState.injectSecretKey(OWNER.secretKey);
- const txSuccess = await token._unsafeTransfer(recipient, AMOUNT);
+ const txSuccess = await token._unsafeTransfer(recipient, AMOUNT);
- expect(txSuccess).toBe(true);
- expect(await token.balanceOf(OWNER.either)).toEqual(0n);
- expect(await token.balanceOf(recipient)).toEqual(AMOUNT);
- });
+ expect(txSuccess).toBe(true);
+ expect(await token.balanceOf(OWNER.either)).toEqual(0n);
+ expect(await token.balanceOf(recipient)).toEqual(AMOUNT);
+ });
- it('should fail with insufficient balance', async () => {
- await token.privateState.injectSecretKey(OWNER.secretKey);
+ it('should fail with insufficient balance', async () => {
+ await token.privateState.injectSecretKey(OWNER.secretKey);
- await expect(
- token._unsafeTransfer(recipient, AMOUNT + 1n),
- ).rejects.toThrow('FungibleToken: insufficient balance');
- });
+ await expect(
+ token._unsafeTransfer(recipient, AMOUNT + 1n),
+ ).rejects.toThrow('FungibleToken: insufficient balance');
+ });
- it('should allow transfer of 0 tokens', async () => {
- await token.privateState.injectSecretKey(OWNER.secretKey);
+ it('should allow transfer of 0 tokens', async () => {
+ await token.privateState.injectSecretKey(OWNER.secretKey);
- const txSuccess = await token._unsafeTransfer(recipient, 0n);
+ const txSuccess = await token._unsafeTransfer(recipient, 0n);
- expect(txSuccess).toBe(true);
- expect(await token.balanceOf(OWNER.either)).toEqual(AMOUNT);
- expect(await token.balanceOf(recipient)).toEqual(0n);
- });
+ expect(txSuccess).toBe(true);
+ expect(await token.balanceOf(OWNER.either)).toEqual(AMOUNT);
+ expect(await token.balanceOf(recipient)).toEqual(0n);
+ });
- it('should handle transfer with empty _balances', async () => {
- await token.privateState.injectSecretKey(SPENDER.secretKey);
+ it('should handle transfer with empty _balances', async () => {
+ await token.privateState.injectSecretKey(SPENDER.secretKey);
- await expect(token._unsafeTransfer(recipient, 1n)).rejects.toThrow(
- 'FungibleToken: insufficient balance',
- );
- });
- });
+ await expect(token._unsafeTransfer(recipient, 1n)).rejects.toThrow(
+ 'FungibleToken: insufficient balance',
+ );
+ });
+ },
+ );
it('should fail with transfer to zero (accountId)', async () => {
await token._mint(OWNER.either, AMOUNT);
@@ -637,89 +641,90 @@ describe('FungibleToken', () => {
expect(await token.totalSupply()).toEqual(AMOUNT);
});
- describe.each(
- recipientTypes,
- )('when the recipient is a %s', (_, recipient) => {
- it('should transferFrom spender (partial)', async () => {
- await token.privateState.injectSecretKey(SPENDER.secretKey);
-
- const partialAmt = AMOUNT - 1n;
- const txSuccess = await token._unsafeTransferFrom(
- OWNER.either,
- recipient,
- partialAmt,
- );
- expect(txSuccess).toBe(true);
-
- expect(await token.balanceOf(OWNER.either)).toEqual(1n);
- expect(await token.balanceOf(recipient)).toEqual(partialAmt);
- expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual(
- 1n,
- );
- });
+ describe.each(recipientTypes)(
+ 'when the recipient is a %s',
+ (_, recipient) => {
+ it('should transferFrom spender (partial)', async () => {
+ await token.privateState.injectSecretKey(SPENDER.secretKey);
- it('should transferFrom spender (full)', async () => {
- await token.privateState.injectSecretKey(SPENDER.secretKey);
-
- const txSuccess = await token._unsafeTransferFrom(
- OWNER.either,
- recipient,
- AMOUNT,
- );
- expect(txSuccess).toBe(true);
-
- expect(await token.balanceOf(OWNER.either)).toEqual(0n);
- expect(await token.balanceOf(recipient)).toEqual(AMOUNT);
- expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual(
- 0n,
- );
- });
-
- it('should transferFrom and not consume infinite allowance', async () => {
- await token.privateState.injectSecretKey(OWNER.secretKey);
- await token.approve(SPENDER.either, MAX_UINT128);
-
- await token.privateState.injectSecretKey(SPENDER.secretKey);
- const txSuccess = await token._unsafeTransferFrom(
- OWNER.either,
- recipient,
- AMOUNT,
- );
- expect(txSuccess).toBe(true);
-
- expect(await token.balanceOf(OWNER.either)).toEqual(0n);
- expect(await token.balanceOf(recipient)).toEqual(AMOUNT);
- expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual(
- MAX_UINT128,
- );
- });
-
- it('should fail when transfer amount exceeds allowance', async () => {
- await token.privateState.injectSecretKey(SPENDER.secretKey);
-
- await expect(
- token._unsafeTransferFrom(OWNER.either, recipient, AMOUNT + 1n),
- ).rejects.toThrow('FungibleToken: insufficient allowance');
- });
-
- it('should fail when transfer amount exceeds balance', async () => {
- await token.privateState.injectSecretKey(OWNER.secretKey);
- await token.approve(SPENDER.either, AMOUNT + 1n);
+ const partialAmt = AMOUNT - 1n;
+ const txSuccess = await token._unsafeTransferFrom(
+ OWNER.either,
+ recipient,
+ partialAmt,
+ );
+ expect(txSuccess).toBe(true);
- await token.privateState.injectSecretKey(SPENDER.secretKey);
- await expect(
- token._unsafeTransferFrom(OWNER.either, recipient, AMOUNT + 1n),
- ).rejects.toThrow('FungibleToken: insufficient balance');
- });
+ expect(await token.balanceOf(OWNER.either)).toEqual(1n);
+ expect(await token.balanceOf(recipient)).toEqual(partialAmt);
+ expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual(
+ 1n,
+ );
+ });
- it('should fail when spender does not have allowance', async () => {
- await token.privateState.injectSecretKey(UNAUTHORIZED.secretKey);
+ it('should transferFrom spender (full)', async () => {
+ await token.privateState.injectSecretKey(SPENDER.secretKey);
- await expect(
- token._unsafeTransferFrom(OWNER.either, recipient, AMOUNT),
- ).rejects.toThrow('FungibleToken: insufficient allowance');
- });
- });
+ const txSuccess = await token._unsafeTransferFrom(
+ OWNER.either,
+ recipient,
+ AMOUNT,
+ );
+ expect(txSuccess).toBe(true);
+
+ expect(await token.balanceOf(OWNER.either)).toEqual(0n);
+ expect(await token.balanceOf(recipient)).toEqual(AMOUNT);
+ expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual(
+ 0n,
+ );
+ });
+
+ it('should transferFrom and not consume infinite allowance', async () => {
+ await token.privateState.injectSecretKey(OWNER.secretKey);
+ await token.approve(SPENDER.either, MAX_UINT128);
+
+ await token.privateState.injectSecretKey(SPENDER.secretKey);
+ const txSuccess = await token._unsafeTransferFrom(
+ OWNER.either,
+ recipient,
+ AMOUNT,
+ );
+ expect(txSuccess).toBe(true);
+
+ expect(await token.balanceOf(OWNER.either)).toEqual(0n);
+ expect(await token.balanceOf(recipient)).toEqual(AMOUNT);
+ expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual(
+ MAX_UINT128,
+ );
+ });
+
+ it('should fail when transfer amount exceeds allowance', async () => {
+ await token.privateState.injectSecretKey(SPENDER.secretKey);
+
+ await expect(
+ token._unsafeTransferFrom(OWNER.either, recipient, AMOUNT + 1n),
+ ).rejects.toThrow('FungibleToken: insufficient allowance');
+ });
+
+ it('should fail when transfer amount exceeds balance', async () => {
+ await token.privateState.injectSecretKey(OWNER.secretKey);
+ await token.approve(SPENDER.either, AMOUNT + 1n);
+
+ await token.privateState.injectSecretKey(SPENDER.secretKey);
+ await expect(
+ token._unsafeTransferFrom(OWNER.either, recipient, AMOUNT + 1n),
+ ).rejects.toThrow('FungibleToken: insufficient balance');
+ });
+
+ it('should fail when spender does not have allowance', async () => {
+ await token.privateState.injectSecretKey(UNAUTHORIZED.secretKey);
+
+ await expect(
+ token._unsafeTransferFrom(OWNER.either, recipient, AMOUNT),
+ ).rejects.toThrow('FungibleToken: insufficient allowance');
+ });
+ },
+ );
it('should fail to transfer to the zero address (accountId)', async () => {
await token.privateState.injectSecretKey(SPENDER.secretKey);
@@ -771,44 +776,49 @@ describe('FungibleToken', () => {
expect(await token.totalSupply()).toEqual(AMOUNT);
});
- describe.each(
- recipientTypes,
- )('when the recipient is a %s', (_, recipient) => {
- it('should update balances (partial)', async () => {
- const partialAmt = AMOUNT - 1n;
- await token._unsafeUncheckedTransfer(
- OWNER.either,
- recipient,
- partialAmt,
- );
-
- expect(await token.balanceOf(OWNER.either)).toEqual(1n);
- expect(await token.balanceOf(recipient)).toEqual(partialAmt);
- });
-
- it('should update balances (full)', async () => {
- await token._unsafeUncheckedTransfer(OWNER.either, recipient, AMOUNT);
+ describe.each(recipientTypes)(
+ 'when the recipient is a %s',
+ (_, recipient) => {
+ it('should update balances (partial)', async () => {
+ const partialAmt = AMOUNT - 1n;
+ await token._unsafeUncheckedTransfer(
+ OWNER.either,
+ recipient,
+ partialAmt,
+ );
- expect(await token.balanceOf(OWNER.either)).toEqual(0n);
- expect(await token.balanceOf(recipient)).toEqual(AMOUNT);
- });
+ expect(await token.balanceOf(OWNER.either)).toEqual(1n);
+ expect(await token.balanceOf(recipient)).toEqual(partialAmt);
+ });
- it('should fail when transfer amount exceeds balance', async () => {
- await expect(
- token._unsafeUncheckedTransfer(
+ it('should update balances (full)', async () => {
+ await token._unsafeUncheckedTransfer(
OWNER.either,
recipient,
- AMOUNT + 1n,
- ),
- ).rejects.toThrow('FungibleToken: insufficient balance');
- });
-
- it('should fail when transfer from zero', async () => {
- await expect(
- token._unsafeUncheckedTransfer(ZERO_CONTRACT, recipient, AMOUNT),
- ).rejects.toThrow('FungibleToken: invalid sender');
- });
- });
+ AMOUNT,
+ );
+
+ expect(await token.balanceOf(OWNER.either)).toEqual(0n);
+ expect(await token.balanceOf(recipient)).toEqual(AMOUNT);
+ });
+
+ it('should fail when transfer amount exceeds balance', async () => {
+ await expect(
+ token._unsafeUncheckedTransfer(
+ OWNER.either,
+ recipient,
+ AMOUNT + 1n,
+ ),
+ ).rejects.toThrow('FungibleToken: insufficient balance');
+ });
+
+ it('should fail when transfer from zero', async () => {
+ await expect(
+ token._unsafeUncheckedTransfer(ZERO_CONTRACT, recipient, AMOUNT),
+ ).rejects.toThrow('FungibleToken: invalid sender');
+ });
+ },
+ );
it('should fail when transfer to zero (accountId)', async () => {
await expect(
@@ -917,31 +927,32 @@ describe('FungibleToken', () => {
});
describe('_unsafeMint', () => {
- describe.each(
- recipientTypes,
- )('when the recipient is a %s', (_, recipient) => {
- it('should mint and update supply', async () => {
- expect(await token.totalSupply()).toEqual(0n);
-
- await token._unsafeMint(recipient, AMOUNT);
- expect(await token.totalSupply()).toEqual(AMOUNT);
- expect(await token.balanceOf(recipient)).toEqual(AMOUNT);
- });
-
- it('should catch mint overflow', async () => {
- await token._unsafeMint(recipient, MAX_UINT128);
-
- await expect(token._unsafeMint(recipient, 1n)).rejects.toThrow(
- 'FungibleToken: arithmetic overflow',
- );
- });
-
- it('should allow mint of 0 tokens', async () => {
- await token._unsafeMint(recipient, 0n);
- expect(await token.totalSupply()).toEqual(0n);
- expect(await token.balanceOf(recipient)).toEqual(0n);
- });
- });
+ describe.each(recipientTypes)(
+ 'when the recipient is a %s',
+ (_, recipient) => {
+ it('should mint and update supply', async () => {
+ expect(await token.totalSupply()).toEqual(0n);
+
+ await token._unsafeMint(recipient, AMOUNT);
+ expect(await token.totalSupply()).toEqual(AMOUNT);
+ expect(await token.balanceOf(recipient)).toEqual(AMOUNT);
+ });
+
+ it('should catch mint overflow', async () => {
+ await token._unsafeMint(recipient, MAX_UINT128);
+
+ await expect(token._unsafeMint(recipient, 1n)).rejects.toThrow(
+ 'FungibleToken: arithmetic overflow',
+ );
+ });
+
+ it('should allow mint of 0 tokens', async () => {
+ await token._unsafeMint(recipient, 0n);
+ expect(await token.totalSupply()).toEqual(0n);
+ expect(await token.balanceOf(recipient)).toEqual(0n);
+ });
+ },
+ );
it('should not mint to zero (accountId)', async () => {
await expect(token._unsafeMint(ZERO_ACCOUNT, AMOUNT)).rejects.toThrow(
diff --git a/contracts/src/token/test/MultiToken.test.ts b/contracts/src/token/test/MultiToken.test.ts
index a7376e5ee..04461a34a 100644
--- a/contracts/src/token/test/MultiToken.test.ts
+++ b/contracts/src/token/test/MultiToken.test.ts
@@ -668,134 +668,139 @@ describe('MultiToken', () => {
await token.privateState.injectSecretKey(caller.secretKey);
});
- describe.each(
- recipientTypes,
- )('when the recipient is a %s', (_, recipient) => {
- it('should transfer whole', async () => {
- await token._unsafeTransferFrom(
- OWNER.either,
- recipient,
- TOKEN_ID,
- AMOUNT,
- );
-
- expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n);
- expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(AMOUNT);
- });
-
- it('should transfer partial', async () => {
- const partialAmt = AMOUNT - 1n;
- await token._unsafeTransferFrom(
- OWNER.either,
- recipient,
- TOKEN_ID,
- partialAmt,
- );
-
- expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(
- AMOUNT - partialAmt,
- );
- expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(
- partialAmt,
- );
- });
-
- it('should allow transfer of 0 tokens', async () => {
- await token._unsafeTransferFrom(
- OWNER.either,
- recipient,
- TOKEN_ID,
- 0n,
- );
-
- expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(
- AMOUNT,
- );
- expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(0n);
- });
+ describe.each(recipientTypes)(
+ 'when the recipient is a %s',
+ (_, recipient) => {
+ it('should transfer whole', async () => {
+ await token._unsafeTransferFrom(
+ OWNER.either,
+ recipient,
+ TOKEN_ID,
+ AMOUNT,
+ );
- it('should handle self-transfer', async () => {
- await token._unsafeTransferFrom(
- OWNER.either,
- OWNER.either,
- TOKEN_ID,
- AMOUNT,
- );
- expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(
- AMOUNT,
- );
- });
+ expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n);
+ expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(
+ AMOUNT,
+ );
+ });
- it('should handle MAX_UINT128 transfer amount', async () => {
- await token._mint(OWNER.either, TOKEN_ID, MAX_UINT128 - AMOUNT);
+ it('should transfer partial', async () => {
+ const partialAmt = AMOUNT - 1n;
+ await token._unsafeTransferFrom(
+ OWNER.either,
+ recipient,
+ TOKEN_ID,
+ partialAmt,
+ );
- await token._unsafeTransferFrom(
- OWNER.either,
- recipient,
- TOKEN_ID,
- MAX_UINT128,
- );
- expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(
- MAX_UINT128,
- );
- });
+ expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(
+ AMOUNT - partialAmt,
+ );
+ expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(
+ partialAmt,
+ );
+ });
- it('should handle rapid state changes', async () => {
- await token.privateState.injectSecretKey(OWNER.secretKey);
- await token.setApprovalForAll(SPENDER.either, true);
+ it('should allow transfer of 0 tokens', async () => {
+ await token._unsafeTransferFrom(
+ OWNER.either,
+ recipient,
+ TOKEN_ID,
+ 0n,
+ );
- await token._unsafeTransferFrom(
- OWNER.either,
- recipient,
- TOKEN_ID,
- AMOUNT,
- );
- expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(AMOUNT);
+ expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(
+ AMOUNT,
+ );
+ expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(0n);
+ });
- await token.setApprovalForAll(SPENDER.either, false);
- expect(
- await token.isApprovedForAll(OWNER.either, SPENDER.either),
- ).toBe(false);
+ it('should handle self-transfer', async () => {
+ await token._unsafeTransferFrom(
+ OWNER.either,
+ OWNER.either,
+ TOKEN_ID,
+ AMOUNT,
+ );
+ expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(
+ AMOUNT,
+ );
+ });
- await token.setApprovalForAll(SPENDER.either, true);
- expect(
- await token.isApprovedForAll(OWNER.either, SPENDER.either),
- ).toBe(true);
- });
+ it('should handle MAX_UINT128 transfer amount', async () => {
+ await token._mint(OWNER.either, TOKEN_ID, MAX_UINT128 - AMOUNT);
- it('should fail with insufficient balance', async () => {
- await expect(
- token._unsafeTransferFrom(
+ await token._unsafeTransferFrom(
OWNER.either,
recipient,
TOKEN_ID,
- AMOUNT + 1n,
- ),
- ).rejects.toThrow('MultiToken: insufficient balance');
- });
-
- it('should fail with nonexistent id', async () => {
- await expect(
- token._unsafeTransferFrom(
+ MAX_UINT128,
+ );
+ expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(
+ MAX_UINT128,
+ );
+ });
+
+ it('should handle rapid state changes', async () => {
+ await token.privateState.injectSecretKey(OWNER.secretKey);
+ await token.setApprovalForAll(SPENDER.either, true);
+
+ await token._unsafeTransferFrom(
OWNER.either,
recipient,
- NONEXISTENT_ID,
- AMOUNT,
- ),
- ).rejects.toThrow('MultiToken: insufficient balance');
- });
-
- it('should fail with transfer from zero', async () => {
- await expect(
- token._unsafeTransferFrom(
- ZERO_ACCOUNT,
- recipient,
TOKEN_ID,
AMOUNT,
- ),
- ).rejects.toThrow('MultiToken: unauthorized operator');
- });
- });
+ );
+ expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(
+ AMOUNT,
+ );
+
+ await token.setApprovalForAll(SPENDER.either, false);
+ expect(
+ await token.isApprovedForAll(OWNER.either, SPENDER.either),
+ ).toBe(false);
+
+ await token.setApprovalForAll(SPENDER.either, true);
+ expect(
+ await token.isApprovedForAll(OWNER.either, SPENDER.either),
+ ).toBe(true);
+ });
+
+ it('should fail with insufficient balance', async () => {
+ await expect(
+ token._unsafeTransferFrom(
+ OWNER.either,
+ recipient,
+ TOKEN_ID,
+ AMOUNT + 1n,
+ ),
+ ).rejects.toThrow('MultiToken: insufficient balance');
+ });
+
+ it('should fail with nonexistent id', async () => {
+ await expect(
+ token._unsafeTransferFrom(
+ OWNER.either,
+ recipient,
+ NONEXISTENT_ID,
+ AMOUNT,
+ ),
+ ).rejects.toThrow('MultiToken: insufficient balance');
+ });
+
+ it('should fail with transfer from zero', async () => {
+ await expect(
+ token._unsafeTransferFrom(
+ ZERO_ACCOUNT,
+ recipient,
+ TOKEN_ID,
+ AMOUNT,
+ ),
+ ).rejects.toThrow('MultiToken: unauthorized operator');
+ });
+ },
+ );
it('should fail with transfer to zero (id)', async () => {
await expect(
@@ -921,75 +926,81 @@ describe('MultiToken', () => {
await token.privateState.injectSecretKey(UNAUTHORIZED.secretKey);
});
- describe.each(
- recipientTypes,
- )('when recipient is %s', (_, recipient) => {
- it('should fail when transfer whole', async () => {
- await expect(
- token._unsafeTransferFrom(
- OWNER.either,
- recipient,
- TOKEN_ID,
- AMOUNT,
- ),
- ).rejects.toThrow('MultiToken: unauthorized operator');
- });
-
- it('should fail when transfer partial', async () => {
- const partialAmt = AMOUNT - 1n;
- await expect(
- token._unsafeTransferFrom(
- OWNER.either,
- recipient,
- TOKEN_ID,
- partialAmt,
- ),
- ).rejects.toThrow('MultiToken: unauthorized operator');
- });
-
- it('should fail when transfer zero', async () => {
- await expect(
- token._unsafeTransferFrom(OWNER.either, recipient, TOKEN_ID, 0n),
- ).rejects.toThrow('MultiToken: unauthorized operator');
- });
-
- it('should fail with insufficient balance', async () => {
- await expect(
- token._unsafeTransferFrom(
- OWNER.either,
- recipient,
- TOKEN_ID,
- AMOUNT + 1n,
- ),
- ).rejects.toThrow('MultiToken: unauthorized operator');
- });
-
- it('should fail with nonexistent id', async () => {
- await expect(
- token._unsafeTransferFrom(
- OWNER.either,
- recipient,
- NONEXISTENT_ID,
- AMOUNT,
- ),
- ).rejects.toThrow('MultiToken: unauthorized operator');
- });
-
- it('should fail with transfer from zero', async () => {
- // With witness-based identity, the caller is H(sk) which is
- // always non-zero. Transferring from ZERO_ACCOUNT means
- // canonFrom != caller → isApprovedForAll(zeroAccount, caller) → false
- // → "unauthorized operator"
- await expect(
- token._unsafeTransferFrom(
- ZERO_ACCOUNT,
- recipient,
- TOKEN_ID,
- AMOUNT,
- ),
- ).rejects.toThrow('MultiToken: unauthorized operator');
- });
- });
+ describe.each(recipientTypes)(
+ 'when recipient is %s',
+ (_, recipient) => {
+ it('should fail when transfer whole', async () => {
+ await expect(
+ token._unsafeTransferFrom(
+ OWNER.either,
+ recipient,
+ TOKEN_ID,
+ AMOUNT,
+ ),
+ ).rejects.toThrow('MultiToken: unauthorized operator');
+ });
+
+ it('should fail when transfer partial', async () => {
+ const partialAmt = AMOUNT - 1n;
+ await expect(
+ token._unsafeTransferFrom(
+ OWNER.either,
+ recipient,
+ TOKEN_ID,
+ partialAmt,
+ ),
+ ).rejects.toThrow('MultiToken: unauthorized operator');
+ });
+
+ it('should fail when transfer zero', async () => {
+ await expect(
+ token._unsafeTransferFrom(
+ OWNER.either,
+ recipient,
+ TOKEN_ID,
+ 0n,
+ ),
+ ).rejects.toThrow('MultiToken: unauthorized operator');
+ });
+
+ it('should fail with insufficient balance', async () => {
+ await expect(
+ token._unsafeTransferFrom(
+ OWNER.either,
+ recipient,
+ TOKEN_ID,
+ AMOUNT + 1n,
+ ),
+ ).rejects.toThrow('MultiToken: unauthorized operator');
+ });
+
+ it('should fail with nonexistent id', async () => {
+ await expect(
+ token._unsafeTransferFrom(
+ OWNER.either,
+ recipient,
+ NONEXISTENT_ID,
+ AMOUNT,
+ ),
+ ).rejects.toThrow('MultiToken: unauthorized operator');
+ });
+
+ it('should fail with transfer from zero', async () => {
+ // With witness-based identity, the caller is H(sk) which is
+ // always non-zero. Transferring from ZERO_ACCOUNT means
+ // canonFrom != caller → isApprovedForAll(zeroAccount, caller) → false
+ // → "unauthorized operator"
+ await expect(
+ token._unsafeTransferFrom(
+ ZERO_ACCOUNT,
+ recipient,
+ TOKEN_ID,
+ AMOUNT,
+ ),
+ ).rejects.toThrow('MultiToken: unauthorized operator');
+ });
+ },
+ );
});
});
@@ -1120,79 +1131,82 @@ describe('MultiToken', () => {
expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(0n);
});
- describe.each(
- recipientTypes,
- )('when the recipient is a %s', (_, recipient) => {
- it('should transfer whole', async () => {
- await token._unsafeTransfer(
- OWNER.either,
- recipient,
- TOKEN_ID,
- AMOUNT,
- );
-
- expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n);
- expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(AMOUNT);
- });
-
- it('should transfer partial', async () => {
- const partialAmt = AMOUNT - 1n;
- await token._unsafeTransfer(
- OWNER.either,
- recipient,
- TOKEN_ID,
- partialAmt,
- );
-
- expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(
- AMOUNT - partialAmt,
- );
- expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(
- partialAmt,
- );
- });
-
- it('should allow transfer of 0 tokens', async () => {
- await token._unsafeTransfer(OWNER.either, recipient, TOKEN_ID, 0n);
-
- expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(AMOUNT);
- expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(0n);
- });
-
- it('should fail with insufficient balance', async () => {
- await expect(
- token._unsafeTransfer(
+ describe.each(recipientTypes)(
+ 'when the recipient is a %s',
+ (_, recipient) => {
+ it('should transfer whole', async () => {
+ await token._unsafeTransfer(
OWNER.either,
recipient,
TOKEN_ID,
- AMOUNT + 1n,
- ),
- ).rejects.toThrow('MultiToken: insufficient balance');
- });
+ AMOUNT,
+ );
- it('should fail with nonexistent id', async () => {
- await expect(
- token._unsafeTransfer(
+ expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n);
+ expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(AMOUNT);
+ });
+
+ it('should transfer partial', async () => {
+ const partialAmt = AMOUNT - 1n;
+ await token._unsafeTransfer(
OWNER.either,
recipient,
- NONEXISTENT_ID,
+ TOKEN_ID,
+ partialAmt,
+ );
+
+ expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(
+ AMOUNT - partialAmt,
+ );
+ expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(
+ partialAmt,
+ );
+ });
+
+ it('should allow transfer of 0 tokens', async () => {
+ await token._unsafeTransfer(OWNER.either, recipient, TOKEN_ID, 0n);
+
+ expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(
AMOUNT,
- ),
- ).rejects.toThrow('MultiToken: insufficient balance');
- });
+ );
+ expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(0n);
+ });
- it('should fail when transfer from 0 (id)', async () => {
- await expect(
- token._unsafeTransfer(ZERO_ACCOUNT, recipient, TOKEN_ID, AMOUNT),
- ).rejects.toThrow('MultiToken: invalid sender');
- });
+ it('should fail with insufficient balance', async () => {
+ await expect(
+ token._unsafeTransfer(
+ OWNER.either,
+ recipient,
+ TOKEN_ID,
+ AMOUNT + 1n,
+ ),
+ ).rejects.toThrow('MultiToken: insufficient balance');
+ });
- it('should fail when transfer from 0 (contract address)', async () => {
- await expect(
- token._unsafeTransfer(ZERO_CONTRACT, recipient, TOKEN_ID, AMOUNT),
- ).rejects.toThrow('MultiToken: invalid sender');
- });
- });
+ it('should fail with nonexistent id', async () => {
+ await expect(
+ token._unsafeTransfer(
+ OWNER.either,
+ recipient,
+ NONEXISTENT_ID,
+ AMOUNT,
+ ),
+ ).rejects.toThrow('MultiToken: insufficient balance');
+ });
+
+ it('should fail when transfer from 0 (id)', async () => {
+ await expect(
+ token._unsafeTransfer(ZERO_ACCOUNT, recipient, TOKEN_ID, AMOUNT),
+ ).rejects.toThrow('MultiToken: invalid sender');
+ });
+
+ it('should fail when transfer from 0 (contract address)', async () => {
+ await expect(
+ token._unsafeTransfer(ZERO_CONTRACT, recipient, TOKEN_ID, AMOUNT),
+ ).rejects.toThrow('MultiToken: invalid sender');
+ });
+ },
+ );
it('should handle non-canonical fromAddress (id)', async () => {
const nonCanonical = nonCanonicalLeft(OWNER.accountId);
@@ -1363,31 +1377,32 @@ describe('MultiToken', () => {
});
describe('_unsafeMint', () => {
- describe.each(
- recipientTypes,
- )('when the recipient is a %s', (_, recipient) => {
- it('should update balance when minting', async () => {
- await token._unsafeMint(recipient, TOKEN_ID, AMOUNT);
+ describe.each(recipientTypes)(
+ 'when the recipient is a %s',
+ (_, recipient) => {
+ it('should update balance when minting', async () => {
+ await token._unsafeMint(recipient, TOKEN_ID, AMOUNT);
- expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(AMOUNT);
- });
+ expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(AMOUNT);
+ });
- it('should update balance with multiple mints', async () => {
- for (let i = 0; i < 3; i++) {
- await token._unsafeMint(recipient, TOKEN_ID, 1n);
- }
+ it('should update balance with multiple mints', async () => {
+ for (let i = 0; i < 3; i++) {
+ await token._unsafeMint(recipient, TOKEN_ID, 1n);
+ }
- expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(3n);
- });
+ expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(3n);
+ });
- it('should fail when overflowing uint128', async () => {
- await token._unsafeMint(recipient, TOKEN_ID, MAX_UINT128);
+ it('should fail when overflowing uint128', async () => {
+ await token._unsafeMint(recipient, TOKEN_ID, MAX_UINT128);
- await expect(
- token._unsafeMint(recipient, TOKEN_ID, 1n),
- ).rejects.toThrow('MultiToken: arithmetic overflow');
- });
- });
+ await expect(
+ token._unsafeMint(recipient, TOKEN_ID, 1n),
+ ).rejects.toThrow('MultiToken: arithmetic overflow');
+ });
+ },
+ );
it('should fail when minting to zero address (id)', async () => {
await expect(
diff --git a/contracts/src/token/test/NativeShieldedToken.test.ts b/contracts/src/token/test/NativeShieldedToken.test.ts
index 962e2fe0f..41cc97ed8 100644
--- a/contracts/src/token/test/NativeShieldedToken.test.ts
+++ b/contracts/src/token/test/NativeShieldedToken.test.ts
@@ -92,13 +92,14 @@ describe('NativeShieldedToken (Fungible profile)', () => {
],
];
- it.each(
- circuitsToFail,
- )('should revert %s before initialize', async (method, args) => {
- await expect(
- (token[method] as (...a: unknown[]) => Promise)(...args),
- ).rejects.toThrow('NativeShieldedToken: contract not initialized');
- });
+ it.each(circuitsToFail)(
+ 'should revert %s before initialize',
+ async (method, args) => {
+ await expect(
+ (token[method] as (...a: unknown[]) => Promise)(...args),
+ ).rejects.toThrow('NativeShieldedToken: contract not initialized');
+ },
+ );
});
describe('_mint', () => {
diff --git a/contracts/src/token/test/NativeShieldedTokenCore.test.ts b/contracts/src/token/test/NativeShieldedTokenCore.test.ts
index ea8734e6e..db5e863e9 100644
--- a/contracts/src/token/test/NativeShieldedTokenCore.test.ts
+++ b/contracts/src/token/test/NativeShieldedTokenCore.test.ts
@@ -135,13 +135,14 @@ describe('NativeShieldedTokenCore (bare base)', () => {
],
];
- it.each(
- circuitsToFail,
- )('should revert %s before initialize', async (method, args) => {
- await expect(
- (token[method] as (...a: unknown[]) => Promise)(...args),
- ).rejects.toThrow('NativeShieldedToken: contract not initialized');
- });
+ it.each(circuitsToFail)(
+ 'should revert %s before initialize',
+ async (method, args) => {
+ await expect(
+ (token[method] as (...a: unknown[]) => Promise)(...args),
+ ).rejects.toThrow('NativeShieldedToken: contract not initialized');
+ },
+ );
});
describe('_mint (per domain)', () => {
diff --git a/contracts/src/token/test/NativeShieldedTokenFamily.test.ts b/contracts/src/token/test/NativeShieldedTokenFamily.test.ts
index db5ba0cc5..a54d4e92c 100644
--- a/contracts/src/token/test/NativeShieldedTokenFamily.test.ts
+++ b/contracts/src/token/test/NativeShieldedTokenFamily.test.ts
@@ -78,13 +78,14 @@ describe('NativeShieldedTokenFamily (Family profile)', () => {
],
];
- it.each(
- circuitsToFail,
- )('should revert %s before initialize', async (method, args) => {
- await expect(
- (token[method] as (...a: unknown[]) => Promise)(...args),
- ).rejects.toThrow('NativeShieldedToken: contract not initialized');
- });
+ it.each(circuitsToFail)(
+ 'should revert %s before initialize',
+ async (method, args) => {
+ await expect(
+ (token[method] as (...a: unknown[]) => Promise)(...args),
+ ).rejects.toThrow('NativeShieldedToken: contract not initialized');
+ },
+ );
});
describe('_mint (per domain)', () => {
diff --git a/contracts/src/token/test/mocks/MockConfidentialNoteFungibleToken.compact b/contracts/src/token/test/mocks/MockConfidentialNoteFungibleToken.compact
new file mode 100644
index 000000000..c94e99f2c
--- /dev/null
+++ b/contracts/src/token/test/mocks/MockConfidentialNoteFungibleToken.compact
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: MIT
+
+// WARNING: FOR TESTING PURPOSES ONLY.
+
+pragma language_version >= 0.23.0;
+
+import CompactStandardLibrary;
+import "../../ConfidentialNoteFungibleToken" prefix Core_;
+
+export { Core_Note }
+
+export { Core__commitments, Core__nullifiers };
+
+export circuit transfer(recipientPk: Field, value: Uint<128>): [Core_Note, Core_Note] {
+ return Core_transfer(recipientPk, value);
+}
+
+export circuit burn(value: Uint<128>): Core_Note {
+ return Core_burn(value);
+}
+
+// Building blocks, exposed for composition-level tests.
+
+export circuit _spenderPk(): Field {
+ // Exported-boundary marker: returns only to the local caller.
+ return disclose(Core__spenderPk());
+}
+
+export circuit _inputNote(): Core_Note {
+ // Exported-boundary marker: returns only to the local caller.
+ return disclose(Core__inputNote());
+}
+
+export circuit _mintNote(note: Core_Note, ownerPk: Field): [] {
+ return Core__mintNote(note, ownerPk);
+}
+
+export circuit _mint(recipientPk: Field, value: Uint<128>): Core_Note {
+ return Core__mint(recipientPk, value);
+}
+
+export circuit _transfer(spenderPk: Field, recipientPk: Field, outNote: Core_Note, changeNote: Core_Note): [] {
+ return Core__transfer(spenderPk, recipientPk, outNote, changeNote);
+}
+
+export circuit _burn(spenderPk: Field, value: Uint<128>, changeNote: Core_Note): [] {
+ return Core__burn(spenderPk, value, changeNote);
+}
+
+export circuit _consumeNote(ownerPk: Field): Core_Note {
+ // Exported-boundary marker: the consumed note returns only to the local
+ // caller (who supplied it as a witness in the first place).
+ return disclose(Core__consumeNote(ownerPk));
+}
+
+export pure circuit derivePk(sk: Bytes<32>): Field {
+ return Core_derivePk(sk);
+}
+
+export pure circuit commitOf(note: Core_Note, pk: Field): Bytes<32> {
+ return Core_commitOf(note, pk);
+}
+
+export pure circuit nullifierOf(note: Core_Note): Bytes<32> {
+ return Core_nullifierOf(note);
+}
diff --git a/contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenAllowlist.compact b/contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenAllowlist.compact
new file mode 100644
index 000000000..6866c613c
--- /dev/null
+++ b/contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenAllowlist.compact
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: MIT
+
+// WARNING: FOR TESTING PURPOSES ONLY.
+
+pragma language_version >= 0.23.0;
+
+import CompactStandardLibrary;
+import "../../extensions/ConfidentialNoteFungibleTokenAllowlist" prefix Allow_;
+
+export { Allow__allowed };
+
+export circuit _addAllowed(pk: Field): [] {
+ return Allow__addAllowed(pk);
+}
+
+export circuit _removeAllowed(index: Uint<64>): [] {
+ return Allow__removeAllowed(index);
+}
+
+export circuit _assertAllowed(pk: Field): [] {
+ return Allow__assertAllowed(pk);
+}
+
+export pure circuit leafOf(pk: Field): Bytes<32> {
+ return Allow_leafOf(pk);
+}
diff --git a/contracts/src/token/test/mocks/MockConfidentialNoteTokenAudit.compact b/contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenAudit.compact
similarity index 82%
rename from contracts/src/token/test/mocks/MockConfidentialNoteTokenAudit.compact
rename to contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenAudit.compact
index 204d3891c..298e403cc 100644
--- a/contracts/src/token/test/mocks/MockConfidentialNoteTokenAudit.compact
+++ b/contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenAudit.compact
@@ -5,7 +5,7 @@
pragma language_version >= 0.23.0;
import CompactStandardLibrary;
-import "../../extensions/ConfidentialNoteTokenAudit" prefix Audit_;
+import "../../extensions/ConfidentialNoteFungibleTokenAudit" prefix Audit_;
export { Audit_AuditRecord, Audit_AuditView }
@@ -21,6 +21,10 @@ export circuit _emitAuditedOutput(ownerPk: Field, value: Uint<128>, slot: Bytes<
return disclose(Audit__emitAuditedOutput(ownerPk, value, slot));
}
+export circuit _rotateAuditKey(newKey: JubjubPoint): [] {
+ return Audit__rotateAuditKey(newKey);
+}
+
export pure circuit recoverAuditRecord(record: Audit_AuditRecord, auditSk: Field): Audit_AuditView {
return Audit_recoverAuditRecord(record, auditSk);
}
diff --git a/contracts/src/token/test/mocks/MockConfidentialNoteTokenDelivery.compact b/contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenDelivery.compact
similarity index 88%
rename from contracts/src/token/test/mocks/MockConfidentialNoteTokenDelivery.compact
rename to contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenDelivery.compact
index d310f90e2..914b81e4c 100644
--- a/contracts/src/token/test/mocks/MockConfidentialNoteTokenDelivery.compact
+++ b/contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenDelivery.compact
@@ -5,7 +5,7 @@
pragma language_version >= 0.23.0;
import CompactStandardLibrary;
-import "../../extensions/ConfidentialNoteTokenDelivery" prefix Delivery_;
+import "../../extensions/ConfidentialNoteFungibleTokenDelivery" prefix Delivery_;
import "../../../crypto/NoteDelivery" prefix NoteDelivery_;
export { NoteDelivery_FullDelivery, NoteDelivery_Recovered }
diff --git a/contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenFreeze.compact b/contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenFreeze.compact
new file mode 100644
index 000000000..ed507f806
--- /dev/null
+++ b/contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenFreeze.compact
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: MIT
+
+// WARNING: FOR TESTING PURPOSES ONLY.
+
+pragma language_version >= 0.23.0;
+
+import CompactStandardLibrary;
+import "../../extensions/ConfidentialNoteFungibleTokenFreeze" prefix Freeze_;
+
+export { Freeze__frozen };
+
+export circuit _freeze(nf: Bytes<32>): [] {
+ return Freeze__freeze(nf);
+}
+
+export circuit _unfreeze(nf: Bytes<32>): [] {
+ return Freeze__unfreeze(nf);
+}
+
+export circuit _assertNotFrozen(nf: Bytes<32>): [] {
+ return Freeze__assertNotFrozen(nf);
+}
diff --git a/contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenIssuer.compact b/contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenIssuer.compact
new file mode 100644
index 000000000..0c425c380
--- /dev/null
+++ b/contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenIssuer.compact
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: MIT
+
+// WARNING: FOR TESTING PURPOSES ONLY.
+
+pragma language_version >= 0.23.0;
+
+import CompactStandardLibrary;
+import "../../extensions/ConfidentialNoteFungibleTokenIssuer" prefix Issuer_;
+
+export { Issuer__isInitialized, Issuer__issuerPk };
+
+export circuit initialize(issuerPk: Field): [] {
+ return Issuer_initialize(issuerPk);
+}
+
+export circuit _assertIssuer(): [] {
+ return Issuer__assertIssuer();
+}
+
+export circuit _rotateIssuer(newIssuerPk: Field): [] {
+ return Issuer__rotateIssuer(newIssuerPk);
+}
diff --git a/contracts/src/token/test/mocks/MockConfidentialNoteTokenSupply.compact b/contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenPrivateSupply.compact
similarity index 81%
rename from contracts/src/token/test/mocks/MockConfidentialNoteTokenSupply.compact
rename to contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenPrivateSupply.compact
index 95fa862e9..3557abe69 100644
--- a/contracts/src/token/test/mocks/MockConfidentialNoteTokenSupply.compact
+++ b/contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenPrivateSupply.compact
@@ -5,7 +5,7 @@
pragma language_version >= 0.23.0;
import CompactStandardLibrary;
-import "../../extensions/ConfidentialNoteTokenSupply" prefix Supply_;
+import "../../extensions/ConfidentialNoteFungibleTokenPrivateSupply" prefix Supply_;
import "../../../crypto/ElGamal" prefix ElGamal_;
export { ElGamal_Ciphertext }
@@ -34,6 +34,10 @@ export circuit attestSupply(total: Uint<128>): [] {
return Supply_attestSupply(total);
}
+export circuit _rotateSupplyKey(newKey: JubjubPoint, total: Uint<128>): [] {
+ return Supply__rotateSupplyKey(newKey, total);
+}
+
// Off-chain helper surfaced for tests: derive the supply public key from its
// secret the way the extension's attestation does.
export pure circuit derivePk(ek: Bytes<32>): JubjubPoint {
diff --git a/contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenReview.compact b/contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenReview.compact
new file mode 100644
index 000000000..3359bc042
--- /dev/null
+++ b/contracts/src/token/test/mocks/MockConfidentialNoteFungibleTokenReview.compact
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: MIT
+
+// WARNING: FOR TESTING PURPOSES ONLY.
+
+pragma language_version >= 0.23.0;
+
+import CompactStandardLibrary;
+import "../../extensions/ConfidentialNoteFungibleTokenReview" prefix Review_;
+
+export { Review_ReviewRecord, Review_ReviewView }
+
+export { Review__reviewers, Review__reviewTrail };
+
+export circuit _addReviewer(reviewerKey: JubjubPoint): [] {
+ return Review__addReviewer(reviewerKey);
+}
+
+export circuit _removeReviewer(reviewerKey: JubjubPoint): [] {
+ return Review__removeReviewer(reviewerKey);
+}
+
+export circuit _emitReviewRecord(
+ reviewerKey: JubjubPoint,
+ ownerPk: Field,
+ value: Uint<128>,
+ nonce: Field,
+ slot: Bytes<32>
+): [] {
+ return Review__emitReviewRecord(reviewerKey, ownerPk, value, nonce, slot);
+}
+
+export pure circuit reviewerKeyHashOf(reviewerKey: JubjubPoint): Bytes<32> {
+ return Review_reviewerKeyHashOf(reviewerKey);
+}
+
+export pure circuit recoverReviewRecord(record: Review_ReviewRecord, reviewSk: Field): Review_ReviewView {
+ return Review_recoverReviewRecord(record, reviewSk);
+}
diff --git a/contracts/src/token/test/mocks/MockConfidentialNoteToken.compact b/contracts/src/token/test/mocks/MockConfidentialNoteToken.compact
deleted file mode 100644
index 952afdf9d..000000000
--- a/contracts/src/token/test/mocks/MockConfidentialNoteToken.compact
+++ /dev/null
@@ -1,74 +0,0 @@
-// SPDX-License-Identifier: MIT
-
-// WARNING: FOR TESTING PURPOSES ONLY.
-
-pragma language_version >= 0.23.0;
-
-import CompactStandardLibrary;
-import "../../ConfidentialNoteToken" prefix CNT_;
-
-export { CNT_Note }
-
-export { CNT__isInitialized, CNT__issuerPk, CNT__commitments, CNT__nullifiers };
-
-export circuit initialize(issuerPk: Field): [] {
- return CNT_initialize(issuerPk);
-}
-
-export circuit mint(recipientPk: Field, value: Uint<128>): CNT_Note {
- return CNT_mint(recipientPk, value);
-}
-
-export circuit transfer(recipientPk: Field, value: Uint<128>): [CNT_Note, CNT_Note] {
- return CNT_transfer(recipientPk, value);
-}
-
-export circuit burn(value: Uint<128>): CNT_Note {
- return CNT_burn(value);
-}
-
-// Building blocks, exposed for composition-level tests.
-
-export circuit _spenderPk(): Field {
- // Exported-boundary marker: returns only to the local caller.
- return disclose(CNT__spenderPk());
-}
-
-export circuit _assertIssuer(): [] {
- return CNT__assertIssuer();
-}
-
-export circuit _inputNote(): CNT_Note {
- // Exported-boundary marker: returns only to the local caller.
- return disclose(CNT__inputNote());
-}
-
-export circuit _mint(note: CNT_Note, ownerPk: Field): [] {
- return CNT__mint(note, ownerPk);
-}
-
-export circuit _transfer(spenderPk: Field, recipientPk: Field, outNote: CNT_Note, changeNote: CNT_Note): [] {
- return CNT__transfer(spenderPk, recipientPk, outNote, changeNote);
-}
-
-export circuit _burn(spenderPk: Field, value: Uint<128>, changeNote: CNT_Note): [] {
- return CNT__burn(spenderPk, value, changeNote);
-}
-
-export circuit _consumeNote(ownerPk: Field): CNT_Note {
- // Exported-boundary marker: the consumed note returns only to the local
- // caller (who supplied it as a witness in the first place).
- return disclose(CNT__consumeNote(ownerPk));
-}
-
-export pure circuit derivePk(sk: Bytes<32>): Field {
- return CNT_derivePk(sk);
-}
-
-export pure circuit commitOf(note: CNT_Note, pk: Field): Bytes<32> {
- return CNT_commitOf(note, pk);
-}
-
-export pure circuit nullifierOf(note: CNT_Note): Bytes<32> {
- return CNT_nullifierOf(note);
-}
diff --git a/contracts/src/token/test/mocks/MockRegulatedConfidentialNoteFungibleToken.compact b/contracts/src/token/test/mocks/MockRegulatedConfidentialNoteFungibleToken.compact
new file mode 100644
index 000000000..b4d8651ea
--- /dev/null
+++ b/contracts/src/token/test/mocks/MockRegulatedConfidentialNoteFungibleToken.compact
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: MIT
+
+// WARNING: FOR TESTING PURPOSES ONLY.
+
+pragma language_version >= 0.23.0;
+
+import CompactStandardLibrary;
+import "../../presets/RegulatedConfidentialNoteFungibleToken" prefix Token_;
+
+// Surface the preset module's re-exported observable state and artifact types
+// under their stable bare names (exercising the module's own re-export path).
+import {
+ Note,
+ AuditRecord,
+ AuditView,
+ FullDelivery,
+ _isInitialized,
+ _issuerPk,
+ _authorityPk,
+ _seizureCount,
+ _commitments,
+ _nullifiers,
+ _auditKey,
+ _auditTrail,
+ _deliveries,
+ _supplyKey,
+ _encSupply,
+ _attestedSupply,
+ _attestationCount
+} from "../../presets/RegulatedConfidentialNoteFungibleToken";
+
+export { Note, AuditRecord, AuditView, FullDelivery }
+
+export {
+ _isInitialized,
+ _issuerPk,
+ _authorityPk,
+ _seizureCount,
+ _commitments,
+ _nullifiers,
+ _auditKey,
+ _auditTrail,
+ _deliveries,
+ _supplyKey,
+ _encSupply,
+ _attestedSupply,
+ _attestationCount
+};
+
+constructor(
+ issuerPk: Field,
+ authorityPk: Field,
+ auditKey: JubjubPoint,
+ supplyKey: JubjubPoint
+) {
+ Token_initialize(issuerPk, authorityPk, auditKey, supplyKey);
+}
+
+export circuit mint(recipientPk: Field, recipientEncPk: JubjubPoint, value: Uint<128>): [] {
+ return Token_mint(recipientPk, recipientEncPk, value);
+}
+
+export circuit transfer(
+ recipientPk: Field,
+ recipientEncPk: JubjubPoint,
+ senderEncPk: JubjubPoint,
+ value: Uint<128>
+): [] {
+ return Token_transfer(recipientPk, recipientEncPk, senderEncPk, value);
+}
+
+export circuit burn(senderEncPk: JubjubPoint, value: Uint<128>): [] {
+ return Token_burn(senderEncPk, value);
+}
+
+export circuit seize(targetOwnerPk: Field, recoveryPk: Field, recoveryEncPk: JubjubPoint): [] {
+ return Token_seize(targetOwnerPk, recoveryPk, recoveryEncPk);
+}
+
+export circuit attestSupply(total: Uint<128>): [] {
+ return Token_attestSupply(total);
+}
+
+export circuit rotateIssuer(newIssuerPk: Field): [] {
+ return Token_rotateIssuer(newIssuerPk);
+}
+
+export circuit rotateAuthority(newAuthorityPk: Field): [] {
+ return Token_rotateAuthority(newAuthorityPk);
+}
+
+export circuit rotateAuditKey(newKey: JubjubPoint): [] {
+ return Token_rotateAuditKey(newKey);
+}
+
+export circuit rotateSupplyKey(newKey: JubjubPoint, total: Uint<128>): [] {
+ return Token_rotateSupplyKey(newKey, total);
+}
diff --git a/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenAllowlistSimulator.ts b/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenAllowlistSimulator.ts
new file mode 100644
index 000000000..15f8a26ad
--- /dev/null
+++ b/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenAllowlistSimulator.ts
@@ -0,0 +1,58 @@
+import {
+ createSimulator,
+ type SimulatorOptions,
+} from '@openzeppelin/compact-simulator';
+import {
+ ledger,
+ Contract as MockAllowlist,
+} from '../../../../artifacts/MockConfidentialNoteFungibleTokenAllowlist/contract/index.js';
+import {
+ type ConfidentialNoteFungibleTokenAllowlistPrivateState,
+ ConfidentialNoteFungibleTokenAllowlistWitnesses,
+ ConfidentialNoteFungibleTokenAllowlistPrivateState as PrivateState,
+} from '../witnesses/ConfidentialNoteFungibleTokenAllowlistWitnesses.js';
+
+const ConfidentialNoteFungibleTokenAllowlistSimulatorBase = createSimulator<
+ ConfidentialNoteFungibleTokenAllowlistPrivateState,
+ ReturnType,
+ ReturnType,
+ MockAllowlist,
+ readonly []
+>({
+ contractFactory: (witnesses) =>
+ new MockAllowlist(
+ witnesses,
+ ),
+ defaultPrivateState: () => PrivateState.generate(),
+ contractArgs: () => [],
+ ledgerExtractor: (state) => ledger(state),
+ witnessesFactory: () => ConfidentialNoteFungibleTokenAllowlistWitnesses(),
+ artifactName: 'MockConfidentialNoteFungibleTokenAllowlist',
+});
+
+export class ConfidentialNoteFungibleTokenAllowlistSimulator extends ConfidentialNoteFungibleTokenAllowlistSimulatorBase {
+ static async create(
+ options: SimulatorOptions<
+ ConfidentialNoteFungibleTokenAllowlistPrivateState,
+ ReturnType
+ > = {},
+ ): Promise {
+ // biome-ignore lint/complexity/noThisInStatic: super.create keeps subclass `this`
+ return super.create(
+ [],
+ options,
+ ) as Promise;
+ }
+
+ public addAllowed(pk: bigint): Promise<[]> {
+ return this.circuits.impure._addAllowed(pk);
+ }
+
+ public removeAllowed(index: bigint): Promise<[]> {
+ return this.circuits.impure._removeAllowed(index);
+ }
+
+ public assertAllowed(pk: bigint): Promise<[]> {
+ return this.circuits.impure._assertAllowed(pk);
+ }
+}
diff --git a/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenAuditSimulator.ts b/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenAuditSimulator.ts
new file mode 100644
index 000000000..8c8c1957b
--- /dev/null
+++ b/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenAuditSimulator.ts
@@ -0,0 +1,71 @@
+import type { JubjubPoint } from '@midnight-ntwrk/compact-runtime';
+import {
+ createSimulator,
+ type SimulatorOptions,
+} from '@openzeppelin/compact-simulator';
+import {
+ ledger,
+ Contract as MockAudit,
+} from '../../../../artifacts/MockConfidentialNoteFungibleTokenAudit/contract/index.js';
+import {
+ type ConfidentialNoteFungibleTokenAuditPrivateState,
+ ConfidentialNoteFungibleTokenAuditWitnesses,
+ ConfidentialNoteFungibleTokenAuditPrivateState as PrivateState,
+} from '../witnesses/ConfidentialNoteFungibleTokenAuditWitnesses.js';
+
+const ConfidentialNoteFungibleTokenAuditSimulatorBase = createSimulator<
+ ConfidentialNoteFungibleTokenAuditPrivateState,
+ ReturnType,
+ ReturnType,
+ MockAudit,
+ readonly []
+>({
+ contractFactory: (witnesses) =>
+ new MockAudit(witnesses),
+ defaultPrivateState: () => PrivateState.generate(),
+ contractArgs: () => [],
+ ledgerExtractor: (state) => ledger(state),
+ witnessesFactory: () => ConfidentialNoteFungibleTokenAuditWitnesses(),
+ artifactName: 'MockConfidentialNoteFungibleTokenAudit',
+});
+
+export class ConfidentialNoteFungibleTokenAuditSimulator extends ConfidentialNoteFungibleTokenAuditSimulatorBase {
+ static async create(
+ options: SimulatorOptions<
+ ConfidentialNoteFungibleTokenAuditPrivateState,
+ ReturnType
+ > = {},
+ ): Promise {
+ // biome-ignore lint/complexity/noThisInStatic: super.create keeps subclass `this`
+ return super.create(
+ [],
+ options,
+ ) as Promise;
+ }
+
+ public initialize(auditKey: JubjubPoint): Promise<[]> {
+ return this.circuits.impure.initialize(auditKey);
+ }
+
+ public emitAuditedOutput(
+ ownerPk: bigint,
+ value: bigint,
+ slot: Uint8Array,
+ ): Promise {
+ return this.circuits.impure._emitAuditedOutput(ownerPk, value, slot);
+ }
+
+ public rotateAuditKey(newKey: JubjubPoint): Promise<[]> {
+ return this.circuits.impure._rotateAuditKey(newKey);
+ }
+
+ public readonly privateState = {
+ set: async (
+ partial: Partial,
+ ): Promise => {
+ const updated = { ...(await this.getPrivateState()), ...partial };
+ this.setPrivateState(updated);
+ return updated;
+ },
+ };
+}
diff --git a/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenDeliverySimulator.ts b/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenDeliverySimulator.ts
new file mode 100644
index 000000000..0881337c6
--- /dev/null
+++ b/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenDeliverySimulator.ts
@@ -0,0 +1,56 @@
+import type { JubjubPoint } from '@midnight-ntwrk/compact-runtime';
+import {
+ createSimulator,
+ type SimulatorOptions,
+} from '@openzeppelin/compact-simulator';
+import {
+ ledger,
+ Contract as MockDelivery,
+} from '../../../../artifacts/MockConfidentialNoteFungibleTokenDelivery/contract/index.js';
+import {
+ type ConfidentialNoteFungibleTokenDeliveryPrivateState,
+ ConfidentialNoteFungibleTokenDeliveryWitnesses,
+ ConfidentialNoteFungibleTokenDeliveryPrivateState as PrivateState,
+} from '../witnesses/ConfidentialNoteFungibleTokenDeliveryWitnesses.js';
+
+const ConfidentialNoteFungibleTokenDeliverySimulatorBase = createSimulator<
+ ConfidentialNoteFungibleTokenDeliveryPrivateState,
+ ReturnType,
+ ReturnType,
+ MockDelivery,
+ readonly []
+>({
+ contractFactory: (witnesses) =>
+ new MockDelivery(
+ witnesses,
+ ),
+ defaultPrivateState: () => PrivateState.generate(),
+ contractArgs: () => [],
+ ledgerExtractor: (state) => ledger(state),
+ witnessesFactory: () => ConfidentialNoteFungibleTokenDeliveryWitnesses(),
+ artifactName: 'MockConfidentialNoteFungibleTokenDelivery',
+});
+
+export class ConfidentialNoteFungibleTokenDeliverySimulator extends ConfidentialNoteFungibleTokenDeliverySimulatorBase {
+ static async create(
+ options: SimulatorOptions<
+ ConfidentialNoteFungibleTokenDeliveryPrivateState,
+ ReturnType
+ > = {},
+ ): Promise {
+ // biome-ignore lint/complexity/noThisInStatic: super.create keeps subclass `this`
+ return super.create(
+ [],
+ options,
+ ) as Promise;
+ }
+
+ public deliver(
+ encPk: JubjubPoint,
+ value: bigint,
+ nonce: bigint,
+ slot: Uint8Array,
+ ): Promise<[]> {
+ return this.circuits.impure._deliver(encPk, value, nonce, slot);
+ }
+}
diff --git a/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenFreezeSimulator.ts b/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenFreezeSimulator.ts
new file mode 100644
index 000000000..3116212de
--- /dev/null
+++ b/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenFreezeSimulator.ts
@@ -0,0 +1,59 @@
+import {
+ createSimulator,
+ type SimulatorOptions,
+} from '@openzeppelin/compact-simulator';
+import {
+ ledger,
+ Contract as MockFreeze,
+} from '../../../../artifacts/MockConfidentialNoteFungibleTokenFreeze/contract/index.js';
+
+// The freeze extension declares no witnesses and keeps no private state.
+export type ConfidentialNoteFungibleTokenFreezePrivateState = Record<
+ string,
+ never
+>;
+
+const emptyWitnesses = () => ({});
+
+const ConfidentialNoteFungibleTokenFreezeSimulatorBase = createSimulator<
+ ConfidentialNoteFungibleTokenFreezePrivateState,
+ ReturnType,
+ ReturnType,
+ MockFreeze,
+ readonly []
+>({
+ contractFactory: (witnesses) =>
+ new MockFreeze(witnesses),
+ defaultPrivateState: () => ({}),
+ contractArgs: () => [],
+ ledgerExtractor: (state) => ledger(state),
+ witnessesFactory: emptyWitnesses,
+ artifactName: 'MockConfidentialNoteFungibleTokenFreeze',
+});
+
+export class ConfidentialNoteFungibleTokenFreezeSimulator extends ConfidentialNoteFungibleTokenFreezeSimulatorBase {
+ static async create(
+ options: SimulatorOptions<
+ ConfidentialNoteFungibleTokenFreezePrivateState,
+ ReturnType
+ > = {},
+ ): Promise {
+ // biome-ignore lint/complexity/noThisInStatic: super.create keeps subclass `this`
+ return super.create(
+ [],
+ options,
+ ) as Promise;
+ }
+
+ public freeze(nf: Uint8Array): Promise<[]> {
+ return this.circuits.impure._freeze(nf);
+ }
+
+ public unfreeze(nf: Uint8Array): Promise<[]> {
+ return this.circuits.impure._unfreeze(nf);
+ }
+
+ public assertNotFrozen(nf: Uint8Array): Promise<[]> {
+ return this.circuits.impure._assertNotFrozen(nf);
+ }
+}
diff --git a/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenIssuerSimulator.ts b/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenIssuerSimulator.ts
new file mode 100644
index 000000000..a32455630
--- /dev/null
+++ b/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenIssuerSimulator.ts
@@ -0,0 +1,67 @@
+import {
+ createSimulator,
+ type SimulatorOptions,
+} from '@openzeppelin/compact-simulator';
+import {
+ ledger,
+ Contract as MockIssuer,
+} from '../../../../artifacts/MockConfidentialNoteFungibleTokenIssuer/contract/index.js';
+import {
+ type ConfidentialNoteFungibleTokenIssuerPrivateState,
+ ConfidentialNoteFungibleTokenIssuerWitnesses,
+ ConfidentialNoteFungibleTokenIssuerPrivateState as PrivateState,
+} from '../witnesses/ConfidentialNoteFungibleTokenIssuerWitnesses.js';
+
+const ConfidentialNoteFungibleTokenIssuerSimulatorBase = createSimulator<
+ ConfidentialNoteFungibleTokenIssuerPrivateState,
+ ReturnType,
+ ReturnType,
+ MockIssuer,
+ readonly []
+>({
+ contractFactory: (witnesses) =>
+ new MockIssuer(witnesses),
+ defaultPrivateState: () => PrivateState.generate(),
+ contractArgs: () => [],
+ ledgerExtractor: (state) => ledger(state),
+ witnessesFactory: () => ConfidentialNoteFungibleTokenIssuerWitnesses(),
+ artifactName: 'MockConfidentialNoteFungibleTokenIssuer',
+});
+
+export class ConfidentialNoteFungibleTokenIssuerSimulator extends ConfidentialNoteFungibleTokenIssuerSimulatorBase {
+ static async create(
+ options: SimulatorOptions<
+ ConfidentialNoteFungibleTokenIssuerPrivateState,
+ ReturnType
+ > = {},
+ ): Promise {
+ // biome-ignore lint/complexity/noThisInStatic: super.create keeps subclass `this`
+ return super.create(
+ [],
+ options,
+ ) as Promise;
+ }
+
+ public initialize(issuerPk: bigint): Promise<[]> {
+ return this.circuits.impure.initialize(issuerPk);
+ }
+
+ public assertIssuer(): Promise<[]> {
+ return this.circuits.impure._assertIssuer();
+ }
+
+ public rotateIssuer(newIssuerPk: bigint): Promise<[]> {
+ return this.circuits.impure._rotateIssuer(newIssuerPk);
+ }
+
+ public readonly privateState = {
+ // Configure the issuer secret proven by the next call.
+ set: async (
+ partial: Partial,
+ ): Promise => {
+ const updated = { ...(await this.getPrivateState()), ...partial };
+ this.setPrivateState(updated);
+ return updated;
+ },
+ };
+}
diff --git a/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenPrivateSupplySimulator.ts b/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenPrivateSupplySimulator.ts
new file mode 100644
index 000000000..84a1e2f43
--- /dev/null
+++ b/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenPrivateSupplySimulator.ts
@@ -0,0 +1,77 @@
+import type { JubjubPoint } from '@midnight-ntwrk/compact-runtime';
+import {
+ createSimulator,
+ type SimulatorOptions,
+} from '@openzeppelin/compact-simulator';
+import {
+ ledger,
+ Contract as MockSupply,
+} from '../../../../artifacts/MockConfidentialNoteFungibleTokenPrivateSupply/contract/index.js';
+import {
+ type ConfidentialNoteFungibleTokenPrivateSupplyPrivateState,
+ ConfidentialNoteFungibleTokenPrivateSupplyWitnesses,
+ ConfidentialNoteFungibleTokenPrivateSupplyPrivateState as PrivateState,
+} from '../witnesses/ConfidentialNoteFungibleTokenPrivateSupplyWitnesses.js';
+
+const ConfidentialNoteFungibleTokenPrivateSupplySimulatorBase = createSimulator<
+ ConfidentialNoteFungibleTokenPrivateSupplyPrivateState,
+ ReturnType,
+ ReturnType,
+ MockSupply,
+ readonly []
+>({
+ contractFactory: (witnesses) =>
+ new MockSupply(
+ witnesses,
+ ),
+ defaultPrivateState: () => PrivateState.generate(),
+ contractArgs: () => [],
+ ledgerExtractor: (state) => ledger(state),
+ witnessesFactory: () => ConfidentialNoteFungibleTokenPrivateSupplyWitnesses(),
+ artifactName: 'MockConfidentialNoteFungibleTokenPrivateSupply',
+});
+
+export class ConfidentialNoteFungibleTokenPrivateSupplySimulator extends ConfidentialNoteFungibleTokenPrivateSupplySimulatorBase {
+ static async create(
+ options: SimulatorOptions<
+ ConfidentialNoteFungibleTokenPrivateSupplyPrivateState,
+ ReturnType
+ > = {},
+ ): Promise {
+ // biome-ignore lint/complexity/noThisInStatic: super.create keeps subclass `this`
+ return super.create(
+ [],
+ options,
+ ) as Promise;
+ }
+
+ public initialize(supplyKey: JubjubPoint): Promise<[]> {
+ return this.circuits.impure.initialize(supplyKey);
+ }
+
+ public addMinted(value: bigint): Promise<[]> {
+ return this.circuits.impure._addMinted(value);
+ }
+
+ public addBurned(value: bigint): Promise<[]> {
+ return this.circuits.impure._addBurned(value);
+ }
+
+ public attestSupply(total: bigint): Promise<[]> {
+ return this.circuits.impure.attestSupply(total);
+ }
+
+ public rotateSupplyKey(newKey: JubjubPoint, total: bigint): Promise<[]> {
+ return this.circuits.impure._rotateSupplyKey(newKey, total);
+ }
+
+ public readonly privateState = {
+ set: async (
+ partial: Partial,
+ ): Promise => {
+ const updated = { ...(await this.getPrivateState()), ...partial };
+ this.setPrivateState(updated);
+ return updated;
+ },
+ };
+}
diff --git a/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenReviewSimulator.ts b/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenReviewSimulator.ts
new file mode 100644
index 000000000..2e35ff464
--- /dev/null
+++ b/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenReviewSimulator.ts
@@ -0,0 +1,79 @@
+import type { JubjubPoint } from '@midnight-ntwrk/compact-runtime';
+import {
+ createSimulator,
+ type SimulatorOptions,
+} from '@openzeppelin/compact-simulator';
+import {
+ ledger,
+ Contract as MockReview,
+} from '../../../../artifacts/MockConfidentialNoteFungibleTokenReview/contract/index.js';
+import {
+ type ConfidentialNoteFungibleTokenReviewPrivateState,
+ ConfidentialNoteFungibleTokenReviewWitnesses,
+ ConfidentialNoteFungibleTokenReviewPrivateState as PrivateState,
+} from '../witnesses/ConfidentialNoteFungibleTokenReviewWitnesses.js';
+
+const ConfidentialNoteFungibleTokenReviewSimulatorBase = createSimulator<
+ ConfidentialNoteFungibleTokenReviewPrivateState,
+ ReturnType,
+ ReturnType,
+ MockReview,
+ readonly []
+>({
+ contractFactory: (witnesses) =>
+ new MockReview(witnesses),
+ defaultPrivateState: () => PrivateState.generate(),
+ contractArgs: () => [],
+ ledgerExtractor: (state) => ledger(state),
+ witnessesFactory: () => ConfidentialNoteFungibleTokenReviewWitnesses(),
+ artifactName: 'MockConfidentialNoteFungibleTokenReview',
+});
+
+export class ConfidentialNoteFungibleTokenReviewSimulator extends ConfidentialNoteFungibleTokenReviewSimulatorBase {
+ static async create(
+ options: SimulatorOptions<
+ ConfidentialNoteFungibleTokenReviewPrivateState,
+ ReturnType
+ > = {},
+ ): Promise {
+ // biome-ignore lint/complexity/noThisInStatic: super.create keeps subclass `this`
+ return super.create(
+ [],
+ options,
+ ) as Promise;
+ }
+
+ public addReviewer(reviewerKey: JubjubPoint): Promise<[]> {
+ return this.circuits.impure._addReviewer(reviewerKey);
+ }
+
+ public removeReviewer(reviewerKey: JubjubPoint): Promise<[]> {
+ return this.circuits.impure._removeReviewer(reviewerKey);
+ }
+
+ public emitReviewRecord(
+ reviewerKey: JubjubPoint,
+ ownerPk: bigint,
+ value: bigint,
+ nonce: bigint,
+ slot: Uint8Array,
+ ): Promise<[]> {
+ return this.circuits.impure._emitReviewRecord(
+ reviewerKey,
+ ownerPk,
+ value,
+ nonce,
+ slot,
+ );
+ }
+
+ public readonly privateState = {
+ set: async (
+ partial: Partial,
+ ): Promise => {
+ const updated = { ...(await this.getPrivateState()), ...partial };
+ this.setPrivateState(updated);
+ return updated;
+ },
+ };
+}
diff --git a/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenSimulator.ts b/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenSimulator.ts
new file mode 100644
index 000000000..529a956d4
--- /dev/null
+++ b/contracts/src/token/test/simulators/ConfidentialNoteFungibleTokenSimulator.ts
@@ -0,0 +1,72 @@
+import {
+ createSimulator,
+ type SimulatorOptions,
+} from '@openzeppelin/compact-simulator';
+import {
+ ledger,
+ Contract as MockCore,
+} from '../../../../artifacts/MockConfidentialNoteFungibleToken/contract/index.js';
+import {
+ type ConfidentialNoteFungibleTokenPrivateState,
+ ConfidentialNoteFungibleTokenWitnesses,
+ type Note,
+ ConfidentialNoteFungibleTokenPrivateState as PrivateState,
+} from '../witnesses/ConfidentialNoteFungibleTokenWitnesses.js';
+
+const ConfidentialNoteFungibleTokenSimulatorBase = createSimulator<
+ ConfidentialNoteFungibleTokenPrivateState,
+ ReturnType,
+ ReturnType,
+ MockCore,
+ readonly []
+>({
+ contractFactory: (witnesses) =>
+ new MockCore(witnesses),
+ defaultPrivateState: () => PrivateState.generate(),
+ contractArgs: () => [],
+ ledgerExtractor: (state) => ledger(state),
+ witnessesFactory: () => ConfidentialNoteFungibleTokenWitnesses(),
+ artifactName: 'MockConfidentialNoteFungibleToken',
+});
+
+export class ConfidentialNoteFungibleTokenSimulator extends ConfidentialNoteFungibleTokenSimulatorBase {
+ static async create(
+ options: SimulatorOptions<
+ ConfidentialNoteFungibleTokenPrivateState,
+ ReturnType
+ > = {},
+ ): Promise {
+ // biome-ignore lint/complexity/noThisInStatic: super.create keeps subclass `this`
+ return super.create(
+ [],
+ options,
+ ) as Promise;
+ }
+
+ public mint(recipientPk: bigint, value: bigint): Promise {
+ return this.circuits.impure._mint(recipientPk, value);
+ }
+
+ public transfer(recipientPk: bigint, value: bigint): Promise<[Note, Note]> {
+ return this.circuits.impure.transfer(recipientPk, value);
+ }
+
+ public burn(value: bigint): Promise {
+ return this.circuits.impure.burn(value);
+ }
+
+ public consumeNote(ownerPk: bigint): Promise {
+ return this.circuits.impure._consumeNote(ownerPk);
+ }
+
+ public readonly privateState = {
+ // Configure the caller's identity and the note being spent next.
+ set: async (
+ partial: Partial,
+ ): Promise => {
+ const updated = { ...(await this.getPrivateState()), ...partial };
+ this.setPrivateState(updated);
+ return updated;
+ },
+ };
+}
diff --git a/contracts/src/token/test/simulators/ConfidentialNoteTokenAuditSimulator.ts b/contracts/src/token/test/simulators/ConfidentialNoteTokenAuditSimulator.ts
deleted file mode 100644
index ff65953c6..000000000
--- a/contracts/src/token/test/simulators/ConfidentialNoteTokenAuditSimulator.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import type { JubjubPoint } from '@midnight-ntwrk/compact-runtime';
-import {
- createSimulator,
- type SimulatorOptions,
-} from '@openzeppelin/compact-simulator';
-import {
- ledger,
- Contract as MockAudit,
-} from '../../../../artifacts/MockConfidentialNoteTokenAudit/contract/index.js';
-import {
- type ConfidentialNoteTokenAuditPrivateState,
- ConfidentialNoteTokenAuditWitnesses,
- ConfidentialNoteTokenAuditPrivateState as PrivateState,
-} from '../witnesses/ConfidentialNoteTokenAuditWitnesses.js';
-
-const ConfidentialNoteTokenAuditSimulatorBase = createSimulator<
- ConfidentialNoteTokenAuditPrivateState,
- ReturnType,
- ReturnType,
- MockAudit,
- readonly []
->({
- contractFactory: (witnesses) =>
- new MockAudit(witnesses),
- defaultPrivateState: () => PrivateState.generate(),
- contractArgs: () => [],
- ledgerExtractor: (state) => ledger(state),
- witnessesFactory: () => ConfidentialNoteTokenAuditWitnesses(),
- artifactName: 'MockConfidentialNoteTokenAudit',
-});
-
-export class ConfidentialNoteTokenAuditSimulator extends ConfidentialNoteTokenAuditSimulatorBase {
- static async create(
- options: SimulatorOptions<
- ConfidentialNoteTokenAuditPrivateState,
- ReturnType
- > = {},
- ): Promise {
- // biome-ignore lint/complexity/noThisInStatic: super.create keeps subclass `this`
- return super.create(
- [],
- options,
- ) as Promise;
- }
-
- public initialize(auditKey: JubjubPoint): Promise<[]> {
- return this.circuits.impure.initialize(auditKey);
- }
-
- public emitAuditedOutput(
- ownerPk: bigint,
- value: bigint,
- slot: Uint8Array,
- ): Promise {
- return this.circuits.impure._emitAuditedOutput(ownerPk, value, slot);
- }
-
- public readonly privateState = {
- set: async (
- partial: Partial,
- ): Promise => {
- const updated = { ...(await this.getPrivateState()), ...partial };
- this.setPrivateState(updated);
- return updated;
- },
- };
-}
diff --git a/contracts/src/token/test/simulators/ConfidentialNoteTokenDeliverySimulator.ts b/contracts/src/token/test/simulators/ConfidentialNoteTokenDeliverySimulator.ts
deleted file mode 100644
index d550e3e1e..000000000
--- a/contracts/src/token/test/simulators/ConfidentialNoteTokenDeliverySimulator.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import type { JubjubPoint } from '@midnight-ntwrk/compact-runtime';
-import {
- createSimulator,
- type SimulatorOptions,
-} from '@openzeppelin/compact-simulator';
-import {
- ledger,
- Contract as MockDelivery,
-} from '../../../../artifacts/MockConfidentialNoteTokenDelivery/contract/index.js';
-import {
- type ConfidentialNoteTokenDeliveryPrivateState,
- ConfidentialNoteTokenDeliveryWitnesses,
- ConfidentialNoteTokenDeliveryPrivateState as PrivateState,
-} from '../witnesses/ConfidentialNoteTokenDeliveryWitnesses.js';
-
-const ConfidentialNoteTokenDeliverySimulatorBase = createSimulator<
- ConfidentialNoteTokenDeliveryPrivateState,
- ReturnType,
- ReturnType,
- MockDelivery,
- readonly []
->({
- contractFactory: (witnesses) =>
- new MockDelivery(witnesses),
- defaultPrivateState: () => PrivateState.generate(),
- contractArgs: () => [],
- ledgerExtractor: (state) => ledger(state),
- witnessesFactory: () => ConfidentialNoteTokenDeliveryWitnesses(),
- artifactName: 'MockConfidentialNoteTokenDelivery',
-});
-
-export class ConfidentialNoteTokenDeliverySimulator extends ConfidentialNoteTokenDeliverySimulatorBase {
- static async create(
- options: SimulatorOptions<
- ConfidentialNoteTokenDeliveryPrivateState,
- ReturnType
- > = {},
- ): Promise {
- // biome-ignore lint/complexity/noThisInStatic: super.create keeps subclass `this`
- return super.create(
- [],
- options,
- ) as Promise;
- }
-
- public deliver(
- encPk: JubjubPoint,
- value: bigint,
- nonce: bigint,
- slot: Uint8Array,
- ): Promise<[]> {
- return this.circuits.impure._deliver(encPk, value, nonce, slot);
- }
-}
diff --git a/contracts/src/token/test/simulators/ConfidentialNoteTokenSimulator.ts b/contracts/src/token/test/simulators/ConfidentialNoteTokenSimulator.ts
deleted file mode 100644
index 0663aabd7..000000000
--- a/contracts/src/token/test/simulators/ConfidentialNoteTokenSimulator.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import {
- createSimulator,
- type SimulatorOptions,
-} from '@openzeppelin/compact-simulator';
-import {
- ledger,
- Contract as MockCNT,
-} from '../../../../artifacts/MockConfidentialNoteToken/contract/index.js';
-import {
- type ConfidentialNoteTokenPrivateState,
- ConfidentialNoteTokenWitnesses,
- type Note,
- ConfidentialNoteTokenPrivateState as PrivateState,
-} from '../witnesses/ConfidentialNoteTokenWitnesses.js';
-
-const ConfidentialNoteTokenSimulatorBase = createSimulator<
- ConfidentialNoteTokenPrivateState,
- ReturnType,
- ReturnType,
- MockCNT,
- readonly []
->({
- contractFactory: (witnesses) =>
- new MockCNT(witnesses),
- defaultPrivateState: () => PrivateState.generate(),
- contractArgs: () => [],
- ledgerExtractor: (state) => ledger(state),
- witnessesFactory: () => ConfidentialNoteTokenWitnesses(),
- artifactName: 'MockConfidentialNoteToken',
-});
-
-export class ConfidentialNoteTokenSimulator extends ConfidentialNoteTokenSimulatorBase {
- static async create(
- options: SimulatorOptions<
- ConfidentialNoteTokenPrivateState,
- ReturnType
- > = {},
- ): Promise {
- // biome-ignore lint/complexity/noThisInStatic: super.create keeps subclass `this`
- return super.create([], options) as Promise;
- }
-
- public initialize(issuerPk: bigint): Promise<[]> {
- return this.circuits.impure.initialize(issuerPk);
- }
-
- public mint(recipientPk: bigint, value: bigint): Promise {
- return this.circuits.impure.mint(recipientPk, value);
- }
-
- public transfer(recipientPk: bigint, value: bigint): Promise<[Note, Note]> {
- return this.circuits.impure.transfer(recipientPk, value);
- }
-
- public burn(value: bigint): Promise {
- return this.circuits.impure.burn(value);
- }
-
- public consumeNote(ownerPk: bigint): Promise {
- return this.circuits.impure._consumeNote(ownerPk);
- }
-
- public readonly privateState = {
- // Configure the caller's identity and the note being spent next.
- set: async (
- partial: Partial,
- ): Promise => {
- const updated = { ...(await this.getPrivateState()), ...partial };
- this.setPrivateState(updated);
- return updated;
- },
- };
-}
diff --git a/contracts/src/token/test/simulators/ConfidentialNoteTokenSupplySimulator.ts b/contracts/src/token/test/simulators/ConfidentialNoteTokenSupplySimulator.ts
deleted file mode 100644
index 5a233fc61..000000000
--- a/contracts/src/token/test/simulators/ConfidentialNoteTokenSupplySimulator.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import type { JubjubPoint } from '@midnight-ntwrk/compact-runtime';
-import {
- createSimulator,
- type SimulatorOptions,
-} from '@openzeppelin/compact-simulator';
-import {
- ledger,
- Contract as MockSupply,
-} from '../../../../artifacts/MockConfidentialNoteTokenSupply/contract/index.js';
-import {
- type ConfidentialNoteTokenSupplyPrivateState,
- ConfidentialNoteTokenSupplyWitnesses,
- ConfidentialNoteTokenSupplyPrivateState as PrivateState,
-} from '../witnesses/ConfidentialNoteTokenSupplyWitnesses.js';
-
-const ConfidentialNoteTokenSupplySimulatorBase = createSimulator<
- ConfidentialNoteTokenSupplyPrivateState,
- ReturnType,
- ReturnType,
- MockSupply,
- readonly []
->({
- contractFactory: (witnesses) =>
- new MockSupply(witnesses),
- defaultPrivateState: () => PrivateState.generate(),
- contractArgs: () => [],
- ledgerExtractor: (state) => ledger(state),
- witnessesFactory: () => ConfidentialNoteTokenSupplyWitnesses(),
- artifactName: 'MockConfidentialNoteTokenSupply',
-});
-
-export class ConfidentialNoteTokenSupplySimulator extends ConfidentialNoteTokenSupplySimulatorBase {
- static async create(
- options: SimulatorOptions<
- ConfidentialNoteTokenSupplyPrivateState,
- ReturnType
- > = {},
- ): Promise {
- // biome-ignore lint/complexity/noThisInStatic: super.create keeps subclass `this`
- return super.create(
- [],
- options,
- ) as Promise;
- }
-
- public initialize(supplyKey: JubjubPoint): Promise<[]> {
- return this.circuits.impure.initialize(supplyKey);
- }
-
- public addMinted(value: bigint): Promise<[]> {
- return this.circuits.impure._addMinted(value);
- }
-
- public addBurned(value: bigint): Promise<[]> {
- return this.circuits.impure._addBurned(value);
- }
-
- public attestSupply(total: bigint): Promise<[]> {
- return this.circuits.impure.attestSupply(total);
- }
-
- public readonly privateState = {
- set: async (
- partial: Partial,
- ): Promise => {
- const updated = { ...(await this.getPrivateState()), ...partial };
- this.setPrivateState(updated);
- return updated;
- },
- };
-}
diff --git a/contracts/src/token/test/simulators/RegulatedConfidentialNoteTokenSimulator.ts b/contracts/src/token/test/simulators/RegulatedConfidentialNoteFungibleTokenSimulator.ts
similarity index 52%
rename from contracts/src/token/test/simulators/RegulatedConfidentialNoteTokenSimulator.ts
rename to contracts/src/token/test/simulators/RegulatedConfidentialNoteFungibleTokenSimulator.ts
index c8d731c33..a4805d044 100644
--- a/contracts/src/token/test/simulators/RegulatedConfidentialNoteTokenSimulator.ts
+++ b/contracts/src/token/test/simulators/RegulatedConfidentialNoteFungibleTokenSimulator.ts
@@ -5,31 +5,33 @@ import {
} from '@openzeppelin/compact-simulator';
import {
ledger,
- Contract as RegulatedCNT,
-} from '../../../../artifacts/RegulatedConfidentialNoteToken/contract/index.js';
+ Contract as RegulatedToken,
+} from '../../../../artifacts/MockRegulatedConfidentialNoteFungibleToken/contract/index.js';
import {
type Note,
- RegulatedConfidentialNoteTokenPrivateState as PrivateState,
- type RegulatedConfidentialNoteTokenPrivateState,
- RegulatedConfidentialNoteTokenWitnesses,
-} from '../witnesses/RegulatedConfidentialNoteTokenWitnesses.js';
+ RegulatedConfidentialNoteFungibleTokenPrivateState as PrivateState,
+ type RegulatedConfidentialNoteFungibleTokenPrivateState,
+ RegulatedConfidentialNoteFungibleTokenWitnesses,
+} from '../witnesses/RegulatedConfidentialNoteFungibleTokenWitnesses.js';
-type RegulatedConfidentialNoteTokenArgs = readonly [
+type RegulatedConfidentialNoteFungibleTokenArgs = readonly [
issuerPk: bigint,
authorityPk: bigint,
auditKey: JubjubPoint,
supplyKey: JubjubPoint,
];
-const RegulatedConfidentialNoteTokenSimulatorBase = createSimulator<
- RegulatedConfidentialNoteTokenPrivateState,
+const RegulatedConfidentialNoteFungibleTokenSimulatorBase = createSimulator<
+ RegulatedConfidentialNoteFungibleTokenPrivateState,
ReturnType,
- ReturnType,
- RegulatedCNT,
- RegulatedConfidentialNoteTokenArgs
+ ReturnType,
+ RegulatedToken,
+ RegulatedConfidentialNoteFungibleTokenArgs
>({
contractFactory: (witnesses) =>
- new RegulatedCNT(witnesses),
+ new RegulatedToken(
+ witnesses,
+ ),
defaultPrivateState: () => PrivateState.generate(),
contractArgs: (issuerPk, authorityPk, auditKey, supplyKey) => [
issuerPk,
@@ -38,26 +40,26 @@ const RegulatedConfidentialNoteTokenSimulatorBase = createSimulator<
supplyKey,
],
ledgerExtractor: (state) => ledger(state),
- witnessesFactory: () => RegulatedConfidentialNoteTokenWitnesses(),
- artifactName: 'RegulatedConfidentialNoteToken',
+ witnessesFactory: () => RegulatedConfidentialNoteFungibleTokenWitnesses(),
+ artifactName: 'MockRegulatedConfidentialNoteFungibleToken',
});
-export class RegulatedConfidentialNoteTokenSimulator extends RegulatedConfidentialNoteTokenSimulatorBase {
+export class RegulatedConfidentialNoteFungibleTokenSimulator extends RegulatedConfidentialNoteFungibleTokenSimulatorBase {
static async create(
issuerPk: bigint,
authorityPk: bigint,
auditKey: JubjubPoint,
supplyKey: JubjubPoint,
options: SimulatorOptions<
- RegulatedConfidentialNoteTokenPrivateState,
- ReturnType
+ RegulatedConfidentialNoteFungibleTokenPrivateState,
+ ReturnType
> = {},
- ): Promise {
+ ): Promise {
// biome-ignore lint/complexity/noThisInStatic: super.create keeps subclass `this`
return super.create(
[issuerPk, authorityPk, auditKey, supplyKey],
options,
- ) as Promise;
+ ) as Promise;
}
public mint(
@@ -98,11 +100,27 @@ export class RegulatedConfidentialNoteTokenSimulator extends RegulatedConfidenti
return this.circuits.impure.attestSupply(total);
}
+ public rotateIssuer(newIssuerPk: bigint): Promise<[]> {
+ return this.circuits.impure.rotateIssuer(newIssuerPk);
+ }
+
+ public rotateAuthority(newAuthorityPk: bigint): Promise<[]> {
+ return this.circuits.impure.rotateAuthority(newAuthorityPk);
+ }
+
+ public rotateAuditKey(newKey: JubjubPoint): Promise<[]> {
+ return this.circuits.impure.rotateAuditKey(newKey);
+ }
+
+ public rotateSupplyKey(newKey: JubjubPoint, total: bigint): Promise<[]> {
+ return this.circuits.impure.rotateSupplyKey(newKey, total);
+ }
+
public readonly privateState = {
// Configure the caller's identity and the note being spent next.
set: async (
- partial: Partial,
- ): Promise => {
+ partial: Partial,
+ ): Promise => {
const updated = { ...(await this.getPrivateState()), ...partial };
this.setPrivateState(updated);
return updated;
diff --git a/contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenAllowlistWitnesses.ts b/contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenAllowlistWitnesses.ts
new file mode 100644
index 000000000..5081b7b8e
--- /dev/null
+++ b/contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenAllowlistWitnesses.ts
@@ -0,0 +1,38 @@
+// TEST-ONLY WITNESS. NOT FOR PRODUCTION USE.
+// Drives ConfidentialNoteFungibleTokenAllowlist (KYC allowlist) circuits in
+// off-chain tests.
+
+import type {
+ MerkleTreePath,
+ WitnessContext,
+} from '@midnight-ntwrk/compact-runtime';
+import type { Ledger } from '../../../../artifacts/MockConfidentialNoteFungibleTokenAllowlist/contract/index.js';
+
+export type ConfidentialNoteFungibleTokenAllowlistPrivateState = Record<
+ string,
+ never
+>;
+
+export const ConfidentialNoteFungibleTokenAllowlistPrivateState = {
+ generate: (): ConfidentialNoteFungibleTokenAllowlistPrivateState => ({}),
+};
+
+export interface IConfidentialNoteFungibleTokenAllowlistWitnesses {
+ wit_AllowlistPath(
+ context: WitnessContext,
+ leaf: Uint8Array,
+ ): [P, MerkleTreePath];
+}
+
+export const ConfidentialNoteFungibleTokenAllowlistWitnesses =
+ (): IConfidentialNoteFungibleTokenAllowlistWitnesses => ({
+ // The circuit passes the prover's identity leaf; we return its Merkle path
+ // by reading the live allowlist tree from the ledger.
+ wit_AllowlistPath(context, leaf) {
+ const path = context.ledger.Allow__allowed.findPathForLeaf(leaf);
+ if (path === undefined) {
+ throw new Error('wit_AllowlistPath: leaf not found in allowlist');
+ }
+ return [context.privateState, path];
+ },
+ });
diff --git a/contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenAuditWitnesses.ts b/contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenAuditWitnesses.ts
new file mode 100644
index 000000000..64e3a123f
--- /dev/null
+++ b/contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenAuditWitnesses.ts
@@ -0,0 +1,47 @@
+// TEST-ONLY WITNESS. NOT FOR PRODUCTION USE.
+// Drives ConfidentialNoteFungibleTokenAudit (auditor viewing) circuits in off-chain
+// tests.
+
+import { getRandomValues } from 'node:crypto';
+import type { WitnessContext } from '@midnight-ntwrk/compact-runtime';
+import type { Ledger } from '../../../../artifacts/MockConfidentialNoteFungibleTokenAudit/contract/index.js';
+
+export type ConfidentialNoteFungibleTokenAuditPrivateState = {
+ /**
+ * Optional fixed randomness seed. Leave undefined for the production-correct
+ * behavior (a fresh secret seed per witness call); set it only in tests that
+ * need deterministic ephemerals.
+ */
+ randomnessSeed?: Uint8Array;
+ /**
+ * Audit secret scalar (auditKey = g^auditSk); consumed only by
+ * `_rotateAuditKey`. Defaults to 0n, which fails the rotation gate — tests
+ * exercising rotation must set it.
+ */
+ auditKeySecret?: bigint;
+};
+
+export const ConfidentialNoteFungibleTokenAuditPrivateState = {
+ generate: (): ConfidentialNoteFungibleTokenAuditPrivateState => ({}),
+};
+
+export interface IConfidentialNoteFungibleTokenAuditWitnesses {
+ wit_AuditRandomness(context: WitnessContext): [P, Uint8Array];
+ wit_AuditKeySecret(context: WitnessContext): [P, bigint];
+}
+
+export const ConfidentialNoteFungibleTokenAuditWitnesses =
+ (): IConfidentialNoteFungibleTokenAuditWitnesses => ({
+ // Fresh + secret per call, as the extension requires; a fixed seed is only
+ // honored when a test explicitly plants one.
+ wit_AuditRandomness(context) {
+ return [
+ context.privateState,
+ context.privateState.randomnessSeed ??
+ new Uint8Array(getRandomValues(Buffer.alloc(32))),
+ ];
+ },
+ wit_AuditKeySecret(context) {
+ return [context.privateState, context.privateState.auditKeySecret ?? 0n];
+ },
+ });
diff --git a/contracts/src/token/test/witnesses/ConfidentialNoteTokenDeliveryWitnesses.ts b/contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenDeliveryWitnesses.ts
similarity index 59%
rename from contracts/src/token/test/witnesses/ConfidentialNoteTokenDeliveryWitnesses.ts
rename to contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenDeliveryWitnesses.ts
index 59e24fb5a..9e0311b84 100644
--- a/contracts/src/token/test/witnesses/ConfidentialNoteTokenDeliveryWitnesses.ts
+++ b/contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenDeliveryWitnesses.ts
@@ -1,12 +1,12 @@
// TEST-ONLY WITNESS. NOT FOR PRODUCTION USE.
-// Drives ConfidentialNoteTokenDelivery (note delivery) circuits in off-chain
+// Drives ConfidentialNoteFungibleTokenDelivery (note delivery) circuits in off-chain
// tests.
import { getRandomValues } from 'node:crypto';
import type { WitnessContext } from '@midnight-ntwrk/compact-runtime';
-import type { Ledger } from '../../../../artifacts/MockConfidentialNoteTokenDelivery/contract/index.js';
+import type { Ledger } from '../../../../artifacts/MockConfidentialNoteFungibleTokenDelivery/contract/index.js';
-export type ConfidentialNoteTokenDeliveryPrivateState = {
+export type ConfidentialNoteFungibleTokenDeliveryPrivateState = {
/**
* Optional fixed randomness seed. Leave undefined for the production-correct
* behavior (a fresh secret seed per witness call).
@@ -14,16 +14,16 @@ export type ConfidentialNoteTokenDeliveryPrivateState = {
randomnessSeed?: Uint8Array;
};
-export const ConfidentialNoteTokenDeliveryPrivateState = {
- generate: (): ConfidentialNoteTokenDeliveryPrivateState => ({}),
+export const ConfidentialNoteFungibleTokenDeliveryPrivateState = {
+ generate: (): ConfidentialNoteFungibleTokenDeliveryPrivateState => ({}),
};
-export interface IConfidentialNoteTokenDeliveryWitnesses {
+export interface IConfidentialNoteFungibleTokenDeliveryWitnesses
{
wit_DeliveryRandomness(context: WitnessContext): [P, Uint8Array];
}
-export const ConfidentialNoteTokenDeliveryWitnesses =
- (): IConfidentialNoteTokenDeliveryWitnesses => ({
+export const ConfidentialNoteFungibleTokenDeliveryWitnesses =
+ (): IConfidentialNoteFungibleTokenDeliveryWitnesses => ({
// Fresh + secret per call, as the extension requires; a fixed seed is only
// honored when a test explicitly plants one.
wit_DeliveryRandomness(context) {
diff --git a/contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenIssuerWitnesses.ts b/contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenIssuerWitnesses.ts
new file mode 100644
index 000000000..93162e348
--- /dev/null
+++ b/contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenIssuerWitnesses.ts
@@ -0,0 +1,29 @@
+// TEST-ONLY WITNESS. NOT FOR PRODUCTION USE.
+// Drives ConfidentialNoteFungibleTokenIssuer (issuer gating) circuits in
+// off-chain tests.
+
+import { getRandomValues } from 'node:crypto';
+import type { WitnessContext } from '@midnight-ntwrk/compact-runtime';
+import type { Ledger } from '../../../../artifacts/MockConfidentialNoteFungibleTokenIssuer/contract/index.js';
+
+export type ConfidentialNoteFungibleTokenIssuerPrivateState = {
+ /** Issuer secret (issuerPk = Hf(issuerSecret)). */
+ issuerSecret: Uint8Array;
+};
+
+export const ConfidentialNoteFungibleTokenIssuerPrivateState = {
+ generate: (): ConfidentialNoteFungibleTokenIssuerPrivateState => ({
+ issuerSecret: new Uint8Array(getRandomValues(Buffer.alloc(32))),
+ }),
+};
+
+export interface IConfidentialNoteFungibleTokenIssuerWitnesses {
+ wit_IssuerSecret(context: WitnessContext): [P, Uint8Array];
+}
+
+export const ConfidentialNoteFungibleTokenIssuerWitnesses =
+ (): IConfidentialNoteFungibleTokenIssuerWitnesses => ({
+ wit_IssuerSecret(context) {
+ return [context.privateState, context.privateState.issuerSecret];
+ },
+ });
diff --git a/contracts/src/token/test/witnesses/ConfidentialNoteTokenSupplyWitnesses.ts b/contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenPrivateSupplyWitnesses.ts
similarity index 66%
rename from contracts/src/token/test/witnesses/ConfidentialNoteTokenSupplyWitnesses.ts
rename to contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenPrivateSupplyWitnesses.ts
index 2933c60c8..7a7e343d4 100644
--- a/contracts/src/token/test/witnesses/ConfidentialNoteTokenSupplyWitnesses.ts
+++ b/contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenPrivateSupplyWitnesses.ts
@@ -1,12 +1,12 @@
// TEST-ONLY WITNESS. NOT FOR PRODUCTION USE.
-// Drives ConfidentialNoteTokenSupply (confidential supply) circuits in
+// Drives ConfidentialNoteFungibleTokenPrivateSupply (confidential supply) circuits in
// off-chain tests.
import { getRandomValues } from 'node:crypto';
import type { WitnessContext } from '@midnight-ntwrk/compact-runtime';
-import type { Ledger } from '../../../../artifacts/MockConfidentialNoteTokenSupply/contract/index.js';
+import type { Ledger } from '../../../../artifacts/MockConfidentialNoteFungibleTokenPrivateSupply/contract/index.js';
-export type ConfidentialNoteTokenSupplyPrivateState = {
+export type ConfidentialNoteFungibleTokenPrivateSupplyPrivateState = {
/** Supply-key secret (supplyKey = derivePk(secret)); consumed by attestSupply. */
supplyKeySecret: Uint8Array;
/**
@@ -16,19 +16,19 @@ export type ConfidentialNoteTokenSupplyPrivateState = {
randomnessSeed?: Uint8Array;
};
-export const ConfidentialNoteTokenSupplyPrivateState = {
- generate: (): ConfidentialNoteTokenSupplyPrivateState => ({
+export const ConfidentialNoteFungibleTokenPrivateSupplyPrivateState = {
+ generate: (): ConfidentialNoteFungibleTokenPrivateSupplyPrivateState => ({
supplyKeySecret: new Uint8Array(getRandomValues(Buffer.alloc(32))),
}),
};
-export interface IConfidentialNoteTokenSupplyWitnesses {
+export interface IConfidentialNoteFungibleTokenPrivateSupplyWitnesses
{
wit_SupplyRandomness(context: WitnessContext): [P, Uint8Array];
wit_SupplyKeySecret(context: WitnessContext): [P, Uint8Array];
}
-export const ConfidentialNoteTokenSupplyWitnesses =
- (): IConfidentialNoteTokenSupplyWitnesses => ({
+export const ConfidentialNoteFungibleTokenPrivateSupplyWitnesses =
+ (): IConfidentialNoteFungibleTokenPrivateSupplyWitnesses => ({
// Fresh + secret per call, as the extension requires; a fixed seed is only
// honored when a test explicitly plants one.
wit_SupplyRandomness(context) {
diff --git a/contracts/src/token/test/witnesses/ConfidentialNoteTokenAuditWitnesses.ts b/contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenReviewWitnesses.ts
similarity index 52%
rename from contracts/src/token/test/witnesses/ConfidentialNoteTokenAuditWitnesses.ts
rename to contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenReviewWitnesses.ts
index 6855f518c..c300e1eaf 100644
--- a/contracts/src/token/test/witnesses/ConfidentialNoteTokenAuditWitnesses.ts
+++ b/contracts/src/token/test/witnesses/ConfidentialNoteFungibleTokenReviewWitnesses.ts
@@ -1,12 +1,12 @@
// TEST-ONLY WITNESS. NOT FOR PRODUCTION USE.
-// Drives ConfidentialNoteTokenAudit (auditor viewing) circuits in off-chain
-// tests.
+// Drives ConfidentialNoteFungibleTokenReview (selective disclosure) circuits
+// in off-chain tests.
import { getRandomValues } from 'node:crypto';
import type { WitnessContext } from '@midnight-ntwrk/compact-runtime';
-import type { Ledger } from '../../../../artifacts/MockConfidentialNoteTokenAudit/contract/index.js';
+import type { Ledger } from '../../../../artifacts/MockConfidentialNoteFungibleTokenReview/contract/index.js';
-export type ConfidentialNoteTokenAuditPrivateState = {
+export type ConfidentialNoteFungibleTokenReviewPrivateState = {
/**
* Optional fixed randomness seed. Leave undefined for the production-correct
* behavior (a fresh secret seed per witness call); set it only in tests that
@@ -15,19 +15,19 @@ export type ConfidentialNoteTokenAuditPrivateState = {
randomnessSeed?: Uint8Array;
};
-export const ConfidentialNoteTokenAuditPrivateState = {
- generate: (): ConfidentialNoteTokenAuditPrivateState => ({}),
+export const ConfidentialNoteFungibleTokenReviewPrivateState = {
+ generate: (): ConfidentialNoteFungibleTokenReviewPrivateState => ({}),
};
-export interface IConfidentialNoteTokenAuditWitnesses {
- wit_AuditRandomness(context: WitnessContext): [P, Uint8Array];
+export interface IConfidentialNoteFungibleTokenReviewWitnesses {
+ wit_ReviewRandomness(context: WitnessContext): [P, Uint8Array];
}
-export const ConfidentialNoteTokenAuditWitnesses =
- (): IConfidentialNoteTokenAuditWitnesses