@openzeppelin/compact-contracts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,728 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.1.0 (token/MultiToken.compact)
3
+
4
+ pragma language_version >= 0.21.0;
5
+
6
+ /**
7
+ * @module MultiToken
8
+ * @description An unshielded MultiToken library. This library is inspired by
9
+ * ERC1155. Many aspects of the EIP-1155 spec cannot be implemented in Compact;
10
+ * therefore, the MultiToken module should be treated as an approximation of
11
+ * the ERC1155 standard and not necessarily a compliant implementation.
12
+ *
13
+ * Authorization is based on a witness-derived identity scheme. Each caller proves knowledge
14
+ * of a secret key by injecting it via the `wit_MultiTokenSK` witness. The module computes
15
+ * an account identifier as `persistentHash(secretKey)` which is a commitment that hides the key
16
+ * while providing a stable, pseudonymous on-chain identity.
17
+ *
18
+ * Because the account identifier is `H(secretKey)` with no per-deployment salt or domain
19
+ * separator, the same secret key produces the same identity across all contracts. This is
20
+ * intentional. It provides a linkable pseudonymous identity analogous to Solidity's
21
+ * `msg.sender`. Users who desire cross-contract unlinkability can use different secret keys
22
+ * per contract at the wallet layer.
23
+ *
24
+ * @notice One notable difference regarding this implementation and the EIP1155 spec
25
+ * consists of the token size. Uint<128> is used as the token size because Uint<256>
26
+ * cannot be supported. This is true for both token IDs and for amounts.
27
+ * This is due to encoding limits on the midnight circuit backend:
28
+ * https://github.com/midnightntwrk/compactc/issues/929
29
+ *
30
+ * @notice Some features defined in the EIP1155 spec are NOT included.
31
+ * Such features include:
32
+ *
33
+ * 1. Batch mint, burn, transfer - Without support for dynamic arrays,
34
+ * batching transfers is difficult to do without a hacky solution.
35
+ * For instance, we could change the `to` and `fromAddress` parameters to be
36
+ * vectors. This would change the signature and would be both difficult
37
+ * to use and easy to misuse.
38
+ *
39
+ * 2. Querying batched balances - This can be somewhat supported.
40
+ * The issue, without dynamic arrays, is that the module circuit must use
41
+ * Vector<n> for accounts and ids; therefore, the implementing contract must
42
+ * explicitly define the number of balances to query in the circuit i.e.
43
+ *
44
+ * balanceOfBatch_10(
45
+ * accounts: Vector<10, Either<Bytes<32>, ContractAddress>>,
46
+ * ids: Vector<10, Uint<128>>
47
+ * ): Vector<10, Uint<128>>
48
+ *
49
+ * Since this module does not offer mint or transfer batching,
50
+ * balance batching is also not included at this time.
51
+ *
52
+ * 3. Introspection - Compact currently cannot support contract-to-contract
53
+ * queries for introspection. ERC165 (or an equivalent thereof) is NOT
54
+ * included in the contract.
55
+ *
56
+ * 4. Safe transfers - The lack of an introspection mechanism means
57
+ * safe transfers of any kind can not be supported.
58
+ * BE AWARE: Tokens sent to a contract address MAY be lost forever.
59
+ *
60
+ * Due to the vast incompatibilities with the EIP1155 spec, it is our
61
+ * opinion that this implementation should not be called ERC1155 at this time
62
+ * as this would be both very confusing and misleading. This may change as more
63
+ * features become available. The list of missing features is as follows:
64
+ *
65
+ * - Full uint256 support.
66
+ * - Events.
67
+ * - Dynamic arrays.
68
+ * - Introspection.
69
+ * - Contract-to-contract calls for acceptance callback.
70
+ *
71
+ * @dev Canonicalization
72
+ * All `Either<Bytes<32>, ContractAddress>` values are canonicalized before use as map keys
73
+ * or in ledger writes. Canonicalization zeroes out the inactive branch of the Either,
74
+ * ensuring that two values with the same active branch always resolve to the same map key
75
+ * regardless of what data the inactive branch carries. Write paths are canonicalized in
76
+ * `_update` (for `_balances`) and `_setApprovalForAll` (for `_operatorApprovals`).
77
+ * Read paths are canonicalized in `balanceOf` and `isApprovedForAll`.
78
+ * `_unsafeTransferFrom` canonicalizes `fromAddress` before comparing against the caller identity.
79
+ *
80
+ * @dev Security Considerations:
81
+ * - The `secretKey` must be kept private. Loss of the key prevents token holders
82
+ * from proving ownership or authorization. Key exposure allows impersonation.
83
+ * - It is strongly recommended to use cryptographically secure random values for the secret key
84
+ * (e.g., `crypto.getRandomValues()`). Weak or predictable keys can be brute-forced.
85
+ * - The `secretKey` is provided via the `wit_MultiTokenSK` witness. As with all witnesses,
86
+ * the contract must not assume that the witness implementation matches the developer's code.
87
+ * Any DApp may provide any implementation. The ZK proof system constrains what values
88
+ * can produce valid proofs.
89
+ *
90
+ * @dev Migration plan (safe -> C2C-capable transfers)
91
+ *
92
+ * The main circuits (`transferFrom`, `_transfer`, `_mint`) reject `ContractAddress` recipients via
93
+ * an `isContract` guard, because a contract that receives tokens currently cannot move them back
94
+ * out. The `_unsafe` variants (`_unsafeTransferFrom`, `_unsafeTransfer`, `_unsafeMint`) skip the
95
+ * guard for callers who need contract recipients (accepting that such tokens are stranded until
96
+ * C2C lands).
97
+ *
98
+ * The recipient type is the final `Either<Bytes<32>, ContractAddress>` from day one, in both
99
+ * circuit signatures and ledger maps. This is deliberate. Midnight's upgrade mechanism, the
100
+ * Contract Maintenance Authority (CMA), can rotate a circuit's verifier key but cannot migrate
101
+ * ledger state (there is no state-rewrite primitive; see the `updatability` tutorial). Fixing the
102
+ * types now makes the future migration a pure verifier-key rotation with zero ledger-state change,
103
+ * which is the only kind of upgrade the CMA supports. Widening the types later would instead force
104
+ * a ledger-layout rewrite, which the CMA cannot perform.
105
+ *
106
+ * When C2C lands, an already-deployed token migrates without changing its address or ledger state:
107
+ *
108
+ * - Minor release: mark the `_unsafe` variants deprecated (and emit a warning if possible); keep
109
+ * their implementation intact so existing callers continue to work.
110
+ * - Major release: ship the main circuits with the `isContract` guard removed and drop the
111
+ * `_unsafe` variants. Operators adopt the new logic by signing a CMA `MaintenanceUpdate` that
112
+ * rotates the affected verifier keys (`VerifierKeyRemove` + `VerifierKeyInsert` for each guarded
113
+ * circuit, and a `VerifierKeyRemove` for each `_unsafe` variant). The contract address and all
114
+ * ledger state are preserved. Because the CMA allows multiple active VK versions per operation,
115
+ * old and new keys can coexist during a transition window so in-flight proofs still verify. By
116
+ * this point, anyone using the `_unsafe` variants should have migrated to the now C2C-capable
117
+ * circuits.
118
+ *
119
+ * @notice Further discussion and consideration required:
120
+ *
121
+ * - Consider changing the underscore in the internal methods to `unsafe` or
122
+ * adopting dot notation for prefixing imports.
123
+ * - Revise logic once contract-to-contract interactions are available on midnight.
124
+ */
125
+ module MultiToken {
126
+ import CompactStandardLibrary;
127
+ import "../security/Initializable" prefix Initializable_;
128
+ import "../utils/Utils" prefix Utils_;
129
+
130
+ /**
131
+ * @description Mapping from token ID to account balances.
132
+ * @type {Uint<128>} id - The token identifier.
133
+ * @type {Either<Bytes<32>, ContractAddress>} account - The account address.
134
+ * @type {Uint<128>} balance - The balance of the account for the token.
135
+ * @type {Map<id, Map<account, balance>>}
136
+ * @type {Map<Uint<128>, Map<Either<Bytes<32>, ContractAddress>, Uint<128>>>} _balances
137
+ */
138
+ export ledger _balances: Map<Uint<128>,
139
+ Map<Either<Bytes<32>, ContractAddress>, Uint<128>>>;
140
+
141
+ /**
142
+ * @description Mapping from account to operator approvals.
143
+ * @type {Either<Bytes<32>, ContractAddress>} account - The account address.
144
+ * @type {Either<Bytes<32>, ContractAddress>} operator - The operator address.
145
+ * @type {Boolean} approved - The approval status of the operator for the account.
146
+ * @type {Map<account, Map<operator, approved>>}
147
+ * @type {Map<Either<Bytes<32>, ContractAddress>, Map<Either<Bytes<32>, ContractAddress>, Boolean>>}
148
+ */
149
+ export ledger _operatorApprovals: Map<Either<Bytes<32>, ContractAddress>,
150
+ Map<Either<Bytes<32>, ContractAddress>, Boolean>>;
151
+
152
+ /**
153
+ * @description Base URI for computing token URIs.
154
+ * Used as the URI for all token types by relying on ID substitution, e.g. https://token-cdn-domain/{id}.json
155
+ * @type {Opaque<"string">} _uri - The base URI for all token URIs.
156
+ */
157
+ export ledger _uri: Opaque<"string">;
158
+
159
+ /**
160
+ * @witness wit_MultiTokenSK
161
+ * @description Returns the caller's secret key used in deriving the account identifier.
162
+ *
163
+ * The same key produces the same account identifier across all contracts. Users who
164
+ * desire cross-contract unlinkability should use different keys per contract.
165
+ *
166
+ * @returns {Bytes<32>} secretKey - A 32-byte cryptographically secure random value.
167
+ */
168
+ witness wit_MultiTokenSK(): Bytes<32>;
169
+
170
+ /**
171
+ * @description Returns a canonical zero Either value (left variant with zero Bytes<32>).
172
+ * Used as the zero value for mint/burn operations in `_update`, where a zero
173
+ * `fromAddress` signals a mint and a zero `to` signals a burn.
174
+ *
175
+ * The left variant is chosen so that `_isTargetZero` checks against `default<Bytes<32>>`,
176
+ * which is consistent with how `_update` dispatches on the zero check. A right variant
177
+ * with `default<ContractAddress>` would also pass `_isTargetZero`, but using the left
178
+ * variant avoids triggering contract address guards in circuits like `_mint` that check
179
+ * `!to.is_left` before delegating to `_unsafeMint`.
180
+ *
181
+ * @return {Either<Bytes<32>, ContractAddress>} - The zero value.
182
+ */
183
+ export pure circuit ZERO(): Either<Bytes<32>, ContractAddress> {
184
+ return Either<Bytes<32>, ContractAddress> {
185
+ is_left: true, left: default<Bytes<32>>, right: default<ContractAddress>
186
+ };
187
+ }
188
+
189
+ /**
190
+ * @description Initializes the contract by setting the base URI for all tokens.
191
+ *
192
+ * @circuitInfo k=6, rows=33
193
+ *
194
+ * Requirements:
195
+ *
196
+ * - Must be called in the contract's constructor.
197
+ *
198
+ * @param {Opaque<"string">} uri_ - The base URI for all token URIs.
199
+ * @return {[]} - Empty tuple.
200
+ */
201
+ export circuit initialize(uri_: Opaque<"string">): [] {
202
+ Initializable_initialize();
203
+ _setURI(uri_);
204
+ }
205
+
206
+ /**
207
+ * @description This implementation returns the same URI for *all* token types. It relies
208
+ * on the token type ID substitution mechanism defined in the EIP:
209
+ * https://eips.ethereum.org/EIPS/eip-1155#metadata.
210
+ * Clients calling this function must replace the `\{id\}` substring with the
211
+ * actual token type ID.
212
+ *
213
+ * @circuitInfo k=8, rows=176
214
+ *
215
+ * Requirements:
216
+ *
217
+ * - Contract is initialized.
218
+ *
219
+ * @param {Uint<128>} id - The token identifier to query.
220
+ * @return {Opaque<"string">} - The base URI for all tokens.
221
+ */
222
+ export circuit uri(id: Uint<128>): Opaque<"string"> {
223
+ Initializable_assertInitialized();
224
+ return _uri;
225
+ }
226
+
227
+ /**
228
+ * @description Returns the amount of `id` tokens owned by `account`.
229
+ *
230
+ * @circuitInfo k=10, rows=875
231
+ *
232
+ * Requirements:
233
+ *
234
+ * - Contract is initialized.
235
+ *
236
+ * @param {Either<Bytes<32>, ContractAddress>} account - The account balance to query.
237
+ * @param {Uint<128>} id - The token identifier to query.
238
+ * @return {Uint<128>} - The quantity of `id` tokens that `account` owns.
239
+ */
240
+ export circuit balanceOf(
241
+ account: Either<Bytes<32>, ContractAddress>,
242
+ id: Uint<128>
243
+ ): Uint<128> {
244
+ Initializable_assertInitialized();
245
+ const canonAcct = Utils_canonicalize<Bytes<32>, ContractAddress>(account);
246
+
247
+ if (!_balances.member(disclose(id)) || !_balances.lookup(id).member(disclose(canonAcct))) {
248
+ return 0;
249
+ }
250
+ return _balances.lookup(id).lookup(disclose(canonAcct));
251
+ }
252
+
253
+ /**
254
+ * @description Enables or disables approval for `operator` to manage all of the caller's assets.
255
+ *
256
+ * In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_MultiTokenSK`
257
+ * witness as `persistentHash(secretKey)`.
258
+ *
259
+ * @circuitInfo k=13, rows=2944
260
+ *
261
+ * Requirements:
262
+ *
263
+ * - Contract is initialized.
264
+ * - `operator` is not zero.
265
+ *
266
+ * @param {Either<Bytes<32>, ContractAddress>} operator - The account whose approval is set
267
+ * for the caller's assets.
268
+ * @param {Boolean} approved - The boolean value determining if the operator may or may not handle the
269
+ * caller's assets.
270
+ * @return {[]} - Empty tuple.
271
+ */
272
+ export circuit setApprovalForAll(
273
+ operator: Either<Bytes<32>, ContractAddress>,
274
+ approved: Boolean
275
+ ): [] {
276
+ Initializable_assertInitialized();
277
+ const caller = left<Bytes<32>, ContractAddress>(_computeAccountId());
278
+ _setApprovalForAll(caller, operator, approved);
279
+ }
280
+
281
+ /**
282
+ * @description Queries if `operator` is an authorized operator for `account`.
283
+ *
284
+ * @circuitInfo k=11, rows=1343
285
+ *
286
+ * Requirements:
287
+ *
288
+ * - Contract is initialized.
289
+ *
290
+ * @param {Either<Bytes<32>, ContractAddress>} account - The queried possessor of assets.
291
+ * @param {Either<Bytes<32>, ContractAddress>} operator - The queried handler of `account`'s assets.
292
+ * @return {Boolean} - Whether or not `operator` has permission to handle `account`'s assets.
293
+ */
294
+ export circuit isApprovedForAll(
295
+ account: Either<Bytes<32>, ContractAddress>,
296
+ operator: Either<Bytes<32>, ContractAddress>
297
+ ): Boolean {
298
+ Initializable_assertInitialized();
299
+ const canonAcct = Utils_canonicalize<Bytes<32>, ContractAddress>(account);
300
+ const canonOp = Utils_canonicalize<Bytes<32>, ContractAddress>(operator);
301
+
302
+ if (!_operatorApprovals.member(disclose(canonAcct)) ||
303
+ !_operatorApprovals.lookup(canonAcct).member(disclose(canonOp))) {
304
+ return false;
305
+ }
306
+
307
+ return _operatorApprovals.lookup(canonAcct).lookup(disclose(canonOp));
308
+ }
309
+
310
+ /**
311
+ * @description Transfers ownership of `value` amount of `id` tokens from `fromAddress` to `to`.
312
+ * The caller must be `fromAddress` or approved to transfer on their behalf.
313
+ *
314
+ * In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_MultiTokenSK`
315
+ * witness as `persistentHash(secretKey)`.
316
+ *
317
+ * @notice Transfers to contract addresses are currently disallowed until contract-to-contract
318
+ * interactions are supported in Compact. This restriction prevents assets from
319
+ * being inadvertently locked in contracts that cannot currently handle token receipt.
320
+ *
321
+ * @circuitInfo k=13, rows=4926
322
+ *
323
+ * Requirements:
324
+ *
325
+ * - Contract is initialized.
326
+ * - `to` is not a ContractAddress.
327
+ * - `to` is not zero.
328
+ * - `fromAddress` is not zero.
329
+ * - Caller must be `fromAddress` or approved via `setApprovalForAll`.
330
+ * - `fromAddress` must have an `id` balance of at least `value`.
331
+ *
332
+ * @param {Either<Bytes<32>, ContractAddress>} fromAddress - The owner from which the transfer originates.
333
+ * @param {Either<Bytes<32>, ContractAddress>} to - The recipient of the transferred assets.
334
+ * @param {Uint<128>} id - The unique identifier of the asset type.
335
+ * @param {Uint<128>} value - The quantity of `id` tokens to transfer.
336
+ * @return {[]} - Empty tuple.
337
+ */
338
+ export circuit transferFrom(
339
+ fromAddress: Either<Bytes<32>, ContractAddress>,
340
+ to: Either<Bytes<32>, ContractAddress>,
341
+ id: Uint<128>,
342
+ value: Uint<128>
343
+ ): [] {
344
+ const isContractAddr = !to.is_left;
345
+ assert(!isContractAddr, "MultiToken: unsafe transfer");
346
+ _unsafeTransferFrom(fromAddress, to, id, value);
347
+ }
348
+
349
+ /**
350
+ * @description Transfers ownership of `value` amount of `id` tokens from `fromAddress` to `to`.
351
+ * Does not impose restrictions on the caller, making it suitable for composition
352
+ * in higher-level contract logic.
353
+ *
354
+ * @notice Transfers to contract addresses are currently disallowed until contract-to-contract
355
+ * interactions are supported in Compact. This restriction prevents assets from
356
+ * being inadvertently locked in contracts that cannot currently handle token receipt.
357
+ *
358
+ * @circuitInfo k=12, rows=2450
359
+ *
360
+ * Requirements:
361
+ *
362
+ * - Contract is initialized.
363
+ * - `to` is not a ContractAddress.
364
+ * - `to` is not zero.
365
+ * - `fromAddress` is not zero.
366
+ * - `fromAddress` must have an `id` balance of at least `value`.
367
+ *
368
+ * @param {Either<Bytes<32>, ContractAddress>} fromAddress - The owner from which the transfer originates.
369
+ * @param {Either<Bytes<32>, ContractAddress>} to - The recipient of the transferred assets.
370
+ * @param {Uint<128>} id - The unique identifier of the asset type.
371
+ * @param {Uint<128>} value - The quantity of `id` tokens to transfer.
372
+ * @return {[]} - Empty tuple.
373
+ */
374
+ export circuit _transfer(
375
+ fromAddress: Either<Bytes<32>, ContractAddress>,
376
+ to: Either<Bytes<32>, ContractAddress>,
377
+ id: Uint<128>,
378
+ value: Uint<128>
379
+ ): [] {
380
+ const isContractAddr = !to.is_left;
381
+ assert(!isContractAddr, "MultiToken: unsafe transfer");
382
+ _unsafeTransfer(fromAddress, to, id, value);
383
+ }
384
+
385
+ /**
386
+ * @description Transfers a value amount of tokens of type id from fromAddress to to.
387
+ * This circuit will mint (or burn) if `fromAddress` (or `to`) is zero.
388
+ *
389
+ * @notice Both `fromAddress` and `to` are canonicalized internally before use as map keys.
390
+ *
391
+ * Requirements:
392
+ *
393
+ * - Contract is initialized.
394
+ * - If `fromAddress` is not zero, the balance of `id` of `fromAddress` must be >= `value`.
395
+ *
396
+ * @param {Either<Bytes<32>, ContractAddress>} fromAddress - The origin of the transfer.
397
+ * @param {Either<Bytes<32>, ContractAddress>} to - The destination of the transfer.
398
+ * @param {Uint<128>} id - The unique identifier of the asset type.
399
+ * @param {Uint<128>} value - The quantity of `id` tokens to transfer.
400
+ * @return {[]} - Empty tuple.
401
+ */
402
+ circuit _update(fromAddress: Either<Bytes<32>, ContractAddress>,
403
+ to: Either<Bytes<32>, ContractAddress>,
404
+ id: Uint<128>,
405
+ value: Uint<128>
406
+ ): [] {
407
+ Initializable_assertInitialized();
408
+ const canonFrom = Utils_canonicalize<Bytes<32>, ContractAddress>(fromAddress);
409
+ const canonTo = Utils_canonicalize<Bytes<32>, ContractAddress>(to);
410
+
411
+ if (!_isTargetZero(disclose(canonFrom))) {
412
+ const fromBalance = balanceOf(canonFrom, id);
413
+ assert(fromBalance >= value, "MultiToken: insufficient balance");
414
+ const newBalance = fromBalance - value;
415
+ _balances.lookup(id).insert(disclose(canonFrom), disclose(newBalance));
416
+ }
417
+
418
+ if (!_isTargetZero(disclose(canonTo))) {
419
+ if (!_balances.member(disclose(id))) {
420
+ _balances.insert(
421
+ disclose(id),
422
+ default<Map<Either<Bytes<32>, ContractAddress>, Uint<128>>>
423
+ );
424
+ _balances.lookup(id).insert(disclose(canonTo), disclose(value as Uint<128>));
425
+ } else {
426
+ const toBalance = balanceOf(canonTo, id);
427
+ const MAX_UINT128 = 340282366920938463463374607431768211455;
428
+ assert(MAX_UINT128 - toBalance >= value, "MultiToken: arithmetic overflow");
429
+ _balances.lookup(id).insert(disclose(canonTo), disclose(toBalance + value as Uint<128>));
430
+ }
431
+ }
432
+ }
433
+
434
+ /**
435
+ * @description Unsafe variant of `transferFrom` which allows transfers to contract addresses.
436
+ * The caller must be `fromAddress` or approved to transfer on their behalf.
437
+ *
438
+ * In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_MultiTokenSK`
439
+ * witness as `persistentHash(secretKey)`.
440
+ *
441
+ * @warning Transfers to contract addresses are considered unsafe because contract-to-contract
442
+ * calls are not currently supported. Tokens sent to a contract address may become irretrievable.
443
+ * Once contract-to-contract calls are supported, this circuit may be deprecated.
444
+ *
445
+ * @circuitInfo k=13, rows=4925
446
+ *
447
+ * Requirements:
448
+ *
449
+ * - Contract is initialized.
450
+ * - `to` is not zero.
451
+ * - `fromAddress` is not zero.
452
+ * - Caller must be `fromAddress` or approved via `setApprovalForAll`.
453
+ * - `fromAddress` must have an `id` balance of at least `value`.
454
+ *
455
+ * @param {Either<Bytes<32>, ContractAddress>} fromAddress - The owner from which the transfer originates.
456
+ * @param {Either<Bytes<32>, ContractAddress>} to - The recipient of the transferred assets.
457
+ * @param {Uint<128>} id - The unique identifier of the asset type.
458
+ * @param {Uint<128>} value - The quantity of `id` tokens to transfer.
459
+ * @return {[]} - Empty tuple.
460
+ */
461
+ export circuit _unsafeTransferFrom(
462
+ fromAddress: Either<Bytes<32>, ContractAddress>,
463
+ to: Either<Bytes<32>, ContractAddress>,
464
+ id: Uint<128>,
465
+ value: Uint<128>
466
+ ): [] {
467
+ Initializable_assertInitialized();
468
+
469
+ const caller = left<Bytes<32>, ContractAddress>(_computeAccountId());
470
+ const canonFrom = Utils_canonicalize<Bytes<32>, ContractAddress>(fromAddress);
471
+ if (disclose(canonFrom) != disclose(caller)) {
472
+ assert(isApprovedForAll(canonFrom, caller), "MultiToken: unauthorized operator");
473
+ }
474
+
475
+ _unsafeTransfer(canonFrom, to, id, value);
476
+ }
477
+
478
+ /**
479
+ * @description Unsafe variant of `_transfer` which allows transfers to contract addresses.
480
+ * Does not impose restrictions on the caller, making it suitable as a low-level
481
+ * building block for advanced contract logic.
482
+ *
483
+ * @warning Transfers to contract addresses are considered unsafe because contract-to-contract
484
+ * calls are not currently supported. Tokens sent to a contract address may become irretrievable.
485
+ * Once contract-to-contract calls are supported, this circuit may be deprecated.
486
+ *
487
+ * @circuitInfo k=12, rows=2449
488
+ *
489
+ * Requirements:
490
+ *
491
+ * - Contract is initialized.
492
+ * - `fromAddress` is not zero.
493
+ * - `to` is not zero.
494
+ * - `fromAddress` must have an `id` balance of at least `value`.
495
+ *
496
+ * @param {Either<Bytes<32>, ContractAddress>} fromAddress - The owner from which the transfer originates.
497
+ * @param {Either<Bytes<32>, ContractAddress>} to - The recipient of the transferred assets.
498
+ * @param {Uint<128>} id - The unique identifier of the asset type.
499
+ * @param {Uint<128>} value - The quantity of `id` tokens to transfer.
500
+ * @return {[]} - Empty tuple.
501
+ */
502
+ export circuit _unsafeTransfer(
503
+ fromAddress: Either<Bytes<32>, ContractAddress>,
504
+ to: Either<Bytes<32>, ContractAddress>,
505
+ id: Uint<128>,
506
+ value: Uint<128>
507
+ ): [] {
508
+ Initializable_assertInitialized();
509
+
510
+ assert(!_isTargetZero(fromAddress), "MultiToken: invalid sender");
511
+ assert(!_isTargetZero(to), "MultiToken: invalid receiver");
512
+ _update(fromAddress, to, id, value);
513
+ }
514
+
515
+ /**
516
+ * @description Sets a new URI for all token types, by relying on the token type ID
517
+ * substitution mechanism defined in the MultiToken standard.
518
+ * See https://eips.ethereum.org/EIPS/eip-1155#metadata.
519
+ *
520
+ * @notice By this mechanism, any occurrence of the `\{id\}` substring in either the
521
+ * URI or any of the values in the JSON file at said URI will be replaced by
522
+ * clients with the token type ID.
523
+ *
524
+ * For example, the `https://token-cdn-domain/\{id\}.json` URI would be
525
+ * interpreted by clients as
526
+ * `https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json`
527
+ * for token type ID 0x4cce0.
528
+ *
529
+ * @circuitInfo k=6, rows=28
530
+ *
531
+ * Requirements:
532
+ *
533
+ * - Contract is initialized.
534
+ *
535
+ * @param {Opaque<"string">} newURI - The new base URI for all tokens.
536
+ * @return {[]} - Empty tuple.
537
+ */
538
+ export circuit _setURI(newURI: Opaque<"string">): [] {
539
+ Initializable_assertInitialized();
540
+ _uri = disclose(newURI);
541
+ }
542
+
543
+ /**
544
+ * @description Creates a `value` amount of tokens of type `id`, and assigns them to `to`.
545
+ *
546
+ * @notice Transfers to contract addresses are currently disallowed until contract-to-contract
547
+ * interactions are supported in Compact. This restriction prevents assets from
548
+ * being inadvertently locked in contracts that cannot currently handle token receipt.
549
+ *
550
+ * @circuitInfo k=11, rows=1531
551
+ *
552
+ * Requirements:
553
+ *
554
+ * - Contract is initialized.
555
+ * - `to` is not zero.
556
+ * - `to` is not a ContractAddress.
557
+ *
558
+ * @param {Either<Bytes<32>, ContractAddress>} to - The recipient of the minted tokens.
559
+ * @param {Uint<128>} id - The unique identifier for the token type.
560
+ * @param {Uint<128>} value - The quantity of `id` tokens that are minted to `to`.
561
+ * @return {[]} - Empty tuple.
562
+ */
563
+ export circuit _mint(to: Either<Bytes<32>, ContractAddress>,
564
+ id: Uint<128>,
565
+ value: Uint<128>
566
+ ): [] {
567
+ const isContractAddr = !to.is_left;
568
+ assert(!isContractAddr, "MultiToken: unsafe transfer");
569
+ _unsafeMint(to, id, value);
570
+ }
571
+
572
+ /**
573
+ * @description Unsafe variant of `_mint` which allows transfers to contract addresses.
574
+ *
575
+ * @warning Transfers to contract addresses are considered unsafe because contract-to-contract
576
+ * calls are not currently supported. Tokens sent to a contract address may become irretrievable.
577
+ * Once contract-to-contract calls are supported, this circuit may be deprecated.
578
+ *
579
+ * @circuitInfo k=11, rows=1530
580
+ *
581
+ * Requirements:
582
+ *
583
+ * - Contract is initialized.
584
+ * - `to` is not zero.
585
+ *
586
+ * @param {Either<Bytes<32>, ContractAddress>} to - The recipient of the minted tokens.
587
+ * @param {Uint<128>} id - The unique identifier for the token type.
588
+ * @param {Uint<128>} value - The quantity of `id` tokens that are minted to `to`.
589
+ * @return {[]} - Empty tuple.
590
+ */
591
+ export circuit _unsafeMint(
592
+ to: Either<Bytes<32>, ContractAddress>,
593
+ id: Uint<128>,
594
+ value: Uint<128>
595
+ ): [] {
596
+ Initializable_assertInitialized();
597
+ assert(!_isTargetZero(to), "MultiToken: invalid receiver");
598
+ _update(ZERO(), to, id, value);
599
+ }
600
+
601
+ /**
602
+ * @description Destroys a `value` amount of tokens of type `id` from `fromAddress`.
603
+ *
604
+ * @circuitInfo k=11, rows=1222
605
+ *
606
+ * Requirements:
607
+ *
608
+ * - Contract is initialized.
609
+ * - `fromAddress` is not zero.
610
+ * - `fromAddress` must have an `id` balance of at least `value`.
611
+ *
612
+ * @param {Either<Bytes<32>, ContractAddress>} fromAddress - The owner whose tokens will be destroyed.
613
+ * @param {Uint<128>} id - The unique identifier of the token type.
614
+ * @param {Uint<128>} value - The quantity of `id` tokens that will be destroyed from `fromAddress`.
615
+ * @return {[]} - Empty tuple.
616
+ */
617
+ export circuit _burn(fromAddress: Either<Bytes<32>, ContractAddress>,
618
+ id: Uint<128>,
619
+ value: Uint<128>
620
+ ): [] {
621
+ Initializable_assertInitialized();
622
+ assert(!_isTargetZero(fromAddress), "MultiToken: invalid sender");
623
+ _update(fromAddress, ZERO(), id, value);
624
+ }
625
+
626
+ /**
627
+ * @description Enables or disables approval for `operator` to manage all of `owner`'s assets.
628
+ *
629
+ * @notice This circuit does not check for access permissions but can be useful as a building block
630
+ * for more complex contract logic.
631
+ *
632
+ * @circuitInfo k=11, rows=1279
633
+ *
634
+ * Requirements:
635
+ *
636
+ * - Contract is initialized.
637
+ * - `owner` is not zero.
638
+ * - `operator` is not zero.
639
+ *
640
+ * @param {Either<Bytes<32>, ContractAddress>} owner - The account whose assets are managed.
641
+ * @param {Either<Bytes<32>, ContractAddress>} operator - The account whose approval is set for
642
+ * the `owner`'s assets.
643
+ * @param {Boolean} approved - The boolean value determining if the operator may or may not handle the
644
+ * `owner`'s assets.
645
+ * @return {[]} - Empty tuple.
646
+ */
647
+ export circuit _setApprovalForAll(
648
+ owner: Either<Bytes<32>, ContractAddress>,
649
+ operator: Either<Bytes<32>, ContractAddress>,
650
+ approved: Boolean
651
+ ): [] {
652
+ Initializable_assertInitialized();
653
+ const canonOwner = Utils_canonicalize<Bytes<32>, ContractAddress>(owner);
654
+ assert(!_isTargetZero(canonOwner), "MultiToken: invalid owner");
655
+
656
+ const canonOp = Utils_canonicalize<Bytes<32>, ContractAddress>(operator);
657
+ assert(!_isTargetZero(canonOp), "MultiToken: invalid operator");
658
+
659
+ if (!_operatorApprovals.member(disclose(canonOwner))) {
660
+ _operatorApprovals.insert(
661
+ disclose(canonOwner),
662
+ default<Map<Either<Bytes<32>, ContractAddress>, Boolean>>
663
+ );
664
+ }
665
+
666
+ _operatorApprovals.lookup(canonOwner).insert(disclose(canonOp), disclose(approved));
667
+ }
668
+
669
+ /**
670
+ * @description Computes the caller's account identifier from the `wit_MultiTokenSK` witness.
671
+ *
672
+ * ## ID Derivation
673
+ * `accountId = persistentHash(secretKey)`
674
+ *
675
+ * The result is a 32-byte commitment that uniquely identifies the caller.
676
+ *
677
+ * @returns {Bytes<32>} accountId - The computed account identifier.
678
+ */
679
+ circuit _computeAccountId(): Bytes<32> {
680
+ return computeAccountId(wit_MultiTokenSK());
681
+ }
682
+
683
+ /**
684
+ * @description Computes an account identifier without on-chain state, allowing a user to derive
685
+ * their identity commitment before submitting it in a token operation.
686
+ * This is the off-chain counterpart to {_computeAccountId} and produces an identical result
687
+ * given the same inputs.
688
+ *
689
+ * @warning OpSec: The `secretKey` parameter is a sensitive secret. Mishandling it can
690
+ * permanently compromise the security of this system:
691
+ *
692
+ * - **Never log or persist** the `secretKey` in plaintext — avoid browser devtools,
693
+ * application logs, analytics pipelines, or any observable side-channel.
694
+ * - **Store offline or in secure enclaves** — hardware security modules (HSMs),
695
+ * air-gapped devices, or encrypted vaults are strongly preferred over hot storage.
696
+ * - **Use cryptographically secure randomness** — generate keys with `crypto.getRandomValues()`
697
+ * or equivalent; weak or predictable keys can be brute-forced to reveal your identity.
698
+ * - **Treat key loss as identity loss** — a lost key cannot be recovered.
699
+ * - **Avoid calling this circuit in untrusted environments** — executing this in an
700
+ * unverified browser extension, compromised runtime, or shared machine may expose
701
+ * the key to a malicious observer.
702
+ *
703
+ * ## ID Derivation
704
+ * `accountId = persistentHash(secretKey)`
705
+ *
706
+ * @param {Bytes<32>} secretKey - A 32-byte cryptographically secure random value.
707
+ *
708
+ * @returns {Bytes<32>} accountId - The computed account identifier.
709
+ */
710
+ export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> {
711
+ return persistentHash<Vector<1, Bytes<32>>>([secretKey]);
712
+ }
713
+
714
+ /**
715
+ * @description Returns `true` if `target`'s active branch (as indicated by `is_left`)
716
+ * holds the zero value.
717
+ *
718
+ * @param {Either<Bytes<32>, ContractAddress>} target - The value to check.
719
+ * @returns {Boolean} - `true` if the active branch is zero, `false` otherwise.
720
+ */
721
+ circuit _isTargetZero(target: Either<Bytes<32>, ContractAddress>): Boolean {
722
+ if (target.is_left) {
723
+ return target.left == default<Bytes<32>>;
724
+ } else {
725
+ return target.right == default<ContractAddress>;
726
+ }
727
+ }
728
+ }