@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,954 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.1.0 (token/NonFungibleToken.compact)
3
+
4
+ pragma language_version >= 0.21.0;
5
+
6
+ /**
7
+ * @module NonFungibleToken
8
+ * @description An unshielded Non-Fungible Token library.
9
+ *
10
+ * Authorization is based on a witness-derived identity scheme. Each caller proves knowledge
11
+ * of a secret key by injecting it via the `wit_NonFungibleTokenSK` witness. The module computes
12
+ * an account identifier as `persistentHash(secretKey)` which is a commitment that hides the key
13
+ * while providing a stable, pseudonymous on-chain identity.
14
+ *
15
+ * Because the account identifier is `H(secretKey)` with no per-deployment salt or domain
16
+ * separator, the same secret key produces the same identity across all contracts. This is
17
+ * intentional. It provides a linkable pseudonymous identity analogous to Solidity's
18
+ * `msg.sender`. Users who desire cross-contract unlinkability can use different secret keys
19
+ * per contract at the wallet layer.
20
+ *
21
+ * @notice One notable difference regarding this implementation and the EIP721 spec
22
+ * consists of the token size. Uint<128> is used as the token size because Uint<256>
23
+ * cannot be supported.
24
+ * This is due to encoding limits on the midnight circuit backend:
25
+ * https://github.com/midnightntwrk/compactc/issues/929
26
+ *
27
+ * @notice At the moment Midnight does not support contract-to-contract (C2C) communication, but
28
+ * there are ongoing efforts to enable this in the future. Thus, the main circuits of this module
29
+ * (`transfer`, `transferFrom`, `_transfer`, `_mint`) reject `ContractAddress` recipients via an
30
+ * `isContract` guard, because a contract that receives a token currently cannot move it back out.
31
+ * Developers who need to experiment with contract recipients can use the `_unsafe` variants, which
32
+ * skip the guard (the caller accepts that such tokens are stranded until C2C lands).
33
+ *
34
+ * @dev Migration plan (safe -> C2C-capable `transfer`)
35
+ *
36
+ * The recipient type is the final `Either<Bytes<32>, ContractAddress>` from day one, in both
37
+ * circuit signatures and ledger maps. This is deliberate. Midnight's upgrade mechanism, the
38
+ * Contract Maintenance Authority (CMA), can rotate a circuit's verifier key but cannot migrate
39
+ * ledger state (there is no state-rewrite primitive; see the `updatability` tutorial). Fixing the
40
+ * types now makes the future migration a pure verifier-key rotation with zero ledger-state change,
41
+ * which is the only kind of upgrade the CMA supports. Widening the types later would instead force
42
+ * a ledger-layout rewrite, which the CMA cannot perform.
43
+ *
44
+ * When C2C lands, an already-deployed token migrates without changing its address or ledger state:
45
+ *
46
+ * - Minor release: mark `_unsafeTransfer` deprecated (and emit a warning if possible); keep its
47
+ * implementation intact so existing callers continue to work.
48
+ * - Major release: ship `transfer` with the `isContract` guard removed and drop `_unsafeTransfer`.
49
+ * Operators adopt the new logic by signing a CMA `MaintenanceUpdate` that rotates the affected
50
+ * verifier keys (`VerifierKeyRemove` + `VerifierKeyInsert` for `transfer`, and a `VerifierKeyRemove`
51
+ * for `_unsafeTransfer`). The contract address and all ledger state are preserved. Because the CMA
52
+ * allows multiple active VK versions per operation, the old and new `transfer` keys can coexist
53
+ * during a transition window so in-flight proofs still verify. By this point, anyone using
54
+ * `_unsafeTransfer` should have migrated to the now C2C-capable `transfer`.
55
+ *
56
+ * @dev Canonicalization
57
+ * All `Either<Bytes<32>, ContractAddress>` values are canonicalized before use as map keys
58
+ * or in ledger writes. Canonicalization zeroes out the inactive branch of the Either,
59
+ * ensuring that two values with the same active branch always resolve to the same map key
60
+ * regardless of what data the inactive branch carries. Write paths are canonicalized in
61
+ * `_update` (for `_owners` and `_balances`), `_approve` (for `_tokenApprovals`), and
62
+ * `_setApprovalForAll` (for `_operatorApprovals`). Read paths are canonicalized in
63
+ * `balanceOf`, `isApprovedForAll`, and `_isAuthorized`.
64
+ *
65
+ * @dev Security Considerations:
66
+ * - The `secretKey` must be kept private. Loss of the key prevents token holders
67
+ * from proving ownership or authorization. Key exposure allows impersonation.
68
+ * - It is strongly recommended to use cryptographically secure random values for the secret key
69
+ * (e.g., `crypto.getRandomValues()`). Weak or predictable keys can be brute-forced.
70
+ * - The `secretKey` is provided via the `wit_NonFungibleTokenSK` witness. As with all witnesses,
71
+ * the contract must not assume that the witness implementation matches the developer's code.
72
+ * Any DApp may provide any implementation. The ZK proof system constrains what values
73
+ * can produce valid proofs.
74
+ *
75
+ * @notice Missing Features and Improvements:
76
+ *
77
+ * - Uint256 token IDs
78
+ * - Transfer/Approval events
79
+ * - safeTransfer functions
80
+ * - _baseURI() support
81
+ * - An ERC165-like interface
82
+ */
83
+
84
+ module NonFungibleToken {
85
+ import CompactStandardLibrary;
86
+ import "../security/Initializable" prefix Initializable_;
87
+ import "../utils/Utils" prefix Utils_;
88
+
89
+ /// Public state
90
+ export sealed ledger _name: Opaque<"string">;
91
+ export sealed ledger _symbol: Opaque<"string">;
92
+
93
+ /**
94
+ * @description Mapping from token IDs to their owner addresses.
95
+ * @type {Uint<128>} tokenId - The unique identifier for a token.
96
+ * @type {Either<Bytes<32>, ContractAddress>} owner - The owner address (account ID or contract).
97
+ * @type {Map<tokenId, owner>}
98
+ * @type {Map<Uint<128>, Either<Bytes<32>, ContractAddress>>} _owners
99
+ */
100
+ export ledger _owners: Map<Uint<128>, Either<Bytes<32>, ContractAddress>>;
101
+
102
+ /**
103
+ * @description Mapping from account addresses to their token balances.
104
+ * @type {Either<Bytes<32>, ContractAddress>} owner - The owner address.
105
+ * @type {Uint<128>} balance - The balance of the owner.
106
+ * @type {Map<owner, balance>}
107
+ * @type {Map<Either<Bytes<32>, ContractAddress>, Uint<128>>} _balances
108
+ */
109
+ export ledger _balances: Map<Either<Bytes<32>, ContractAddress>, Uint<128>>;
110
+
111
+ /**
112
+ * @description Mapping from token IDs to approved addresses.
113
+ * @type {Uint<128>} tokenId - The unique identifier for a token.
114
+ * @type {Either<Bytes<32>, ContractAddress>} approved - The approved address (account ID or contract).
115
+ * @type {Map<tokenId, approved>}
116
+ * @type {Map<Uint<128>, Either<Bytes<32>, ContractAddress>>} _tokenApprovals
117
+ */
118
+ export ledger _tokenApprovals: Map<Uint<128>, Either<Bytes<32>, ContractAddress>>;
119
+
120
+ /**
121
+ * @description Mapping from owner addresses to operator approvals.
122
+ * @type {Either<Bytes<32>, ContractAddress>} owner - The owner address.
123
+ * @type {Either<Bytes<32>, ContractAddress>} operator - The operator address.
124
+ * @type {Boolean} approved - Whether the operator is approved.
125
+ * @type {Map<owner, Map<operator, approved>>}
126
+ * @type {Map<Either<Bytes<32>, ContractAddress>, Map<Either<Bytes<32>, ContractAddress>, Boolean>>} _operatorApprovals
127
+ */
128
+ export ledger _operatorApprovals: Map<Either<Bytes<32>, ContractAddress>,
129
+ Map<Either<Bytes<32>, ContractAddress>, Boolean>>;
130
+
131
+ /**
132
+ * @description Mapping from token IDs to their metadata URIs.
133
+ * @type {Uint<128>} tokenId - The unique identifier for a token.
134
+ * @type {Opaque<"string">} uri - The metadata URI for the token.
135
+ * @type {Map<tokenId, uri>}
136
+ * @type {Map<Uint<128>, Opaque<"string">>} _tokenURIs
137
+ */
138
+ export ledger _tokenURIs: Map<Uint<128>, Opaque<"string">>;
139
+
140
+ /**
141
+ * @witness wit_NonFungibleTokenSK
142
+ * @description Returns the caller's secret key used in deriving the account identifier.
143
+ *
144
+ * The same key produces the same account identifier across all contracts. Users who
145
+ * desire cross-contract unlinkability should use different keys per contract.
146
+ *
147
+ * @returns {Bytes<32>} secretKey - A 32-byte cryptographically secure random value.
148
+ */
149
+ witness wit_NonFungibleTokenSK(): Bytes<32>;
150
+
151
+ /**
152
+ * @description Returns a canonical zero Either value (left variant with zero Bytes<32>).
153
+ * Used as the zero value for mint/burn operations and cleared approvals.
154
+ *
155
+ * @return {Either<Bytes<32>, ContractAddress>} - The zero value.
156
+ */
157
+ export pure circuit ZERO(): Either<Bytes<32>, ContractAddress> {
158
+ return Either<Bytes<32>, ContractAddress> {
159
+ is_left: true, left: default<Bytes<32>>, right: default<ContractAddress>
160
+ };
161
+ }
162
+
163
+ /**
164
+ * @description Initializes the contract by setting the name and symbol.
165
+ *
166
+ * This MUST be called in the implementing contract's constructor.
167
+ * Failure to do so can lead to an irreparable contract.
168
+ *
169
+ * @circuitInfo k=6, rows=52
170
+ *
171
+ * Requirements:
172
+ *
173
+ * - Contract is not initialized.
174
+ *
175
+ * @param {Opaque<"string">} name_ - The name of the token.
176
+ * @param {Opaque<"string">} symbol_ - The symbol of the token.
177
+ * @return {[]} - Empty tuple.
178
+ */
179
+ export circuit initialize(name_: Opaque<"string">, symbol_: Opaque<"string">): [] {
180
+ Initializable_initialize();
181
+ _name = disclose(name_);
182
+ _symbol = disclose(symbol_);
183
+ }
184
+
185
+ /**
186
+ * @description Returns the number of tokens in `owner`'s account.
187
+ *
188
+ * @circuitInfo k=10, rows=673
189
+ *
190
+ * Requirements:
191
+ *
192
+ * - The contract is initialized.
193
+ *
194
+ * @param {Either<Bytes<32>, ContractAddress>} owner - The account to query.
195
+ * @return {Uint<128>} - The number of tokens in `owner`'s account.
196
+ */
197
+ export circuit balanceOf(owner: Either<Bytes<32>, ContractAddress>): Uint<128> {
198
+ Initializable_assertInitialized();
199
+ const canonOwner = Utils_canonicalize<Bytes<32>, ContractAddress>(owner);
200
+
201
+ if (!_balances.member(disclose(canonOwner))) {
202
+ return 0;
203
+ }
204
+
205
+ return _balances.lookup(disclose(canonOwner));
206
+ }
207
+
208
+ /**
209
+ * @description Returns the owner of the `tokenId` token.
210
+ *
211
+ * @circuitInfo k=9, rows=333
212
+ *
213
+ * Requirements:
214
+ *
215
+ * - The contract is initialized.
216
+ * - The `tokenId` must exist.
217
+ *
218
+ * @param {Uint<128>} tokenId - The identifier for a token.
219
+ * @return {Either<Bytes<32>, ContractAddress>} - The account that owns the token.
220
+ */
221
+ export circuit ownerOf(tokenId: Uint<128>): Either<Bytes<32>, ContractAddress> {
222
+ Initializable_assertInitialized();
223
+ return _requireOwned(tokenId);
224
+ }
225
+
226
+ /**
227
+ * @description Returns the token name.
228
+ *
229
+ * @circuitInfo k=6, rows=28
230
+ *
231
+ * Requirements:
232
+ *
233
+ * - The contract is initialized.
234
+ *
235
+ * @return {Opaque<"string">} - The token name.
236
+ */
237
+ export circuit name(): Opaque<"string"> {
238
+ Initializable_assertInitialized();
239
+ return _name;
240
+ }
241
+
242
+ /**
243
+ * @description Returns the symbol of the token.
244
+ *
245
+ * @circuitInfo k=6, rows=28
246
+ *
247
+ * Requirements:
248
+ *
249
+ * - The contract is initialized.
250
+ *
251
+ * @return {Opaque<"string">} - The token symbol.
252
+ */
253
+ export circuit symbol(): Opaque<"string"> {
254
+ Initializable_assertInitialized();
255
+ return _symbol;
256
+ }
257
+
258
+ /**
259
+ * @description Returns the token URI for the given `tokenId`. Returns the empty
260
+ * string if a tokenURI does not exist.
261
+ *
262
+ * @circuitInfo k=9, rows=326
263
+ *
264
+ * Requirements:
265
+ *
266
+ * - The contract is initialized.
267
+ * - The `tokenId` must exist.
268
+ *
269
+ * @notice Native strings and string operations aren't supported within the Compact language,
270
+ * e.g. concatenating a base URI + token ID is not possible like in other NFT implementations.
271
+ * Therefore, we propose the URI storage approach; whereby, NFTs may or may not have unique "base" URIs.
272
+ * It's up to the implementation to decide on how to handle this.
273
+ *
274
+ * @param {Uint<128>} tokenId - The identifier for a token.
275
+ * @return {Opaque<"string">} - the token id's URI.
276
+ */
277
+ export circuit tokenURI(tokenId: Uint<128>): Opaque<"string"> {
278
+ Initializable_assertInitialized();
279
+ _requireOwned(tokenId);
280
+
281
+ if (!_tokenURIs.member(disclose(tokenId))) {
282
+ return Utils_emptyString();
283
+ }
284
+
285
+ return _tokenURIs.lookup(disclose(tokenId));
286
+ }
287
+
288
+ /**
289
+ * @description Sets the URI as `tokenURI` for the given `tokenId`.
290
+ *
291
+ * @circuitInfo k=9, rows=289
292
+ *
293
+ * Requirements:
294
+ *
295
+ * - The contract is initialized.
296
+ * - The `tokenId` must exist.
297
+ *
298
+ * @notice The URI for a given NFT is usually set when the NFT is minted.
299
+ *
300
+ * @param {Uint<128>} tokenId - The identifier of the token.
301
+ * @param {Opaque<"string">} tokenURI - The URI of `tokenId`.
302
+ * @return {[]} - Empty tuple.
303
+ */
304
+ export circuit _setTokenURI(tokenId: Uint<128>, tokenURI: Opaque<"string">): [] {
305
+ Initializable_assertInitialized();
306
+ _requireOwned(tokenId);
307
+
308
+ return _tokenURIs.insert(disclose(tokenId), disclose(tokenURI));
309
+ }
310
+
311
+ /**
312
+ * @description Gives permission to `to` to transfer `tokenId` token to another account.
313
+ * The approval is cleared when the token is transferred.
314
+ *
315
+ * Only a single account can be approved at a time, so approving the zero value clears previous approvals.
316
+ *
317
+ * In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_NonFungibleTokenSK`
318
+ * witness as `persistentHash(secretKey)`.
319
+ *
320
+ * @circuitInfo k=13, rows=3468
321
+ *
322
+ * Requirements:
323
+ *
324
+ * - The contract is initialized.
325
+ * - The caller must either own the token or be an approved operator.
326
+ * - `tokenId` must exist.
327
+ *
328
+ * @param {Either<Bytes<32>, ContractAddress>} to - The account receiving the approval
329
+ * @param {Uint<128>} tokenId - The token `to` may be permitted to transfer
330
+ * @return {[]} - Empty tuple.
331
+ */
332
+ export circuit approve(to: Either<Bytes<32>, ContractAddress>, tokenId: Uint<128>): [] {
333
+ Initializable_assertInitialized();
334
+ const auth = left<Bytes<32>, ContractAddress>(_computeAccountId());
335
+ _approve(to, tokenId, auth);
336
+ }
337
+
338
+ /**
339
+ * @description Returns the account approved for `tokenId` token.
340
+ *
341
+ * @circuitInfo k=9, rows=422
342
+ *
343
+ * Requirements:
344
+ *
345
+ * - The contract is initialized.
346
+ * - `tokenId` must exist.
347
+ *
348
+ * @param {Uint<128>} tokenId - The token an account may be approved to manage
349
+ * @return {Either<Bytes<32>, ContractAddress>} - The account approved to manage the token
350
+ */
351
+ export circuit getApproved(tokenId: Uint<128>): Either<Bytes<32>, ContractAddress> {
352
+ Initializable_assertInitialized();
353
+ _requireOwned(tokenId);
354
+
355
+ return _getApproved(tokenId);
356
+ }
357
+
358
+ /**
359
+ * @description Approve or remove `operator` as an operator for the caller.
360
+ * Operators can call {transferFrom} for any token owned by the caller.
361
+ *
362
+ * In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_NonFungibleTokenSK`
363
+ * witness as `persistentHash(secretKey)`.
364
+ *
365
+ * @circuitInfo k=13, rows=2936
366
+ *
367
+ * Requirements:
368
+ *
369
+ * - The contract is initialized.
370
+ * - The `operator` cannot be zero.
371
+ *
372
+ * @param {Either<Bytes<32>, ContractAddress>} operator - An operator to manage the caller's tokens
373
+ * @param {Boolean} approved - A boolean determining if `operator` may manage all tokens of the caller
374
+ * @return {[]} - Empty tuple.
375
+ */
376
+ export circuit setApprovalForAll(
377
+ operator: Either<Bytes<32>, ContractAddress>,
378
+ approved: Boolean
379
+ ): [] {
380
+ Initializable_assertInitialized();
381
+ const owner = left<Bytes<32>, ContractAddress>(_computeAccountId());
382
+ _setApprovalForAll(owner, operator, approved);
383
+ }
384
+
385
+ /**
386
+ * @description Returns if the `operator` is allowed to manage all of the assets of `owner`.
387
+ *
388
+ * @circuitInfo k=11, rows=1343
389
+ *
390
+ * Requirements:
391
+ *
392
+ * - The contract is initialized.
393
+ *
394
+ * @param {Either<Bytes<32>, ContractAddress>} owner - The owner of a token
395
+ * @param {Either<Bytes<32>, ContractAddress>} operator - An account that may operate on `owner`'s tokens
396
+ * @return {Boolean} - A boolean determining if `operator` is allowed to manage all of the tokens of `owner`
397
+ */
398
+ export circuit isApprovedForAll(
399
+ owner: Either<Bytes<32>, ContractAddress>,
400
+ operator: Either<Bytes<32>, ContractAddress>
401
+ ): Boolean {
402
+ Initializable_assertInitialized();
403
+ const canonOwner = Utils_canonicalize<Bytes<32>, ContractAddress>(owner);
404
+ const canonOp = Utils_canonicalize<Bytes<32>, ContractAddress>(operator);
405
+
406
+ return _operatorApprovals.member(disclose(canonOwner)) &&
407
+ _operatorApprovals.lookup(canonOwner).member(disclose(canonOp)) &&
408
+ _operatorApprovals.lookup(canonOwner).lookup(disclose(canonOp));
409
+ }
410
+
411
+ /**
412
+ * @description Transfers `tokenId` token from `fromAddress` to `to`.
413
+ *
414
+ * @notice Transfers to contract addresses are currently disallowed until contract-to-contract interactions
415
+ * are supported in Compact. This restriction prevents assets from being inadvertently locked in contracts that cannot
416
+ * currently handle token receipt.
417
+ *
418
+ * In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_NonFungibleTokenSK`
419
+ * witness as `persistentHash(secretKey)`.
420
+ *
421
+ * @circuitInfo k=13, rows=4800
422
+ *
423
+ * Requirements:
424
+ *
425
+ * - The contract is initialized.
426
+ * - `fromAddress` is not zero.
427
+ * - `to` is not zero.
428
+ * - `to` is not a ContractAddress.
429
+ * - `tokenId` token must be owned by `fromAddress`.
430
+ * - If the caller is not `fromAddress`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
431
+ *
432
+ * @param {Either<Bytes<32>, ContractAddress>} fromAddress - The source account from which the token is being transferred
433
+ * @param {Either<Bytes<32>, ContractAddress>} to - The target account to transfer token to
434
+ * @param {Uint<128>} tokenId - The token being transferred
435
+ * @return {[]} - Empty tuple.
436
+ */
437
+ export circuit transferFrom(
438
+ fromAddress: Either<Bytes<32>, ContractAddress>,
439
+ to: Either<Bytes<32>, ContractAddress>,
440
+ tokenId: Uint<128>
441
+ ): [] {
442
+ Initializable_assertInitialized();
443
+ const isContractAddr = !to.is_left;
444
+ assert(!isContractAddr, "NonFungibleToken: unsafe transfer");
445
+
446
+ _unsafeTransferFrom(fromAddress, to, tokenId);
447
+ }
448
+
449
+ /**
450
+ * @description Transfers `tokenId` token from `fromAddress` to `to`. It does NOT check if the recipient is a ContractAddress.
451
+ *
452
+ * In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_NonFungibleTokenSK`
453
+ * witness as `persistentHash(secretKey)`.
454
+ *
455
+ * @warning Transfers to contract addresses are considered unsafe because contract-to-contract calls
456
+ * are not currently supported. Tokens sent to a contract address may become irretrievable.
457
+ * Once contract-to-contract calls are supported, this circuit may be deprecated.
458
+ *
459
+ * @circuitInfo k=13, rows=4797
460
+ *
461
+ * Requirements:
462
+ *
463
+ * - The contract is initialized.
464
+ * - `fromAddress` is not zero.
465
+ * - `to` is not zero.
466
+ * - `tokenId` token must be owned by `fromAddress`.
467
+ * - If the caller is not `fromAddress`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
468
+ *
469
+ * @param {Either<Bytes<32>, ContractAddress>} fromAddress - The source account from which the token is being transferred
470
+ * @param {Either<Bytes<32>, ContractAddress>} to - The target account to transfer token to
471
+ * @param {Uint<128>} tokenId - The token being transferred
472
+ * @return {[]} - Empty tuple.
473
+ */
474
+ export circuit _unsafeTransferFrom(
475
+ fromAddress: Either<Bytes<32>, ContractAddress>,
476
+ to: Either<Bytes<32>, ContractAddress>,
477
+ tokenId: Uint<128>
478
+ ): [] {
479
+ Initializable_assertInitialized();
480
+ assert(!_isTargetZero(to), "NonFungibleToken: invalid receiver");
481
+ // Setting an "auth" argument enables the `_isAuthorized` check which verifies that the token exists
482
+ // (fromAddress != 0). Therefore, it is not needed to verify that the return value is not 0 here.
483
+ const auth = left<Bytes<32>, ContractAddress>(_computeAccountId());
484
+ const previousOwner = _update(to, tokenId, auth);
485
+
486
+ const canonFrom = Utils_canonicalize<Bytes<32>, ContractAddress>(fromAddress);
487
+ assert(previousOwner == canonFrom, "NonFungibleToken: incorrect owner");
488
+ }
489
+
490
+ /**
491
+ * @description Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist.
492
+ *
493
+ * @circuitInfo k=9, rows=308
494
+ *
495
+ * Requirements:
496
+ *
497
+ * - The contract is initialized.
498
+ *
499
+ * @param {Uint<128>} tokenId - The target token of the owner query
500
+ * @return {Either<Bytes<32>, ContractAddress>} - The owner of the token
501
+ */
502
+ export circuit _ownerOf(tokenId: Uint<128>): Either<Bytes<32>, ContractAddress> {
503
+ Initializable_assertInitialized();
504
+ if (!_owners.member(disclose(tokenId))) {
505
+ return ZERO();
506
+ }
507
+
508
+ return _owners.lookup(disclose(tokenId));
509
+ }
510
+
511
+ /**
512
+ * @description Returns the approved address for `tokenId`. Returns the zero value if `tokenId` is not minted.
513
+ *
514
+ * @circuitInfo k=9, rows=308
515
+ *
516
+ * Requirements:
517
+ *
518
+ * - The contract is initialized.
519
+ *
520
+ * @param {Uint<128>} tokenId - The token to query
521
+ * @return {Either<Bytes<32>, ContractAddress>} - An account approved to spend `tokenId`
522
+ */
523
+ export circuit _getApproved(tokenId: Uint<128>): Either<Bytes<32>, ContractAddress> {
524
+ Initializable_assertInitialized();
525
+ if (!_tokenApprovals.member(disclose(tokenId))) {
526
+ return ZERO();
527
+ }
528
+ return _tokenApprovals.lookup(disclose(tokenId));
529
+ }
530
+
531
+ /**
532
+ * @description Returns whether `spender` is allowed to manage `owner`'s tokens, or `tokenId` in
533
+ * particular (ignoring whether it is owned by `owner`).
534
+ *
535
+ * @circuitInfo k=11, rows=1798
536
+ *
537
+ * Requirements:
538
+ *
539
+ * - The contract is initialized.
540
+ *
541
+ * @warning This function assumes that `owner` is the actual owner of `tokenId` and does not verify this
542
+ * assumption.
543
+ *
544
+ * @param {Either<Bytes<32>, ContractAddress>} owner - Owner of the token
545
+ * @param {Either<Bytes<32>, ContractAddress>} spender - Account that wishes to spend `tokenId`
546
+ * @param {Uint<128>} tokenId - Token to spend
547
+ * @return {Boolean} - A boolean determining if `spender` may manage `tokenId`
548
+ */
549
+ export circuit _isAuthorized(
550
+ owner: Either<Bytes<32>, ContractAddress>,
551
+ spender: Either<Bytes<32>, ContractAddress>,
552
+ tokenId: Uint<128>
553
+ ): Boolean {
554
+ Initializable_assertInitialized();
555
+ const canonOwner = Utils_canonicalize<Bytes<32>, ContractAddress>(owner);
556
+ const canonSpender = Utils_canonicalize<Bytes<32>, ContractAddress>(spender);
557
+
558
+ return (!_isTargetZero(disclose(canonSpender)) &&
559
+ (disclose(canonOwner) == disclose(canonSpender) ||
560
+ isApprovedForAll(canonOwner, canonSpender) ||
561
+ _getApproved(tokenId) == disclose(canonSpender)));
562
+ }
563
+
564
+ /**
565
+ * @description Checks if `spender` can operate on `tokenId`, assuming the provided `owner` is the actual owner.
566
+ *
567
+ * @circuitInfo k=11, rows=1804
568
+ *
569
+ * Requirements:
570
+ *
571
+ * - The contract is initialized.
572
+ * - `spender` has approval from `owner` for `tokenId` OR `spender` has approval to manage all of `owner`'s assets.
573
+ *
574
+ * @warning This function assumes that `owner` is the actual owner of `tokenId` and does not verify this
575
+ * assumption.
576
+ *
577
+ * @param {Either<Bytes<32>, ContractAddress>} owner - Owner of the token
578
+ * @param {Either<Bytes<32>, ContractAddress>} spender - Account operating on `tokenId`
579
+ * @param {Uint<128>} tokenId - The token to spend
580
+ * @return {[]} - Empty tuple.
581
+ */
582
+ export circuit _checkAuthorized(
583
+ owner: Either<Bytes<32>, ContractAddress>,
584
+ spender: Either<Bytes<32>, ContractAddress>,
585
+ tokenId: Uint<128>
586
+ ): [] {
587
+ Initializable_assertInitialized();
588
+ if (!_isAuthorized(owner, spender, tokenId)) {
589
+ assert(!_isTargetZero(owner), "NonFungibleToken: nonexistent token");
590
+ assert(false, "NonFungibleToken: insufficient approval");
591
+ }
592
+ }
593
+
594
+ /**
595
+ * @description Transfers `tokenId` from its current owner to `to`, or alternatively mints (or burns) if the current owner
596
+ * (or `to`) is zero. Returns the owner of the `tokenId` before the update.
597
+ *
598
+ * Requirements:
599
+ *
600
+ * - The contract is initialized.
601
+ * - If `auth` is non 0, then this function will check that `auth` is either the owner of the token,
602
+ * or approved to operate on the token (by the owner).
603
+ *
604
+ * @param {Either<Bytes<32>, ContractAddress>} to - The intended recipient of the token transfer
605
+ * @param {Uint<128>} tokenId - The token being transferred
606
+ * @param {Either<Bytes<32>, ContractAddress>} auth - An account authorized to transfer the token
607
+ * @return {Either<Bytes<32>, ContractAddress>} - Owner of the token before it was transferred
608
+ */
609
+ circuit _update(to: Either<Bytes<32>, ContractAddress>,
610
+ tokenId: Uint<128>,
611
+ auth: Either<Bytes<32>, ContractAddress>
612
+ ): Either<Bytes<32>, ContractAddress> {
613
+ Initializable_assertInitialized();
614
+ const canonTo = Utils_canonicalize<Bytes<32>, ContractAddress>(to);
615
+ const canonAuth = Utils_canonicalize<Bytes<32>, ContractAddress>(auth);
616
+ const fromAddress = _ownerOf(tokenId);
617
+
618
+ // Perform (optional) operator check
619
+ if (!_isTargetZero(disclose(canonAuth))) {
620
+ _checkAuthorized(fromAddress, canonAuth, tokenId);
621
+ }
622
+
623
+ // Execute the update
624
+ if (!_isTargetZero(disclose(fromAddress))) {
625
+ // Clear approval. No need to re-authorize
626
+ _approve(ZERO(), tokenId, ZERO());
627
+ const canonFrom = Utils_canonicalize<Bytes<32>, ContractAddress>(fromAddress);
628
+ const newBalance = _balances.lookup(disclose(canonFrom)) - 1 as Uint<128>;
629
+ _balances.insert(disclose(canonFrom), disclose(newBalance));
630
+ }
631
+
632
+ if (!_isTargetZero(disclose(canonTo))) {
633
+ if (!_balances.member(disclose(canonTo))) {
634
+ _balances.insert(disclose(canonTo), 0);
635
+ }
636
+ const newBalance = _balances.lookup(disclose(canonTo)) + 1 as Uint<128>;
637
+ _balances.insert(disclose(canonTo), disclose(newBalance));
638
+ }
639
+
640
+ _owners.insert(disclose(tokenId), disclose(canonTo));
641
+
642
+ return fromAddress;
643
+ }
644
+
645
+ /**
646
+ * @description Mints `tokenId` and transfers it to `to`.
647
+ *
648
+ * @circuitInfo k=11, rows=1473
649
+ *
650
+ * Requirements:
651
+ *
652
+ * - The contract is initialized.
653
+ * - `tokenId` must not exist.
654
+ * - `to` is not zero.
655
+ * - `to` is not a ContractAddress.
656
+ *
657
+ * @param {Either<Bytes<32>, ContractAddress>} to - The account receiving `tokenId`
658
+ * @param {Uint<128>} tokenId - The token to transfer
659
+ * @return {[]} - Empty tuple.
660
+ */
661
+ export circuit _mint(to: Either<Bytes<32>, ContractAddress>, tokenId: Uint<128>): [] {
662
+ Initializable_assertInitialized();
663
+ const isContractAddr = !to.is_left;
664
+ assert(!isContractAddr, "NonFungibleToken: unsafe transfer");
665
+
666
+ _unsafeMint(to, tokenId);
667
+ }
668
+
669
+ /**
670
+ * @description Mints `tokenId` and transfers it to `to`. It does NOT check if the recipient is a ContractAddress.
671
+ *
672
+ * @circuitInfo k=11, rows=1470
673
+ *
674
+ * Requirements:
675
+ *
676
+ * - The contract is initialized.
677
+ * - `tokenId` must not exist.
678
+ * - `to` is not zero.
679
+ *
680
+ * @warning Transfers to contract addresses are considered unsafe because contract-to-contract
681
+ * calls are not currently supported. Tokens sent to a contract address may become irretrievable.
682
+ * Once contract-to-contract calls are supported, this circuit may be deprecated.
683
+ *
684
+ * @param {Either<Bytes<32>, ContractAddress>} to - The account receiving `tokenId`
685
+ * @param {Uint<128>} tokenId - The token to transfer
686
+ * @return {[]} - Empty tuple.
687
+ */
688
+ export circuit _unsafeMint(
689
+ to: Either<Bytes<32>, ContractAddress>,
690
+ tokenId: Uint<128>
691
+ ): [] {
692
+ Initializable_assertInitialized();
693
+ assert(!_isTargetZero(to), "NonFungibleToken: invalid receiver");
694
+
695
+ const previousOwner = _update(to, tokenId, ZERO());
696
+
697
+ assert(_isTargetZero(previousOwner), "NonFungibleToken: invalid sender");
698
+ }
699
+
700
+ /**
701
+ * @description Destroys `tokenId`.
702
+ * The approval is cleared when the token is burned.
703
+ * This circuit does not check if the sender is authorized to operate on the token.
704
+ *
705
+ * @circuitInfo k=10, rows=509
706
+ *
707
+ * Requirements:
708
+ *
709
+ * - The contract is initialized.
710
+ * - `tokenId` must exist.
711
+ *
712
+ * @param {Uint<128>} tokenId - The token to burn
713
+ * @return {[]} - Empty tuple.
714
+ */
715
+ export circuit _burn(tokenId: Uint<128>): [] {
716
+ Initializable_assertInitialized();
717
+ const previousOwner = _update(ZERO(), tokenId, ZERO());
718
+ assert(!_isTargetZero(previousOwner), "NonFungibleToken: invalid sender");
719
+ _tokenURIs.remove(disclose(tokenId));
720
+ }
721
+
722
+ /**
723
+ * @description Transfers `tokenId` from `fromAddress` to `to`.
724
+ * As opposed to {transferFrom}, this imposes no restrictions on the caller's identity.
725
+ *
726
+ * @notice Transfers to contract addresses are currently disallowed until contract-to-contract
727
+ * interactions are supported in Compact. This restriction prevents assets from being inadvertently
728
+ * locked in contracts that cannot currently handle token receipt.
729
+ *
730
+ * @circuitInfo k=12, rows=2067
731
+ *
732
+ * Requirements:
733
+ *
734
+ * - The contract is initialized.
735
+ * - `to` is not zero.
736
+ * - `to` is not a ContractAddress.
737
+ * - `tokenId` token must be owned by `fromAddress`.
738
+ *
739
+ * @param {Either<Bytes<32>, ContractAddress>} fromAddress - The source account of the token transfer
740
+ * @param {Either<Bytes<32>, ContractAddress>} to - The target account of the token transfer
741
+ * @param {Uint<128>} tokenId - The token to transfer
742
+ * @return {[]} - Empty tuple.
743
+ */
744
+ export circuit _transfer(
745
+ fromAddress: Either<Bytes<32>, ContractAddress>,
746
+ to: Either<Bytes<32>, ContractAddress>,
747
+ tokenId: Uint<128>
748
+ ): [] {
749
+ Initializable_assertInitialized();
750
+ const isContractAddr = !to.is_left;
751
+ assert(!isContractAddr, "NonFungibleToken: unsafe transfer");
752
+
753
+ _unsafeTransfer(fromAddress, to, tokenId);
754
+ }
755
+
756
+ /**
757
+ * @description Transfers `tokenId` from `fromAddress` to `to`.
758
+ * As opposed to {_unsafeTransferFrom}, this imposes no restrictions on the caller's identity.
759
+ * It does NOT check if the recipient is a ContractAddress.
760
+ *
761
+ * @circuitInfo k=12, rows=2064
762
+ *
763
+ * Requirements:
764
+ *
765
+ * - The contract is initialized.
766
+ * - `to` is not zero.
767
+ * - `tokenId` token must be owned by `fromAddress`.
768
+ *
769
+ * @warning Transfers to contract addresses are considered unsafe because contract-to-contract
770
+ * calls are not currently supported. Tokens sent to a contract address may become irretrievable.
771
+ * Once contract-to-contract calls are supported, this circuit may be deprecated.
772
+ *
773
+ * @param {Either<Bytes<32>, ContractAddress>} fromAddress - The source account of the token transfer
774
+ * @param {Either<Bytes<32>, ContractAddress>} to - The target account of the token transfer
775
+ * @param {Uint<128>} tokenId - The token to transfer
776
+ * @return {[]} - Empty tuple.
777
+ */
778
+ export circuit _unsafeTransfer(
779
+ fromAddress: Either<Bytes<32>, ContractAddress>,
780
+ to: Either<Bytes<32>, ContractAddress>,
781
+ tokenId: Uint<128>
782
+ ): [] {
783
+ Initializable_assertInitialized();
784
+ assert(!_isTargetZero(to), "NonFungibleToken: invalid receiver");
785
+
786
+ const previousOwner = _update(to, tokenId, ZERO());
787
+
788
+ assert(!_isTargetZero(previousOwner), "NonFungibleToken: nonexistent token");
789
+
790
+ const canonFrom = Utils_canonicalize<Bytes<32>, ContractAddress>(fromAddress);
791
+ assert(previousOwner == canonFrom, "NonFungibleToken: incorrect owner");
792
+ }
793
+
794
+ /**
795
+ * @description Approve `to` to operate on `tokenId`.
796
+ *
797
+ * @circuitInfo k=11, rows=1810
798
+ *
799
+ * Requirements:
800
+ *
801
+ * - The contract is initialized.
802
+ * - If `auth` is non 0, then this function will check that `auth` is either the owner of the token,
803
+ * or approved to operate on the token (by the owner).
804
+ *
805
+ * @param {Either<Bytes<32>, ContractAddress>} to - The target account to approve
806
+ * @param {Uint<128>} tokenId - The token to approve
807
+ * @param {Either<Bytes<32>, ContractAddress>} auth - An account authorized to operate on all tokens held by the owner of the token
808
+ * @return {[]} - Empty tuple.
809
+ */
810
+ export circuit _approve(
811
+ to: Either<Bytes<32>, ContractAddress>,
812
+ tokenId: Uint<128>,
813
+ auth: Either<Bytes<32>, ContractAddress>
814
+ ): [] {
815
+ Initializable_assertInitialized();
816
+ // Canonicalize + normalize `to`
817
+ const canonTo = Utils_canonicalize<Bytes<32>, ContractAddress>(to);
818
+ const normalizedTo = _isTargetZero(canonTo) ? ZERO() : canonTo;
819
+
820
+ // Canonicalize auth
821
+ const canonAuth = Utils_canonicalize<Bytes<32>, ContractAddress>(auth);
822
+
823
+ if (!_isTargetZero(disclose(canonAuth))) {
824
+ const owner = _requireOwned(tokenId);
825
+
826
+ // We do not use _isAuthorized because single-token approvals should not be able to call approve
827
+ assert((owner == disclose(canonAuth) || isApprovedForAll(owner, canonAuth)),
828
+ "NonFungibleToken: invalid approver"
829
+ );
830
+ }
831
+
832
+ _tokenApprovals.insert(disclose(tokenId), disclose(normalizedTo));
833
+ }
834
+
835
+ /**
836
+ * @description Approve `operator` to operate on all of `owner` tokens.
837
+ *
838
+ * @circuitInfo k=11, rows=1261
839
+ *
840
+ * Requirements:
841
+ *
842
+ * - The contract is initialized.
843
+ * - `owner` is not zero.
844
+ * - `operator` is not zero.
845
+ *
846
+ * @param {Either<Bytes<32>, ContractAddress>} owner - Owner of a token
847
+ * @param {Either<Bytes<32>, ContractAddress>} operator - The account to approve
848
+ * @param {Boolean} approved - A boolean determining if `operator` may operate on all of `owner` tokens
849
+ * @return {[]} - Empty tuple.
850
+ */
851
+ export circuit _setApprovalForAll(
852
+ owner: Either<Bytes<32>, ContractAddress>,
853
+ operator: Either<Bytes<32>, ContractAddress>,
854
+ approved: Boolean
855
+ ): [] {
856
+ Initializable_assertInitialized();
857
+ const canonOwner = Utils_canonicalize<Bytes<32>, ContractAddress>(owner);
858
+ assert(!_isTargetZero(canonOwner), "NonFungibleToken: invalid owner");
859
+
860
+ const canonOp = Utils_canonicalize<Bytes<32>, ContractAddress>(operator);
861
+ assert(!_isTargetZero(canonOp), "NonFungibleToken: invalid operator");
862
+
863
+ if (!_operatorApprovals.member(disclose(canonOwner))) {
864
+ _operatorApprovals.insert(
865
+ disclose(canonOwner),
866
+ default<Map<Either<Bytes<32>, ContractAddress>, Boolean>>
867
+ );
868
+ }
869
+
870
+ _operatorApprovals.lookup(canonOwner).insert(disclose(canonOp), disclose(approved));
871
+ }
872
+
873
+ /**
874
+ * @description Reverts if the `tokenId` doesn't have a current owner (it hasn't been minted, or it has been burned).
875
+ * Returns the owner.
876
+ *
877
+ * @circuitInfo k=9, rows=331
878
+ *
879
+ * Requirements:
880
+ *
881
+ * - The contract is initialized.
882
+ * - `tokenId` must exist.
883
+ *
884
+ * @param {Uint<128>} tokenId - The token that should be owned
885
+ * @return {Either<Bytes<32>, ContractAddress>} - The owner of `tokenId`
886
+ */
887
+ export circuit _requireOwned(tokenId: Uint<128>): Either<Bytes<32>, ContractAddress> {
888
+ Initializable_assertInitialized();
889
+ const owner = _ownerOf(tokenId);
890
+
891
+ assert(!_isTargetZero(owner), "NonFungibleToken: nonexistent token");
892
+ return owner;
893
+ }
894
+
895
+ /**
896
+ * @description Computes the caller's account identifier from the `wit_NonFungibleTokenSK` witness.
897
+ *
898
+ * ## ID Derivation
899
+ * `accountId = persistentHash(secretKey)`
900
+ *
901
+ * The result is a 32-byte commitment that uniquely identifies the caller.
902
+ *
903
+ * @returns {Bytes<32>} accountId - The computed account identifier.
904
+ */
905
+ circuit _computeAccountId(): Bytes<32> {
906
+ return computeAccountId(wit_NonFungibleTokenSK());
907
+ }
908
+
909
+ /**
910
+ * @description Computes an account identifier without on-chain state, allowing a user to derive
911
+ * their identity commitment before submitting it in a token operation.
912
+ * This is the off-chain counterpart to {_computeAccountId} and produces an identical result
913
+ * given the same inputs.
914
+ *
915
+ * @warning OpSec: The `secretKey` parameter is a sensitive secret. Mishandling it can
916
+ * permanently compromise the security of this system:
917
+ *
918
+ * - **Never log or persist** the `secretKey` in plaintext — avoid browser devtools,
919
+ * application logs, analytics pipelines, or any observable side-channel.
920
+ * - **Store offline or in secure enclaves** — hardware security modules (HSMs),
921
+ * air-gapped devices, or encrypted vaults are strongly preferred over hot storage.
922
+ * - **Use cryptographically secure randomness** — generate keys with `crypto.getRandomValues()`
923
+ * or equivalent; weak or predictable keys can be brute-forced to reveal your identity.
924
+ * - **Treat key loss as identity loss** — a lost key cannot be recovered.
925
+ * - **Avoid calling this circuit in untrusted environments** — executing this in an
926
+ * unverified browser extension, compromised runtime, or shared machine may expose
927
+ * the key to a malicious observer.
928
+ *
929
+ * ## ID Derivation
930
+ * `accountId = persistentHash(secretKey)`
931
+ *
932
+ * @param {Bytes<32>} secretKey - A 32-byte cryptographically secure random value.
933
+ *
934
+ * @returns {Bytes<32>} accountId - The computed account identifier.
935
+ */
936
+ export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> {
937
+ return persistentHash<Vector<1, Bytes<32>>>([secretKey]);
938
+ }
939
+
940
+ /**
941
+ * @description Returns `true` if `target`'s active branch (as indicated by `is_left`)
942
+ * holds the zero value.
943
+ *
944
+ * @param {Either<Bytes<32>, ContractAddress>} target - The value to check.
945
+ * @returns {Boolean} - `true` if the active branch is zero, `false` otherwise.
946
+ */
947
+ circuit _isTargetZero(target: Either<Bytes<32>, ContractAddress>): Boolean {
948
+ if (target.is_left) {
949
+ return target.left == default<Bytes<32>>;
950
+ } else {
951
+ return target.right == default<ContractAddress>;
952
+ }
953
+ }
954
+ }