UNPKG

opnet

Version:

The perfect library for building Bitcoin-based applications.

878 lines (712 loc) 22.3 kB
# OP721 NFT Examples This guide provides comprehensive examples for working with OP721 (non-fungible) tokens. ## Table of Contents - [Overview](#overview) - [Setup](#setup) - [Collection Information](#collection-information) - [Ownership Queries](#ownership-queries) - [Transfer Examples](#transfer-examples) - [Approval Management](#approval-management) - [Extended NFT Features](#extended-nft-features) - [Reserve and Claim Pattern](#reserve-and-claim-pattern) - [Airdrop Example (Batch Distribution)](#airdrop-example-batch-distribution) - [Set Token URI (Owner Only)](#set-token-uri-owner-only) - [Burn NFT](#burn-nft) - [Complete NFT Service](#complete-nft-service) --- ## Overview OP721 is OPNet's NFT standard, similar to ERC721 on Ethereum. It supports individual token ownership, metadata, and advanced features like reservations and minting. ```mermaid classDiagram class IOP721Contract { +name() string +symbol() string +maxSupply() bigint +totalSupply() bigint +balanceOf(owner) bigint +ownerOf(tokenId) Address +tokenURI(tokenId) string +safeTransfer(to, tokenId, data) +approve(operator, tokenId) +setApprovalForAll(operator, approved) } class IExtendedOP721Contract { +setMintEnabled(enabled) +isMintEnabled() boolean +reserve(quantity) ReserveResult +claim() ClaimResult +airdrop(addresses, amounts) +getStatus() StatusResult } IOP721Contract <|-- IExtendedOP721Contract ``` --- ## Setup ```typescript import { getContract, IOP721Contract, IExtendedOP721Contract, JSONRpcProvider, OP_721_ABI, EXTENDED_OP721_ABI, TransactionParameters, } from 'opnet'; import { Address, AddressTypes, Mnemonic, MLDSASecurityLevel, Wallet, } from '@btc-vision/transaction'; import { Network, networks } from '@btc-vision/bitcoin'; const network: Network = networks.regtest; const provider: JSONRpcProvider = new JSONRpcProvider({ url: 'https://regtest.opnet.org', network, }); const mnemonic = new Mnemonic('your seed phrase here ...', '', network, MLDSASecurityLevel.LEVEL2); const wallet: Wallet = mnemonic.deriveUnisat(AddressTypes.P2TR, 0); // OPWallet-compatible // Standard OP721 contract const nftAddress: Address = Address.fromString('0x...'); const nft: IOP721Contract = getContract<IOP721Contract>( nftAddress, OP_721_ABI, provider, network, wallet.address ); // Extended OP721 with minting features const extendedNftAddress: Address = Address.fromString('0x...'); const extendedNft: IExtendedOP721Contract = getContract<IExtendedOP721Contract>( extendedNftAddress, EXTENDED_OP721_ABI, provider, network, wallet.address ); ``` --- ## Collection Information ### Get Metadata ```typescript async function getCollectionInfo(nft: IOP721Contract) { // Get all collection metadata in one call const metadata = await nft.metadata(); console.log('Collection:', metadata.properties.name); console.log('Symbol:', metadata.properties.symbol); console.log('Icon:', metadata.properties.icon); console.log('Banner:', metadata.properties.banner); console.log('Description:', metadata.properties.description); console.log('Website:', metadata.properties.website); console.log('Total Supply:', metadata.properties.totalSupply); return metadata.properties; } // Or fetch individual properties async function getBasicInfo(nft: IOP721Contract) { const [name, symbol, maxSupply, totalSupply] = await Promise.all([ nft.name(), nft.symbol(), nft.maxSupply(), nft.totalSupply(), ]); return { name: name.properties.name, symbol: symbol.properties.symbol, maxSupply: maxSupply.properties.maxSupply, totalSupply: totalSupply.properties.totalSupply, }; } ``` ### Get Token URI ```typescript async function getTokenMetadata( nft: IOP721Contract, tokenId: bigint ): Promise<string> { const result = await nft.tokenURI(tokenId); return result.properties.uri; } // Usage const uri = await getTokenMetadata(nft, 1n); console.log('Token URI:', uri); // e.g., "ipfs://Qm..." ``` --- ## Ownership Queries ### Check Balance ```typescript async function getNFTBalance( nft: IOP721Contract, owner: Address ): Promise<bigint> { const result = await nft.balanceOf(owner); return result.properties.balance; } // Usage const balance = await getNFTBalance(nft, wallet.address); console.log('NFTs owned:', balance); ``` ### Get Token Owner ```typescript async function getTokenOwner( nft: IOP721Contract, tokenId: bigint ): Promise<Address> { const result = await nft.ownerOf(tokenId); return result.properties.owner; } // Usage const owner = await getTokenOwner(nft, 1n); console.log('Token 1 owned by:', owner.toHex()); ``` ### Enumerate Owned Tokens ```typescript async function getOwnedTokens( nft: IOP721Contract, owner: Address ): Promise<bigint[]> { const balanceResult = await nft.balanceOf(owner); const balance = balanceResult.properties.balance; const tokenIds: bigint[] = []; for (let i = 0n; i < balance; i++) { const result = await nft.tokenOfOwnerByIndex(owner, i); tokenIds.push(result.properties.tokenId); } return tokenIds; } // Usage const myTokens = await getOwnedTokens(nft, wallet.address); console.log('My tokens:', myTokens); ``` --- ## Transfer Examples ### Basic Transfer ```typescript async function transferNFT( nft: IOP721Contract, recipient: Address, tokenId: bigint, wallet: Wallet ): Promise<string> { // safeTransfer with empty data const simulation = await nft.safeTransfer( recipient, tokenId, new Uint8Array() ); // Note: The simulation call above throws if the contract reverts. // Use try/catch around the simulation call to handle reverts gracefully. const params: TransactionParameters = { signer: wallet.keypair, mldsaSigner: wallet.mldsaKeypair, refundTo: wallet.p2tr, maximumAllowedSatToSpend: 10000n, feeRate: 10, network: network, }; const receipt = await simulation.sendTransaction(params); return receipt.transactionId; } // Usage const txId = await transferNFT( nft, Address.fromString('0x...'), 1n, wallet ); console.log('Transfer TX:', txId); ``` ### Transfer with Data ```typescript async function transferWithData( nft: IOP721Contract, recipient: Address, tokenId: bigint, data: Uint8Array, wallet: Wallet ): Promise<string> { const simulation = await nft.safeTransfer(recipient, tokenId, data); const params: TransactionParameters = { signer: wallet.keypair, mldsaSigner: wallet.mldsaKeypair, refundTo: wallet.p2tr, maximumAllowedSatToSpend: 10000n, feeRate: 10, network: network, }; const receipt = await simulation.sendTransaction(params); return receipt.transactionId; } // Usage with custom data const customData = new TextEncoder().encode('Hello from OPNet!'); await transferWithData(nft, recipient, 1n, customData, wallet); ``` ### Transfer From (Operator) ```typescript async function transferFrom( nft: IOP721Contract, from: Address, to: Address, tokenId: bigint, operatorWallet: Wallet ): Promise<string> { // Create contract with operator as sender const operatorNft = getContract<IOP721Contract>( nft.address, OP_721_ABI, provider, network, operatorWallet.address ); const simulation = await operatorNft.safeTransferFrom( from, to, tokenId, new Uint8Array() ); const params: TransactionParameters = { signer: operatorWallet.keypair, mldsaSigner: operatorWallet.mldsaKeypair, refundTo: operatorWallet.p2tr, maximumAllowedSatToSpend: 10000n, feeRate: 10, network: network, }; const receipt = await simulation.sendTransaction(params); return receipt.transactionId; } ``` --- ## Approval Management ### Approve Single Token ```typescript async function approveToken( nft: IOP721Contract, operator: Address, tokenId: bigint, wallet: Wallet ): Promise<string> { const simulation = await nft.approve(operator, tokenId); const params: TransactionParameters = { signer: wallet.keypair, mldsaSigner: wallet.mldsaKeypair, refundTo: wallet.p2tr, maximumAllowedSatToSpend: 10000n, feeRate: 10, network: network, }; const receipt = await simulation.sendTransaction(params); return receipt.transactionId; } ``` ### Approve All Tokens ```typescript async function setApprovalForAll( nft: IOP721Contract, operator: Address, approved: boolean, wallet: Wallet ): Promise<string> { const simulation = await nft.setApprovalForAll(operator, approved); const params: TransactionParameters = { signer: wallet.keypair, mldsaSigner: wallet.mldsaKeypair, refundTo: wallet.p2tr, maximumAllowedSatToSpend: 10000n, feeRate: 10, network: network, }; const receipt = await simulation.sendTransaction(params); return receipt.transactionId; } // Grant approval to marketplace await setApprovalForAll(nft, marketplaceAddress, true, wallet); // Revoke approval await setApprovalForAll(nft, marketplaceAddress, false, wallet); ``` ### Check Approvals ```typescript async function checkApprovals( nft: IOP721Contract, owner: Address, operator: Address ): Promise<boolean> { const result = await nft.isApprovedForAll(owner, operator); return result.properties.approved; } // Usage const isApproved = await checkApprovals(nft, wallet.address, marketplaceAddress); console.log('Marketplace approved:', isApproved); ``` --- ## Extended NFT Features ### Check Minting Status ```typescript async function getCollectionStatus(nft: IExtendedOP721Contract) { const status = await nft.getStatus(); console.log('Status:', { minted: status.properties.minted, reserved: status.properties.reserved, available: status.properties.available, maxSupply: status.properties.maxSupply, pricePerToken: status.properties.pricePerToken, reservationFeePercent: status.properties.reservationFeePercent, }); return status.properties; } async function isMintingEnabled(nft: IExtendedOP721Contract): Promise<boolean> { const result = await nft.isMintEnabled(); return result.properties.enabled; } ``` ### Enable/Disable Minting (Owner Only) ```typescript async function setMintEnabled( nft: IExtendedOP721Contract, enabled: boolean, ownerWallet: Wallet ): Promise<string> { const simulation = await nft.setMintEnabled(enabled); const params: TransactionParameters = { signer: ownerWallet.keypair, mldsaSigner: ownerWallet.mldsaKeypair, refundTo: ownerWallet.p2tr, maximumAllowedSatToSpend: 10000n, feeRate: 5, network: network, }; const receipt = await simulation.sendTransaction(params); return receipt.transactionId; } ``` --- ## Reserve and Claim Pattern The reserve/claim pattern allows users to pay a reservation fee upfront, then claim their NFTs later. ### Calculate Reservation Fee ```typescript function calculateReservationFee( quantity: bigint, pricePerToken: bigint, reservationFeePercent: bigint, minReservationFee: bigint ): bigint { const totalCost = pricePerToken * quantity; const calculatedFee = (totalCost * reservationFeePercent) / 100n; return calculatedFee < minReservationFee ? minReservationFee : calculatedFee; } ``` ### Reserve NFTs ```typescript import { PsbtOutputExtended } from '@btc-vision/bitcoin'; import { TransactionOutputFlags } from 'opnet'; function createTreasuryOutput( treasuryAddress: string, amount: bigint, contract: IExtendedOP721Contract ): PsbtOutputExtended { const output: PsbtOutputExtended = { address: treasuryAddress, value: Number(amount), }; // Tell the contract about the payment output contract.setTransactionDetails({ inputs: [], outputs: [ { to: output.address, value: BigInt(output.value), index: 1, scriptPubKey: undefined, flags: TransactionOutputFlags.hasTo, }, ], }); return output; } async function reserveNFTs( nft: IExtendedOP721Contract, quantity: bigint, treasuryAddress: string, wallet: Wallet ): Promise<string> { // Get status to calculate fee const status = await nft.getStatus(); const reservationFee = calculateReservationFee( quantity, status.properties.pricePerToken, status.properties.reservationFeePercent, status.properties.minReservationFee ); // Create treasury output and inform contract const extraOutput = createTreasuryOutput(treasuryAddress, reservationFee, nft); // Simulate reservation const simulation = await nft.reserve(quantity); console.log('Remaining payment:', simulation.properties.remainingPayment); console.log('Reservation block:', simulation.properties.reservationBlock); const params: TransactionParameters = { signer: wallet.keypair, mldsaSigner: wallet.mldsaKeypair, refundTo: wallet.p2tr, maximumAllowedSatToSpend: 10000n, feeRate: 5, network: network, extraOutputs: [extraOutput], }; const receipt = await simulation.sendTransaction(params); return receipt.transactionId; } // Usage const txId = await reserveNFTs(extendedNft, 5n, treasuryAddress, wallet); console.log('Reservation TX:', txId); ``` ### Claim Reserved NFTs ```typescript async function claimNFTs( nft: IExtendedOP721Contract, remainingPayment: bigint, treasuryAddress: string, wallet: Wallet ): Promise<string> { // Create treasury output for remaining payment const extraOutput = createTreasuryOutput(treasuryAddress, remainingPayment, nft); // Simulate claim const simulation = await nft.claim(); const params: TransactionParameters = { signer: wallet.keypair, mldsaSigner: wallet.mldsaKeypair, refundTo: wallet.p2tr, maximumAllowedSatToSpend: 10000n, feeRate: 5, network: network, extraOutputs: [extraOutput], }; const receipt = await simulation.sendTransaction(params); // Check events for minted token IDs if (simulation.events.length > 0) { console.log('Claim events:', simulation.events); } return receipt.transactionId; } ``` --- ## Airdrop Example (Batch Distribution) Distribute NFTs to multiple recipients in a single transaction. ```typescript async function airdropNFTs( nft: IExtendedOP721Contract, recipients: Address[], quantities: number[], wallet: Wallet ): Promise<string> { if (recipients.length !== quantities.length) { throw new Error('Recipients and quantities arrays must match'); } // Simulate airdrop const simulation = await nft.airdrop(recipients, quantities); const params: TransactionParameters = { signer: wallet.keypair, mldsaSigner: wallet.mldsaKeypair, refundTo: wallet.p2tr, maximumAllowedSatToSpend: 10000n, feeRate: 5, network: network, }; const receipt = await simulation.sendTransaction(params); return receipt.transactionId; } // Usage - airdrop to multiple recipients const recipients = [ Address.fromString('0x...'), Address.fromString('0x...'), Address.fromString('0x...'), ]; const quantities = [2, 5, 3]; // 2 NFTs to first, 5 to second, 3 to third const txId = await airdropNFTs(extendedNft, recipients, quantities, wallet); console.log('Airdrop TX:', txId); ``` ### Batch Airdrop (Large Lists) For large airdrops, split into chunks: ```typescript async function batchAirdrop( nft: IExtendedOP721Contract, recipients: Address[], quantityEach: number, wallet: Wallet ): Promise<string[]> { const maxPerTx = 1000; // Maximum recipients per transaction const txIds: string[] = []; for (let i = 0; i < recipients.length; i += maxPerTx) { const chunk = recipients.slice(i, i + maxPerTx); const quantities = chunk.map(() => quantityEach); console.log(`Airdropping to ${chunk.length} recipients...`); try { const simulation = await nft.airdrop(chunk, quantities); const params: TransactionParameters = { signer: wallet.keypair, mldsaSigner: wallet.mldsaKeypair, refundTo: wallet.p2tr, maximumAllowedSatToSpend: 10000n, feeRate: 5, network: network, }; const receipt = await simulation.sendTransaction(params); txIds.push(receipt.transactionId); console.log(`Chunk ${i / maxPerTx + 1} complete: ${receipt.transactionId}`); } catch (error) { console.error(`Chunk ${i / maxPerTx + 1} failed:`, error); continue; } } return txIds; } ``` --- ## Set Token URI (Owner Only) ```typescript async function setTokenURI( nft: IExtendedOP721Contract, tokenId: bigint, uri: string, ownerWallet: Wallet ): Promise<string> { const simulation = await nft.setTokenURI(tokenId, uri); const params: TransactionParameters = { signer: ownerWallet.keypair, mldsaSigner: ownerWallet.mldsaKeypair, refundTo: ownerWallet.p2tr, maximumAllowedSatToSpend: 10000n, feeRate: 5, network: network, }; const receipt = await simulation.sendTransaction(params); return receipt.transactionId; } // Usage await setTokenURI( extendedNft, 1n, 'ipfs://QmNewMetadataHash...', ownerWallet ); ``` --- ## Burn NFT ```typescript async function burnNFT( nft: IOP721Contract, tokenId: bigint, wallet: Wallet ): Promise<string> { const simulation = await nft.burn(tokenId); const params: TransactionParameters = { signer: wallet.keypair, mldsaSigner: wallet.mldsaKeypair, refundTo: wallet.p2tr, maximumAllowedSatToSpend: 10000n, feeRate: 10, network: network, }; const receipt = await simulation.sendTransaction(params); return receipt.transactionId; } // Usage const burnTxId = await burnNFT(nft, 42n, wallet); console.log('Burned NFT, TX:', burnTxId); ``` --- ## Complete NFT Service ```typescript class NFTService { private nft: IExtendedOP721Contract; private wallet: Wallet; private network: Network; constructor( nftAddress: Address, provider: JSONRpcProvider, wallet: Wallet, network: Network ) { this.wallet = wallet; this.network = network; this.nft = getContract<IExtendedOP721Contract>( nftAddress, EXTENDED_OP721_ABI, provider, network, wallet.address ); } async getInfo() { const metadata = await this.nft.metadata(); const status = await this.nft.getStatus(); return { name: metadata.properties.name, symbol: metadata.properties.symbol, description: metadata.properties.description, totalSupply: metadata.properties.totalSupply, maxSupply: status.properties.maxSupply, available: status.properties.available, pricePerToken: status.properties.pricePerToken, }; } async getMyBalance(): Promise<bigint> { const result = await this.nft.balanceOf(this.wallet.address); return result.properties.balance; } async getMyTokens(): Promise<bigint[]> { const balance = await this.getMyBalance(); const tokens: bigint[] = []; for (let i = 0n; i < balance; i++) { const result = await this.nft.tokenOfOwnerByIndex( this.wallet.address, i ); tokens.push(result.properties.tokenId); } return tokens; } async transfer(to: Address, tokenId: bigint): Promise<string> { const simulation = await this.nft.safeTransfer( to, tokenId, new Uint8Array() ); const receipt = await simulation.sendTransaction({ signer: this.wallet.keypair, mldsaSigner: this.wallet.mldsaKeypair, refundTo: this.wallet.p2tr, maximumAllowedSatToSpend: 10000n, feeRate: 10, network: this.network, }); return receipt.transactionId; } async approveAll(operator: Address): Promise<string> { const simulation = await this.nft.setApprovalForAll(operator, true); const receipt = await simulation.sendTransaction({ signer: this.wallet.keypair, mldsaSigner: this.wallet.mldsaKeypair, refundTo: this.wallet.p2tr, maximumAllowedSatToSpend: 10000n, feeRate: 10, network: this.network, }); return receipt.transactionId; } } // Usage const nftService = new NFTService(nftAddress, provider, wallet, network); const info = await nftService.getInfo(); console.log(`${info.name}: ${info.available}/${info.maxSupply} available`); const myTokens = await nftService.getMyTokens(); console.log('My tokens:', myTokens); const txId = await nftService.transfer(recipientAddress, myTokens[0]); console.log('Transfer TX:', txId); ``` --- ## Next Steps - [Advanced Swaps](./advanced-swaps.md) - DEX interactions - [Deployment Examples](./deployment-examples.md) - Deploy contracts - [OP20 Examples](./op20-examples.md) - Fungible token operations --- [ Previous: OP20 Examples](./op20-examples.md) | [Next: Advanced Swaps ](./advanced-swaps.md)