A popular and practical use case for NFTs is generating tickets to live events. Blockchains such as can the ownership, originator, and authenticity of a digital item, effectively solving the problem of counterfeit tickets. While major players such as struggle to mitigate scalpers (trying desperately to control who can resell tickets, where, and for how much) and ticket fraud— already has a solution. The ticketing industry is ripe for disruption. Ethereum guarantee Ticketmaster web3 In this tutorial, we’ll look at how to create such a ticketing solution using ConsenSys , , and the Infura We’ll deploy a smart contract that acts as a ticketing service and creates tickets as ERC-20 non-fungible tokens (NFTs). We’ll also walk through a few architectures of potential frontends that could interface with the contract, and together function as an integrated, full-stack, web3 ticketing system. Truffle Infura NFT API. Let’s get building! Create an NFT ticketing system on Ethereum The basic architecture of our system is intended to create a smart contract that issues our tickets as non-fungible tokens (NFTs). NFTs are for what we want to build. They are provably unique digital tokens that allow us to ensure that every ticket is unique and cannot be copied or forged. This not only guarantees a secure ticketing experience for concertgoers, but also empowers artists (and event organizers) with greater control over ticket distribution, pricing, and resale. Using smart contracts and NFTs even allows for new revenue streams such as royalty payments and revenue sharing! perfect (If you need background info on any of these terms, blockchain technology, or web3 in general, check out this article on k). Learning to Become a Web3 Developer by Exploring the Web3 Stac Step 1: Install MetaMask The first thing we’re going to do is set up a MetaMask wallet and add the Sepolia test network to it. MetaMask is the world’s most popular, secure, and easy to use self-custodial digital wallet. First, . After you install the extension, MetaMask will set up the wallet for you. In the process, you will be given a secret phrase. download the MetaMask extension Keep that safe, and under no circumstances should you make it public. Once you’ve set up MetaMask, click on the Network tab on the top-right. You will see an option to show/hide test networks. Once you turn test networks on, you should be able to see the Sepolia test network in the drop-down menu. We want to use the Sepolia network so that we can deploy and test our system without spending any real money. Step 2: Get some test ETH In order to deploy our smart contract and interact with it, we will require some free test ETH. You can obtain free Sepolia ETH from the . Sepolia faucet Once you fund your wallet, you should see a non-zero balance when you switch to the Sepolia test network on MetaMask. Step 3: Install NPM and Node Like all Ethereum dapps, we will build our project using node and npm. In case you don't have these installed on your local machine, you can do so . here To ensure everything is working correctly, run the following command: $ node -v If all goes well, you should see a version number for Node. Step 4: Sign up for an Infura account In order to deploy our contract to the Sepolia network, we will need an Infura account. Infura gives us access to RPC endpoints which allow for fast, reliable, and easy access to the blockchain of our choice. . Once you’ve created your account, navigate to the and select . Sign up for a free account dashboard Create New Key For network, choose and name it , or something of your choosing. Web3 API Ticketing System Once you click on , Infura will generate an API key for you and give you RPC endpoints to Ethereum, Goerli, Sepolia, L2s, and non-EVM L1s (and their corresponding testnets) automatically. Create For this tutorial, we are only interested in the Sepolia RPC endpoint. This URL is of the form https://sepolia.infura.io/v3/←API KEY→ Step 5: Create a Node project and install necessary packages Let's set up an empty project repository by running the following commands: $ mkdir nft-ticketing && cd nft-ticketing $ npm init -y We will be using Truffle, a world-class development environment and testing framework for EVM smart contracts, to build and deploy our cryptocurrency smart contract. Install Truffle by running: $ npm install —save truffle We can now create a barebones Truffle project by running the following command: $ npx truffle init To check if everything works properly, run: $ npx truffle test We now have Truffle successfully configured. Let us next install the contracts package. This package will give us access to the ERC-721 base implementation (the standard for non-fungible tokens) as well as a few helpful additional functionalities. OpenZeppelin $ npm install @openzeppelin/contracts To allow Truffle to use our MetaMask wallet, sign transactions, and pay gas on our behalf, we will require another package called . Install it by using the following command: hdwalletprovider $ npm install @truffle/hdwallet-provider Finally, in order to keep our sensitive wallet information safe, we will use the package. dotenv $ npm install dotenv Step 6: Create the ticketing smart contract for the NFT Open the project repository in a code editor (for example: VS Code). In the folder, create a new file called . contracts NftTicketing.sol Our ticketing contract will inherit all functionality offered by the implementation of OpenZeppelin. This includes transfers, metadata tracking, ownership data, etc. ERC721Enumerable We will implement the following features from scratch: : Our contract will give its owner the power to sell tickets at a particular price. The owner will have the power to open and close sales, update ticket prices, and withdraw any money sent to the contract for ticket purchases. The public will have the opportunity to mint tickets at sale price whenever the sale is open and tickets are still in supply. Public Primary Sale : The owner will be able to airdrop tickets to a list of wallet addresses. Airdropping : The owner will also be able to reserve tickets for himself/herself without having to pay the public sale price. Reservation Add the following code to . NftTicketing.sol //SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; import "@openzeppelin/contracts/utils/Base64.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; contract NftTicketing is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable { using Counters for Counters.Counter; Counters.Counter private _tokenIds; // Total number of tickets available for the event uint public constant MAX_SUPPLY = 10000; // Number of tickets you can book at a time; prevents spamming uint public constant MAX_PER_MINT = 5; string public baseTokenURI; // Price of a single ticket uint public price = 0.05 ether; // Flag to turn sales on and off bool public saleIsActive = false; // Give collection a name and a ticker constructor() ERC721("My NFT Tickets", "MNT") {} // Generate NFT metadata function generateMetadata(uint tokenId) public pure returns (string memory) { string memory svg = string(abi.encodePacked( "<svg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='xMinyMin meet' viewBox='0 0 350 350'>", "<style>.base { fill: white; font-family: serif; font-size: 25px; }</style>", "<rect width='100%' height='100%' fill='red' />", "<text x='50%' y='40%' class='base' dominant-baseline='middle' text-anchor='middle'>", "<tspan y='50%' x='50%'>NFT Ticket #", Strings.toString(tokenId), "</tspan></text></svg>" )); string memory json = Base64.encode( bytes( string( abi.encodePacked( '{"name": "NFT Ticket #', Strings.toString(tokenId), '", "description": "A ticket that gives you access to a cool event!", "image": "data:image/svg+xml;base64,', Base64.encode(bytes(svg)), '", "attributes": [{"trait_type": "Type", "value": "Base Ticket"}]}' ) ) ) ); string memory metadata = string( abi.encodePacked("data:application/json;base64,", json) ); return metadata; } // Reserve tickets to creator wallet function reserveNfts(uint _count) public onlyOwner { uint nextId = _tokenIds.current(); require(nextId + _count < MAX_SUPPLY, "Not enough NFTs left to reserve"); for (uint i = 0; i < _count; i++) { string memory metadata = generateMetadata(nextId + i); _mintSingleNft(msg.sender, metadata); } } // Airdrop NFTs function airDropNfts(address[] calldata _wAddresses) public onlyOwner { uint nextId = _tokenIds.current(); uint count = _wAddresses.length; require(nextId + count < MAX_SUPPLY, "Not enough NFTs left to reserve"); for (uint i = 0; i < count; i++) { string memory metadata = generateMetadata(nextId + i); _mintSingleNft(_wAddresses[i], metadata); } } // Set Sale state function setSaleState(bool _activeState) public onlyOwner { saleIsActive = _activeState; } // Allow public to mint NFTs function mintNfts(uint _count) public payable { uint nextId = _tokenIds.current(); require(nextId + _count < MAX_SUPPLY, "Not enough NFT tickets left!"); require(_count > 0 && _count <= MAX_PER_MINT, "Cannot mint specified number of NFT tickets."); require(saleIsActive, "Sale is not currently active!"); require(msg.value >= price * _count, "Not enough ether to purchase NFTs."); for (uint i = 0; i < _count; i++) { string memory metadata = generateMetadata(nextId + i); _mintSingleNft(msg.sender, metadata); } } // Mint a single NFT ticket function _mintSingleNft(address _wAddress, string memory _tokenURI) private { // Sanity check for absolute worst case scenario require(totalSupply() == _tokenIds.current(), "Indexing has broken down!"); uint newTokenID = _tokenIds.current(); _safeMint(_wAddress, newTokenID); _setTokenURI(newTokenID, _tokenURI); _tokenIds.increment(); } // Update price function updatePrice(uint _newPrice) public onlyOwner { price = _newPrice; } // Withdraw ether function withdraw() public payable onlyOwner { uint balance = address(this).balance; require(balance > 0, "No ether left to withdraw"); (bool success, ) = (msg.sender).call{value: balance}(""); require(success, "Transfer failed."); } // Get tokens of an owner function tokensOfOwner(address _owner) external view returns (uint[] memory) { uint tokenCount = balanceOf(_owner); uint[] memory tokensId = new uint256[](tokenCount); for (uint i = 0; i < tokenCount; i++) { tokensId[i] = tokenOfOwnerByIndex(_owner, i); } return tokensId; } // The following functions are overrides required by Solidity. function _beforeTokenTransfer(address from, address to, uint256 tokenId, uint256 batchSize) internal override(ERC721, ERC721Enumerable) { super._beforeTokenTransfer(from, to, tokenId, batchSize); } function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) { super._burn(tokenId); } function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) { return super.tokenURI(tokenId); } function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool) { return super.supportsInterface(interfaceId); } } Make sure the contract is compiling correctly by running: npx truffle compile Our contract is pretty complex already, but it is possible to add some extra features as you see fit. For example, you can implement an anti-scalping mechanism within your contract. The steps to do so would be as follows: Define a Solidity mapping that acts as an allowlist for wallets that can hold more than one ticket. Create a function that allows the owner to add addresses to this allowlist. Introduce a check in _ that allows mint or transfer to a wallet already holding a ticket only if it is in the allowlist. beforeTokenTransfer Add the following snippet below the contract’s constructor: mapping(address => bool) canMintMultiple; // Function that allowlists addresses to hold multiple NFTs. function addToAllowlist(address[] calldata _wAddresses) public onlyOwner { for (uint i = 0; i < _wAddresses.length; i++) { canMintMultiple[_wAddresses[i]] = true; } } Finally, modify the _ function to the following: beforeTokenTranfer // The following functions are overrides required by Solidity. function _beforeTokenTransfer(address from, address to, uint256 tokenId, uint256 batchSize) internal override(ERC721, ERC721Enumerable) { if (balanceOf(to) > 0) { require(to == owner() || canMintMultiple[to], "Not authorized to hold more than one ticket"); } super._beforeTokenTransfer(from, to, tokenId, batchSize); } Compile the contract once again using the Truffle command above. Step 7: Update Truffle config and create a .env file Create a new file in the project’s root directory called and add the following contents: .env INFURA_API_KEY = "https://sepolia.infura.io/v3/<Your-API-Key>" MNEMONIC = "<Your-MetaMask-Secret-Recovery-Phrase>" Next, let’s add information about our wallet, the Infura RPC endpoint, and the Sepolia network to our Truffle config file. Replace the contents of with the following: truffle.config.js require('dotenv').config(); const HDWalletProvider = require('@truffle/hdwallet-provider'); const { INFURA_API_KEY, MNEMONIC } = process.env; module.exports = { networks: { development: { host: "127.0.0.1", port: 8545, network_id: "*" }, sepolia: { provider: () => new HDWalletProvider(MNEMONIC, INFURA_API_KEY), network_id: '5', } } }; Step 8: Deploy the NFT Smart Contract Let us now write a script to deploy our contract to the Sepolia blockchain. In the folder, create a new file called and add the following code: migrations 1_deploy_contract.js // Get instance of the NFT contract const nftContract = artifacts.require("NftTicketing"); module.exports = async function (deployer) { // Deploy the contract await deployer.deploy(nftContract); const contract = await nftContract.deployed(); // Mint 5 tickets await contract.reserveNfts(5); console.log("5 NFT Tickets have been minted!") }; We’re all set! Deploy the contract by running the following command: truffle migrate --network sepolia If all goes well, you should see output (containing the contract address) that looks something like this: Starting migrations... ====================== > Network name: 'sepolia' > Network id: 5 > Block gas limit: 30000000 (0x1c9c380) 1_deploy_contract.js ==================== Deploying 'NftTicketing' ----------------------- > transaction hash: … > Blocks: 2 Seconds: 23 … > Saving artifacts ------------------------------------- > Total cost: 0.1201 ETH Summary ======= > Total deployments: 1 > Final cost: 0.1201 ETH You can search for your contract address on Sepolia etherscan and see it live. Congratulations! You’ve successfully deployed the contract to Sepolia. Step 9: Interface with the smart contract We have our smart contract! The next step is to deploy frontends that interface with the contract and allow anyone to call the mint function to make a donation and mint a ticket for themselves. For a fully functional ticketing service, you would typically need the following frontends: A website (with a great user experience) where public users can pay and mint their tickets. An admin portal where the owner can reserve and airdrop tickets, update pricing, transfer admin role to another wallet, withdraw sales revenue, open and close sale, etc. A tool which verifies that a person has a particular ticket both online and IRL. Building these systems from scratch is out of scope for this tutorial, but we will leave you with a few resources and tips. For the frontend minting website, check out the frontend I built in the tutorial as a starting point. Thank You NFT If you verify your contract on Etherscan, it will automatically give you an admin portal where you can call any function on your contract. This is a good first step before you decide on building a custom solution. Verifying that a wallet has a ticket from your collection is extremely simple using the function. If someone can prove that they own a wallet containing one of our tickets, it’s basically proof that they have a ticket. This can be achieved using digital signatures. balanceOf Verification using the Infura NFT API One more hint: once you have your smart contract and frontend (or even before your frontend is complete and you want to prove out that everything works), you can use the to verify that your new NFT exists. The Infura NFT API is a quick way to replace a lot of NFT-related code with a single API call. Infura NFT API For example, the information we need to show ownership of our NFT is easily available to us through the API. All we need to supply is the wallet address. The code would look something like this: const walletAddress = <your wallet address> const chainId = "1" const baseUrl = "https://nft.api.infura.io" const url = `${baseUrl}/networks/${chainId}/accounts/${walletAddress}/assets/nfts` // API request const config = { method: 'get', url: url, auth: { username: '<-- INFURA_API_KEY –>', password: '<-- INFURA_API_SECRET –>', } }; // API Request axios(config) .then(response => { console.log(response['data']) }) .catch(error => console.log('error', error)); Run it … $ node <filename>.js And you should see something like this: { total: 1, pageNumber: 1, pageSize: 100, network: 'ETHEREUM', account: <account address>, cursor: null, assets: [ { contract: <NFT contract address>, tokenId: '0', supply: '1', type: 'ERC20', metadata: [Object] }, … ] } Conclusion In this tutorial, we deployed a fully functional NFT ticketing service using , , and the Infura . Truffle Infura NFT API It’s obviously not you would need to disrupt Ticketmaster—but it’s a solid start and a great proof of concept! Even if you don’t take this code and start your own NFT ticketing platform, hopefully you’ve learned a little about web3 in the process. everything The lead image for this article was generated by HackerNoon's via the prompt "a rock concert in a big stadium". AI Image Generator