@btc-vision/btc-runtime 1.10.10 → 1.10.12

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 (45) 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 +731 -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 +370 -0
  26. package/docs/core-concepts/storage-system.md +938 -0
  27. package/docs/examples/basic-token.md +745 -0
  28. package/docs/examples/nft-with-reservations.md +1210 -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 +721 -0
  35. package/docs/storage/stored-arrays.md +714 -0
  36. package/docs/storage/stored-maps.md +686 -0
  37. package/docs/storage/stored-primitives.md +608 -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 +403 -0
  42. package/package.json +51 -26
  43. package/runtime/memory/MapOfMap.ts +1 -0
  44. package/runtime/types/SafeMath.ts +121 -1
  45. package/LICENSE.md +0 -21
@@ -0,0 +1,785 @@
1
+ # OP721 NFT Standard
2
+
3
+ OP721 is OPNet's non-fungible token standard, equivalent to Ethereum's ERC721. It provides a complete implementation for creating NFTs with ownership tracking, transfers, approvals, and metadata management.
4
+
5
+ ## Overview
6
+
7
+ ```typescript
8
+ import { u256 } from '@btc-vision/as-bignum/assembly';
9
+ import {
10
+ OP721,
11
+ OP721InitParameters,
12
+ Blockchain,
13
+ Calldata,
14
+ BytesWriter,
15
+ } from '@btc-vision/btc-runtime/runtime';
16
+
17
+ @final
18
+ export class MyNFT extends OP721 {
19
+ public constructor() {
20
+ super();
21
+ }
22
+
23
+ public override onDeployment(_calldata: Calldata): void {
24
+ this.instantiate(new OP721InitParameters(
25
+ 'My NFT Collection', // name
26
+ 'MNFT', // symbol
27
+ 'https://example.com/nft/', // baseURI
28
+ u256.fromU64(10000) // maxSupply
29
+ ));
30
+ }
31
+ }
32
+ ```
33
+
34
+ ## ERC721 vs OP721 Comparison
35
+
36
+ | Feature | ERC721 (Solidity) | OP721 (OPNet) |
37
+ |---------|-------------------|---------------|
38
+ | Language | Solidity | AssemblyScript |
39
+ | Runtime | EVM | WASM |
40
+ | Token ID Type | `uint256` | `u256` |
41
+ | Enumeration | Optional (ERC721Enumerable) | Built-in |
42
+ | Safe Transfer | `safeTransferFrom` + receiver check | Same pattern |
43
+ | Operator Approval | `setApprovalForAll` | Same |
44
+ | Metadata | Optional (ERC721Metadata) | Built-in `tokenURI` |
45
+ | Address Storage | 20 bytes | 30 bytes (truncated internally) |
46
+
47
+ ## Initialization
48
+
49
+ ### OP721InitParameters
50
+
51
+ | Parameter | Type | Required | Description |
52
+ |-----------|------|----------|-------------|
53
+ | `name` | `string` | Yes | Collection name |
54
+ | `symbol` | `string` | Yes | Collection symbol |
55
+ | `baseURI` | `string` | Yes | Base URI for token metadata |
56
+ | `maxSupply` | `u256` | Yes | Maximum number of tokens that can be minted |
57
+ | `collectionBanner` | `string` | No | Collection banner URL (default: '') |
58
+ | `collectionIcon` | `string` | No | Collection icon URL (default: '') |
59
+ | `collectionWebsite` | `string` | No | Collection website URL (default: '') |
60
+ | `collectionDescription` | `string` | No | Collection description (default: '') |
61
+
62
+ ```typescript
63
+ this.instantiate(new OP721InitParameters(
64
+ 'My NFT Collection', // name
65
+ 'MNFT', // symbol
66
+ 'https://example.com/nft/', // baseURI
67
+ u256.fromU64(10000), // maxSupply
68
+ 'https://example.com/banner.png', // collectionBanner (optional)
69
+ 'https://example.com/icon.png', // collectionIcon (optional)
70
+ 'https://example.com', // collectionWebsite (optional)
71
+ 'My awesome NFT collection' // collectionDescription (optional)
72
+ ));
73
+ ```
74
+
75
+ ## Minting Flow
76
+
77
+ The following diagram shows how NFT minting works:
78
+
79
+ ```mermaid
80
+ ---
81
+ config:
82
+ theme: dark
83
+ ---
84
+ flowchart LR
85
+ A[👤 User calls mint] --> B[Validate authorization]
86
+ B --> C{Authorized?}
87
+ C -->|No| D[Revert]
88
+ C -->|Yes| E[Check token exists]
89
+ E --> F{Token minted?}
90
+ F -->|Yes| G[Revert: Already exists]
91
+ F -->|No| H[Set owner mapping]
92
+ H --> I[Update balance]
93
+ I --> J[Update enumeration]
94
+ J --> K[Increment totalSupply]
95
+ K --> L[Emit TransferEvent]
96
+ L --> M[Complete]
97
+ ```
98
+
99
+ ## Transfer Sequence
100
+
101
+ The following sequence diagram shows the detailed transfer process including all storage updates:
102
+
103
+ ```mermaid
104
+ sequenceDiagram
105
+ participant User as 👤 User/Operator Wallet
106
+ participant Blockchain as Bitcoin L1
107
+ participant VM as WASM Runtime
108
+ participant OP721 as OP721 Contract
109
+ participant Storage as Storage Pointers
110
+ participant OwnersMap as _owners Map<br/>(Pointer 3)
111
+ participant BalancesMap as _balances Map<br/>(Pointer 4)
112
+ participant ApprovalsMap as _tokenApprovals<br/>(Pointer 5)
113
+ participant OwnedTokens as _ownedTokens<br/>(Pointer 7)
114
+ participant TokenIndex as _ownedTokensIndex<br/>(Pointer 8)
115
+ participant EventLog as Event Log
116
+
117
+ User->>Blockchain: Submit safeTransferFrom(from, to, tokenId, data) TX
118
+ Blockchain->>VM: Route transaction
119
+ VM->>OP721: Call safeTransferFrom
120
+
121
+ activate OP721
122
+
123
+ OP721->>OP721: Get caller = Blockchain.tx.sender
124
+
125
+ OP721->>OwnersMap: get(tokenId)
126
+ OwnersMap->>Storage: Read owner
127
+ Storage-->>OwnersMap: currentOwner
128
+ OwnersMap-->>OP721: owner address
129
+
130
+ OP721->>ApprovalsMap: get(tokenId)
131
+ ApprovalsMap->>Storage: Read approved address
132
+ Storage-->>ApprovalsMap: approved
133
+ ApprovalsMap-->>OP721: approved address
134
+
135
+ OP721->>OP721: isApprovedForAll(owner, caller)
136
+ Note over OP721: Check if caller is operator
137
+
138
+ alt Not owner AND not approved AND not operator
139
+ OP721->>VM: Revert('Not authorized')
140
+ VM->>User: Transaction failed
141
+ else Authorized to transfer
142
+ OP721->>OP721: _transfer(from, to, tokenId, data)
143
+
144
+ OP721->>OP721: Validate currentOwner == from
145
+
146
+ OP721->>ApprovalsMap: set(tokenId, Address.zero())
147
+ ApprovalsMap->>Storage: Clear approval
148
+ Note over Storage: Remove single-token approval
149
+
150
+ OP721->>BalancesMap: get(from)
151
+ BalancesMap->>Storage: Read from's balance
152
+ Storage-->>BalancesMap: fromBalance
153
+ BalancesMap-->>OP721: balance count
154
+
155
+ OP721->>OP721: SafeMath.sub(fromBalance, 1)
156
+ OP721->>BalancesMap: set(from, newFromBalance)
157
+ BalancesMap->>Storage: Write updated balance
158
+ Note over Storage: Decrement sender balance
159
+
160
+ OP721->>BalancesMap: get(to)
161
+ BalancesMap->>Storage: Read to's balance
162
+ Storage-->>BalancesMap: toBalance
163
+ BalancesMap-->>OP721: balance count
164
+
165
+ OP721->>OP721: SafeMath.add(toBalance, 1)
166
+ OP721->>BalancesMap: set(to, newToBalance)
167
+ BalancesMap->>Storage: Write updated balance
168
+ Note over Storage: Increment recipient balance
169
+
170
+ OP721->>OP721: Remove from enumeration (from)
171
+ OP721->>OwnedTokens: Get last token of 'from'
172
+ OwnedTokens->>Storage: Read last tokenId
173
+ Storage-->>OwnedTokens: lastTokenId
174
+ OP721->>TokenIndex: get(tokenId)
175
+ TokenIndex->>Storage: Get index to remove
176
+ Storage-->>TokenIndex: tokenIndex
177
+ OP721->>OwnedTokens: set(from->tokenIndex, lastTokenId)
178
+ Note over OwnedTokens: Swap-last pattern:<br/>Replace removed with last
179
+ OP721->>TokenIndex: set(lastTokenId, tokenIndex)
180
+ Note over TokenIndex: Update last token's index
181
+
182
+ OP721->>OP721: Add to enumeration (to)
183
+ OP721->>OwnedTokens: set(to->newIndex, tokenId)
184
+ OwnedTokens->>Storage: Write token to recipient list
185
+ OP721->>TokenIndex: set(tokenId, newIndex)
186
+ TokenIndex->>Storage: Write index mapping
187
+
188
+ OP721->>OwnersMap: set(tokenId, to)
189
+ OwnersMap->>Storage: Write new owner
190
+ Note over Storage: Ownership transferred
191
+
192
+ OP721->>OP721: Create TransferEvent(from, to, tokenId)
193
+ OP721->>EventLog: emitEvent
194
+ Note over EventLog: Log ownership change
195
+
196
+ OP721->>VM: Return BytesWriter(0)
197
+ deactivate OP721
198
+
199
+ VM->>Blockchain: Commit all storage changes
200
+ Blockchain->>User: Transaction success
201
+ Note over User: NFT ownership transferred<br/>All mappings updated
202
+ end
203
+ ```
204
+
205
+ ## Safe Transfer Pattern
206
+
207
+ Safe transfers check if the recipient is a contract and call `onOP721Received`:
208
+
209
+ ```mermaid
210
+ ---
211
+ config:
212
+ theme: dark
213
+ ---
214
+ flowchart LR
215
+ A[safeTransferFrom] --> B[_transfer]
216
+ B --> C[Check if contract]
217
+ C --> D{Is contract?}
218
+ D -->|No| E[Return success]
219
+ D -->|Yes| F[Call onOP721Received]
220
+ F --> G{Valid response?}
221
+ G -->|No| H[Revert]
222
+ G -->|Yes| I[Return success]
223
+ ```
224
+
225
+ ## NFT Lifecycle
226
+
227
+ ```mermaid
228
+ stateDiagram-v2
229
+ [*] --> Unminted
230
+ Unminted --> Minted: _mint(to, tokenId)
231
+
232
+ state Minted {
233
+ [*] --> OwnedByA
234
+ OwnedByA --> OwnedByB: transfer(A, B, tokenId)
235
+ OwnedByB --> OwnedByC: transfer(B, C, tokenId)
236
+ OwnedByC --> OwnedByA: transfer(C, A, tokenId)
237
+
238
+ state "Approval State" as Approval {
239
+ [*] --> NoApproval
240
+ NoApproval --> ApprovedAddress: approve(spender, tokenId)
241
+ ApprovedAddress --> NoApproval: transfer (clears approval)
242
+ NoApproval --> OperatorApproved: setApprovalForAll(operator, true)
243
+ OperatorApproved --> NoApproval: setApprovalForAll(operator, false)
244
+ }
245
+ }
246
+
247
+ Minted --> Burned: _burn(tokenId)
248
+ Burned --> [*]
249
+ ```
250
+
251
+ ## Token Existence States
252
+
253
+ The following state diagram shows the complete lifecycle of an NFT token:
254
+
255
+ ```mermaid
256
+ ---
257
+ config:
258
+ theme: dark
259
+ ---
260
+ stateDiagram-v2
261
+ [*] --> NonExistent: Token ID available
262
+
263
+ NonExistent --> Owned: _mint(to, tokenId)
264
+
265
+ state Owned {
266
+ [*] --> Active
267
+ Active --> Active: transfer
268
+ Active --> Active: safeTransfer
269
+
270
+ state "Approval Status" as ApprovalStatus {
271
+ [*] --> Unapproved
272
+ Unapproved --> SingleApproval: approve(spender)
273
+ SingleApproval --> Unapproved: transfer clears
274
+ Unapproved --> OperatorApproved: setApprovalForAll
275
+ OperatorApproved --> Unapproved: revoke operator
276
+ }
277
+ }
278
+
279
+ Owned --> Burned: _burn(tokenId)
280
+ Burned --> [*]: Token destroyed
281
+
282
+ note right of NonExistent
283
+ ownerOf() reverts
284
+ tokenURI() reverts
285
+ end note
286
+
287
+ note right of Burned
288
+ Token ID can never
289
+ be reused
290
+ end note
291
+ ```
292
+
293
+ ## Built-in Methods
294
+
295
+ ### Query Methods
296
+
297
+ | Method | Returns | Description |
298
+ |--------|---------|-------------|
299
+ | `name()` | `string` | Collection name |
300
+ | `symbol()` | `string` | Collection symbol |
301
+ | `totalSupply()` | `u256` | Total minted NFTs |
302
+ | `maxSupply()` | `u256` | Maximum supply limit |
303
+ | `balanceOf(owner)` | `u256` | NFT count for address |
304
+ | `ownerOf(tokenId)` | `Address` | Owner of token |
305
+ | `tokenURI(tokenId)` | `string` | Metadata URI |
306
+ | `tokenOfOwnerByIndex(owner, index)` | `u256` | Token ID at index |
307
+ | `collectionInfo()` | `(icon, banner, description, website)` | Collection metadata |
308
+ | `metadata()` | `(name, symbol, icon, banner, description, website, totalSupply, domainSeparator)` | Full collection metadata |
309
+ | `domainSeparator()` | `bytes32` | EIP-712 domain separator |
310
+ | `getApproveNonce(owner)` | `u256` | Signature nonce for owner |
311
+
312
+ ### Transfer Methods
313
+
314
+ | Method | Description |
315
+ |--------|-------------|
316
+ | `safeTransfer(to, tokenId, data)` | Transfer NFT from sender to recipient |
317
+ | `safeTransferFrom(from, to, tokenId, data)` | Safe transfer with callback |
318
+ | `burn(tokenId)` | Burn token (owner or approved only) |
319
+
320
+ ### Approval Methods
321
+
322
+ | Method | Description |
323
+ |--------|-------------|
324
+ | `approve(operator, tokenId)` | Approve address for token |
325
+ | `setApprovalForAll(operator, approved)` | Approve operator for all tokens |
326
+ | `getApproved(tokenId)` | Get approved address |
327
+ | `isApprovedForAll(owner, operator)` | Check operator approval |
328
+ | `approveBySignature(...)` | Approve via EIP-712 signature |
329
+ | `setApprovalForAllBySignature(...)` | Set operator approval via signature |
330
+
331
+ ### Admin Methods
332
+
333
+ | Method | Description |
334
+ |--------|-------------|
335
+ | `setBaseURI(baseURI)` | Update base URI (deployer only) |
336
+ | `changeMetadata(icon, banner, description, website)` | Update collection metadata (deployer only) |
337
+
338
+ ## Solidity Comparison
339
+
340
+ <table>
341
+ <tr>
342
+ <th>ERC721 (Solidity)</th>
343
+ <th>OP721 (OPNet)</th>
344
+ </tr>
345
+ <tr>
346
+ <td>
347
+
348
+ ```solidity
349
+ contract MyNFT is ERC721 {
350
+ uint256 private _tokenIds;
351
+
352
+ constructor()
353
+ ERC721("MyNFT", "MNFT")
354
+ { }
355
+
356
+ function mint(address to)
357
+ public returns (uint256)
358
+ {
359
+ _tokenIds++;
360
+ _mint(to, _tokenIds);
361
+ return _tokenIds;
362
+ }
363
+ }
364
+ ```
365
+
366
+ </td>
367
+ <td>
368
+
369
+ ```typescript
370
+ // OP721 base class already manages _nextTokenId internally
371
+
372
+ @final
373
+ export class MyNFT extends OP721 {
374
+ public constructor() {
375
+ super();
376
+ }
377
+
378
+ public override onDeployment(_: Calldata): void {
379
+ // Base class sets _nextTokenId to 1 automatically
380
+ this.instantiate(new OP721InitParameters(
381
+ 'MyNFT', // name
382
+ 'MNFT', // symbol
383
+ 'https://example.com/nft/', // baseURI
384
+ u256.fromU64(10000) // maxSupply
385
+ ));
386
+ }
387
+
388
+ @method({ name: 'to', type: ABIDataTypes.ADDRESS })
389
+ @returns({ name: 'tokenId', type: ABIDataTypes.UINT256 })
390
+ @emit('Transferred')
391
+ public mint(calldata: Calldata): BytesWriter {
392
+ const to = calldata.readAddress();
393
+ // Use base class _nextTokenId
394
+ const tokenId = this._nextTokenId.value;
395
+
396
+ this._mint(to, tokenId);
397
+ this._nextTokenId.value = SafeMath.add(tokenId, u256.One);
398
+
399
+ const writer = new BytesWriter(32);
400
+ writer.writeU256(tokenId);
401
+ return writer;
402
+ }
403
+ }
404
+ ```
405
+
406
+ </td>
407
+ </tr>
408
+ </table>
409
+
410
+ ## Storage Layout
411
+
412
+ OP721 uses these storage pointers internally (allocated via `Blockchain.nextPointer`):
413
+
414
+ | Storage Variable | Type | Description |
415
+ |------------------|------|-------------|
416
+ | `stringPointer` | StoredString | Stores name, symbol, baseURI, banner, icon, description, website |
417
+ | `totalSupplyPointer` | StoredU256 | Total minted count |
418
+ | `maxSupplyPointer` | StoredU256 | Maximum supply limit |
419
+ | `ownerOfMapPointer` | StoredMapU256 | tokenId -> owner mapping |
420
+ | `tokenApprovalMapPointer` | StoredMapU256 | tokenId -> approved address |
421
+ | `operatorApprovalMapPointer` | MapOfMap | owner -> operator -> bool |
422
+ | `balanceOfMapPointer` | AddressMemoryMap | address -> balance mapping |
423
+ | `tokenURIMapPointer` | StoredMapU256 | tokenId -> URI index mapping |
424
+ | `nextTokenIdPointer` | StoredU256 | Next token ID to mint |
425
+ | `ownerTokensMapPointer` | StoredU256Array | owner -> array of token IDs |
426
+ | `tokenIndexMapPointer` | StoredMapU256 | tokenId -> index in owner's list |
427
+ | `initializedPointer` | StoredU256 | Initialization flag |
428
+ | `tokenURICounterPointer` | StoredU256 | Counter for custom URIs |
429
+ | `approveNonceMapPointer` | AddressMemoryMap | address -> signature nonce |
430
+
431
+ ## Extending OP721
432
+
433
+ ### Adding Minting
434
+
435
+ ```typescript
436
+ import { u256 } from '@btc-vision/as-bignum/assembly';
437
+ import {
438
+ OP721,
439
+ OP721InitParameters,
440
+ Blockchain,
441
+ Calldata,
442
+ BytesWriter,
443
+ SafeMath,
444
+ ABIDataTypes,
445
+ } from '@btc-vision/btc-runtime/runtime';
446
+
447
+ @final
448
+ export class MyNFT extends OP721 {
449
+ public constructor() {
450
+ super();
451
+ }
452
+
453
+ public override onDeployment(_calldata: Calldata): void {
454
+ // Base class sets _nextTokenId to 1 automatically
455
+ this.instantiate(new OP721InitParameters(
456
+ 'MyNFT', // name
457
+ 'MNFT', // symbol
458
+ 'https://example.com/nft/', // baseURI
459
+ u256.fromU64(10000) // maxSupply
460
+ ));
461
+ }
462
+
463
+ @method({ name: 'to', type: ABIDataTypes.ADDRESS })
464
+ @returns({ name: 'tokenId', type: ABIDataTypes.UINT256 })
465
+ @emit('Transferred')
466
+ public mint(calldata: Calldata): BytesWriter {
467
+ const to = calldata.readAddress();
468
+
469
+ // Use base class _nextTokenId (already initialized to 1)
470
+ const tokenId = this._nextTokenId.value;
471
+ this._mint(to, tokenId);
472
+ this._nextTokenId.value = SafeMath.add(tokenId, u256.One);
473
+
474
+ const writer = new BytesWriter(32);
475
+ writer.writeU256(tokenId);
476
+ return writer;
477
+ }
478
+ }
479
+ ```
480
+
481
+ ### Setting Custom Token URIs
482
+
483
+ The OP721 base class already includes `baseURI` support and a `setBaseURI` method. You can also set custom URIs per token:
484
+
485
+ ```typescript
486
+ @final
487
+ export class MyNFT extends OP721 {
488
+ public constructor() {
489
+ super();
490
+ }
491
+
492
+ public override onDeployment(_calldata: Calldata): void {
493
+ this.instantiate(new OP721InitParameters(
494
+ 'MyNFT',
495
+ 'MNFT',
496
+ 'https://example.com/nft/', // Default baseURI
497
+ u256.fromU64(10000)
498
+ ));
499
+ }
500
+
501
+ // Set custom URI for a specific token
502
+ @method(
503
+ { name: 'tokenId', type: ABIDataTypes.UINT256 },
504
+ { name: 'uri', type: ABIDataTypes.STRING },
505
+ )
506
+ public setTokenURI(calldata: Calldata): BytesWriter {
507
+ this.onlyDeployer(Blockchain.tx.sender);
508
+ const tokenId = calldata.readU256();
509
+ const uri = calldata.readStringWithLength();
510
+
511
+ // Uses internal _setTokenURI from OP721 base class
512
+ this._setTokenURI(tokenId, uri);
513
+
514
+ return new BytesWriter(0);
515
+ }
516
+ }
517
+ ```
518
+
519
+ Note: The base class automatically handles token URI resolution - if a custom URI is set for a token, it returns that; otherwise, it returns `baseURI + tokenId`.
520
+
521
+ ### Collection Metadata
522
+
523
+ The OP721 base class includes built-in collection metadata support. You can set it during initialization:
524
+
525
+ ```typescript
526
+ @final
527
+ export class MyNFT extends OP721 {
528
+ public constructor() {
529
+ super();
530
+ }
531
+
532
+ public override onDeployment(_calldata: Calldata): void {
533
+ this.instantiate(new OP721InitParameters(
534
+ 'MyNFT', // name
535
+ 'MNFT', // symbol
536
+ 'https://example.com/nft/', // baseURI
537
+ u256.fromU64(10000), // maxSupply
538
+ 'https://example.com/banner.png', // collectionBanner
539
+ 'https://example.com/icon.png', // collectionIcon
540
+ 'https://example.com', // collectionWebsite
541
+ 'An awesome NFT collection' // collectionDescription
542
+ ));
543
+ }
544
+ }
545
+ ```
546
+
547
+ The built-in `collectionInfo()` method returns the icon, banner, description, and website. The `metadata()` method returns all collection data including name, symbol, and totalSupply.
548
+
549
+ Use `changeMetadata(icon, banner, description, website)` to update collection metadata after deployment (deployer only).
550
+
551
+ ## Internal Methods
552
+
553
+ | Method | Description |
554
+ |--------|-------------|
555
+ | `_mint(to, tokenId)` | Mint new token |
556
+ | `_burn(tokenId)` | Burn token |
557
+ | `_transfer(from, to, tokenId, data)` | Internal transfer with data |
558
+ | `_approve(operator, tokenId)` | Internal approval |
559
+ | `_setApprovalForAll(owner, operator, approved)` | Internal operator approval |
560
+ | `_setTokenURI(tokenId, uri)` | Set custom token URI |
561
+ | `_setBaseURI(baseURI)` | Set base URI |
562
+ | `_exists(tokenId)` | Check if token exists |
563
+ | `_ownerOf(tokenId)` | Get owner (throws if not exists) |
564
+ | `_balanceOf(owner)` | Get balance (throws if zero address) |
565
+ | `_isApprovedForAll(owner, operator)` | Check operator approval |
566
+
567
+ ## Enumeration
568
+
569
+ OP721 includes enumeration support (like ERC721Enumerable):
570
+
571
+ ```typescript
572
+ // Get all tokens owned by address
573
+ const balance = nft.balanceOf(owner);
574
+ for (let i: u256 = u256.Zero; i < balance; i = SafeMath.add(i, u256.One)) {
575
+ const tokenId = nft.tokenOfOwnerByIndex(owner, i);
576
+ // Process token...
577
+ }
578
+ ```
579
+
580
+ ### Swap-Last Removal Pattern
581
+
582
+ When transferring, OP721 uses the "swap last" pattern for efficient enumeration:
583
+
584
+ ```
585
+ Owner's tokens: [A, B, C, D] (indices 0, 1, 2, 3)
586
+
587
+ Transfer B:
588
+ 1. Swap B with last element (D): [A, D, C, B]
589
+ 2. Remove last: [A, D, C]
590
+ 3. Update indices: A=0, D=1, C=2
591
+
592
+ This is O(1) instead of O(n) shifting
593
+ ```
594
+
595
+ ## Events
596
+
597
+ OP721 emits:
598
+
599
+ ```typescript
600
+ // On transfer, mint, burn
601
+ TransferredEvent(operator: Address, from: Address, to: Address, tokenId: u256)
602
+ // operator = Blockchain.tx.sender
603
+ // For mint: from = Address.zero()
604
+ // For burn: to = Address.zero()
605
+
606
+ // On approval
607
+ ApprovedEvent(owner: Address, spender: Address, tokenId: u256)
608
+
609
+ // On operator approval
610
+ ApprovedForAllEvent(owner: Address, operator: Address, approved: bool)
611
+
612
+ // On URI change
613
+ URIEvent(value: string, id: u256)
614
+ ```
615
+
616
+ ## Edge Cases
617
+
618
+ The following state diagram shows how ownership transitions work for a specific token:
619
+
620
+ ```mermaid
621
+ ---
622
+ config:
623
+ theme: dark
624
+ ---
625
+ stateDiagram-v2
626
+ [*] --> Unminted
627
+
628
+ Unminted --> OwnedBy_A: mint to A
629
+
630
+ OwnedBy_A --> OwnedBy_B: A transfers to B
631
+ OwnedBy_A --> OwnedBy_B: Approved transfers to B
632
+ OwnedBy_A --> OwnedBy_B: Operator transfers to B
633
+
634
+ OwnedBy_B --> OwnedBy_A: B transfers to A
635
+ OwnedBy_B --> OwnedBy_C: B transfers to C
636
+
637
+ OwnedBy_C --> Burned: Owner burns
638
+ OwnedBy_A --> Burned: Owner burns
639
+ OwnedBy_B --> Burned: Owner burns
640
+
641
+ Burned --> [*]
642
+ ```
643
+
644
+ ### Token ID Uniqueness
645
+
646
+ ```typescript
647
+ // Token IDs must be unique
648
+ _mint(owner1, u256.fromU64(1)); // OK
649
+ _mint(owner2, u256.fromU64(1)); // FAILS - token exists
650
+
651
+ // Use incrementing IDs to ensure uniqueness
652
+ private nextTokenId: StoredU256 = new StoredU256(ptr, EMPTY_POINTER);
653
+ // Set initial value in onDeployment:
654
+ // this.nextTokenId.value = u256.One;
655
+ ```
656
+
657
+ ### Zero Token ID
658
+
659
+ ```typescript
660
+ // Token ID 0 is valid
661
+ _mint(owner, u256.Zero); // OK
662
+
663
+ // But be careful with uninitialized checks
664
+ if (tokenId.isZero()) {
665
+ // This doesn't mean "no token" - 0 could be valid!
666
+ }
667
+ ```
668
+
669
+ ### Owner Truncation
670
+
671
+ **IMPORTANT:** In OP721's enumeration, addresses are truncated to 30 bytes internally for storage efficiency:
672
+
673
+ ```typescript
674
+ // 32-byte address -> 30-byte storage key
675
+ // This is handled internally, but be aware of it
676
+ ```
677
+
678
+ ## Complete NFT Example
679
+
680
+ ```typescript
681
+ import { u256 } from '@btc-vision/as-bignum/assembly';
682
+ import {
683
+ OP721,
684
+ OP721InitParameters,
685
+ Blockchain,
686
+ Address,
687
+ Calldata,
688
+ BytesWriter,
689
+ StoredU256,
690
+ StoredBoolean,
691
+ SafeMath,
692
+ Revert,
693
+ ABIDataTypes,
694
+ EMPTY_POINTER,
695
+ } from '@btc-vision/btc-runtime/runtime';
696
+
697
+ @final
698
+ export class MyNFTCollection extends OP721 {
699
+ // Configuration - additional storage beyond base class
700
+ private pricePointer: u16 = Blockchain.nextPointer;
701
+ private mintingOpenPointer: u16 = Blockchain.nextPointer;
702
+
703
+ private _price: StoredU256;
704
+ private _mintingOpen: StoredBoolean;
705
+
706
+ public constructor() {
707
+ super();
708
+ this._price = new StoredU256(this.pricePointer, EMPTY_POINTER);
709
+ this._mintingOpen = new StoredBoolean(this.mintingOpenPointer, false);
710
+ }
711
+
712
+ public override onDeployment(calldata: Calldata): void {
713
+ const name = calldata.readStringWithLength();
714
+ const symbol = calldata.readStringWithLength();
715
+ const baseURI = calldata.readStringWithLength();
716
+ const maxSupply = calldata.readU256();
717
+ const price = calldata.readU256();
718
+
719
+ // Initialize OP721 base class with all required parameters
720
+ this.instantiate(new OP721InitParameters(
721
+ name,
722
+ symbol,
723
+ baseURI,
724
+ maxSupply
725
+ ));
726
+
727
+ this._price.value = price;
728
+ }
729
+
730
+ // Public mint - uses internal _nextTokenId from base class
731
+ @method({ name: 'quantity', type: ABIDataTypes.UINT256 })
732
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
733
+ @emit('Transferred')
734
+ public mint(calldata: Calldata): BytesWriter {
735
+ if (!this._mintingOpen.value) {
736
+ throw new Revert('Minting not open');
737
+ }
738
+
739
+ const quantity = calldata.readU256();
740
+ const currentSupply = this.totalSupply;
741
+ const max = this.maxSupply;
742
+
743
+ // Check supply
744
+ if (SafeMath.add(currentSupply, quantity) > max) {
745
+ throw new Revert('Exceeds max supply');
746
+ }
747
+
748
+ // Mint tokens using base class _nextTokenId
749
+ const to = Blockchain.tx.sender;
750
+ for (let i: u256 = u256.Zero; i < quantity; i = SafeMath.add(i, u256.One)) {
751
+ const tokenId = this._nextTokenId.value;
752
+ this._mint(to, tokenId);
753
+ this._nextTokenId.value = SafeMath.add(tokenId, u256.One);
754
+ }
755
+
756
+ return new BytesWriter(0);
757
+ }
758
+
759
+ // Admin: Open minting
760
+ @method()
761
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
762
+ public openMinting(_calldata: Calldata): BytesWriter {
763
+ this.onlyDeployer(Blockchain.tx.sender);
764
+ this._mintingOpen.value = true;
765
+ return new BytesWriter(0);
766
+ }
767
+ }
768
+ ```
769
+
770
+ Note: The base class handles `tokenURI()`, `maxSupply`, `totalSupply`, and `_nextTokenId` - you don't need to redefine these unless you want custom behavior.
771
+
772
+ ## Best Practices
773
+
774
+ 1. **Use the built-in `_nextTokenId`** for automatic token ID management
775
+ 2. **Use the built-in `tokenURI`** or set custom URIs via `_setTokenURI` for marketplace compatibility
776
+ 3. **Set collection metadata** via `OP721InitParameters` for discoverability
777
+ 4. **Use `safeTransferFrom`** when receiver might be a contract (calls `onOP721Received`)
778
+ 5. **Events are emitted automatically** by internal methods like `_mint`, `_burn`, `_transfer`, `_approve`
779
+ 6. **Use `_exists(tokenId)`** to validate token existence before operations
780
+
781
+ ---
782
+
783
+ **Navigation:**
784
+ - Previous: [OP20S Signatures](./op20s-signatures.md)
785
+ - Next: [ReentrancyGuard](./reentrancy-guard.md)