@btc-vision/btc-runtime 1.10.8 → 1.10.11

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.
Files changed (44) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +258 -137
  3. package/SECURITY.md +226 -0
  4. package/docs/README.md +614 -0
  5. package/docs/advanced/bitcoin-scripts.md +939 -0
  6. package/docs/advanced/cross-contract-calls.md +579 -0
  7. package/docs/advanced/plugins.md +1006 -0
  8. package/docs/advanced/quantum-resistance.md +660 -0
  9. package/docs/advanced/signature-verification.md +715 -0
  10. package/docs/api-reference/blockchain.md +729 -0
  11. package/docs/api-reference/events.md +642 -0
  12. package/docs/api-reference/op20.md +902 -0
  13. package/docs/api-reference/op721.md +819 -0
  14. package/docs/api-reference/safe-math.md +510 -0
  15. package/docs/api-reference/storage.md +840 -0
  16. package/docs/contracts/op-net-base.md +786 -0
  17. package/docs/contracts/op20-token.md +687 -0
  18. package/docs/contracts/op20s-signatures.md +614 -0
  19. package/docs/contracts/op721-nft.md +785 -0
  20. package/docs/contracts/reentrancy-guard.md +787 -0
  21. package/docs/core-concepts/blockchain-environment.md +724 -0
  22. package/docs/core-concepts/decorators.md +466 -0
  23. package/docs/core-concepts/events.md +652 -0
  24. package/docs/core-concepts/pointers.md +391 -0
  25. package/docs/core-concepts/security.md +473 -0
  26. package/docs/core-concepts/storage-system.md +969 -0
  27. package/docs/examples/basic-token.md +745 -0
  28. package/docs/examples/nft-with-reservations.md +1440 -0
  29. package/docs/examples/oracle-integration.md +1212 -0
  30. package/docs/examples/stablecoin.md +1180 -0
  31. package/docs/getting-started/first-contract.md +575 -0
  32. package/docs/getting-started/installation.md +384 -0
  33. package/docs/getting-started/project-structure.md +630 -0
  34. package/docs/storage/memory-maps.md +764 -0
  35. package/docs/storage/stored-arrays.md +778 -0
  36. package/docs/storage/stored-maps.md +758 -0
  37. package/docs/storage/stored-primitives.md +655 -0
  38. package/docs/types/address.md +773 -0
  39. package/docs/types/bytes-writer-reader.md +938 -0
  40. package/docs/types/calldata.md +744 -0
  41. package/docs/types/safe-math.md +446 -0
  42. package/package.json +52 -27
  43. package/runtime/memory/MapOfMap.ts +1 -0
  44. package/LICENSE.md +0 -21
@@ -0,0 +1,1440 @@
1
+ # NFT with Reservations Example
2
+
3
+ An advanced OP721 NFT collection with time-based reservations, whitelist minting, and reveal mechanics.
4
+
5
+ ## Overview
6
+
7
+ This example demonstrates:
8
+ - OP721 NFT implementation
9
+ - Time-based reservation system
10
+ - Whitelist/allowlist minting
11
+ - Reveal mechanism
12
+ - Multiple sale phases
13
+ - Collection metadata
14
+ - Decorators for ABI generation
15
+
16
+ ## Sale Phase States
17
+
18
+ The NFT collection progresses through multiple sale phases:
19
+
20
+ ```mermaid
21
+ ---
22
+ config:
23
+ theme: dark
24
+ ---
25
+ stateDiagram-v2
26
+ [*] --> INACTIVE: Deploy Contract
27
+ INACTIVE --> RESERVATION: startReservation() TX
28
+ RESERVATION --> WHITELIST: setSalePhase() TX
29
+ WHITELIST --> PUBLIC: setSalePhase() TX
30
+ PUBLIC --> REVEALED: reveal() TX
31
+
32
+ state INACTIVE {
33
+ [*] --> Configured
34
+ note right of Configured
35
+ No minting allowed
36
+ Admin configures collection
37
+ Storage: Initialize parameters
38
+ end note
39
+ }
40
+
41
+ state RESERVATION {
42
+ [*] --> AcceptingReservations
43
+ AcceptingReservations --> ReservationEnded: Time expires
44
+ note right of AcceptingReservations
45
+ Users call reserve(quantity)
46
+ Reservations stored on-chain
47
+ Can submit cancel TX before end
48
+ end note
49
+ note left of ReservationEnded
50
+ Users call claimReserved()
51
+ NFTs minted to reservers
52
+ Update ownership storage
53
+ end note
54
+ }
55
+
56
+ state WHITELIST {
57
+ [*] --> WhitelistMinting
58
+ note right of WhitelistMinting
59
+ Only whitelisted addresses
60
+ whitelistMint() TX
61
+ Max per wallet enforced
62
+ end note
63
+ }
64
+
65
+ state PUBLIC {
66
+ [*] --> PublicMinting
67
+ note right of PublicMinting
68
+ Anyone can mint
69
+ publicMint() TX
70
+ Max per wallet enforced
71
+ end note
72
+ }
73
+
74
+ state REVEALED {
75
+ [*] --> MetadataVisible
76
+ note right of MetadataVisible
77
+ tokenURI shows real metadata
78
+ baseURI + tokenId + .json
79
+ end note
80
+ }
81
+ ```
82
+
83
+ ## Reservation Flow
84
+
85
+ Each token slot in the collection progresses through distinct states during the reservation lifecycle:
86
+
87
+ ```mermaid
88
+ ---
89
+ config:
90
+ theme: dark
91
+ ---
92
+ stateDiagram-v2
93
+ [*] --> Available: Collection Deployed
94
+
95
+ Available --> Reserved: reserve(quantity) TX
96
+ Reserved --> Available: cancelReservation() TX
97
+ Reserved --> Expired: Reservation period ends<br/>without claim
98
+ Reserved --> Minted: claimReserved() TX
99
+
100
+ Minted --> [*]: Token Owned
101
+
102
+ state Available {
103
+ [*] --> OpenForReservation
104
+ note right of OpenForReservation
105
+ Token slot not yet claimed
106
+ Can be reserved by any user
107
+ Within maxPerWallet limit
108
+ end note
109
+ }
110
+
111
+ state Reserved {
112
+ [*] --> HeldForUser
113
+ note right of HeldForUser
114
+ Slot reserved for address
115
+ Stored in _reservedBy map
116
+ Awaiting claim or cancel
117
+ end note
118
+ }
119
+
120
+ state Expired {
121
+ [*] --> Unclaimed
122
+ note right of Unclaimed
123
+ Reservation period ended
124
+ User did not claim
125
+ Slot may be released
126
+ end note
127
+ }
128
+
129
+ state Minted {
130
+ [*] --> TokenOwned
131
+ note right of TokenOwned
132
+ NFT minted to owner
133
+ Stored in _owners map
134
+ Transferable via OP721
135
+ end note
136
+ }
137
+ ```
138
+
139
+ The reservation system allows users to reserve NFTs before minting begins:
140
+
141
+ ```mermaid
142
+ sequenceDiagram
143
+ participant Admin as 👤 Admin
144
+ participant BTC as Bitcoin Network
145
+ participant Contract as Contract Execution
146
+ participant User1 as 👤 User1
147
+ participant User2 as 👤 User2
148
+ participant Storage as Storage Layer
149
+
150
+ Admin->>BTC: Submit startReservation(86400) TX
151
+ BTC->>Contract: Execute startReservation
152
+ Contract->>Storage: Write reservationEnd = now + 86400s
153
+ Contract-->>BTC: Success
154
+ BTC-->>Admin: TX Confirmed
155
+
156
+ Note over User1,Storage: Reservation Period Active
157
+
158
+ User1->>BTC: Submit reserve(quantity=2) TX
159
+ BTC->>Contract: Execute reserve
160
+ Contract->>Contract: Check now < reservationEnd
161
+ Contract->>Storage: Read current reservation for User1
162
+ Contract->>Contract: newTotal = current + 2
163
+ Contract->>Contract: Check newTotal <= maxPerWallet
164
+ Contract->>Storage: Write User1 reserved = 2
165
+ Contract-->>BTC: Success
166
+ BTC-->>User1: TX Confirmed
167
+
168
+ User2->>BTC: Submit reserve(quantity=5) TX
169
+ BTC->>Contract: Execute reserve
170
+ Contract->>Contract: Check now < reservationEnd
171
+ Contract->>Storage: Read current reservation for User2
172
+ Contract->>Contract: newTotal = 0 + 5
173
+ alt Exceeds max per wallet
174
+ Contract-->>BTC: Revert: Exceeds max per wallet
175
+ BTC-->>User2: TX Failed
176
+ else Within limit
177
+ Contract->>Storage: Write User2 reserved = 5
178
+ Contract-->>BTC: Success
179
+ BTC-->>User2: TX Confirmed
180
+ end
181
+
182
+ Note over Contract: Time passes... Reservation period ends
183
+
184
+ User1->>BTC: Submit claimReserved() TX
185
+ BTC->>Contract: Execute claimReserved
186
+ Contract->>Contract: Check now >= reservationEnd
187
+ Contract->>Storage: Read User1 reservation = 2
188
+ Contract->>Storage: Write User1 reservation = 0
189
+ Contract->>Contract: Mint token #1 to User1
190
+ Contract->>Contract: Mint token #2 to User1
191
+ Contract->>Storage: Write nextTokenId = 3
192
+ Contract->>Storage: Write NFT ownership
193
+ Contract-->>BTC: Success
194
+ BTC-->>User1: TX Confirmed (2 NFTs minted)
195
+
196
+ User1->>BTC: Submit claimReserved() TX again
197
+ BTC->>Contract: Execute claimReserved
198
+ Contract->>Storage: Read User1 reservation = 0
199
+ Contract-->>BTC: Revert: No reservations
200
+ BTC-->>User1: TX Failed
201
+ ```
202
+
203
+ ### Reservation Implementation
204
+
205
+ ```typescript
206
+ @method({ name: 'quantity', type: ABIDataTypes.UINT256 })
207
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
208
+ @emit('Reserved')
209
+ public reserve(calldata: Calldata): BytesWriter {
210
+ const quantity = calldata.readU256();
211
+ const sender = Blockchain.tx.sender;
212
+
213
+ // Check reservation is active
214
+ const now = u256.fromU64(Blockchain.block.medianTime);
215
+ if (now >= this._reservationEnd.value) {
216
+ throw new Revert('Reservation period ended');
217
+ }
218
+
219
+ // Check quantity limits
220
+ const currentReserved = this._reservedBy.get(sender);
221
+ const newTotal = SafeMath.add(currentReserved, quantity);
222
+
223
+ if (newTotal > this._maxPerWallet.value) {
224
+ throw new Revert('Exceeds max per wallet');
225
+ }
226
+
227
+ // Update reservation
228
+ this._reservedBy.set(sender, newTotal);
229
+
230
+ return new BytesWriter(0);
231
+ }
232
+ ```
233
+
234
+ ## Whitelist Verification
235
+
236
+ Whitelist minting validates that users are on the allowlist:
237
+
238
+ ```mermaid
239
+ ---
240
+ config:
241
+ theme: dark
242
+ ---
243
+ flowchart LR
244
+ A["👤 User submits whitelistMint TX"] --> B{Sale phase = WHITELIST?}
245
+ B -->|No| C[Revert: Whitelist sale not active]
246
+ B -->|Yes| D{User in whitelist?}
247
+ D -->|No| E[Revert: Not whitelisted]
248
+ D -->|Yes| F{Within limits?}
249
+ F -->|No| G[Revert: Exceeds limits]
250
+ F -->|Yes| H[Mint tokens]
251
+ H --> I[TX Success]
252
+ ```
253
+
254
+ ### Whitelist Implementation
255
+
256
+ ```typescript
257
+ @method({ name: 'quantity', type: ABIDataTypes.UINT256 })
258
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
259
+ @emit('Minted')
260
+ public whitelistMint(calldata: Calldata): BytesWriter {
261
+ const quantity = calldata.readU256();
262
+ const sender = Blockchain.tx.sender;
263
+
264
+ // Check phase
265
+ if (this._salePhase.value != PHASE_WHITELIST) {
266
+ throw new Revert('Whitelist sale not active');
267
+ }
268
+
269
+ // Check whitelist (AddressMemoryMap returns u256; non-zero = whitelisted)
270
+ if (this._whitelist.get(sender).isZero()) {
271
+ throw new Revert('Not whitelisted');
272
+ }
273
+
274
+ this._mintInternal(sender, quantity);
275
+
276
+ return new BytesWriter(0);
277
+ }
278
+ ```
279
+
280
+ **Solidity Comparison:**
281
+
282
+ ```solidity
283
+ // Solidity - Using Merkle proof for whitelist
284
+ function whitelistMint(uint256 quantity, bytes32[] calldata proof) external {
285
+ require(salePhase == Phase.WHITELIST, "Whitelist sale not active");
286
+ require(MerkleProof.verify(proof, merkleRoot, keccak256(abi.encodePacked(msg.sender))), "Not whitelisted");
287
+ _mintInternal(msg.sender, quantity);
288
+ }
289
+
290
+ // OPNet - Using on-chain mapping (simpler approach)
291
+ @method({ name: 'quantity', type: ABIDataTypes.UINT256 })
292
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
293
+ @emit('Minted')
294
+ public whitelistMint(calldata: Calldata): BytesWriter {
295
+ // AddressMemoryMap returns u256; non-zero = whitelisted
296
+ if (this._whitelist.get(sender).isZero()) {
297
+ throw new Revert('Not whitelisted');
298
+ }
299
+ // ...
300
+ }
301
+ ```
302
+
303
+ ## Reservation Cancellation
304
+
305
+ Users can cancel their reservations during the reservation period:
306
+
307
+ ```mermaid
308
+ ---
309
+ config:
310
+ theme: dark
311
+ ---
312
+ flowchart LR
313
+ A["👤 User submits cancelReservation TX"] --> B{Period active?}
314
+ B -->|No| C[Revert: Period ended]
315
+ B -->|Yes| D{Has reservation?}
316
+ D -->|No| E[Revert: No reservations]
317
+ D -->|Yes| F[Clear reservation]
318
+ F --> G[Process refund]
319
+ G --> H[TX Success]
320
+ ```
321
+
322
+ ## Token URI Reveal Mechanism
323
+
324
+ The reveal mechanism hides metadata until the collection is revealed:
325
+
326
+ ```mermaid
327
+ ---
328
+ config:
329
+ theme: dark
330
+ ---
331
+ flowchart LR
332
+ A["👤 User: Call tokenURI"] --> B{Token exists in storage?}
333
+ B -->|No| C[Revert: Token does not exist]
334
+ B -->|Yes| D{Collection revealed?}
335
+
336
+ D -->|No - _revealed = false| E["Return hiddenURI<br/>e.g., ipfs://QmHidden/hidden.json"]
337
+ D -->|Yes - _revealed = true| F["Return baseURI + tokenId + .json<br/>e.g., ipfs://QmReal/1.json"]
338
+
339
+ E --> G[All tokens show same metadata]
340
+ F --> H[Each token shows unique metadata]
341
+
342
+ subgraph "Before Reveal "
343
+ I[Token #1 -> hidden.json]
344
+ J[Token #2 -> hidden.json]
345
+ K[Token #3 -> hidden.json]
346
+ end
347
+
348
+ subgraph "After Reveal "
349
+ L[Token #1 -> 1.json]
350
+ M[Token #2 -> 2.json]
351
+ N[Token #3 -> 3.json]
352
+ end
353
+ ```
354
+
355
+ ### Reveal Implementation
356
+
357
+ ```typescript
358
+ public override tokenURI(tokenId: u256): string {
359
+ // Check token exists
360
+ if (this.ownerOf(tokenId).equals(Address.zero())) {
361
+ throw new Revert('Token does not exist');
362
+ }
363
+
364
+ if (!this._revealed.value) {
365
+ return this._hiddenURI.value;
366
+ }
367
+
368
+ return this._baseURI.value + tokenId.toString() + '.json';
369
+ }
370
+ ```
371
+
372
+ ## Sale Timeline
373
+
374
+ The complete sale lifecycle follows a typical NFT launch pattern:
375
+
376
+ ```mermaid
377
+ gantt
378
+ title NFT Collection Sale Timeline
379
+ dateFormat YYYY-MM-DD
380
+ axisFormat %b %d
381
+
382
+ section Preparation
383
+ Deploy Contract :done, deploy, 2024-01-01, 1d
384
+ Configure Collection :done, config, after deploy, 1d
385
+ Set Whitelist :done, whitelist, after config, 2d
386
+
387
+ section Reservation Phase
388
+ Start Reservation :crit, res_start, 2024-01-05, 1d
389
+ Reservation Period :active, res_period, after res_start, 2d
390
+ Claim Reserved NFTs :claim, after res_period, 1d
391
+
392
+ section Whitelist Sale
393
+ Whitelist Phase :wl_phase, 2024-01-09, 3d
394
+
395
+ section Public Sale
396
+ Public Phase :pub_phase, after wl_phase, 5d
397
+
398
+ section Reveal
399
+ Reveal Metadata :milestone, reveal, after pub_phase, 1d
400
+ Collection Complete :done, after reveal, 1d
401
+ ```
402
+
403
+ ## Complete Implementation
404
+
405
+ ```typescript
406
+ import { u256 } from '@btc-vision/as-bignum/assembly';
407
+ import {
408
+ OP721,
409
+ OP721InitParameters,
410
+ Blockchain,
411
+ Address,
412
+ Calldata,
413
+ BytesWriter,
414
+ SafeMath,
415
+ Revert,
416
+ StoredU256,
417
+ StoredString,
418
+ StoredBoolean,
419
+ StoredU8,
420
+ AddressMemoryMap,
421
+ ABIDataTypes,
422
+ EMPTY_POINTER,
423
+ } from '@btc-vision/btc-runtime/runtime';
424
+
425
+ // Sale phases
426
+ const PHASE_INACTIVE: u8 = 0;
427
+ const PHASE_WHITELIST: u8 = 1;
428
+ const PHASE_PUBLIC: u8 = 2;
429
+
430
+ @final
431
+ export class NFTWithReservations extends OP721 {
432
+ // Configuration storage
433
+ private maxSupplyPointer: u16 = Blockchain.nextPointer;
434
+ private pricePointer: u16 = Blockchain.nextPointer;
435
+ private maxPerWalletPointer: u16 = Blockchain.nextPointer;
436
+ private baseURIPointer: u16 = Blockchain.nextPointer;
437
+ private hiddenURIPointer: u16 = Blockchain.nextPointer;
438
+ private revealedPointer: u16 = Blockchain.nextPointer;
439
+ private salePhasePointer: u16 = Blockchain.nextPointer;
440
+ private nextTokenIdPointer: u16 = Blockchain.nextPointer;
441
+
442
+ // Reservation storage
443
+ private reservationEndPointer: u16 = Blockchain.nextPointer;
444
+ private reservedByPointer: u16 = Blockchain.nextPointer;
445
+ private reservationPricePointer: u16 = Blockchain.nextPointer;
446
+
447
+ // Whitelist storage
448
+ private whitelistPointer: u16 = Blockchain.nextPointer;
449
+ private mintedCountPointer: u16 = Blockchain.nextPointer;
450
+
451
+ // Stored values
452
+ private _maxSupply: StoredU256;
453
+ private _price: StoredU256;
454
+ private _maxPerWallet: StoredU256;
455
+ private _baseURI: StoredString;
456
+ private _hiddenURI: StoredString;
457
+ private _revealed: StoredBoolean;
458
+ private _salePhase: StoredU8;
459
+ private _nextTokenId: StoredU256;
460
+
461
+ private _reservationEnd: StoredU256;
462
+ private _reservedBy: AddressMemoryMap;
463
+ private _reservationPrice: StoredU256;
464
+
465
+ private _whitelist: AddressMemoryMap;
466
+ private _mintedCount: AddressMemoryMap;
467
+
468
+ public constructor() {
469
+ super();
470
+
471
+ // Initialize storage
472
+ this._maxSupply = new StoredU256(this.maxSupplyPointer, EMPTY_POINTER);
473
+ this._price = new StoredU256(this.pricePointer, EMPTY_POINTER);
474
+ this._maxPerWallet = new StoredU256(this.maxPerWalletPointer, EMPTY_POINTER);
475
+ this._baseURI = new StoredString(this.baseURIPointer, 0);
476
+ this._hiddenURI = new StoredString(this.hiddenURIPointer, 1);
477
+ this._revealed = new StoredBoolean(this.revealedPointer, false);
478
+ this._salePhase = new StoredU8(this.salePhasePointer, PHASE_INACTIVE);
479
+ this._nextTokenId = new StoredU256(this.nextTokenIdPointer, EMPTY_POINTER);
480
+
481
+ this._reservationEnd = new StoredU256(this.reservationEndPointer, EMPTY_POINTER);
482
+ this._reservedBy = new AddressMemoryMap(this.reservedByPointer);
483
+ this._reservationPrice = new StoredU256(this.reservationPricePointer, EMPTY_POINTER);
484
+
485
+ this._whitelist = new AddressMemoryMap(this.whitelistPointer);
486
+ this._mintedCount = new AddressMemoryMap(this.mintedCountPointer);
487
+ }
488
+
489
+ public override onDeployment(calldata: Calldata): void {
490
+ const name = calldata.readString();
491
+ const symbol = calldata.readString();
492
+ const maxSupply = calldata.readU256();
493
+ const price = calldata.readU256();
494
+ const maxPerWallet = calldata.readU256();
495
+ const hiddenURI = calldata.readString();
496
+
497
+ // OP721InitParameters requires: name, symbol, baseURI, maxSupply
498
+ // baseURI is empty initially since we use hiddenURI before reveal
499
+ this.instantiate(new OP721InitParameters(name, symbol, '', maxSupply));
500
+
501
+ this._maxSupply.value = maxSupply;
502
+ this._price.value = price;
503
+ this._maxPerWallet.value = maxPerWallet;
504
+ this._hiddenURI.value = hiddenURI;
505
+ this._nextTokenId.value = u256.One; // Set initial token ID
506
+ }
507
+
508
+ // ============ RESERVATION SYSTEM ============
509
+
510
+ /**
511
+ * Reserve tokens during reservation phase.
512
+ * Tokens are held until reservation period ends.
513
+ */
514
+ @method({ name: 'quantity', type: ABIDataTypes.UINT256 })
515
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
516
+ @emit('Reserved')
517
+ public reserve(calldata: Calldata): BytesWriter {
518
+ const quantity = calldata.readU256();
519
+ const sender = Blockchain.tx.sender;
520
+
521
+ // Check reservation is active
522
+ const now = u256.fromU64(Blockchain.block.medianTime);
523
+ if (now >= this._reservationEnd.value) {
524
+ throw new Revert('Reservation period ended');
525
+ }
526
+
527
+ // Check quantity limits
528
+ const currentReserved = this._reservedBy.get(sender);
529
+ const newTotal = SafeMath.add(currentReserved, quantity);
530
+
531
+ if (newTotal > this._maxPerWallet.value) {
532
+ throw new Revert('Exceeds max per wallet');
533
+ }
534
+
535
+ // Update reservation
536
+ this._reservedBy.set(sender, newTotal);
537
+
538
+ return new BytesWriter(0);
539
+ }
540
+
541
+ /**
542
+ * Claim reserved tokens after reservation period.
543
+ */
544
+ @method()
545
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
546
+ @emit('ReservationClaimed')
547
+ public claimReserved(_calldata: Calldata): BytesWriter {
548
+ const sender = Blockchain.tx.sender;
549
+
550
+ // Check reservation period ended
551
+ const now = u256.fromU64(Blockchain.block.medianTime);
552
+ if (now < this._reservationEnd.value) {
553
+ throw new Revert('Reservation period not ended');
554
+ }
555
+
556
+ // Get reserved quantity
557
+ const reserved = this._reservedBy.get(sender);
558
+ if (reserved.isZero()) {
559
+ throw new Revert('No reservations');
560
+ }
561
+
562
+ // Clear reservation
563
+ this._reservedBy.set(sender, u256.Zero);
564
+
565
+ // Mint reserved tokens
566
+ let count = reserved;
567
+ while (!count.isZero()) {
568
+ const tokenId = this._nextTokenId.value;
569
+ this._mint(sender, tokenId);
570
+ this._nextTokenId.value = SafeMath.add(tokenId, u256.One);
571
+ count = SafeMath.sub(count, u256.One);
572
+ }
573
+
574
+ return new BytesWriter(0);
575
+ }
576
+
577
+ /**
578
+ * Cancel reservation and get refund.
579
+ */
580
+ @method()
581
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
582
+ @emit('ReservationCancelled')
583
+ public cancelReservation(_calldata: Calldata): BytesWriter {
584
+ const sender = Blockchain.tx.sender;
585
+
586
+ // Must be during reservation period
587
+ const now = u256.fromU64(Blockchain.block.medianTime);
588
+ if (now >= this._reservationEnd.value) {
589
+ throw new Revert('Reservation period ended');
590
+ }
591
+
592
+ // Clear reservation
593
+ const reserved = this._reservedBy.get(sender);
594
+ if (reserved.isZero()) {
595
+ throw new Revert('No reservations');
596
+ }
597
+
598
+ this._reservedBy.set(sender, u256.Zero);
599
+
600
+ // Refund logic would go here
601
+
602
+ return new BytesWriter(0);
603
+ }
604
+
605
+ // ============ MINTING ============
606
+
607
+ /**
608
+ * Whitelist mint during whitelist phase.
609
+ */
610
+ @method({ name: 'quantity', type: ABIDataTypes.UINT256 })
611
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
612
+ @emit('Minted')
613
+ public whitelistMint(calldata: Calldata): BytesWriter {
614
+ const quantity = calldata.readU256();
615
+ const sender = Blockchain.tx.sender;
616
+
617
+ // Check phase
618
+ if (this._salePhase.value != PHASE_WHITELIST) {
619
+ throw new Revert('Whitelist sale not active');
620
+ }
621
+
622
+ // Check whitelist (AddressMemoryMap returns u256; non-zero = whitelisted)
623
+ if (this._whitelist.get(sender).isZero()) {
624
+ throw new Revert('Not whitelisted');
625
+ }
626
+
627
+ this._mintInternal(sender, quantity);
628
+
629
+ return new BytesWriter(0);
630
+ }
631
+
632
+ /**
633
+ * Public mint during public phase.
634
+ */
635
+ @method({ name: 'quantity', type: ABIDataTypes.UINT256 })
636
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
637
+ @emit('Minted')
638
+ public publicMint(calldata: Calldata): BytesWriter {
639
+ const quantity = calldata.readU256();
640
+ const sender = Blockchain.tx.sender;
641
+
642
+ // Check phase
643
+ if (this._salePhase.value != PHASE_PUBLIC) {
644
+ throw new Revert('Public sale not active');
645
+ }
646
+
647
+ this._mintInternal(sender, quantity);
648
+
649
+ return new BytesWriter(0);
650
+ }
651
+
652
+ private _mintInternal(to: Address, quantity: u256): void {
653
+ // Check supply
654
+ const currentSupply = this.totalSupply();
655
+ const newSupply = SafeMath.add(currentSupply, quantity);
656
+
657
+ if (newSupply > this._maxSupply.value) {
658
+ throw new Revert('Exceeds max supply');
659
+ }
660
+
661
+ // Check per-wallet limit
662
+ const minted = this._mintedCount.get(to);
663
+ const newMinted = SafeMath.add(minted, quantity);
664
+
665
+ if (newMinted > this._maxPerWallet.value) {
666
+ throw new Revert('Exceeds max per wallet');
667
+ }
668
+
669
+ // Update minted count
670
+ this._mintedCount.set(to, newMinted);
671
+
672
+ // Mint tokens
673
+ let count = quantity;
674
+ while (!count.isZero()) {
675
+ const tokenId = this._nextTokenId.value;
676
+ this._mint(to, tokenId);
677
+ this._nextTokenId.value = SafeMath.add(tokenId, u256.One);
678
+ count = SafeMath.sub(count, u256.One);
679
+ }
680
+ }
681
+
682
+ // ============ REVEAL ============
683
+
684
+ public override tokenURI(tokenId: u256): string {
685
+ // Check token exists
686
+ if (this.ownerOf(tokenId).equals(Address.zero())) {
687
+ throw new Revert('Token does not exist');
688
+ }
689
+
690
+ if (!this._revealed.value) {
691
+ return this._hiddenURI.value;
692
+ }
693
+
694
+ return this._baseURI.value + tokenId.toString() + '.json';
695
+ }
696
+
697
+ // ============ ADMIN FUNCTIONS ============
698
+
699
+ @method({ name: 'duration', type: ABIDataTypes.UINT64 })
700
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
701
+ @emit('ReservationStarted')
702
+ public startReservation(calldata: Calldata): BytesWriter {
703
+ this.onlyDeployer(Blockchain.tx.sender);
704
+
705
+ const duration = calldata.readU64();
706
+ const endTime = Blockchain.block.medianTime + duration;
707
+
708
+ this._reservationEnd.value = u256.fromU64(endTime);
709
+
710
+ return new BytesWriter(0);
711
+ }
712
+
713
+ @method({ name: 'phase', type: ABIDataTypes.UINT8 })
714
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
715
+ @emit('SalePhaseChanged')
716
+ public setSalePhase(calldata: Calldata): BytesWriter {
717
+ this.onlyDeployer(Blockchain.tx.sender);
718
+
719
+ const phase = calldata.readU8();
720
+ if (phase > PHASE_PUBLIC) {
721
+ throw new Revert('Invalid phase');
722
+ }
723
+
724
+ this._salePhase.value = phase;
725
+
726
+ return new BytesWriter(0);
727
+ }
728
+
729
+ @method(
730
+ { name: 'addresses', type: ABIDataTypes.ADDRESS_ARRAY },
731
+ { name: 'status', type: ABIDataTypes.BOOL },
732
+ )
733
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
734
+ @emit('WhitelistUpdated')
735
+ public setWhitelist(calldata: Calldata): BytesWriter {
736
+ this.onlyDeployer(Blockchain.tx.sender);
737
+
738
+ const addresses = calldata.readAddressArray();
739
+ const status = calldata.readBoolean();
740
+ // AddressMemoryMap stores u256; convert boolean to u256.One/Zero
741
+ const statusValue = status ? u256.One : u256.Zero;
742
+
743
+ for (let i = 0; i < addresses.length; i++) {
744
+ this._whitelist.set(addresses[i], statusValue);
745
+ }
746
+
747
+ return new BytesWriter(0);
748
+ }
749
+
750
+ @method({ name: 'baseURI', type: ABIDataTypes.STRING })
751
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
752
+ @emit('Revealed')
753
+ public reveal(calldata: Calldata): BytesWriter {
754
+ this.onlyDeployer(Blockchain.tx.sender);
755
+
756
+ const baseURI = calldata.readString();
757
+
758
+ this._baseURI.value = baseURI;
759
+ this._revealed.value = true;
760
+
761
+ return new BytesWriter(0);
762
+ }
763
+
764
+ @method({ name: 'price', type: ABIDataTypes.UINT256 })
765
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
766
+ @emit('PriceChanged')
767
+ public setPrice(calldata: Calldata): BytesWriter {
768
+ this.onlyDeployer(Blockchain.tx.sender);
769
+ this._price.value = calldata.readU256();
770
+ return new BytesWriter(0);
771
+ }
772
+
773
+ // ============ VIEW FUNCTIONS ============
774
+
775
+ @method({ name: 'addr', type: ABIDataTypes.ADDRESS })
776
+ @returns({ name: 'reserved', type: ABIDataTypes.UINT256 })
777
+ public getReservation(calldata: Calldata): BytesWriter {
778
+ const addr = calldata.readAddress();
779
+ const reserved = this._reservedBy.get(addr);
780
+
781
+ const writer = new BytesWriter(32);
782
+ writer.writeU256(reserved);
783
+ return writer;
784
+ }
785
+
786
+ @method({ name: 'addr', type: ABIDataTypes.ADDRESS })
787
+ @returns({ name: 'whitelisted', type: ABIDataTypes.BOOL })
788
+ public isWhitelisted(calldata: Calldata): BytesWriter {
789
+ const addr = calldata.readAddress();
790
+ // AddressMemoryMap.get() returns u256; convert to boolean
791
+ const status = !this._whitelist.get(addr).isZero();
792
+
793
+ const writer = new BytesWriter(1);
794
+ writer.writeBoolean(status);
795
+ return writer;
796
+ }
797
+
798
+ @method()
799
+ @returns(
800
+ { name: 'phase', type: ABIDataTypes.UINT8 },
801
+ { name: 'price', type: ABIDataTypes.UINT256 },
802
+ { name: 'maxSupply', type: ABIDataTypes.UINT256 },
803
+ { name: 'totalSupply', type: ABIDataTypes.UINT256 },
804
+ { name: 'revealed', type: ABIDataTypes.BOOL },
805
+ )
806
+ public getSaleInfo(_calldata: Calldata): BytesWriter {
807
+ const writer = new BytesWriter(128);
808
+
809
+ writer.writeU8(this._salePhase.value);
810
+ writer.writeU256(this._price.value);
811
+ writer.writeU256(this._maxSupply.value);
812
+ writer.writeU256(this.totalSupply());
813
+ writer.writeBoolean(this._revealed.value);
814
+
815
+ return writer;
816
+ }
817
+
818
+ @method({ name: 'addr', type: ABIDataTypes.ADDRESS })
819
+ @returns({ name: 'count', type: ABIDataTypes.UINT256 })
820
+ public getMintedCount(calldata: Calldata): BytesWriter {
821
+ const addr = calldata.readAddress();
822
+ const count = this._mintedCount.get(addr);
823
+
824
+ const writer = new BytesWriter(32);
825
+ writer.writeU256(count);
826
+ return writer;
827
+ }
828
+ }
829
+ ```
830
+
831
+ ## Key Features
832
+
833
+ ### Reservation System
834
+
835
+ ```
836
+ Timeline:
837
+ 1. Admin starts reservation period (e.g., 24 hours)
838
+ 2. Users reserve tokens during period
839
+ 3. Period ends
840
+ 4. Users claim reserved tokens
841
+ ```
842
+
843
+ ### Sale Phases
844
+
845
+ ```
846
+ PHASE_INACTIVE (0) -> PHASE_WHITELIST (1) -> PHASE_PUBLIC (2)
847
+ ```
848
+
849
+ ### Reveal Mechanism
850
+
851
+ Before reveal: All tokens show `hiddenURI`
852
+ After reveal: Tokens show `baseURI + tokenId + .json`
853
+
854
+ ## Usage Timeline
855
+
856
+ ```
857
+ 1. Deploy contract with hidden URI
858
+ 2. Set whitelist addresses
859
+ 3. Start reservation period
860
+ 4. Users reserve tokens
861
+ 5. Reservation ends -> users claim
862
+ 6. Set phase to WHITELIST
863
+ 7. Whitelisted users mint
864
+ 8. Set phase to PUBLIC
865
+ 9. Anyone can mint
866
+ 10. Reveal metadata
867
+ ```
868
+
869
+ ## Best Practices
870
+
871
+ ### Use StoredU8 for Small Enum Values
872
+
873
+ ```typescript
874
+ // Good: Use StoredU8 for phase constants
875
+ const PHASE_INACTIVE: u8 = 0;
876
+ const PHASE_WHITELIST: u8 = 1;
877
+ const PHASE_PUBLIC: u8 = 2;
878
+
879
+ private _salePhase: StoredU8;
880
+
881
+ // Compare using same types
882
+ if (this._salePhase.value != PHASE_WHITELIST) { }
883
+ ```
884
+
885
+ ### Use u256 for Timestamps When Comparing
886
+
887
+ ```typescript
888
+ // Good: Convert timestamps to u256 for comparison with u256 values
889
+ const now = u256.fromU64(Blockchain.block.medianTime);
890
+ if (now >= this._reservationEnd.value) { }
891
+ ```
892
+
893
+ ### Add Decorators for ABI Generation
894
+
895
+ ```typescript
896
+ @method({ name: 'quantity', type: ABIDataTypes.UINT256 })
897
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
898
+ @emit('Reserved')
899
+ public reserve(calldata: Calldata): BytesWriter { }
900
+
901
+ @method()
902
+ @returns(
903
+ { name: 'phase', type: ABIDataTypes.UINT8 },
904
+ { name: 'price', type: ABIDataTypes.UINT256 },
905
+ )
906
+ public getSaleInfo(_calldata: Calldata): BytesWriter { }
907
+ ```
908
+
909
+ ## Solidity Equivalent
910
+
911
+ For developers familiar with Solidity, here is an equivalent ERC721 implementation with reservations and reveal mechanics:
912
+
913
+ ```solidity
914
+ // SPDX-License-Identifier: MIT
915
+ pragma solidity ^0.8.20;
916
+
917
+ import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
918
+ import "@openzeppelin/contracts/access/Ownable.sol";
919
+ import "@openzeppelin/contracts/utils/Strings.sol";
920
+
921
+ contract NFTWithReservations is ERC721, Ownable {
922
+ using Strings for uint256;
923
+
924
+ enum SalePhase { INACTIVE, WHITELIST, PUBLIC }
925
+
926
+ // Configuration
927
+ uint256 public maxSupply;
928
+ uint256 public price;
929
+ uint256 public maxPerWallet;
930
+ string private baseURI_;
931
+ string private hiddenURI;
932
+ bool public revealed;
933
+ SalePhase public salePhase;
934
+ uint256 private nextTokenId = 1;
935
+
936
+ // Reservation system
937
+ uint256 public reservationEnd;
938
+ mapping(address => uint256) public reservations;
939
+
940
+ // Whitelist
941
+ mapping(address => bool) public whitelist;
942
+ mapping(address => uint256) public mintedCount;
943
+
944
+ event Reserved(address indexed user, uint256 quantity);
945
+ event ReservationClaimed(address indexed user, uint256 quantity);
946
+ event ReservationCancelled(address indexed user, uint256 quantity);
947
+
948
+ constructor(
949
+ string memory name,
950
+ string memory symbol,
951
+ uint256 _maxSupply,
952
+ uint256 _price,
953
+ uint256 _maxPerWallet,
954
+ string memory _hiddenURI
955
+ ) ERC721(name, symbol) Ownable(msg.sender) {
956
+ maxSupply = _maxSupply;
957
+ price = _price;
958
+ maxPerWallet = _maxPerWallet;
959
+ hiddenURI = _hiddenURI;
960
+ }
961
+
962
+ // ============ RESERVATION SYSTEM ============
963
+
964
+ function reserve(uint256 quantity) external payable {
965
+ require(block.timestamp < reservationEnd, "Reservation period ended");
966
+ require(reservations[msg.sender] + quantity <= maxPerWallet, "Exceeds max per wallet");
967
+ require(msg.value >= price * quantity, "Insufficient payment");
968
+
969
+ reservations[msg.sender] += quantity;
970
+ emit Reserved(msg.sender, quantity);
971
+ }
972
+
973
+ function claimReserved() external {
974
+ require(block.timestamp >= reservationEnd, "Reservation period not ended");
975
+ uint256 quantity = reservations[msg.sender];
976
+ require(quantity > 0, "No reservations");
977
+
978
+ reservations[msg.sender] = 0;
979
+
980
+ for (uint256 i = 0; i < quantity; i++) {
981
+ _safeMint(msg.sender, nextTokenId++);
982
+ }
983
+
984
+ emit ReservationClaimed(msg.sender, quantity);
985
+ }
986
+
987
+ function cancelReservation() external {
988
+ require(block.timestamp < reservationEnd, "Reservation period ended");
989
+ uint256 quantity = reservations[msg.sender];
990
+ require(quantity > 0, "No reservations");
991
+
992
+ reservations[msg.sender] = 0;
993
+
994
+ // Refund
995
+ uint256 refundAmount = price * quantity;
996
+ (bool success, ) = msg.sender.call{value: refundAmount}("");
997
+ require(success, "Refund failed");
998
+
999
+ emit ReservationCancelled(msg.sender, quantity);
1000
+ }
1001
+
1002
+ // ============ MINTING ============
1003
+
1004
+ function whitelistMint(uint256 quantity) external payable {
1005
+ require(salePhase == SalePhase.WHITELIST, "Whitelist sale not active");
1006
+ require(whitelist[msg.sender], "Not whitelisted");
1007
+ require(msg.value >= price * quantity, "Insufficient payment");
1008
+
1009
+ _mintInternal(msg.sender, quantity);
1010
+ }
1011
+
1012
+ function publicMint(uint256 quantity) external payable {
1013
+ require(salePhase == SalePhase.PUBLIC, "Public sale not active");
1014
+ require(msg.value >= price * quantity, "Insufficient payment");
1015
+
1016
+ _mintInternal(msg.sender, quantity);
1017
+ }
1018
+
1019
+ function _mintInternal(address to, uint256 quantity) internal {
1020
+ require(nextTokenId + quantity - 1 <= maxSupply, "Exceeds max supply");
1021
+ require(mintedCount[to] + quantity <= maxPerWallet, "Exceeds max per wallet");
1022
+
1023
+ mintedCount[to] += quantity;
1024
+
1025
+ for (uint256 i = 0; i < quantity; i++) {
1026
+ _safeMint(to, nextTokenId++);
1027
+ }
1028
+ }
1029
+
1030
+ // ============ REVEAL ============
1031
+
1032
+ function tokenURI(uint256 tokenId) public view override returns (string memory) {
1033
+ require(_ownerOf(tokenId) != address(0), "Token does not exist");
1034
+
1035
+ if (!revealed) {
1036
+ return hiddenURI;
1037
+ }
1038
+
1039
+ return string(abi.encodePacked(baseURI_, tokenId.toString(), ".json"));
1040
+ }
1041
+
1042
+ // ============ ADMIN FUNCTIONS ============
1043
+
1044
+ function startReservation(uint256 duration) external onlyOwner {
1045
+ reservationEnd = block.timestamp + duration;
1046
+ }
1047
+
1048
+ function setSalePhase(SalePhase phase) external onlyOwner {
1049
+ salePhase = phase;
1050
+ }
1051
+
1052
+ function setWhitelist(address[] calldata addresses, bool status) external onlyOwner {
1053
+ for (uint256 i = 0; i < addresses.length; i++) {
1054
+ whitelist[addresses[i]] = status;
1055
+ }
1056
+ }
1057
+
1058
+ function reveal(string calldata _baseURI) external onlyOwner {
1059
+ baseURI_ = _baseURI;
1060
+ revealed = true;
1061
+ }
1062
+
1063
+ function setPrice(uint256 _price) external onlyOwner {
1064
+ price = _price;
1065
+ }
1066
+
1067
+ function withdraw() external onlyOwner {
1068
+ (bool success, ) = owner().call{value: address(this).balance}("");
1069
+ require(success, "Withdrawal failed");
1070
+ }
1071
+
1072
+ // ============ VIEW FUNCTIONS ============
1073
+
1074
+ function totalSupply() public view returns (uint256) {
1075
+ return nextTokenId - 1;
1076
+ }
1077
+
1078
+ function getSaleInfo() external view returns (
1079
+ SalePhase phase,
1080
+ uint256 currentPrice,
1081
+ uint256 maxSupply_,
1082
+ uint256 totalSupply_,
1083
+ bool isRevealed
1084
+ ) {
1085
+ return (salePhase, price, maxSupply, totalSupply(), revealed);
1086
+ }
1087
+ }
1088
+ ```
1089
+
1090
+ ## Solidity vs OPNet Comparison
1091
+
1092
+ ### Key Differences Table
1093
+
1094
+ | Aspect | Solidity (ERC721) | OPNet (OP721) |
1095
+ |--------|-------------------|---------------|
1096
+ | **Inheritance** | `contract NFT is ERC721, Ownable` | `class NFT extends OP721` |
1097
+ | **Constructor** | `constructor() ERC721("Name", "SYM")` | `onDeployment()` + `this.instantiate(...)` |
1098
+ | **Enum Definition** | `enum SalePhase { INACTIVE, WHITELIST }` | `const PHASE_INACTIVE: u8 = 0` |
1099
+ | **Mint** | `_safeMint(to, tokenId)` | `this._mint(to, tokenId)` |
1100
+ | **Token Counter** | `uint256 private nextTokenId` | `StoredU256` with pointer |
1101
+ | **Timestamp** | `block.timestamp` | `Blockchain.block.medianTime` |
1102
+ | **Whitelist Storage** | `mapping(address => bool)` | `AddressMemoryMap` |
1103
+ | **Payment Handling** | `msg.value`, `payable` | Bitcoin UTXO model |
1104
+ | **String Concat** | `string(abi.encodePacked(...))` | `baseURI + tokenId.toString() + '.json'` |
1105
+
1106
+ ### Reservation Pattern Comparison
1107
+
1108
+ **Solidity:**
1109
+ ```solidity
1110
+ mapping(address => uint256) public reservations;
1111
+ uint256 public reservationEnd;
1112
+
1113
+ function reserve(uint256 quantity) external payable {
1114
+ require(block.timestamp < reservationEnd, "Reservation period ended");
1115
+ require(reservations[msg.sender] + quantity <= maxPerWallet, "Exceeds max per wallet");
1116
+ require(msg.value >= price * quantity, "Insufficient payment");
1117
+
1118
+ reservations[msg.sender] += quantity;
1119
+ }
1120
+ ```
1121
+
1122
+ **OPNet:**
1123
+ ```typescript
1124
+ private _reservedBy: AddressMemoryMap;
1125
+ private _reservationEnd: StoredU256;
1126
+
1127
+ @method({ name: 'quantity', type: ABIDataTypes.UINT256 })
1128
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
1129
+ @emit('Reserved')
1130
+ public reserve(calldata: Calldata): BytesWriter {
1131
+ const quantity = calldata.readU256();
1132
+ const sender = Blockchain.tx.sender;
1133
+
1134
+ const now = u256.fromU64(Blockchain.block.medianTime);
1135
+ if (now >= this._reservationEnd.value) {
1136
+ throw new Revert('Reservation period ended');
1137
+ }
1138
+
1139
+ const currentReserved = this._reservedBy.get(sender);
1140
+ const newTotal = SafeMath.add(currentReserved, quantity);
1141
+
1142
+ if (newTotal > this._maxPerWallet.value) {
1143
+ throw new Revert('Exceeds max per wallet');
1144
+ }
1145
+
1146
+ this._reservedBy.set(sender, newTotal);
1147
+ return new BytesWriter(0);
1148
+ }
1149
+ ```
1150
+
1151
+ ### Whitelist Verification Comparison
1152
+
1153
+ **Solidity (Using Merkle Proofs):**
1154
+ ```solidity
1155
+ import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
1156
+
1157
+ bytes32 public merkleRoot;
1158
+
1159
+ function whitelistMint(uint256 quantity, bytes32[] calldata proof) external payable {
1160
+ require(salePhase == SalePhase.WHITELIST, "Whitelist sale not active");
1161
+
1162
+ bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
1163
+ require(MerkleProof.verify(proof, merkleRoot, leaf), "Not whitelisted");
1164
+
1165
+ _mintInternal(msg.sender, quantity);
1166
+ }
1167
+ ```
1168
+
1169
+ **OPNet (On-chain mapping):**
1170
+ ```typescript
1171
+ private _whitelist: AddressMemoryMap;
1172
+
1173
+ @method({ name: 'quantity', type: ABIDataTypes.UINT256 })
1174
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
1175
+ @emit('Minted')
1176
+ public whitelistMint(calldata: Calldata): BytesWriter {
1177
+ const quantity = calldata.readU256();
1178
+ const sender = Blockchain.tx.sender;
1179
+
1180
+ if (this._salePhase.value != PHASE_WHITELIST) {
1181
+ throw new Revert('Whitelist sale not active');
1182
+ }
1183
+
1184
+ // AddressMemoryMap returns u256; non-zero = whitelisted
1185
+ if (this._whitelist.get(sender).isZero()) {
1186
+ throw new Revert('Not whitelisted');
1187
+ }
1188
+
1189
+ this._mintInternal(sender, quantity);
1190
+ return new BytesWriter(0);
1191
+ }
1192
+ ```
1193
+
1194
+ ### Reveal Mechanism Comparison
1195
+
1196
+ **Solidity:**
1197
+ ```solidity
1198
+ string private baseURI_;
1199
+ string private hiddenURI;
1200
+ bool public revealed;
1201
+
1202
+ function tokenURI(uint256 tokenId) public view override returns (string memory) {
1203
+ require(_ownerOf(tokenId) != address(0), "Token does not exist");
1204
+
1205
+ if (!revealed) {
1206
+ return hiddenURI;
1207
+ }
1208
+
1209
+ return string(abi.encodePacked(baseURI_, tokenId.toString(), ".json"));
1210
+ }
1211
+
1212
+ function reveal(string calldata _baseURI) external onlyOwner {
1213
+ baseURI_ = _baseURI;
1214
+ revealed = true;
1215
+ }
1216
+ ```
1217
+
1218
+ **OPNet:**
1219
+ ```typescript
1220
+ private _baseURI: StoredString;
1221
+ private _hiddenURI: StoredString;
1222
+ private _revealed: StoredBoolean;
1223
+
1224
+ public override tokenURI(tokenId: u256): string {
1225
+ if (this.ownerOf(tokenId).equals(Address.zero())) {
1226
+ throw new Revert('Token does not exist');
1227
+ }
1228
+
1229
+ if (!this._revealed.value) {
1230
+ return this._hiddenURI.value;
1231
+ }
1232
+
1233
+ return this._baseURI.value + tokenId.toString() + '.json';
1234
+ }
1235
+
1236
+ @method({ name: 'baseURI', type: ABIDataTypes.STRING })
1237
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
1238
+ @emit('Revealed')
1239
+ public reveal(calldata: Calldata): BytesWriter {
1240
+ this.onlyDeployer(Blockchain.tx.sender);
1241
+ const baseURI = calldata.readString();
1242
+
1243
+ this._baseURI.value = baseURI;
1244
+ this._revealed.value = true;
1245
+
1246
+ return new BytesWriter(0);
1247
+ }
1248
+ ```
1249
+
1250
+ ### Sale Phase Management Comparison
1251
+
1252
+ **Solidity:**
1253
+ ```solidity
1254
+ enum SalePhase { INACTIVE, WHITELIST, PUBLIC }
1255
+ SalePhase public salePhase;
1256
+
1257
+ function setSalePhase(SalePhase phase) external onlyOwner {
1258
+ salePhase = phase;
1259
+ }
1260
+
1261
+ // Usage
1262
+ require(salePhase == SalePhase.WHITELIST, "Whitelist sale not active");
1263
+ ```
1264
+
1265
+ **OPNet:**
1266
+ ```typescript
1267
+ const PHASE_INACTIVE: u8 = 0;
1268
+ const PHASE_WHITELIST: u8 = 1;
1269
+ const PHASE_PUBLIC: u8 = 2;
1270
+
1271
+ private _salePhase: StoredU8;
1272
+
1273
+ @method({ name: 'phase', type: ABIDataTypes.UINT8 })
1274
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
1275
+ @emit('SalePhaseChanged')
1276
+ public setSalePhase(calldata: Calldata): BytesWriter {
1277
+ this.onlyDeployer(Blockchain.tx.sender);
1278
+
1279
+ const phase = calldata.readU8();
1280
+ if (phase > PHASE_PUBLIC) {
1281
+ throw new Revert('Invalid phase');
1282
+ }
1283
+
1284
+ this._salePhase.value = phase;
1285
+ return new BytesWriter(0);
1286
+ }
1287
+
1288
+ // Usage
1289
+ if (this._salePhase.value != PHASE_WHITELIST) {
1290
+ throw new Revert('Whitelist sale not active');
1291
+ }
1292
+ ```
1293
+
1294
+ ### Advantages of OPNet Approach
1295
+
1296
+ | Feature | Benefit |
1297
+ |---------|---------|
1298
+ | **Bitcoin Timestamps** | Uses `medianTime` for manipulation-resistant timing |
1299
+ | **Native u256** | First-class 256-bit integer support |
1300
+ | **Explicit Storage** | Direct control over storage layout with pointers |
1301
+ | **Single Inheritance** | Avoids ERC721's multiple inheritance complexity |
1302
+ | **No payable Complexity** | Bitcoin UTXO model handles value transfers differently |
1303
+ | **Typed Storage** | `StoredU8`, `StoredU256`, `StoredBoolean` for type safety |
1304
+
1305
+ ### Minting Loop Comparison
1306
+
1307
+ **Solidity:**
1308
+ ```solidity
1309
+ function _mintInternal(address to, uint256 quantity) internal {
1310
+ require(nextTokenId + quantity - 1 <= maxSupply, "Exceeds max supply");
1311
+ require(mintedCount[to] + quantity <= maxPerWallet, "Exceeds max per wallet");
1312
+
1313
+ mintedCount[to] += quantity;
1314
+
1315
+ for (uint256 i = 0; i < quantity; i++) {
1316
+ _safeMint(to, nextTokenId++);
1317
+ }
1318
+ }
1319
+ ```
1320
+
1321
+ **OPNet:**
1322
+ ```typescript
1323
+ private _mintInternal(to: Address, quantity: u256): void {
1324
+ const currentSupply = this.totalSupply();
1325
+ const newSupply = SafeMath.add(currentSupply, quantity);
1326
+
1327
+ if (newSupply > this._maxSupply.value) {
1328
+ throw new Revert('Exceeds max supply');
1329
+ }
1330
+
1331
+ const minted = this._mintedCount.get(to);
1332
+ const newMinted = SafeMath.add(minted, quantity);
1333
+
1334
+ if (newMinted > this._maxPerWallet.value) {
1335
+ throw new Revert('Exceeds max per wallet');
1336
+ }
1337
+
1338
+ this._mintedCount.set(to, newMinted);
1339
+
1340
+ let count = quantity;
1341
+ while (!count.isZero()) {
1342
+ const tokenId = this._nextTokenId.value;
1343
+ this._mint(to, tokenId);
1344
+ this._nextTokenId.value = SafeMath.add(tokenId, u256.One);
1345
+ count = SafeMath.sub(count, u256.One);
1346
+ }
1347
+ }
1348
+ ```
1349
+
1350
+ ### Payment and Refund Handling
1351
+
1352
+ **Solidity (ETH-based):**
1353
+ ```solidity
1354
+ function reserve(uint256 quantity) external payable {
1355
+ require(msg.value >= price * quantity, "Insufficient payment");
1356
+ reservations[msg.sender] += quantity;
1357
+ }
1358
+
1359
+ function cancelReservation() external {
1360
+ uint256 quantity = reservations[msg.sender];
1361
+ reservations[msg.sender] = 0;
1362
+
1363
+ // Refund ETH
1364
+ uint256 refundAmount = price * quantity;
1365
+ (bool success, ) = msg.sender.call{value: refundAmount}("");
1366
+ require(success, "Refund failed");
1367
+ }
1368
+ ```
1369
+
1370
+ **OPNet (Bitcoin UTXO model):**
1371
+ ```typescript
1372
+ // Payment handled at Bitcoin transaction level
1373
+ // Refund logic would involve different mechanisms
1374
+
1375
+ @method()
1376
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
1377
+ @emit('ReservationCancelled')
1378
+ public cancelReservation(_calldata: Calldata): BytesWriter {
1379
+ const sender = Blockchain.tx.sender;
1380
+
1381
+ const now = u256.fromU64(Blockchain.block.medianTime);
1382
+ if (now >= this._reservationEnd.value) {
1383
+ throw new Revert('Reservation period ended');
1384
+ }
1385
+
1386
+ const reserved = this._reservedBy.get(sender);
1387
+ if (reserved.isZero()) {
1388
+ throw new Revert('No reservations');
1389
+ }
1390
+
1391
+ this._reservedBy.set(sender, u256.Zero);
1392
+
1393
+ // Refund logic handled at protocol level
1394
+ return new BytesWriter(0);
1395
+ }
1396
+ ```
1397
+
1398
+ ### View Functions Comparison
1399
+
1400
+ **Solidity:**
1401
+ ```solidity
1402
+ function getSaleInfo() external view returns (
1403
+ SalePhase phase,
1404
+ uint256 currentPrice,
1405
+ uint256 maxSupply_,
1406
+ uint256 totalSupply_,
1407
+ bool isRevealed
1408
+ ) {
1409
+ return (salePhase, price, maxSupply, totalSupply(), revealed);
1410
+ }
1411
+ ```
1412
+
1413
+ **OPNet:**
1414
+ ```typescript
1415
+ @method()
1416
+ @returns(
1417
+ { name: 'phase', type: ABIDataTypes.UINT8 },
1418
+ { name: 'price', type: ABIDataTypes.UINT256 },
1419
+ { name: 'maxSupply', type: ABIDataTypes.UINT256 },
1420
+ { name: 'totalSupply', type: ABIDataTypes.UINT256 },
1421
+ { name: 'revealed', type: ABIDataTypes.BOOL },
1422
+ )
1423
+ public getSaleInfo(_calldata: Calldata): BytesWriter {
1424
+ const writer = new BytesWriter(128);
1425
+
1426
+ writer.writeU8(this._salePhase.value);
1427
+ writer.writeU256(this._price.value);
1428
+ writer.writeU256(this._maxSupply.value);
1429
+ writer.writeU256(this.totalSupply());
1430
+ writer.writeBoolean(this._revealed.value);
1431
+
1432
+ return writer;
1433
+ }
1434
+ ```
1435
+
1436
+ ---
1437
+
1438
+ **Navigation:**
1439
+ - Previous: [Basic Token](./basic-token.md)
1440
+ - Next: [Stablecoin](./stablecoin.md)