Um caso de uso popular e prático para NFTs é gerar ingressos para eventos ao vivo. Blockchains como Ethereum podem garantir a propriedade, originador e autenticidade de um item digital, resolvendo efetivamente o problema de bilhetes falsificados. Enquanto os principais players, como a Ticketmaster, lutam para mitigar os cambistas (tentando desesperadamente controlar quem pode revender os ingressos, onde e por quanto) e a fraude de ingressos - a web3 já tem uma solução. A indústria de bilhética está pronta para a disrupção.
Neste tutorial, veremos como criar uma solução de tíquete usando ConsenSys Truffle , Infura e a API Infura NFT. Implantaremos um contrato inteligente que atua como um serviço de tíquetes e cria tíquetes como tokens não fungíveis (NFTs) ERC-20. Também examinaremos algumas arquiteturas de front-ends em potencial que podem interagir com o contrato e, juntos, funcionar como um sistema integrado de tíquetes web3 de pilha completa.
Vamos construir!
A arquitetura básica do nosso sistema visa criar um contrato inteligente que emita nossos tickets como tokens não fungíveis (NFTs). NFTs são perfeitos para o que queremos construir. Eles são tokens digitais comprovadamente exclusivos que nos permitem garantir que cada ingresso seja único e não possa ser copiado ou falsificado. Isso não apenas garante uma experiência segura de ingressos para os espectadores, mas também capacita os artistas (e organizadores de eventos) com maior controle sobre a distribuição, preços e revenda de ingressos. O uso de contratos inteligentes e NFTs permite até mesmo novos fluxos de receita, como pagamentos de royalties e compartilhamento de receita!
(Se você precisar de informações básicas sobre qualquer um desses termos, tecnologia blockchain ou web3 em geral, confira este artigo em Aprendendo a se tornar um desenvolvedor Web3 explorando a pilha Web3 ).
A primeira coisa que vamos fazer é configurar uma carteira MetaMask e adicionar a rede de teste Sepolia a ela. A MetaMask é a carteira digital autocustodial mais popular, segura e fácil de usar do mundo.
Primeiro, baixe a extensão MetaMask . Depois de instalar a extensão, o MetaMask configurará a carteira para você. No processo, você receberá uma frase secreta. Mantenha isso seguro e, sob nenhuma circunstância, você deve torná-lo público.
Depois de configurar o MetaMask, clique na guia Rede no canto superior direito. Você verá uma opção para mostrar/ocultar redes de teste.
Depois de ativar as redes de teste, você poderá ver a rede de teste Sepolia no menu suspenso. Queremos usar a rede Sepolia para que possamos implantar e testar nosso sistema sem gastar dinheiro real.
Para implantar nosso contrato inteligente e interagir com ele, precisaremos de algum ETH de teste gratuito. Você pode obter Sepolia ETH grátis na torneira Sepolia .
Depois de financiar sua carteira, você deve ver um saldo diferente de zero ao mudar para a rede de teste Sepolia no MetaMask.
Como todos os dapps Ethereum, construiremos nosso projeto usando node e npm. Caso você não os tenha instalado em sua máquina local, poderá fazê-lo aqui .
Para garantir que tudo esteja funcionando corretamente, execute o seguinte comando:
$ node -v
Se tudo correr bem, você deverá ver um número de versão para Node.
Para implantar nosso contrato na rede Sepolia, precisaremos de uma conta Infura. A Infura nos dá acesso a endpoints RPC que permitem acesso rápido, confiável e fácil ao blockchain de nossa escolha.
Inscreva-se para uma conta gratuita . Depois de criar sua conta, navegue até o painel e selecione Criar nova chave .
Para rede, escolha Web3 API e nomeie-a Ticketing System ou algo de sua escolha.
Depois de clicar em Criar , o Infura gerará uma chave de API para você e fornecerá pontos de extremidade RPC para Ethereum, Goerli, Sepolia, L2s e não EVM L1s (e seus testnets correspondentes) automaticamente.
Para este tutorial, estamos interessados apenas no endpoint Sepolia RPC. Este URL está no formato https://sepolia.infura.io/v3/←API KEY→
Vamos configurar um repositório de projeto vazio executando os seguintes comandos:
$ mkdir nft-ticketing && cd nft-ticketing $ npm init -y
Usaremos o Truffle, um ambiente de desenvolvimento de classe mundial e estrutura de teste para contratos inteligentes EVM, para criar e implantar nosso contrato inteligente de criptomoeda. Instale o Truffle executando:
$ npm install —save truffle
Agora podemos criar um projeto Trufa barebones executando o seguinte comando:
$ npx truffle init
Para verificar se tudo está funcionando corretamente, execute:
$ npx truffle test
Agora temos trufa configurada com sucesso. Em seguida, vamos instalar o pacote de contratos do OpenZeppelin . Este pacote nos dará acesso à implementação básica do ERC-721 (o padrão para tokens não fungíveis), bem como algumas funcionalidades adicionais úteis.
$ npm install @openzeppelin/contracts
Para permitir que o Truffle use nossa carteira MetaMask, assine transações e pague gasolina em nosso nome, precisaremos de outro pacote chamado hdwalletprovider
. Instale-o usando o seguinte comando:
$ npm install @truffle/hdwallet-provider
Finalmente, para manter nossas informações confidenciais de carteira seguras, usaremos o pacote dotenv
.
$ npm install dotenv
Abra o repositório do projeto em um editor de código (por exemplo: VS Code). Na pasta contracts
, crie um novo arquivo chamado NftTicketing.sol
.
Nosso contrato de emissão de bilhetes herdará todas as funcionalidades oferecidas pela implementação ERC721Enumerable
do OpenZeppelin. Isso inclui transferências, rastreamento de metadados, dados de propriedade, etc.
Vamos implementar os seguintes recursos do zero:
Adicione o código a seguir a 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); } }
Certifique-se de que o contrato está sendo compilado corretamente executando:
npx truffle compile
Nosso contrato já é bastante complexo, mas é possível adicionar alguns recursos extras conforme você achar melhor.
Por exemplo, você pode implementar um mecanismo anti-scalping em seu contrato. Os passos para isso seriam os seguintes:
Adicione o seguinte trecho abaixo do construtor do contrato:
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; } }
Por fim, modifique a função _ beforeTokenTranfer para o seguinte:
// 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 o contrato mais uma vez usando o comando Truffle acima.
Crie um novo arquivo no diretório raiz do projeto chamado .env
e adicione o seguinte conteúdo:
INFURA_API_KEY = "https://sepolia.infura.io/v3/<Your-API-Key>" MNEMONIC = "<Your-MetaMask-Secret-Recovery-Phrase>"
Em seguida, vamos adicionar informações sobre nossa carteira, o endpoint Infura RPC e a rede Sepolia ao nosso arquivo de configuração Truffle. Substitua o conteúdo de truffle.config.js
pelo seguinte:
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', } } };
Vamos agora escrever um script para implantar nosso contrato na blockchain Sepolia.
Na pasta migrations
, crie um novo arquivo chamado 1_deploy_contract.js
e adicione o seguinte código:
// 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!") };
Estamos prontos! Implante o contrato executando o seguinte comando:
truffle migrate --network sepolia
Se tudo correr bem, você deverá ver a saída (contendo o endereço do contrato) mais ou menos assim:
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
Você pode procurar o endereço do seu contrato no Sepolia etherscan e vê-lo ao vivo.
Parabéns! Você implantou o contrato com sucesso na Sepolia.
Passo 9: Interface com o contrato inteligente
Temos nosso contrato inteligente! A próxima etapa é implantar front-ends que façam interface com o contrato e permitam que qualquer pessoa chame a função mint para fazer uma doação e cunhar um tíquete para si.
Para um serviço de emissão de bilhetes totalmente funcional, você normalmente precisa dos seguintes front-ends:
Construir esses sistemas do zero está fora do escopo deste tutorial, mas deixaremos alguns recursos e dicas para você.
Para o site de criação de front-end, confira o front-end que construí no tutorial Thank You NFT como ponto de partida.
Se você verificar seu contrato no Etherscan, ele fornecerá automaticamente um portal de administração onde você poderá chamar qualquer função em seu contrato. Este é um bom primeiro passo antes de decidir criar uma solução personalizada.
Verificar se uma carteira possui um ticket de sua coleção é extremamente simples usando a função balanceOf
. Se alguém puder provar que possui uma carteira contendo um de nossos ingressos, é basicamente uma prova de que possui um ingresso. Isso pode ser obtido usando assinaturas digitais.
Mais uma dica: depois de ter seu contrato inteligente e front-end (ou mesmo antes de seu front-end estar completo e você quiser provar que tudo funciona), você pode usar a API Infura NFT para verificar se seu novo NFT existe. A API Infura NFT é uma maneira rápida de substituir muitos códigos relacionados a NFT por uma única chamada de API.
Por exemplo, as informações de que precisamos para mostrar a propriedade de nosso NFT estão facilmente disponíveis para nós por meio da API. Tudo o que precisamos fornecer é o endereço da carteira. O código ficaria mais ou menos assim:
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));
Executá-lo …
$ node <filename>.js
E você deve ver algo assim:
{ 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] }, … ] }
Neste tutorial, implantamos um serviço de tíquete NFT totalmente funcional usando Truffle , Infura e Infura NFT API .
Obviamente, não é tudo o que você precisa para interromper o Ticketmaster - mas é um começo sólido e uma ótima prova de conceito! Mesmo que você não pegue este código e inicie sua própria plataforma de tíquetes NFT, esperamos que você tenha aprendido um pouco sobre o web3 no processo.
A imagem principal deste artigo foi gerada peloAI Image Generator do HackerNoon por meio do prompt "um show de rock em um grande estádio".