Multisig
The multisig directory provides building blocks for M-of-N governance over funds held by a contract:
a Signer registry with an approval threshold, a Secp256k1EcdsaMultisig signer set that verifies ECDSA approvals,
a ProposalManager that tracks transfer proposals,
three treasuries that hold and move shielded or unshielded tokens,
and three forwarders that pass every deposit on to a parent.
Apart from Secp256k1EcdsaMultisig, which wraps Signer, the modules are independent of one another.
A contract composes the ones it needs, following the Module/Contract Pattern,
and supplies its own authorization policy on top.
The package also ships two presets, modules that compose these into a ready-made ECDSA policy; see Presets.
None of the multisig modules declares a witness.
Every input is a circuit parameter, so what reaches the public ledger is decided by the disclose calls inside each circuit.
The sections below name what each module publishes.
Secp256k1EcdsaMultisig, the presets, and the contracts in the examples directory require ZKIR v3 (--feature-zkir-v3):
the Secp256k1 types and keccak256 are unavailable on the v2 backend.
The library's compile:multisig script passes the flag for the whole directory, but the other multisig modules do not need it.
Signer
The Signer API keeps the registry of who may approve and how many approvals are required.
It is generic over the signer identity type T, which the consuming contract chooses at import time:
Bytes<32> for a commitment such as the hash of a public key,
JubjubPoint for an aggregated key,
or Either<ZswapCoinPublicKey, ContractAddress> for a caller identity.
The module never resolves who the caller is.
The contract passes an identity in, and Signer checks it against the registry.
import "./node_modules/@openzeppelin/compact-contracts/multisig/Signer"<Bytes<32>>
prefix Signer_;Public ledger state
Everything the module keeps is public:
_signers (the full set of registered identities), _signerCount, and _threshold.
isSigner, _addSigner, and _removeSigner disclose the identity they are given, since it is the key of the set lookup or insertion,
and _setThreshold discloses the new threshold.
The signer count and threshold are Uint<8>, so a registry holds at most 255 signers.
Who may call what
- The module has no
initialize. The consuming contract registers signers with _addSigner and sets the threshold with _changeThreshold or _setThreshold, either from the constructor or behind its own authorization policy. - The guards assertSigner and assertThresholdMet and the views
getSignerCount,getThreshold, and isSigner are open to anyone and carry no "configured" flag. Safety comes from the state rather than from a flag: an empty registry makes isSigner false for every input, so assertSigner rejects every caller, and a zero threshold makes assertThresholdMet reject before it compares any approval count. A contract that needs a "configured" signal beyond that tracks one itself. - _addSigner, _removeSigner, _changeThreshold, and _setThreshold have no access control. The consuming contract must gate them behind its own authorization policy. Left ungated, the first caller registers their own signer set and takes control of the contract.
assertThresholdMet compares the supplied count against the threshold and nothing else; it never consults the registry.
The contract's own approval accounting must be gated by assertSigner and must count distinct signers.
The count is intentionally not bounded by _signerCount,
so approvals collected before a signer was removed still count toward a threshold the remaining signers satisfy.
_changeThreshold refuses a threshold above the current signer count. _setThreshold skips that check so a custom setup flow can set the threshold before any signer is registered. A contract must not gate operations on assertThresholdMet while such a setup is incomplete.
Reconfigurations that take several transactions (removing a signer, then lowering the threshold) pass through intermediate states where the module's checks hold but the contract's intended configuration is incomplete. Perform them atomically in a single circuit, or keep a configuration nonce that invalidates proposals created under a stale signer set.
Usage
The example registers three Bytes<32> signer identities at deployment and lets an owner manage the registry afterwards.
The Ownable module authenticates the owner through its wit_OwnableSK witness,
which the consuming contract supplies in its own TypeScript
(the witness implementations in the library repository are test-only and are not published).
pragma language_version >= 0.26.0;
import CompactStandardLibrary;
import "./node_modules/@openzeppelin/compact-contracts/multisig/Signer"<Bytes<32>>
prefix Signer_;
import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"
prefix Ownable_;
constructor(
initialOwner: Either<Bytes<32>, ContractAddress>,
signers: Vector<3, Bytes<32>>,
thresh: Uint<8>
) {
Ownable_initialize(initialOwner);
for (const signer of signers) {
Signer__addSigner(signer);
}
Signer__changeThreshold(thresh);
}
export circuit addSigner(signer: Bytes<32>): [] {
Ownable_assertOnlyOwner();
Signer__addSigner(signer);
}
export circuit removeSigner(signer: Bytes<32>): [] {
Ownable_assertOnlyOwner();
Signer__removeSigner(signer);
}
export circuit changeThreshold(newThreshold: Uint<8>): [] {
Ownable_assertOnlyOwner();
Signer__changeThreshold(newThreshold);
}
export circuit isSigner(account: Bytes<32>): Boolean {
return Signer_isSigner(account);
}
export circuit getThreshold(): Uint<8> {
return Signer_getThreshold();
}Secp256k1EcdsaMultisig
The Secp256k1EcdsaMultisig API is a t-of-n signer set over secp256k1 ECDSA, built on a Signer<Bytes<32>> registry.
n signer commitments are registered at deployment, and a call passes when t distinct registered signers have signed its digest.
The threshold t is the module generic, fixed at import, and Secp256k1EcdsaMultisig initialize fixes n.
No circuit takes a threshold argument, and t = 0 does not compile.
import "./node_modules/@openzeppelin/compact-contracts/multisig/Secp256k1EcdsaMultisig"<2>
prefix Secp256k1EcdsaMultisig_;A signer commitment is calculateSignerId over the public key's affine coordinates, a per-deployment salt, and the domain Secp256k1EcdsaMultisig:signer.
It hashes the Compact encoding of a struct, not a byte concatenation,
so derive commitments with the pure circuit rather than reimplementing it: a wrong derivation registers commitments no key can satisfy.
assertApprovals takes the digest, t public keys, and t signatures in matching positions.
It checks the threshold first, so an uninitialized registry fails before any signature work,
then rejects a repeated signer, then checks each commitment against the registry and each signature through the low-s verifier in Ecdsa.
The high-s encoding of an otherwise valid signature is rejected.
The module verifies whatever digest it is handed: replay, domain, and instance binding belong in the consumer's digest (see Signing approvals).
Public ledger state
The registry fields _signers, _signerCount, and _threshold are re-exported unprefixed, and _instanceSalt is sealed.
assertApprovals discloses the t approving commitments, so which signers approved each call is public.
Public keys and signatures are verified in-circuit and never disclosed.
The salt is public, so an observer holding a candidate key can test membership; one without cannot correlate a signer across deployments.
Who may call what
- Secp256k1EcdsaMultisig initialize writes the sealed salt, so it MUST only be called from the constructor. It rejects a second call while signers are registered.
- assertApprovals, calculateSignerId, and the views are open.
- No signer or threshold change is exported, so the signer set is final once deployed.
Import the module at exactly one width per contract.
Two widths instantiate two modules over one shared Signer<Bytes<32>> registry, so the second initialize trips the once-only guard.
Do not import Signer alongside it either: a direct import instantiates a separate registry that assertApprovals never reads.
Losing more than n - t keys freezes every circuit gated by assertApprovals.
There is no rotation or recovery path.
ProposalManager
The ProposalManager API tracks the lifecycle of transfer proposals independently of the token kind.
A Proposal records a Recipient, a token color, an amount, an expiry, and a stored ProposalStatus (Active, Executed, or Cancelled).
getProposalStatus derives the other two values and never stores them:
Inactive for an id that was never created, and Expired for an Active proposal past its deadline.
A Recipient pairs 32 address bytes with a RecipientKind tag (ShieldedUser, UnshieldedUser, or Contract)
that says how those bytes are to be read.
Build recipients with shieldedUserRecipient, unshieldedUserRecipient, and contractRecipient,
and convert them at execution time with toShieldedRecipient (accepts ShieldedUser and Contract)
or toUnshieldedRecipient (accepts UnshieldedUser and Contract).
Because Contract is accepted by both, the tag alone does not decide the rail;
a contract composing both treasuries chooses which one a proposal executes on.
expiry is a deadline in seconds since the Unix epoch, and _createProposal rejects one that is not in the future.
An Active proposal is executable while the block time is below it and inert from then on:
assertProposalActive fails once it passes, and so do _cancelProposal and _markExecuted.
Expiry is terminal, since block time only moves forward and rebuilding the transaction cannot help.
The module sets no maximum horizon and no execution delay; a contract that needs a bounded lifetime or a minimum wait imposes its own.
Public ledger state
Proposals are public.
_proposals is exported ledger state, so every proposal's recipient address, color, amount, expiry, and status is readable off-chain,
and _nextProposalId reveals how many have been created.
An off-chain reader takes status as stored and, when it is Active, compares expiry with the current block time.
This holds whichever treasury the contract composes: pairing the module with ShieldedTreasuryStateless still publishes
the full intent of every transfer.
The transfer is private; the plan is not.
Who may call what
_createProposal, _cancelProposal, and _markExecuted have no access control and must be gated by the contract.
The guards assertProposalExists and assertProposalActive, the recipient helpers, and the getters are open.
_createProposal increments the counter and reads it back as the map key,
so two transactions that create proposals against the same ledger state conflict and only one of them lands.
_createProposal validates nothing about the recipient beyond its type.
A kind the execution rail cannot accept (an UnshieldedUser recipient on a shielded treasury)
creates a proposal that stays Active and unexecutable until _cancelProposal retires it or its expiry lapses,
and a zero or otherwise unspendable address is accepted, so funds sent to it on execution are unrecoverable.
Treasuries
Each treasury exposes an ungated _deposit and _send pair that the contract gates with its own policy.
They differ in what they keep on the ledger and in who handles the change of a shielded spend.
ShieldedTreasury
The ShieldedTreasury API stores one coin per token color in _coins, a map from color to QualifiedShieldedCoinInfo,
together with cumulative _received and _sent totals per color.
ShieldedTreasury _deposit claims the incoming coin with receiveShielded
and merges it into the stored coin of the same color (mergeCoinImmediate), or inserts it when the color is new,
so the contract never has to choose between several UTXOs of one color at spend time.
ShieldedTreasury _send rejects the contract's own address as recipient,
since a self-addressed coin would be auto-received but never recorded in _coins.
It looks the coin up by color, checks it covers the amount, and calls sendShielded,
which routes any change back to the contract as a self-owned output.
The treasury retains that change: it is written back to _coins for the next spend,
and the color's entry is removed when the send leaves none.
The holdings of this treasury are public.
_coins is exported, so every coin it holds (nonce, color, value, and Merkle index) is readable in plaintext,
as are the received and sent totals.
"Shielded" refers to the token rail: individual transfers are shielded, but what the contract holds is not.
Deposits disclose the incoming coin; sends disclose the color, recipient, and amount.
_send returns a ShieldedSendResult whose sent and change coins are in plaintext.
Consume it inside the circuit or drop it; returning it from an entry point, writing it to the ledger,
or passing it to another contract publishes the coin details.
The change coin already belongs to the treasury and is recorded in _coins:
spending it again in the same transaction double-spends the recorded coin.
ShieldedTreasuryStateless
The ShieldedTreasuryStateless API keeps no ledger state at all: no coin map, no balances, no totals.
_deposit calls receiveShielded and nothing else.
The operator discovers the coin's Merkle index from ZswapOutput events through the indexer,
builds the QualifiedShieldedCoinInfo off-chain, and passes it to ShieldedTreasuryStateless _send at spend time,
so coin data never reaches the public ledger and there is no on-chain balance to query.
Sufficiency is enforced by sendShielded rather than by the module,
and a double spend or a coin the contract does not own is rejected by the protocol when the transaction is applied.
sendShielded routes change back to the contract, and ShieldedTreasuryStateless _send returns it untouched in result.change
as a live, spendable coin.
The contract decides what to do with it: keep it off-chain and pass it back as the coin of a later spend,
or route it onward to a different recipient in the same transaction with sendImmediateShielded.
The only rule is that the change must never be re-spent and still treated as held.
Writing it to the ledger would publish its nonce, color, and value.
Deposits disclose the incoming coin; sends disclose the spent coin, recipient, and amount.
"No ledger state" is not the same as "unobservable".
A coin's nullifier is a hash of the coin and the contract's address,
and sendShielded derives the nonces of the sent coin and of the change from a public hash of the spent coin's nonce.
Whoever knows a deposited coin can therefore find the spend that consumes it, recover the amount sent,
test candidate recipients against the sent coin's commitment, and repeat from the change for every later spend descending from it.
UnshieldedTreasury
The UnshieldedTreasury API tracks a per-color balance in _balances.
UnshieldedTreasury _deposit calls receiveUnshielded and adds to the balance;
UnshieldedTreasury _send subtracts and calls sendUnshielded.
The sufficiency and overflow guards read _balances, not the protocol balance,
which is fixed at the start of execution and cannot see a deposit made earlier in the same transaction.
Route every unshielded credit and debit through the module:
a direct receiveUnshielded or sendUnshielded leaves _balances out of step and getTokenBalance misreports.
Unshielded transfers are fully public: depositor, recipient, color, and amount all appear on the transcript.
UnshieldedTreasury _send rejects the contract's own address, since a self-addressed send would be auto-received
but still debited from _balances, and a zero amount, which the ledger refuses as a zero-value output.
Other recipients are not validated, so an unspendable recipient permanently removes the funds.
Usage
The example composes ShieldedTreasury with Ownable: anyone may fund the treasury, only the owner may spend from it.
The send result is dropped because the change is already recorded in the treasury's coin map.
pragma language_version >= 0.26.0;
import CompactStandardLibrary;
import "./node_modules/@openzeppelin/compact-contracts/multisig/ShieldedTreasury"
prefix Treasury_;
import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"
prefix Ownable_;
constructor(initialOwner: Either<Bytes<32>, ContractAddress>) {
Ownable_initialize(initialOwner);
}
export circuit deposit(coin: ShieldedCoinInfo): [] {
Treasury__deposit(coin);
}
export circuit withdraw(
recipient: Either<ZswapCoinPublicKey, ContractAddress>,
color: Bytes<32>,
amount: Uint<128>
): [] {
Ownable_assertOnlyOwner();
Treasury__send(recipient, color, amount);
}
export circuit getTokenBalance(color: Bytes<32>): Uint<128> {
return Treasury_getTokenBalance(color);
}
export circuit getReceivedTotal(color: Bytes<32>): Uint<128> {
return Treasury_getReceivedTotal(color);
}
export circuit getSentTotal(color: Bytes<32>): Uint<128> {
return Treasury_getSentTotal(color);
}Forwarders
A forwarder is bound to a single parent at deployment and passes every deposit on to it. The modules are permissionless: the recipient is fixed, so any caller may deposit without compromising the parent. None of them exposes a setter; a contract that needs to change the parent adds its own circuit, or deploys a new forwarder. The examples directory has one deployable contract per forwarder.
ForwarderShielded and ForwarderUnshielded
The ForwarderShielded API and ForwarderUnshielded API forward atomically:
_deposit claims the incoming value (receiveShielded or receiveUnshielded) and re-sends it to the parent in the same transaction
(sendImmediateShielded or sendUnshielded).
Each keeps two ledger fields, _parent and _isInitialized, and every circuit except initialize asserts initialization.
The parent is stored as a generic Either so the ledger layout can later accept a contract address without a state migration,
but initialize accepts only a ZswapCoinPublicKey (shielded, stored in the left arm)
or a UserAddress (unshielded, stored in the right arm) today:
an atomic forward can only reach a recipient that needs no in-transaction claim,
and a third-party contract would never claim the output.
initialize rejects only the all-zero key or address.
The deployer must set a parent that can actually spend the forwarded coins; forwarding to a key no one controls strands every deposit.
The shielded forwarder discloses the incoming coin; the unshielded forwarder discloses depositor, recipient, color, and amount.
ForwarderShielded _deposit returns the forwarded coin as a ShieldedSendResult.
The depositor must encrypt the forwarded output to the parent's encryption public key, obtained off-chain.
The ledger cannot check that key against _parent, so a wrong or missing key leaves the coin spendable by the parent but invisible to its wallet.
If unsure, hand the returned result to the parent so it can recover the coin.
ForwarderPrivate
The ForwarderPrivate API hides the parent from the public.
The ledger holds _parentCommitment, computed off-chain by the deployer as
H(pad(32, "ForwarderPrivate:commitment") || parent || opSecret) with the pure _calculateParentCommitment,
plus _isInitialized.
_deposit claims the coin and writes nothing; coins dwell at the contract address until drained.
_drain takes the coin, the parent coin public key, the operational secret, and the value to send:
it checks that the pair hashes to the stored commitment, then sends the value to the parent with sendShielded.
The parent is a ZswapCoinPublicKey and never a ContractAddress:
a coin-public-key recipient is encrypted inside the Zswap output and never appears on the public transcript,
whereas a shielded send to a contract publishes that contract's address.
Any change is routed back to the contract and returned untouched in result.change,
to leave dwelling for a future drain or to route onward with sendImmediateShielded.
Knowledge of the preimage is the sole authorization gate. Two forwarders bound to the same parent with different operational secrets produce different commitments, so the ledger does not link them. Deposits disclose the incoming coin; drains disclose the spent coin and the value.
The commitment hides the parent from the public, not from a depositor.
A contract-owned coin's nullifier is a hash of the coin and the contract address,
and sendShielded derives the nonces of the sent coin and of the change from a public hash of the spent coin's nonce.
Whoever knows a deposited coin can therefore find the drain that spends it, recover the drained amount,
and test candidate parent keys against the sent coin's commitment: a depositor who can guess the parent can confirm it.
Losing the operational secret is permanent fund loss. It is the only drain authorization, with no rotation, revocation, or recovery path: every coin accumulated at the contract becomes inaccessible. Back it up offline before the first deposit and treat it with the hygiene of a signing key.
Presets
The package ships two presets under its presets directory: ShieldedMultiSigV2 and NativeShieldedTokenIssuer.
Each is a module that composes Secp256k1EcdsaMultisig with a treasury or a token and gates every state-changing circuit on t ECDSA approvals.
The threshold t is the import generic, and the preset's initialize fixes the signer count n.
A deployable contract imports the preset at one width, calls initialize from its constructor, and re-exports the circuits it needs;
the examples directory holds a 2-of-3 contract for each preset.
initializewrites sealed fields, so it MUST only be called from the constructor and never exposed as a post-deploy circuit.- Implementing contracts MUST NOT import
SignerorSecp256k1EcdsaMultisigdirectly, and import one width per contract. - No signer or threshold change is exported. Losing more than
n - tkeys freezes the contract.
Signing approvals
Signers approve an operation off-chain by signing an EIP-712 typed-data digest. The circuit rebuilds the same digest with Eip712 and EvmAbi, then passes it to assertApprovals.
- Before deployment, pick a random
instanceSalt, derive each signer commitment with the preset's_calculateSignerId(pk, instanceSalt), and pass the commitments toinitialize. A fresh salt per deployment keeps one public key from producing the same commitment in two deployments. - The EIP-712 domain is
(name, version, salt): the preset name,"1", andinstanceSalt.chainIdandverifyingContractare absent, because no network identifier is available in-circuit and a 32-byte Midnight address does not fit theaddresstype. - Every operation struct starts with
bytes32 contractAddress, filled in-circuit withkernel.self(). The ledger randomizes a contract's address at deployment, so a signature for one deployment cannot authorize another. - Each preset keeps a replay counter that every gated circuit reads into its digest as
nonceand then increments.getNoncereturns the value the next digest binds to. The read pins the counter, so two operations in one block conflict and the losing signers re-sign at the next nonce. - Integers are big-endian ABI words, so an EVM signer's typed-data encoder reproduces the digest.
- Each call carries exactly
tpublic keys andtlow-s signatures from distinct signers, in matchingVector<t>positions. A high-s signature is rejected.
contracts/src/multisig/test/EcdsaTestUtils.ts in the library repository rebuilds every preset digest through ethers' TypedDataEncoder,
which shows what the off-chain signer must produce
(like the witness implementations, it is test-only and is not published).
ShieldedMultiSigV2
The ShieldedMultiSigV2 API is a shielded wallet with no proposal lifecycle: one execute call verifies t approvals and spends.
It composes Secp256k1EcdsaMultisig with ShieldedTreasuryStateless, so coin data never reaches the public ledger.
Anyone may deposit.
The operator tracks the wallet's coins off-chain from ZswapOutput events and passes the coin to spend as an argument.
The digest is the struct
Execute(bytes32 contractAddress,uint256 nonce,uint8 recipientKind,bytes32 recipient,bytes32 coinColor,uint256 amount)
under the domain ("ShieldedMultiSigV2", "1", instanceSalt),
so a signature binds the full recipient (kind and address), the coin color, and the amount.
execute accepts only a ShieldedUser recipient: a coin addressed to a contract would be unclaimable.
It returns the ShieldedSendResult of ShieldedTreasuryStateless _send, whose warning on returning that result applies here.
Each execution publishes which signers approved and what ShieldedTreasuryStateless discloses for the spend.
Keeping coin data off the ledger hides the balance from the public, not from a depositor:
whoever knows a deposited coin can follow every spend descending from it (see ShieldedTreasuryStateless).
NativeShieldedTokenIssuer
The NativeShieldedTokenIssuer API mints and burns one NativeShieldedToken, for tokenized deposits, with every operation carrying t approvals.
Value enters only through mint (to a holder's coin public key) or mintToSelf (a coin the contract holds),
and leaves only through burn (a holder's coin paid into the transaction, with any change refunded to refundTo)
or burnFromSelf (a coin the contract holds).
Coins minted to holders are ordinary Zswap coins that holders transfer without the issuer.
No send circuit exists, so contract-held coins leave only through burnFromSelf, and no supply is tracked.
The four digests share the domain ("NativeShieldedTokenIssuer", "1", instanceSalt):
Mint(bytes32 contractAddress,bytes32 recipient,uint256 nonce,uint256 amount)MintToSelf(bytes32 contractAddress,uint256 nonce,uint256 amount)Burn(bytes32 contractAddress,bytes32 refundTo,uint256 nonce,uint256 amount)BurnFromSelf(bytes32 contractAddress,uint256 nonce,uint256 amount,bytes32 coinNonce,uint256 coinValue)
BurnFromSelf binds the coin's nonce and value, so approvals authorize one holding: pick the coin before collecting signatures.
mint and burn return the only copy of the new coin or refund; hand it to its owner out of band,
since a contract-initiated mint creates no ciphertext a wallet can find by scanning.
mintToSelf and a partial burnFromSelf return the contract's new holding, which the operator must persist
together with its mt_index from the indexer.
Every mint amount is public.
Coin nonces derive from the public _derivedNonceCounter, so anyone can test a candidate recipient against a minted coin's commitment,
and burnFromSelf amounts are recoverable by trial.
A coin burned exactly as it was minted is linkable to its mint,
which exposes the burn amount, the coin value, and refundTo by trial.
For an unlinkable burn, the holder re-nonces the coin first with a wallet self-transfer.
The contract cannot enforce this.
A 2-of-3 shielded multisig
The following contract trims ShieldedMultiSigV2Example from the examples directory.
It fixes t = 2 at import and n = 3 in the constructor, and surfaces the preset's domain separator unprefixed for ledger() readers.
pragma language_version >= 0.26.0;
import CompactStandardLibrary;
import "./node_modules/@openzeppelin/compact-contracts/multisig/presets/ShieldedMultiSigV2"<2>
prefix Multisig_;
export { ShieldedCoinInfo, QualifiedShieldedCoinInfo, ShieldedSendResult };
import { _domainSeparator } from "./node_modules/@openzeppelin/compact-contracts/multisig/presets/ShieldedMultiSigV2"<2>;
export { _domainSeparator };
constructor(
instanceSalt: Bytes<32>,
signerCommitments: Vector<3, Bytes<32>>
) {
Multisig_initialize<3>(instanceSalt, signerCommitments);
}
export circuit deposit(coin: ShieldedCoinInfo): [] {
return Multisig_deposit(coin);
}
export circuit execute(
to: Multisig_Proposal_Recipient,
amount: Uint<128>,
coin: QualifiedShieldedCoinInfo,
pubkeys: Vector<2, Secp256k1Point>,
signatures: Vector<2, Secp256k1EcdsaSignature>
): ShieldedSendResult {
return Multisig_execute(to, amount, coin, pubkeys, signatures);
}
export pure circuit _calculateSignerId(pk: Secp256k1Point, salt: Bytes<32>): Bytes<32> {
return Multisig__calculateSignerId(pk, salt);
}
export circuit getNonce(): Uint<64> {
return Multisig_getNonce();
}A 2-of-3 token issuer
The following contract trims NativeShieldedTokenIssuerExample from the examples directory.
The constructor also fixes the token domain and metadata.
The published example additionally re-exports the preset's ledger fields unprefixed and the remaining views.
pragma language_version >= 0.26.0;
import CompactStandardLibrary;
import "./node_modules/@openzeppelin/compact-contracts/multisig/presets/NativeShieldedTokenIssuer"<2>
prefix Issuer_;
export { ZswapCoinPublicKey, ContractAddress, Either, Maybe };
export { ShieldedCoinInfo, QualifiedShieldedCoinInfo };
constructor(
instanceSalt: Bytes<32>,
tokenDomain: Bytes<32>,
name_: Opaque<"string">,
symbol_: Opaque<"string">,
decimals_: Uint<8>,
signerCommitments: Vector<3, Bytes<32>>
) {
Issuer_initialize<3>(instanceSalt, tokenDomain, name_, symbol_, decimals_, signerCommitments);
}
export circuit mint(
amount: Uint<64>,
recipient: ZswapCoinPublicKey,
pubkeys: Vector<2, Secp256k1Point>,
signatures: Vector<2, Secp256k1EcdsaSignature>
): ShieldedCoinInfo {
return Issuer_mint(amount, recipient, pubkeys, signatures);
}
export circuit mintToSelf(
amount: Uint<64>,
pubkeys: Vector<2, Secp256k1Point>,
signatures: Vector<2, Secp256k1EcdsaSignature>
): ShieldedCoinInfo {
return Issuer_mintToSelf(amount, pubkeys, signatures);
}
export circuit burn(
coin: ShieldedCoinInfo,
amount: Uint<128>,
refundTo: ZswapCoinPublicKey,
pubkeys: Vector<2, Secp256k1Point>,
signatures: Vector<2, Secp256k1EcdsaSignature>
): Maybe<ShieldedCoinInfo> {
return Issuer_burn(coin, amount, refundTo, pubkeys, signatures);
}
export circuit burnFromSelf(
coin: QualifiedShieldedCoinInfo,
amount: Uint<128>,
pubkeys: Vector<2, Secp256k1Point>,
signatures: Vector<2, Secp256k1EcdsaSignature>
): Maybe<ShieldedCoinInfo> {
return Issuer_burnFromSelf(coin, amount, pubkeys, signatures);
}
export pure circuit _calculateSignerId(pk: Secp256k1Point, salt: Bytes<32>): Bytes<32> {
return Issuer__calculateSignerId(pk, salt);
}
export circuit getNonce(): Uint<64> {
return Issuer_getNonce();
}
export circuit tokenColor(): Bytes<32> {
return Issuer_tokenColor();
}