NFT Collection Example¶
Deploy an NFT collection across multiple chains with synchronized metadata.
Overview¶
This example demonstrates:
- ERC721 NFT deployment across chains
- Synchronized metadata and ownership
- Cross-chain minting
- Marketplace integration
Quick Start¶
# Create project
switchboard init my-nft --template nft --dev-mode
cd my-nft
npm install
# Configure and deploy
cp .env.example .env
switchboard deploy --dev-mode
Smart Contract¶
// contracts/evm/MyNFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract MyNFT is ERC721, ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIdCounter;
uint256 public maxSupply;
uint256 public mintPrice;
string public baseURI;
event NFTMinted(address indexed to, uint256 indexed tokenId);
constructor(
string memory name,
string memory symbol,
uint256 _maxSupply,
uint256 _mintPrice,
string memory _baseURI
) ERC721(name, symbol) Ownable(msg.sender) {
maxSupply = _maxSupply;
mintPrice = _mintPrice;
baseURI = _baseURI;
}
function mint(address to) public payable returns (uint256) {
require(_tokenIdCounter.current() < maxSupply, "Max supply reached");
require(msg.value >= mintPrice, "Insufficient payment");
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(to, tokenId);
_setTokenURI(tokenId, string(abi.encodePacked(baseURI, toString(tokenId), ".json")));
emit NFTMinted(to, tokenId);
return tokenId;
}
function totalSupply() public view returns (uint256) {
return _tokenIdCounter.current();
}
function withdraw() public onlyOwner {
payable(owner()).transfer(address(this).balance);
}
// Override required functions
function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
return super.tokenURI(tokenId);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721URIStorage)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
function toString(uint256 value) internal pure returns (string memory) {
if (value == 0) return "0";
uint256 temp = value;
uint256 digits;
while (temp != 0) {
digits++;
temp /= 10;
}
bytes memory buffer = new bytes(digits);
while (value != 0) {
digits -= 1;
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
value /= 10;
}
return string(buffer);
}
}
Deployment Script¶
// scripts/deploy-nft.js
import { Switchboard } from '@switchboard/sdk';
import { readFileSync } from 'fs';
async function main() {
const switchboard = new Switchboard({
solana: { rpcUrl: process.env.SOLANA_RPC_URL },
networks: {
sepolia: {
rpcUrl: process.env.ETHEREUM_RPC_URL,
privateKey: process.env.PRIVATE_KEY,
},
mumbai: {
rpcUrl: process.env.POLYGON_RPC_URL,
privateKey: process.env.PRIVATE_KEY,
},
},
});
const artifact = JSON.parse(
readFileSync('./artifacts/MyNFT.json', 'utf-8')
);
const deployment = await switchboard.deployContract({
name: 'MyNFT',
bytecode: artifact.bytecode,
abi: artifact.abi,
constructorArgs: [
'My NFT Collection', // name
'MNFT', // symbol
10000, // maxSupply
ethers.parseEther('0.01'), // mintPrice
'ipfs://QmYourHash/', // baseURI
],
chains: ['sepolia', 'mumbai'],
options: {
verify: true,
},
});
console.log('Deployment ID:', deployment.id);
// Wait for completion
let status = await switchboard.trackDeployment(deployment.id);
while (status.status === 'pending' || status.status === 'deploying') {
await new Promise((r) => setTimeout(r, 5000));
status = await switchboard.trackDeployment(deployment.id);
}
console.log('NFT Collection Deployed!');
console.log('Addresses:', status.addresses);
}
main().catch(console.error);
Minting Script¶
// scripts/mint.js
import { ethers } from 'ethers';
async function mint(chain, contractAddress, to) {
const rpcUrls = {
sepolia: process.env.ETHEREUM_RPC_URL,
mumbai: process.env.POLYGON_RPC_URL,
};
const provider = new ethers.JsonRpcProvider(rpcUrls[chain]);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
const abi = ['function mint(address) payable returns (uint256)'];
const contract = new ethers.Contract(contractAddress, abi, wallet);
const tx = await contract.mint(to, {
value: ethers.parseEther('0.01'),
});
const receipt = await tx.wait();
console.log(`Minted on ${chain}: ${receipt.hash}`);
// Get token ID from event
const event = receipt.logs.find(
(log) => log.topics[0] === ethers.id('NFTMinted(address,uint256)')
);
if (event) {
const tokenId = ethers.toBigInt(event.topics[2]);
console.log(`Token ID: ${tokenId}`);
return tokenId;
}
}
// Mint on multiple chains
async function mintMultiChain(addresses, to) {
await Promise.all([
mint('sepolia', addresses.sepolia, to),
mint('mumbai', addresses.mumbai, to),
]);
}
Metadata Synchronization¶
Monitor NFT state across chains:
// scripts/sync-metadata.js
import { Switchboard } from '@switchboard/sdk';
import { ethers } from 'ethers';
async function checkOwnership(chain, contractAddress, tokenId) {
const provider = new ethers.JsonRpcProvider(getRpcUrl(chain));
const abi = ['function ownerOf(uint256) view returns (address)'];
const contract = new ethers.Contract(contractAddress, abi, provider);
try {
return await contract.ownerOf(tokenId);
} catch {
return null; // Token doesn't exist on this chain
}
}
async function syncCheck(addresses, tokenId) {
const owners = {};
for (const [chain, address] of Object.entries(addresses)) {
owners[chain] = await checkOwnership(chain, address, tokenId);
}
console.log(`Token ${tokenId} ownership:`, owners);
// Verify consistency
const uniqueOwners = [...new Set(Object.values(owners).filter(Boolean))];
if (uniqueOwners.length > 1) {
console.warn('Ownership mismatch detected!');
}
}
IPFS Metadata¶
Store metadata on IPFS:
// scripts/upload-metadata.js
import { create } from 'ipfs-http-client';
async function uploadMetadata(tokenId, metadata) {
const ipfs = create({ url: 'https://ipfs.infura.io:5001' });
const result = await ipfs.add(JSON.stringify({
name: `My NFT #${tokenId}`,
description: 'A unique cross-chain NFT',
image: `ipfs://${metadata.imageHash}`,
attributes: metadata.attributes,
}));
return result.cid.toString();
}
// Example metadata
const metadata = {
imageHash: 'QmImageHash...',
attributes: [
{ trait_type: 'Background', value: 'Blue' },
{ trait_type: 'Rarity', value: 'Rare' },
],
};
Testing¶
// tests/nft.test.js
import { expect } from 'chai';
import { ethers } from 'hardhat';
describe('MyNFT', () => {
let nft;
beforeEach(async () => {
const MyNFT = await ethers.getContractFactory('MyNFT');
nft = await MyNFT.deploy(
'Test NFT',
'TNFT',
100,
ethers.parseEther('0.01'),
'ipfs://test/'
);
});
it('should mint NFT', async () => {
const [owner] = await ethers.getSigners();
const tx = await nft.mint(owner.address, {
value: ethers.parseEther('0.01'),
});
await tx.wait();
expect(await nft.ownerOf(0)).to.equal(owner.address);
expect(await nft.totalSupply()).to.equal(1);
});
it('should respect max supply', async () => {
// Mint up to max supply and verify it fails after
});
});
Marketplace Integration¶
Integrate with OpenSea and other marketplaces:
// Set OpenSea-compatible metadata
const contractMetadata = {
name: 'My NFT Collection',
description: 'Cross-chain NFT collection',
image: 'ipfs://collection-image',
external_link: 'https://mynft.com',
seller_fee_basis_points: 250, // 2.5% royalty
fee_recipient: '0x...',
};
Next Steps¶
- Token Example - Simple token deployment
- DeFi Example - Cross-chain DeFi
- SDK Reference - Full API documentation