@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.
- 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 +731 -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 +370 -0
- package/docs/core-concepts/storage-system.md +938 -0
- package/docs/examples/basic-token.md +745 -0
- package/docs/examples/nft-with-reservations.md +1210 -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 +721 -0
- package/docs/storage/stored-arrays.md +714 -0
- package/docs/storage/stored-maps.md +686 -0
- package/docs/storage/stored-primitives.md +608 -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 +403 -0
- package/package.json +51 -26
- package/runtime/memory/MapOfMap.ts +1 -0
- package/runtime/types/SafeMath.ts +121 -1
- 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)
|