@btc-vision/btc-runtime
Version:
Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.
820 lines (629 loc) • 21.4 kB
Markdown
# OP721 API Reference
The `OP721` class implements the non-fungible token (NFT) standard, equivalent to ERC721 on Ethereum.
## Import
```typescript
import { OP721, OP721InitParameters } from '@btc-vision/btc-runtime/runtime';
```
## OP721 Architecture
```mermaid
classDiagram
class OP721 {
<<abstract>>
+_name: StoredString
+_symbol: StoredString
+_baseURI: StoredString
+_totalSupply: StoredU256
+_maxSupply: StoredU256
+ownerOfMap: StoredMapU256
+balanceOfMap: AddressMemoryMap
+tokenApprovalMap: StoredMapU256
+operatorApprovalMap: MapOfMap~u256~
+instantiate(params) void
+ownerOf(tokenId) Address
+balanceOf(owner) u256
+approve(operator, tokenId) void
+safeTransfer(to, tokenId, data) void
+safeTransferFrom(from, to, tokenId, data) void
#_mint(to, tokenId) void
#_burn(tokenId) void
#_transfer(from, to, tokenId, data) void
}
class ReentrancyGuard {
<<abstract>>
#reentrancyLevel: ReentrancyLevel
}
class OP_NET {
<<abstract>>
+address: Address
#emitEvent(event) void
Note: @method decorator handles routing
}
class MyNFT {
+_nextTokenId: StoredU256
+_baseURI: StoredString
+constructor()
+onDeployment(calldata) void
+mint(calldata) BytesWriter
+tokenURI(tokenId) string
Note: @method decorator handles routing
}
OP_NET <|-- ReentrancyGuard
ReentrancyGuard <|-- OP721
OP721 <|-- MyNFT
note for OP721 "NFT standard with\nenumeration support\nand approval management"
note for MyNFT "Your NFT collection\nextends OP721"
```
## Class Definition
```typescript
@final
export class MyNFT extends OP721 {
public constructor() {
super();
}
public override onDeployment(calldata: Calldata): void {
const name = calldata.readString();
const symbol = calldata.readString();
const baseURI = calldata.readString();
const maxSupply = calldata.readU256();
this.instantiate(new OP721InitParameters(
name,
symbol,
baseURI,
maxSupply
));
}
}
```
## Initialization
### OP721InitParameters
```typescript
class OP721InitParameters {
constructor(
name: string,
symbol: string,
baseURI: string,
maxSupply: u256,
collectionBanner: string = '',
collectionIcon: string = '',
collectionWebsite: string = '',
collectionDescription: string = ''
)
}
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | `string` | Yes | Collection name |
| `symbol` | `string` | Yes | Collection symbol |
| `baseURI` | `string` | Yes | Base URI for token metadata |
| `maxSupply` | `u256` | Yes | Maximum number of tokens that can be minted |
| `collectionBanner` | `string` | No | Collection banner URL (default: '') |
| `collectionIcon` | `string` | No | Collection icon URL (default: '') |
| `collectionWebsite` | `string` | No | Collection website URL (default: '') |
| `collectionDescription` | `string` | No | Collection description (default: '') |
### instantiate
Initializes the OP721 NFT. Must be called in `onDeployment`.
```typescript
public instantiate(
params: OP721InitParameters,
skipDeployerVerification: boolean = false
): void
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `params` | `OP721InitParameters` | Initialization parameters |
| `skipDeployerVerification` | `boolean` | Skip deployer check (default: false) |
**Solidity Comparison:**
| Solidity (ERC721) | OP_NET (OP721) |
|-------------------|---------------|
| `constructor(string name, string symbol)` | `onDeployment(calldata)` + `instantiate()` |
## View Methods
### name
Returns the collection name.
```typescript
public name(): string
```
### symbol
Returns the collection symbol.
```typescript
public symbol(): string
```
### totalSupply
Returns total minted tokens.
```typescript
public get totalSupply(): u256
```
### maxSupply
Returns maximum supply limit.
```typescript
public get maxSupply(): u256
```
### balanceOf
Returns number of tokens owned by address.
```typescript
public balanceOf(owner: Address): u256
```
### ownerOf
Returns owner of a token.
```typescript
public ownerOf(tokenId: u256): Address
```
### tokenURI
Returns metadata URI for a token.
```typescript
public tokenURI(tokenId: u256): string
```
Override to customize metadata:
```typescript
public override tokenURI(tokenId: u256): string {
return this._baseURI.value + tokenId.toString() + '.json';
}
```
### getApproved
Returns approved address for a token.
```typescript
public getApproved(tokenId: u256): Address
```
### isApprovedForAll
Checks if operator is approved for all tokens.
```typescript
public isApprovedForAll(owner: Address, operator: Address): bool
```
**Solidity Comparison:**
| Solidity (ERC721) | OP_NET (OP721) |
|-------------------|---------------|
| `function ownerOf(uint256) view returns (address)` | `ownerOf(u256): Address` |
| `function balanceOf(address) view returns (uint256)` | `balanceOf(Address): u256` |
| `function tokenURI(uint256) view returns (string)` | `tokenURI(u256): string` |
## Transfer Methods
### safeTransfer (Calldata)
Transfers an NFT from the sender to a recipient.
```typescript
public safeTransfer(calldata: Calldata): BytesWriter
```
**Calldata format:**
| Field | Type | Size |
|-------|------|------|
| to | Address | 32 bytes |
| tokenId | u256 | 32 bytes |
| data | bytes | variable (length-prefixed) |
The following diagram shows the complete NFT transfer validation and execution flow:
```mermaid
flowchart LR
subgraph "Validation"
Start([NFT Transfer]) --> CheckOwner{Verify owner}
CheckOwner -->|Invalid| Revert1[Revert: Wrong owner]
CheckOwner -->|Valid| CheckTo{to != zero?}
CheckTo -->|to == zero| Revert2[Revert: Invalid recipient]
CheckTo -->|Valid| CheckAuth{Authorization}
end
subgraph "Authorization"
CheckAuth -->|Not authorized| Revert3[Revert: Not authorized]
CheckAuth -->|Owner| DoTransfer[Execute transfer]
CheckAuth -->|Approved operator| DoTransfer
CheckAuth -->|Token approved| DoTransfer
end
subgraph "Transfer Execution"
DoTransfer --> ClearApproval[Clear approval]
ClearApproval --> UpdateEnum[Update enumerations]
UpdateEnum --> UpdateBal[Update balances]
UpdateBal --> UpdateOwner[Set new owner]
UpdateOwner --> EmitEvent[Emit TransferEvent]
end
subgraph "Callback Handling"
EmitEvent --> IsContract{Recipient<br/>is contract?}
IsContract -->|No| Success([Complete])
IsContract -->|Yes| Callback[Call onOP721Received]
Callback --> CheckResponse{Valid<br/>response?}
CheckResponse -->|Yes| Success
CheckResponse -->|No| Revert4[Revert: Rejected]
end
```
### safeTransferFrom (Calldata)
Safe transfer with recipient callback.
```typescript
public safeTransferFrom(calldata: Calldata): BytesWriter
```
**Calldata format:**
| Field | Type | Size |
|-------|------|------|
| from | Address | 32 bytes |
| to | Address | 32 bytes |
| tokenId | u256 | 32 bytes |
| data | bytes | variable (length-prefixed) |
Calls `onOP721Received` on recipient if it's a contract.
### burn (Calldata)
Burns a token. Only owner or approved addresses can burn.
```typescript
public burn(calldata: Calldata): BytesWriter
```
**Calldata format:**
| Field | Type | Size |
|-------|------|------|
| tokenId | u256 | 32 bytes |
The following sequence diagram illustrates the complete mint and transfer flow:
```mermaid
sequenceDiagram
participant Owner
participant NFT as OP721 Contract
participant Maps as Storage Maps
participant Recipient
Note over Owner,Recipient: Mint Flow
Owner->>NFT: _mint(to, tokenId)
NFT->>NFT: Check tokenId doesn't exist
NFT->>NFT: Check max supply not reached
NFT->>Maps: ownerOfMap.set(tokenId, to)
NFT->>Maps: balanceOfMap[to] += 1
NFT->>Maps: Add to owner enumeration
NFT->>NFT: totalSupply += 1
NFT->>NFT: Emit TransferEvent(zero, to, tokenId)
Note over Owner,Recipient: Transfer Flow
Owner->>NFT: approve(operator, tokenId)
NFT->>NFT: Check caller is owner or approved for all
NFT->>Maps: tokenApprovalMap.set(tokenId, operator)
NFT->>NFT: Emit ApprovalEvent
Recipient->>NFT: safeTransferFrom(owner, recipient, tokenId, data)
NFT->>NFT: Check authorization
NFT->>NFT: _transfer(owner, recipient, tokenId, data)
NFT->>Maps: Clear token approval
NFT->>Maps: Remove from old owner enumeration
NFT->>Maps: Add to new owner enumeration
NFT->>Maps: balanceOfMap[owner] -= 1
NFT->>Maps: balanceOfMap[recipient] += 1
NFT->>Maps: ownerOfMap.set(tokenId, recipient)
NFT->>NFT: Emit TransferEvent
alt Recipient is Contract
NFT->>Recipient: call onOP721Received
Recipient->>NFT: Return selector
NFT->>NFT: Verify response
end
NFT->>Recipient: Success
```
**Solidity Comparison:**
| Solidity (ERC721) | OP_NET (OP721) |
|-------------------|---------------|
| `function transferFrom(address, address, uint256)` | `safeTransferFrom(calldata): BytesWriter` |
| `function safeTransferFrom(address, address, uint256, bytes)` | `safeTransferFrom(calldata): BytesWriter` |
| N/A | `safeTransfer(calldata): BytesWriter` (from sender) |
## Approval Methods
### approve (Calldata)
Approves an address for a single token.
```typescript
public approve(calldata: Calldata): BytesWriter
```
**Calldata format:**
| Field | Type | Size |
|-------|------|------|
| operator | Address | 32 bytes |
| tokenId | u256 | 32 bytes |
### setApprovalForAll (Calldata)
Sets operator approval for all tokens.
```typescript
public setApprovalForAll(calldata: Calldata): BytesWriter
```
**Calldata format:**
| Field | Type | Size |
|-------|------|------|
| operator | Address | 32 bytes |
| approved | bool | 1 byte |
**Solidity Comparison:**
| Solidity (ERC721) | OP_NET (OP721) |
|-------------------|---------------|
| `function approve(address, uint256)` | `approve(calldata): BytesWriter` |
| `function setApprovalForAll(address, bool)` | `setApprovalForAll(calldata): BytesWriter` |
## Protected Methods
### _mint
Mints a new token.
```typescript
protected _mint(to: Address, tokenId: u256): void
```
```typescript
@method(
{ name: 'to', type: ABIDataTypes.ADDRESS },
{ name: 'tokenId', type: ABIDataTypes.UINT256 },
)
@returns({ name: 'success', type: ABIDataTypes.BOOL })
@emit('Transfer')
public mint(calldata: Calldata): BytesWriter {
this.onlyDeployer(Blockchain.tx.sender);
const to: Address = calldata.readAddress();
const tokenId: u256 = calldata.readU256();
this._mint(to, tokenId);
return new BytesWriter(0);
}
```
### _burn
Burns a token.
```typescript
protected _burn(tokenId: u256): void
```
### _transfer
Internal transfer with data for safe transfer callbacks.
```typescript
protected _transfer(from: Address, to: Address, tokenId: u256, data: Uint8Array): void
```
### _approve
Internal approval.
```typescript
protected _approve(operator: Address, tokenId: u256): void
```
### _setApprovalForAll
Internal operator approval.
```typescript
protected _setApprovalForAll(owner: Address, operator: Address, approved: bool): void
```
### _setTokenURI
Sets a custom URI for a specific token.
```typescript
protected _setTokenURI(tokenId: u256, uri: string): void
```
### _setBaseURI
Sets the base URI for all tokens.
```typescript
protected _setBaseURI(baseURI: string): void
```
### _exists
Checks if a token exists.
```typescript
protected _exists(tokenId: u256): bool
```
### _ownerOf
Returns the owner of a token. Throws if token doesn't exist.
```typescript
protected _ownerOf(tokenId: u256): Address
```
### _balanceOf
Returns the balance of an address. Throws if zero address.
```typescript
protected _balanceOf(owner: Address): u256
```
### _isApprovedForAll
Checks if an operator is approved for all tokens.
```typescript
protected _isApprovedForAll(owner: Address, operator: Address): boolean
```
**Solidity Comparison:**
| Solidity (ERC721) | OP_NET (OP721) |
|-------------------|---------------|
| `function _mint(address, uint256) internal` | `_mint(Address, u256): void` |
| `function _burn(uint256) internal` | `_burn(u256): void` |
| `function _safeMint(address, uint256)` | `_mint()` (automatically checks recipient) |
## Enumeration System
### tokenOfOwnerByIndex
Returns token ID at index for owner.
```typescript
public tokenOfOwnerByIndex(owner: Address, index: u256): u256
```
The enumeration system allows efficient iteration over tokens owned by an address:
```mermaid
graph LR
A[NFT Collection]
subgraph "Owner Enumeration"
B[ownerTokensMap<br/>Address -> StoredU256Array]
C[Owner's Token Array]
D[Token IDs owned<br/>by address]
B --> C
C --> D
end
subgraph "Index Tracking"
E[tokenIndexMap<br/>tokenId -> u256]
F[Index in owner's array]
E --> F
end
subgraph "Add Token Operations"
G[_addTokenToOwnerEnumeration<br/>On mint/transfer in]
H[Push tokenId to array]
I[Set tokenIndexMap]
G --> H --> I
end
subgraph "Remove Token Operations"
J[_removeTokenFromOwnerEnumeration<br/>On burn/transfer out]
K[Get last token in array]
L[Move last to removed position]
M[Delete last element]
N[Update tokenIndexMap]
J --> K --> L --> M --> N
end
A --> B
A --> E
G -.->|modifies| C
J -.->|modifies| C
```
The following sequence diagram shows how enumeration queries work:
```mermaid
sequenceDiagram
participant User as 👤 User
participant NFT as OP721
participant OwnerMap as ownerTokensMap
participant IndexMap as tokenIndexMap
Note over User,IndexMap: Query Owner's Tokens
User->>NFT: balanceOf(owner)
NFT->>NFT: Return balance (e.g., 5 tokens)
User->>NFT: tokenOfOwnerByIndex(owner, 0)
NFT->>OwnerMap: Get owner's token array
OwnerMap->>NFT: Return StoredU256Array
NFT->>NFT: array.get(0)
NFT->>User: Return tokenId (e.g., 42)
User->>NFT: tokenOfOwnerByIndex(owner, 1)
NFT->>NFT: array.get(1)
NFT->>User: Return tokenId (e.g., 137)
Note over User,IndexMap: Iterate All Tokens
loop For each index from 0 to balance-1
User->>NFT: tokenOfOwnerByIndex(owner, index)
NFT->>User: Return tokenId
end
Note over User,IndexMap: Internal: Add Token to Enumeration
NFT->>NFT: _addTokenToOwnerEnumeration(owner, tokenId)
NFT->>OwnerMap: Get owner's array
NFT->>NFT: newIndex = array.length
NFT->>OwnerMap: array.push(tokenId)
NFT->>IndexMap: tokenIndexMap.set(tokenId, newIndex)
Note over User,IndexMap: Internal: Remove Token from Enumeration
NFT->>NFT: _removeTokenFromOwnerEnumeration(owner, tokenId)
NFT->>IndexMap: Get tokenIndex for tokenId
NFT->>OwnerMap: Get last token in array
NFT->>OwnerMap: Move last token to removed position
NFT->>IndexMap: Update index for moved token
NFT->>OwnerMap: Delete last element
NFT->>IndexMap: Delete tokenId mapping
```
```typescript
// Get all tokens owned by address
const balance = this.balanceOf(owner);
for (let i = u256.Zero; i < balance; i = SafeMath.add(i, u256.One)) {
const tokenId = this.tokenOfOwnerByIndex(owner, i);
// Process tokenId
}
```
**Note:** Unlike ERC721Enumerable, OP721 does not include a global `tokenByIndex` method. It only provides `tokenOfOwnerByIndex` for per-owner enumeration.
## Events
### TransferredEvent
Emitted on transfers, mints, and burns.
```typescript
class TransferredEvent extends NetEvent {
constructor(
operator: Address, // The address that initiated the transfer (Blockchain.tx.sender)
from: Address, // Previous owner (Address.zero() for mint)
to: Address, // New owner (Address.zero() for burn)
tokenId: u256 // The token being transferred
)
}
```
### ApprovedEvent
Emitted on token approvals.
```typescript
class ApprovedEvent extends NetEvent {
constructor(
owner: Address, // Token owner
spender: Address, // Approved address
tokenId: u256 // The token being approved
)
}
```
### ApprovedForAllEvent
Emitted on operator approvals.
```typescript
class ApprovedForAllEvent extends NetEvent {
constructor(
owner: Address, // Token owner
operator: Address, // Operator address
approved: bool // Approval status
)
}
```
### URIEvent
Emitted when a token URI is set or changed.
```typescript
class URIEvent extends NetEvent {
constructor(
value: string, // The new URI
id: u256 // The token ID
)
}
```
**Solidity Comparison:**
| Solidity (ERC721) | OP_NET (OP721) |
|-------------------|---------------|
| `event Transfer(address indexed, address indexed, uint256 indexed)` | `TransferredEvent(operator, from, to, tokenId)` |
| `event Approval(address indexed, address indexed, uint256 indexed)` | `ApprovedEvent(owner, spender, tokenId)` |
| `emit Transfer(from, to, tokenId)` | `emitEvent(new TransferredEvent(sender, from, to, tokenId))` |
## Storage Layout
OP721 uses multiple storage pointers:
| Purpose | Description |
|---------|-------------|
| Token owners | Maps tokenId -> owner |
| Balances | Maps owner -> count |
| Approvals | Maps tokenId -> approved |
| Operator approvals | Maps owner+operator -> bool |
| Owned tokens | Maps owner+index -> tokenId |
| Owned token index | Maps tokenId -> index |
| All tokens | Maps index -> tokenId |
| All tokens index | Maps tokenId -> index |
| Name | Collection name |
| Symbol | Collection symbol |
| Total supply | Current count |
## Method Selectors
| Selector | Method |
|----------|--------|
| `name` | Returns name |
| `symbol` | Returns symbol |
| `totalSupply` | Returns total supply |
| `maxSupply` | Returns maximum supply |
| `balanceOf` | Returns balance |
| `ownerOf` | Returns owner |
| `tokenURI` | Returns metadata URI |
| `approve` | Approve address |
| `getApproved` | Get approved address |
| `setApprovalForAll` | Set operator approval |
| `isApprovedForAll` | Check operator approval |
| `safeTransfer` | Transfer from sender |
| `safeTransferFrom` | Safe transfer with from address |
| `burn` | Burn token |
| `tokenOfOwnerByIndex` | Enumerable: owner token at index |
| `collectionInfo` | Returns collection metadata |
| `metadata` | Returns full collection metadata |
| `domainSeparator` | Returns EIP-712 domain separator |
| `getApproveNonce` | Returns signature nonce for address |
| `approveBySignature` | Approve via EIP-712 signature |
| `setApprovalForAllBySignature` | Set operator approval via signature |
| `setBaseURI` | Update base URI (deployer only) |
| `changeMetadata` | Update collection metadata (deployer only) |
## Complete Example
```typescript
import { u256 } from '@btc-vision/as-bignum/assembly';
import {
OP721,
OP721InitParameters,
Blockchain,
Calldata,
BytesWriter,
SafeMath,
Address,
ABIDataTypes,
} from '@btc-vision/btc-runtime/runtime';
@final
export class MyNFT extends OP721 {
public constructor() {
super();
}
public override onDeployment(calldata: Calldata): void {
const name = calldata.readStringWithLength();
const symbol = calldata.readStringWithLength();
const baseURI = calldata.readStringWithLength();
const maxSupply = calldata.readU256();
this.instantiate(new OP721InitParameters(
name,
symbol,
baseURI,
maxSupply
));
}
@method({ name: 'to', type: ABIDataTypes.ADDRESS })
@returns({ name: 'tokenId', type: ABIDataTypes.UINT256 })
@emit('Transferred')
public mint(calldata: Calldata): BytesWriter {
this.onlyDeployer(Blockchain.tx.sender);
const to: Address = calldata.readAddress();
// Use internal _nextTokenId from OP721 base class
const tokenId = this._nextTokenId.value;
this._mint(to, tokenId);
this._nextTokenId.value = SafeMath.add(tokenId, u256.One);
const writer = new BytesWriter(32);
writer.writeU256(tokenId);
return writer;
}
}
```
## Solidity Comparison Summary
| Solidity (ERC721) | OP_NET (OP721) |
|-------------------|---------------|
| `constructor(name, symbol)` | `instantiate(new OP721InitParameters(name, symbol, baseURI, maxSupply, ...))` |
| `function ownerOf(uint256)` | `ownerOf(u256): Address` |
| `_mint(address, uint256)` | `_mint(Address, u256)` |
| `_safeMint(address, uint256)` | `_mint()` (automatically emits TransferredEvent) |
| `emit Transfer(...)` | `emitEvent(new TransferredEvent(operator, from, to, tokenId))` |
| `transferFrom(from, to, tokenId)` | `safeTransferFrom(from, to, tokenId, data)` |
| N/A | `safeTransfer(to, tokenId, data)` (from sender) |
---
**Navigation:**
- Previous: [OP20 API](./op20.md)
- Next: [SafeMath API](./safe-math.md)