UNPKG

@btc-vision/btc-runtime

Version:

Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.

1,211 lines (972 loc) 35.3 kB
# NFT with Reservations Example An advanced OP721 NFT collection with time-based reservations, whitelist minting, and reveal mechanics. ## Overview This example demonstrates: - OP721 NFT implementation - Time-based reservation system - Whitelist/allowlist minting - Reveal mechanism - Multiple sale phases - Collection metadata - Decorators for ABI generation ## Sale Phase States The NFT collection progresses through multiple sale phases: ```mermaid --- config: theme: dark --- stateDiagram-v2 [*] --> INACTIVE: Deploy Contract INACTIVE --> RESERVATION: startReservation() TX RESERVATION --> WHITELIST: setSalePhase() TX WHITELIST --> PUBLIC: setSalePhase() TX PUBLIC --> REVEALED: reveal() TX state INACTIVE { [*] --> Configured note right of Configured No minting allowed Admin configures collection Storage: Initialize parameters end note } state RESERVATION { [*] --> AcceptingReservations AcceptingReservations --> ReservationEnded: Time expires note right of AcceptingReservations Users call reserve(quantity) Reservations stored on-chain Can submit cancel TX before end end note note left of ReservationEnded Users call claimReserved() NFTs minted to reservers Update ownership storage end note } state WHITELIST { [*] --> WhitelistMinting note right of WhitelistMinting Only whitelisted addresses whitelistMint() TX Max per wallet enforced end note } state PUBLIC { [*] --> PublicMinting note right of PublicMinting Anyone can mint publicMint() TX Max per wallet enforced end note } state REVEALED { [*] --> MetadataVisible note right of MetadataVisible tokenURI shows real metadata baseURI + tokenId + .json end note } ``` ## Reservation Flow Each token slot in the collection progresses through distinct states during the reservation lifecycle: ```mermaid --- config: theme: dark --- stateDiagram-v2 [*] --> Available: Collection Deployed Available --> Reserved: reserve(quantity) TX Reserved --> Available: cancelReservation() TX Reserved --> Expired: Reservation period ends<br/>without claim Reserved --> Minted: claimReserved() TX Minted --> [*]: Token Owned state Available { [*] --> OpenForReservation note right of OpenForReservation Token slot not yet claimed Can be reserved by any user Within maxPerWallet limit end note } state Reserved { [*] --> HeldForUser note right of HeldForUser Slot reserved for address Stored in _reservedBy map Awaiting claim or cancel end note } state Expired { [*] --> Unclaimed note right of Unclaimed Reservation period ended User did not claim Slot may be released end note } state Minted { [*] --> TokenOwned note right of TokenOwned NFT minted to owner Stored in _owners map Transferable via OP721 end note } ``` The reservation system allows users to reserve NFTs before minting begins: ```mermaid sequenceDiagram participant Admin as 👤 Admin participant BTC as Bitcoin Network participant Contract as Contract Execution participant User1 as 👤 User1 participant User2 as 👤 User2 participant Storage as Storage Layer Admin->>BTC: Submit startReservation(86400) TX BTC->>Contract: Execute startReservation Contract->>Storage: Write reservationEnd = now + 86400s Contract-->>BTC: Success BTC-->>Admin: TX Confirmed Note over User1,Storage: Reservation Period Active User1->>BTC: Submit reserve(quantity=2) TX BTC->>Contract: Execute reserve Contract->>Contract: Check now < reservationEnd Contract->>Storage: Read current reservation for User1 Contract->>Contract: newTotal = current + 2 Contract->>Contract: Check newTotal <= maxPerWallet Contract->>Storage: Write User1 reserved = 2 Contract-->>BTC: Success BTC-->>User1: TX Confirmed User2->>BTC: Submit reserve(quantity=5) TX BTC->>Contract: Execute reserve Contract->>Contract: Check now < reservationEnd Contract->>Storage: Read current reservation for User2 Contract->>Contract: newTotal = 0 + 5 alt Exceeds max per wallet Contract-->>BTC: Revert: Exceeds max per wallet BTC-->>User2: TX Failed else Within limit Contract->>Storage: Write User2 reserved = 5 Contract-->>BTC: Success BTC-->>User2: TX Confirmed end Note over Contract: Time passes... Reservation period ends User1->>BTC: Submit claimReserved() TX BTC->>Contract: Execute claimReserved Contract->>Contract: Check now >= reservationEnd Contract->>Storage: Read User1 reservation = 2 Contract->>Storage: Write User1 reservation = 0 Contract->>Contract: Mint token #1 to User1 Contract->>Contract: Mint token #2 to User1 Contract->>Storage: Write nextTokenId = 3 Contract->>Storage: Write NFT ownership Contract-->>BTC: Success BTC-->>User1: TX Confirmed (2 NFTs minted) User1->>BTC: Submit claimReserved() TX again BTC->>Contract: Execute claimReserved Contract->>Storage: Read User1 reservation = 0 Contract-->>BTC: Revert: No reservations BTC-->>User1: TX Failed ``` ### Reservation Implementation ```typescript @method({ name: 'quantity', type: ABIDataTypes.UINT256 }) @returns({ name: 'success', type: ABIDataTypes.BOOL }) @emit('Reserved') public reserve(calldata: Calldata): BytesWriter { const quantity = calldata.readU256(); const sender = Blockchain.tx.sender; // Check reservation is active const now = u256.fromU64(Blockchain.block.medianTime); if (now >= this._reservationEnd.value) { throw new Revert('Reservation period ended'); } // Check quantity limits const currentReserved = this._reservedBy.get(sender); const newTotal = SafeMath.add(currentReserved, quantity); if (newTotal > this._maxPerWallet.value) { throw new Revert('Exceeds max per wallet'); } // Update reservation this._reservedBy.set(sender, newTotal); return new BytesWriter(0); } ``` ## Whitelist Verification Whitelist minting validates that users are on the allowlist: ```mermaid --- config: theme: dark --- flowchart LR A["👤 User submits whitelistMint TX"] --> B{Sale phase = WHITELIST?} B -->|No| C[Revert: Whitelist sale not active] B -->|Yes| D{User in whitelist?} D -->|No| E[Revert: Not whitelisted] D -->|Yes| F{Within limits?} F -->|No| G[Revert: Exceeds limits] F -->|Yes| H[Mint tokens] H --> I[TX Success] ``` ### Whitelist Implementation ```typescript @method({ name: 'quantity', type: ABIDataTypes.UINT256 }) @returns({ name: 'success', type: ABIDataTypes.BOOL }) @emit('Minted') public whitelistMint(calldata: Calldata): BytesWriter { const quantity = calldata.readU256(); const sender = Blockchain.tx.sender; // Check phase if (this._salePhase.value != PHASE_WHITELIST) { throw new Revert('Whitelist sale not active'); } // Check whitelist (AddressMemoryMap returns u256; non-zero = whitelisted) if (this._whitelist.get(sender).isZero()) { throw new Revert('Not whitelisted'); } this._mintInternal(sender, quantity); return new BytesWriter(0); } ``` **Solidity Comparison:** ```solidity // Solidity - Using Merkle proof for whitelist function whitelistMint(uint256 quantity, bytes32[] calldata proof) external { require(salePhase == Phase.WHITELIST, "Whitelist sale not active"); require(MerkleProof.verify(proof, merkleRoot, keccak256(abi.encodePacked(msg.sender))), "Not whitelisted"); _mintInternal(msg.sender, quantity); } // OP_NET - Using on-chain mapping (simpler approach) @method({ name: 'quantity', type: ABIDataTypes.UINT256 }) @returns({ name: 'success', type: ABIDataTypes.BOOL }) @emit('Minted') public whitelistMint(calldata: Calldata): BytesWriter { // AddressMemoryMap returns u256; non-zero = whitelisted if (this._whitelist.get(sender).isZero()) { throw new Revert('Not whitelisted'); } // ... } ``` ## Reservation Cancellation Users can cancel their reservations during the reservation period: ```mermaid --- config: theme: dark --- flowchart LR A["👤 User submits cancelReservation TX"] --> B{Period active?} B -->|No| C[Revert: Period ended] B -->|Yes| D{Has reservation?} D -->|No| E[Revert: No reservations] D -->|Yes| F[Clear reservation] F --> G[Process refund] G --> H[TX Success] ``` ## Token URI Reveal Mechanism The reveal mechanism hides metadata until the collection is revealed: ```mermaid --- config: theme: dark --- flowchart LR A["👤 User: Call tokenURI"] --> B{Token exists in storage?} B -->|No| C[Revert: Token does not exist] B -->|Yes| D{Collection revealed?} D -->|No - _revealed = false| E["Return hiddenURI<br/>e.g., ipfs://QmHidden/hidden.json"] D -->|Yes - _revealed = true| F["Return baseURI + tokenId + .json<br/>e.g., ipfs://QmReal/1.json"] E --> G[All tokens show same metadata] F --> H[Each token shows unique metadata] subgraph "Before Reveal " I[Token #1 -> hidden.json] J[Token #2 -> hidden.json] K[Token #3 -> hidden.json] end subgraph "After Reveal " L[Token #1 -> 1.json] M[Token #2 -> 2.json] N[Token #3 -> 3.json] end ``` ### Reveal Implementation ```typescript public override tokenURI(tokenId: u256): string { // Check token exists if (this.ownerOf(tokenId).equals(Address.zero())) { throw new Revert('Token does not exist'); } if (!this._revealed.value) { return this._hiddenURI.value; } return this._baseURI.value + tokenId.toString() + '.json'; } ``` ## Sale Timeline The complete sale lifecycle follows a typical NFT launch pattern: ```mermaid gantt title NFT Collection Sale Timeline dateFormat YYYY-MM-DD axisFormat %b %d section Preparation Deploy Contract :done, deploy, 2024-01-01, 1d Configure Collection :done, config, after deploy, 1d Set Whitelist :done, whitelist, after config, 2d section Reservation Phase Start Reservation :crit, res_start, 2024-01-05, 1d Reservation Period :active, res_period, after res_start, 2d Claim Reserved NFTs :claim, after res_period, 1d section Whitelist Sale Whitelist Phase :wl_phase, 2024-01-09, 3d section Public Sale Public Phase :pub_phase, after wl_phase, 5d section Reveal Reveal Metadata :milestone, reveal, after pub_phase, 1d Collection Complete :done, after reveal, 1d ``` ## Complete Implementation ```typescript import { u256 } from '@btc-vision/as-bignum/assembly'; import { OP721, OP721InitParameters, Blockchain, Address, Calldata, BytesWriter, SafeMath, Revert, StoredU256, StoredString, StoredBoolean, StoredU8, AddressMemoryMap, ABIDataTypes, EMPTY_POINTER, } from '@btc-vision/btc-runtime/runtime'; // Sale phases const PHASE_INACTIVE: u8 = 0; const PHASE_WHITELIST: u8 = 1; const PHASE_PUBLIC: u8 = 2; @final export class NFTWithReservations extends OP721 { // Configuration storage private maxSupplyPointer: u16 = Blockchain.nextPointer; private pricePointer: u16 = Blockchain.nextPointer; private maxPerWalletPointer: u16 = Blockchain.nextPointer; private baseURIPointer: u16 = Blockchain.nextPointer; private hiddenURIPointer: u16 = Blockchain.nextPointer; private revealedPointer: u16 = Blockchain.nextPointer; private salePhasePointer: u16 = Blockchain.nextPointer; private nextTokenIdPointer: u16 = Blockchain.nextPointer; // Reservation storage private reservationEndPointer: u16 = Blockchain.nextPointer; private reservedByPointer: u16 = Blockchain.nextPointer; private reservationPricePointer: u16 = Blockchain.nextPointer; // Whitelist storage private whitelistPointer: u16 = Blockchain.nextPointer; private mintedCountPointer: u16 = Blockchain.nextPointer; // Stored values private _maxSupply: StoredU256; private _price: StoredU256; private _maxPerWallet: StoredU256; private _baseURI: StoredString; private _hiddenURI: StoredString; private _revealed: StoredBoolean; private _salePhase: StoredU8; private _nextTokenId: StoredU256; private _reservationEnd: StoredU256; private _reservedBy: AddressMemoryMap; private _reservationPrice: StoredU256; private _whitelist: AddressMemoryMap; private _mintedCount: AddressMemoryMap; public constructor() { super(); // Initialize storage this._maxSupply = new StoredU256(this.maxSupplyPointer, EMPTY_POINTER); this._price = new StoredU256(this.pricePointer, EMPTY_POINTER); this._maxPerWallet = new StoredU256(this.maxPerWalletPointer, EMPTY_POINTER); this._baseURI = new StoredString(this.baseURIPointer, 0); this._hiddenURI = new StoredString(this.hiddenURIPointer, 1); this._revealed = new StoredBoolean(this.revealedPointer, false); this._salePhase = new StoredU8(this.salePhasePointer, PHASE_INACTIVE); this._nextTokenId = new StoredU256(this.nextTokenIdPointer, EMPTY_POINTER); this._reservationEnd = new StoredU256(this.reservationEndPointer, EMPTY_POINTER); this._reservedBy = new AddressMemoryMap(this.reservedByPointer); this._reservationPrice = new StoredU256(this.reservationPricePointer, EMPTY_POINTER); this._whitelist = new AddressMemoryMap(this.whitelistPointer); this._mintedCount = new AddressMemoryMap(this.mintedCountPointer); } public override onDeployment(calldata: Calldata): void { const name = calldata.readString(); const symbol = calldata.readString(); const maxSupply = calldata.readU256(); const price = calldata.readU256(); const maxPerWallet = calldata.readU256(); const hiddenURI = calldata.readString(); // OP721InitParameters requires: name, symbol, baseURI, maxSupply // baseURI is empty initially since we use hiddenURI before reveal this.instantiate(new OP721InitParameters(name, symbol, '', maxSupply)); this._maxSupply.value = maxSupply; this._price.value = price; this._maxPerWallet.value = maxPerWallet; this._hiddenURI.value = hiddenURI; this._nextTokenId.value = u256.One; // Set initial token ID } // ============ RESERVATION SYSTEM ============ /** * Reserve tokens during reservation phase. * Tokens are held until reservation period ends. */ @method({ name: 'quantity', type: ABIDataTypes.UINT256 }) @returns({ name: 'success', type: ABIDataTypes.BOOL }) @emit('Reserved') public reserve(calldata: Calldata): BytesWriter { const quantity = calldata.readU256(); const sender = Blockchain.tx.sender; // Check reservation is active const now = u256.fromU64(Blockchain.block.medianTime); if (now >= this._reservationEnd.value) { throw new Revert('Reservation period ended'); } // Check quantity limits const currentReserved = this._reservedBy.get(sender); const newTotal = SafeMath.add(currentReserved, quantity); if (newTotal > this._maxPerWallet.value) { throw new Revert('Exceeds max per wallet'); } // Update reservation this._reservedBy.set(sender, newTotal); return new BytesWriter(0); } /** * Claim reserved tokens after reservation period. */ @method() @returns({ name: 'success', type: ABIDataTypes.BOOL }) @emit('ReservationClaimed') public claimReserved(_calldata: Calldata): BytesWriter { const sender = Blockchain.tx.sender; // Check reservation period ended const now = u256.fromU64(Blockchain.block.medianTime); if (now < this._reservationEnd.value) { throw new Revert('Reservation period not ended'); } // Get reserved quantity const reserved = this._reservedBy.get(sender); if (reserved.isZero()) { throw new Revert('No reservations'); } // Clear reservation this._reservedBy.set(sender, u256.Zero); // Mint reserved tokens let count = reserved; while (!count.isZero()) { const tokenId = this._nextTokenId.value; this._mint(sender, tokenId); this._nextTokenId.value = SafeMath.add(tokenId, u256.One); count = SafeMath.sub(count, u256.One); } return new BytesWriter(0); } /** * Cancel reservation and get refund. */ @method() @returns({ name: 'success', type: ABIDataTypes.BOOL }) @emit('ReservationCancelled') public cancelReservation(_calldata: Calldata): BytesWriter { const sender = Blockchain.tx.sender; // Must be during reservation period const now = u256.fromU64(Blockchain.block.medianTime); if (now >= this._reservationEnd.value) { throw new Revert('Reservation period ended'); } // Clear reservation const reserved = this._reservedBy.get(sender); if (reserved.isZero()) { throw new Revert('No reservations'); } this._reservedBy.set(sender, u256.Zero); // Refund logic would go here return new BytesWriter(0); } // ============ MINTING ============ /** * Whitelist mint during whitelist phase. */ @method({ name: 'quantity', type: ABIDataTypes.UINT256 }) @returns({ name: 'success', type: ABIDataTypes.BOOL }) @emit('Minted') public whitelistMint(calldata: Calldata): BytesWriter { const quantity = calldata.readU256(); const sender = Blockchain.tx.sender; // Check phase if (this._salePhase.value != PHASE_WHITELIST) { throw new Revert('Whitelist sale not active'); } // Check whitelist (AddressMemoryMap returns u256; non-zero = whitelisted) if (this._whitelist.get(sender).isZero()) { throw new Revert('Not whitelisted'); } this._mintInternal(sender, quantity); return new BytesWriter(0); } /** * Public mint during public phase. */ @method({ name: 'quantity', type: ABIDataTypes.UINT256 }) @returns({ name: 'success', type: ABIDataTypes.BOOL }) @emit('Minted') public publicMint(calldata: Calldata): BytesWriter { const quantity = calldata.readU256(); const sender = Blockchain.tx.sender; // Check phase if (this._salePhase.value != PHASE_PUBLIC) { throw new Revert('Public sale not active'); } this._mintInternal(sender, quantity); return new BytesWriter(0); } private _mintInternal(to: Address, quantity: u256): void { // Check supply const currentSupply = this.totalSupply(); const newSupply = SafeMath.add(currentSupply, quantity); if (newSupply > this._maxSupply.value) { throw new Revert('Exceeds max supply'); } // Check per-wallet limit const minted = this._mintedCount.get(to); const newMinted = SafeMath.add(minted, quantity); if (newMinted > this._maxPerWallet.value) { throw new Revert('Exceeds max per wallet'); } // Update minted count this._mintedCount.set(to, newMinted); // Mint tokens let count = quantity; while (!count.isZero()) { const tokenId = this._nextTokenId.value; this._mint(to, tokenId); this._nextTokenId.value = SafeMath.add(tokenId, u256.One); count = SafeMath.sub(count, u256.One); } } // ============ REVEAL ============ public override tokenURI(tokenId: u256): string { // Check token exists if (this.ownerOf(tokenId).equals(Address.zero())) { throw new Revert('Token does not exist'); } if (!this._revealed.value) { return this._hiddenURI.value; } return this._baseURI.value + tokenId.toString() + '.json'; } // ============ ADMIN FUNCTIONS ============ @method({ name: 'duration', type: ABIDataTypes.UINT64 }) @returns({ name: 'success', type: ABIDataTypes.BOOL }) @emit('ReservationStarted') public startReservation(calldata: Calldata): BytesWriter { this.onlyDeployer(Blockchain.tx.sender); const duration = calldata.readU64(); const endTime = Blockchain.block.medianTime + duration; this._reservationEnd.value = u256.fromU64(endTime); return new BytesWriter(0); } @method({ name: 'phase', type: ABIDataTypes.UINT8 }) @returns({ name: 'success', type: ABIDataTypes.BOOL }) @emit('SalePhaseChanged') public setSalePhase(calldata: Calldata): BytesWriter { this.onlyDeployer(Blockchain.tx.sender); const phase = calldata.readU8(); if (phase > PHASE_PUBLIC) { throw new Revert('Invalid phase'); } this._salePhase.value = phase; return new BytesWriter(0); } @method( { name: 'addresses', type: ABIDataTypes.ADDRESS_ARRAY }, { name: 'status', type: ABIDataTypes.BOOL }, ) @returns({ name: 'success', type: ABIDataTypes.BOOL }) @emit('WhitelistUpdated') public setWhitelist(calldata: Calldata): BytesWriter { this.onlyDeployer(Blockchain.tx.sender); const addresses = calldata.readAddressArray(); const status = calldata.readBoolean(); // AddressMemoryMap stores u256; convert boolean to u256.One/Zero const statusValue = status ? u256.One : u256.Zero; for (let i = 0; i < addresses.length; i++) { this._whitelist.set(addresses[i], statusValue); } return new BytesWriter(0); } @method({ name: 'baseURI', type: ABIDataTypes.STRING }) @returns({ name: 'success', type: ABIDataTypes.BOOL }) @emit('Revealed') public reveal(calldata: Calldata): BytesWriter { this.onlyDeployer(Blockchain.tx.sender); const baseURI = calldata.readString(); this._baseURI.value = baseURI; this._revealed.value = true; return new BytesWriter(0); } @method({ name: 'price', type: ABIDataTypes.UINT256 }) @returns({ name: 'success', type: ABIDataTypes.BOOL }) @emit('PriceChanged') public setPrice(calldata: Calldata): BytesWriter { this.onlyDeployer(Blockchain.tx.sender); this._price.value = calldata.readU256(); return new BytesWriter(0); } // ============ VIEW FUNCTIONS ============ @method({ name: 'addr', type: ABIDataTypes.ADDRESS }) @returns({ name: 'reserved', type: ABIDataTypes.UINT256 }) public getReservation(calldata: Calldata): BytesWriter { const addr = calldata.readAddress(); const reserved = this._reservedBy.get(addr); const writer = new BytesWriter(32); writer.writeU256(reserved); return writer; } @method({ name: 'addr', type: ABIDataTypes.ADDRESS }) @returns({ name: 'whitelisted', type: ABIDataTypes.BOOL }) public isWhitelisted(calldata: Calldata): BytesWriter { const addr = calldata.readAddress(); // AddressMemoryMap.get() returns u256; convert to boolean const status = !this._whitelist.get(addr).isZero(); const writer = new BytesWriter(1); writer.writeBoolean(status); return writer; } @method() @returns( { name: 'phase', type: ABIDataTypes.UINT8 }, { name: 'price', type: ABIDataTypes.UINT256 }, { name: 'maxSupply', type: ABIDataTypes.UINT256 }, { name: 'totalSupply', type: ABIDataTypes.UINT256 }, { name: 'revealed', type: ABIDataTypes.BOOL }, ) public getSaleInfo(_calldata: Calldata): BytesWriter { const writer = new BytesWriter(128); writer.writeU8(this._salePhase.value); writer.writeU256(this._price.value); writer.writeU256(this._maxSupply.value); writer.writeU256(this.totalSupply()); writer.writeBoolean(this._revealed.value); return writer; } @method({ name: 'addr', type: ABIDataTypes.ADDRESS }) @returns({ name: 'count', type: ABIDataTypes.UINT256 }) public getMintedCount(calldata: Calldata): BytesWriter { const addr = calldata.readAddress(); const count = this._mintedCount.get(addr); const writer = new BytesWriter(32); writer.writeU256(count); return writer; } } ``` ## Key Features ### Reservation System ``` Timeline: 1. Admin starts reservation period (e.g., 24 hours) 2. Users reserve tokens during period 3. Period ends 4. Users claim reserved tokens ``` ### Sale Phases ``` PHASE_INACTIVE (0) -> PHASE_WHITELIST (1) -> PHASE_PUBLIC (2) ``` ### Reveal Mechanism Before reveal: All tokens show `hiddenURI` After reveal: Tokens show `baseURI + tokenId + .json` ## Usage Timeline ``` 1. Deploy contract with hidden URI 2. Set whitelist addresses 3. Start reservation period 4. Users reserve tokens 5. Reservation ends -> users claim 6. Set phase to WHITELIST 7. Whitelisted users mint 8. Set phase to PUBLIC 9. Anyone can mint 10. Reveal metadata ``` ## Best Practices ### Use StoredU8 for Small Enum Values ```typescript // Good: Use StoredU8 for phase constants const PHASE_INACTIVE: u8 = 0; const PHASE_WHITELIST: u8 = 1; const PHASE_PUBLIC: u8 = 2; private _salePhase: StoredU8; // Compare using same types if (this._salePhase.value != PHASE_WHITELIST) { } ``` ### Use u256 for Timestamps When Comparing ```typescript // Good: Convert timestamps to u256 for comparison with u256 values const now = u256.fromU64(Blockchain.block.medianTime); if (now >= this._reservationEnd.value) { } ``` ### Add Decorators for ABI Generation ```typescript @method({ name: 'quantity', type: ABIDataTypes.UINT256 }) @returns({ name: 'success', type: ABIDataTypes.BOOL }) @emit('Reserved') public reserve(calldata: Calldata): BytesWriter { } @method() @returns( { name: 'phase', type: ABIDataTypes.UINT8 }, { name: 'price', type: ABIDataTypes.UINT256 }, ) public getSaleInfo(_calldata: Calldata): BytesWriter { } ``` ## Solidity Comparison | Aspect | Solidity (ERC721) | OP_NET (OP721) | |--------|-------------------|---------------| | Inheritance | `contract NFT is ERC721, Ownable` | `class NFT extends OP721` | | Constructor | `constructor() ERC721("Name", "SYM")` | `onDeployment()` + `this.instantiate(...)` | | Mint | `_safeMint(to, tokenId)` | `this._mint(to, tokenId)` | | Timestamp | `block.timestamp` | `Blockchain.block.medianTime` | | Whitelist Storage | `mapping(address => bool)` | `AddressMemoryMap` | For detailed OP721 API documentation, see [OP721 Contract](../contracts/op721-nft.md). ### Whitelist Implementation Comparison **Solidity (Using Merkle Proofs):** ```solidity import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; bytes32 public merkleRoot; function whitelistMint(uint256 quantity, bytes32[] calldata proof) external payable { require(salePhase == SalePhase.WHITELIST, "Whitelist sale not active"); bytes32 leaf = keccak256(abi.encodePacked(msg.sender)); require(MerkleProof.verify(proof, merkleRoot, leaf), "Not whitelisted"); _mintInternal(msg.sender, quantity); } ``` **OP_NET (On-chain mapping):** ```typescript private _whitelist: AddressMemoryMap; @method({ name: 'quantity', type: ABIDataTypes.UINT256 }) @returns({ name: 'success', type: ABIDataTypes.BOOL }) @emit('Minted') public whitelistMint(calldata: Calldata): BytesWriter { const quantity = calldata.readU256(); const sender = Blockchain.tx.sender; if (this._salePhase.value != PHASE_WHITELIST) { throw new Revert('Whitelist sale not active'); } // AddressMemoryMap returns u256; non-zero = whitelisted if (this._whitelist.get(sender).isZero()) { throw new Revert('Not whitelisted'); } this._mintInternal(sender, quantity); return new BytesWriter(0); } ``` ### Reveal Mechanism Comparison **Solidity:** ```solidity string private baseURI_; string private hiddenURI; bool public revealed; function tokenURI(uint256 tokenId) public view override returns (string memory) { require(_ownerOf(tokenId) != address(0), "Token does not exist"); if (!revealed) { return hiddenURI; } return string(abi.encodePacked(baseURI_, tokenId.toString(), ".json")); } function reveal(string calldata _baseURI) external onlyOwner { baseURI_ = _baseURI; revealed = true; } ``` **OP_NET:** ```typescript private _baseURI: StoredString; private _hiddenURI: StoredString; private _revealed: StoredBoolean; public override tokenURI(tokenId: u256): string { if (this.ownerOf(tokenId).equals(Address.zero())) { throw new Revert('Token does not exist'); } if (!this._revealed.value) { return this._hiddenURI.value; } return this._baseURI.value + tokenId.toString() + '.json'; } @method({ name: 'baseURI', type: ABIDataTypes.STRING }) @returns({ name: 'success', type: ABIDataTypes.BOOL }) @emit('Revealed') public reveal(calldata: Calldata): BytesWriter { this.onlyDeployer(Blockchain.tx.sender); const baseURI = calldata.readString(); this._baseURI.value = baseURI; this._revealed.value = true; return new BytesWriter(0); } ``` ### Sale Phase Management Comparison **Solidity:** ```solidity enum SalePhase { INACTIVE, WHITELIST, PUBLIC } SalePhase public salePhase; function setSalePhase(SalePhase phase) external onlyOwner { salePhase = phase; } // Usage require(salePhase == SalePhase.WHITELIST, "Whitelist sale not active"); ``` **OP_NET:** ```typescript const PHASE_INACTIVE: u8 = 0; const PHASE_WHITELIST: u8 = 1; const PHASE_PUBLIC: u8 = 2; private _salePhase: StoredU8; @method({ name: 'phase', type: ABIDataTypes.UINT8 }) @returns({ name: 'success', type: ABIDataTypes.BOOL }) @emit('SalePhaseChanged') public setSalePhase(calldata: Calldata): BytesWriter { this.onlyDeployer(Blockchain.tx.sender); const phase = calldata.readU8(); if (phase > PHASE_PUBLIC) { throw new Revert('Invalid phase'); } this._salePhase.value = phase; return new BytesWriter(0); } // Usage if (this._salePhase.value != PHASE_WHITELIST) { throw new Revert('Whitelist sale not active'); } ``` ### Advantages of OP_NET Approach | Feature | Benefit | |---------|---------| | **Bitcoin Timestamps** | Uses `medianTime` for manipulation-resistant timing | | **Native u256** | First-class 256-bit integer support | | **Explicit Storage** | Direct control over storage layout with pointers | | **Single Inheritance** | Avoids ERC721's multiple inheritance complexity | | **No payable Complexity** | Bitcoin UTXO model handles value transfers differently | | **Typed Storage** | `StoredU8`, `StoredU256`, `StoredBoolean` for type safety | ### Minting Loop Comparison **Solidity:** ```solidity function _mintInternal(address to, uint256 quantity) internal { require(nextTokenId + quantity - 1 <= maxSupply, "Exceeds max supply"); require(mintedCount[to] + quantity <= maxPerWallet, "Exceeds max per wallet"); mintedCount[to] += quantity; for (uint256 i = 0; i < quantity; i++) { _safeMint(to, nextTokenId++); } } ``` **OP_NET:** ```typescript private _mintInternal(to: Address, quantity: u256): void { const currentSupply = this.totalSupply(); const newSupply = SafeMath.add(currentSupply, quantity); if (newSupply > this._maxSupply.value) { throw new Revert('Exceeds max supply'); } const minted = this._mintedCount.get(to); const newMinted = SafeMath.add(minted, quantity); if (newMinted > this._maxPerWallet.value) { throw new Revert('Exceeds max per wallet'); } this._mintedCount.set(to, newMinted); let count = quantity; while (!count.isZero()) { const tokenId = this._nextTokenId.value; this._mint(to, tokenId); this._nextTokenId.value = SafeMath.add(tokenId, u256.One); count = SafeMath.sub(count, u256.One); } } ``` ### Payment and Refund Handling **Solidity (ETH-based):** ```solidity function reserve(uint256 quantity) external payable { require(msg.value >= price * quantity, "Insufficient payment"); reservations[msg.sender] += quantity; } function cancelReservation() external { uint256 quantity = reservations[msg.sender]; reservations[msg.sender] = 0; // Refund ETH uint256 refundAmount = price * quantity; (bool success, ) = msg.sender.call{value: refundAmount}(""); require(success, "Refund failed"); } ``` **OP_NET (Bitcoin UTXO model):** ```typescript // Payment handled at Bitcoin transaction level // Refund logic would involve different mechanisms @method() @returns({ name: 'success', type: ABIDataTypes.BOOL }) @emit('ReservationCancelled') public cancelReservation(_calldata: Calldata): BytesWriter { const sender = Blockchain.tx.sender; const now = u256.fromU64(Blockchain.block.medianTime); if (now >= this._reservationEnd.value) { throw new Revert('Reservation period ended'); } const reserved = this._reservedBy.get(sender); if (reserved.isZero()) { throw new Revert('No reservations'); } this._reservedBy.set(sender, u256.Zero); // Refund logic handled at protocol level return new BytesWriter(0); } ``` ### View Functions Comparison **Solidity:** ```solidity function getSaleInfo() external view returns ( SalePhase phase, uint256 currentPrice, uint256 maxSupply_, uint256 totalSupply_, bool isRevealed ) { return (salePhase, price, maxSupply, totalSupply(), revealed); } ``` **OP_NET:** ```typescript @method() @returns( { name: 'phase', type: ABIDataTypes.UINT8 }, { name: 'price', type: ABIDataTypes.UINT256 }, { name: 'maxSupply', type: ABIDataTypes.UINT256 }, { name: 'totalSupply', type: ABIDataTypes.UINT256 }, { name: 'revealed', type: ABIDataTypes.BOOL }, ) public getSaleInfo(_calldata: Calldata): BytesWriter { const writer = new BytesWriter(128); writer.writeU8(this._salePhase.value); writer.writeU256(this._price.value); writer.writeU256(this._maxSupply.value); writer.writeU256(this.totalSupply()); writer.writeBoolean(this._revealed.value); return writer; } ``` --- **Navigation:** - Previous: [Basic Token](./basic-token.md) - Next: [Stablecoin](./stablecoin.md)