@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.
- package/LICENSE +190 -0
- package/README.md +258 -137
- package/SECURITY.md +226 -0
- package/docs/README.md +614 -0
- package/docs/advanced/bitcoin-scripts.md +939 -0
- package/docs/advanced/cross-contract-calls.md +579 -0
- package/docs/advanced/plugins.md +1006 -0
- package/docs/advanced/quantum-resistance.md +660 -0
- package/docs/advanced/signature-verification.md +715 -0
- package/docs/api-reference/blockchain.md +729 -0
- package/docs/api-reference/events.md +642 -0
- package/docs/api-reference/op20.md +902 -0
- package/docs/api-reference/op721.md +819 -0
- package/docs/api-reference/safe-math.md +510 -0
- package/docs/api-reference/storage.md +840 -0
- package/docs/contracts/op-net-base.md +786 -0
- package/docs/contracts/op20-token.md +687 -0
- package/docs/contracts/op20s-signatures.md +614 -0
- package/docs/contracts/op721-nft.md +785 -0
- package/docs/contracts/reentrancy-guard.md +787 -0
- package/docs/core-concepts/blockchain-environment.md +724 -0
- package/docs/core-concepts/decorators.md +466 -0
- package/docs/core-concepts/events.md +652 -0
- package/docs/core-concepts/pointers.md +391 -0
- package/docs/core-concepts/security.md +473 -0
- package/docs/core-concepts/storage-system.md +969 -0
- package/docs/examples/basic-token.md +745 -0
- package/docs/examples/nft-with-reservations.md +1440 -0
- package/docs/examples/oracle-integration.md +1212 -0
- package/docs/examples/stablecoin.md +1180 -0
- package/docs/getting-started/first-contract.md +575 -0
- package/docs/getting-started/installation.md +384 -0
- package/docs/getting-started/project-structure.md +630 -0
- package/docs/storage/memory-maps.md +764 -0
- package/docs/storage/stored-arrays.md +778 -0
- package/docs/storage/stored-maps.md +758 -0
- package/docs/storage/stored-primitives.md +655 -0
- package/docs/types/address.md +773 -0
- package/docs/types/bytes-writer-reader.md +938 -0
- package/docs/types/calldata.md +744 -0
- package/docs/types/safe-math.md +446 -0
- package/package.json +52 -27
- package/runtime/memory/MapOfMap.ts +1 -0
- 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)
|