A popular and practical use case for NFTs is generating tickets to live events. Blockchains such as Ethereum can guarantee the ownership, originator, and authenticity of a digital item, effectively solving the problem of counterfeit tickets. While major players such as Ticketmaster struggle to mitigate scalpers (trying desperately to control who can resell tickets, where, and for how much) and ticket fraud—web3 already has a solution. The ticketing industry is ripe for disruption.
In this tutorial, we’ll look at how to create such a ticketing solution using ConsenSys Truffle, Infura, and the Infura NFT API. 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.
Let’s get building!
The basic architecture of our system is intended to create a smart contract that issues our tickets as non-fungible tokens (NFTs). NFTs are perfect 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!
(If you need background info on any of these terms, blockchain technology, or web3 in general, check out this article on Learning to Become a Web3 Developer by Exploring the Web3 Stack).
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, download the MetaMask extension. After you install the extension, MetaMask will set up the wallet for you. In the process, you will be given a secret phrase. 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.
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.
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.
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.
Sign up for a free account. Once you’ve created your account, navigate to the dashboard and select Create New Key.
For network, choose Web3 API and name it Ticketing System, or something of your choosing.
Once you click on Create, 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.
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→
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 OpenZeppelin 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.
$ 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 hdwalletprovider
. Install it by using the following command:
$ npm install @truffle/hdwallet-provider
Finally, in order to keep our sensitive wallet information safe, we will use the dotenv
package.
$ npm install dotenv
Open the project repository in a code editor (for example: VS Code). In the contracts
folder, create a new file called NftTicketing.sol
.
Our ticketing contract will inherit all functionality offered by the ERC721Enumerable
implementation of OpenZeppelin. This includes transfers, metadata tracking, ownership data, etc.
We will implement the following features from scratch:
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:
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 _beforeTokenTranfer function to the following:
// 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.
Create a new file in the project’s root directory called .env
and add the following contents:
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 truffle.config.js
with the following:
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',
}
}
};
Let us now write a script to deploy our contract to the Sepolia blockchain.
In the migrations
folder, create a new file called 1_deploy_contract.js
and add the following code:
// 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:
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 Thank You NFT tutorial as a starting point.
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 balanceOf
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.
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 Infura NFT API 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.
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]
},
…
]
}
In this tutorial, we deployed a fully functional NFT ticketing service using Truffle, Infura, and the Infura NFT API.
It’s obviously not everything 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.
The lead image for this article was generated by HackerNoon's AI Image Generator via the prompt "a rock concert in a big stadium".