Build a Web3 Ticketing System with NFTs and Disrupt Ticketmaster
Tags: web3, tutorial, nft, webdev
Published: 2023-04-21 11:49:28

### Introduction

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](https://trufflesuite.com/), [Infura](https://www.infura.io/), and the Infura [NFT API.](https://www.infura.io/platform/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!


### 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 _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](https://dev.to/mbogan/learn-to-become-a-web3-developer-by-exploring-the-web3-stack-o84)).


#### **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, [download the MetaMask extension](https://metamask.io/download/). 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.


#### **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](https://www.infura.io/faucet).

Once you fund your wallet, you should see a non-zero balance when you switch to the Sepolia test network on MetaMask.


![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x7ddj152imi7itm76fmq.png)

#### **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](https://nodejs.org/en/download/).

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.

[Sign up for a free account](https://app.infura.io/register). Once you’ve created your account, navigate to the [dashboard](https://app.infura.io/dashboard) and select _Create New Key_.


![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j03krof6jd2xyoh2jsec.png)

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→](https://mainnet.infura.io/v3/←API)


#### **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 [OpenZeppelin](https://www.openzeppelin.com/) 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
```



#### **Step 6: Create the ticketing smart contract for the NFT**

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:



1. **Public Primary Sale**: 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.
2. **Airdropping**: The owner will be able to airdrop tickets to a list of wallet addresses.
3. **Reservation**: The owner will also be able to reserve tickets for himself/herself without having to pay the public sale price.

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:



1. Define a Solidity mapping that acts as an allowlist for wallets that can hold more than one ticket.
2. Create a function that allows the owner to add addresses to this allowlist.
3. Introduce a check in __beforeTokenTransfer_ that allows mint or transfer to a wallet already holding a ticket only if it is in the allowlist.

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.


#### **Step 7: Update Truffle config and create a .env file**

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',
    }
  }
};
```



#### **Step 8: Deploy the NFT Smart Contract**

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:



1. A website (with a great user experience) where public users can pay and mint their tickets.
2. 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.
3. 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.



1. For the frontend minting website, check out the frontend I built in the [Thank You NFT](https://dev.to/mbogan/empowering-digital-creators-a-technical-guide-on-how-to-create-an-nft-digital-collectible-to-say-thank-you-cd) tutorial as a starting point.
2. 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.
3. 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.

**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 [Infura NFT API](https://www.infura.io/use-cases/nft) 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]
    },
   …
  ]
}
```

### **Conclusion**

In this tutorial, we deployed a fully functional NFT ticketing service using [Truffle](https://trufflesuite.com/), [Infura](https://www.infura.io/), and the Infura [NFT API](https://www.infura.io/platform/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.