Оглавление
Введение
Популярным и практичным примером использования НФТ является генерирование билетов на живые мероприятия. Блокчейн, такой как Ethereum, может гарантировать право собственности, создателя и подлинность цифрового товара, эффективно решая проблему поддельных билетов. Пока крупные игроки, такие как Ticketmaster, борются со скальперами (отчаянно пытающимися контролировать, кто, где и за сколько может перепродавать билеты) и мошенничеством с билетами, у Web3 уже есть решение. Индустрия продажи билетов созрела для разрушения.
В этом руководстве мы рассмотрим, как создать такое решение для продажи билетов с помощью ConsenSys Truffle, Infura и Infura NFT API. Мы развернем смарт-контракт, который действует как служба продажи билетов и создает билеты в виде неиграбельных токенов ERC-20 (NFT). Мы также рассмотрим несколько архитектур потенциальных фронтендов, которые могут взаимодействовать с контрактом и вместе функционировать как интегрированная, полнофункциональная, web3 система продажи билетов.
Давайте приступим к созданию!
Создание системы продажи билетов NFT на Ethereum
Базовая архитектура нашей системы предполагает создание смарт-контракта, который выпускает наши билеты в виде неиграбельных токенов (NFT). NFT идеально подходят для того, что мы хотим создать. Это доказательно уникальные цифровые токены, которые позволяют нам гарантировать, что каждый билет уникален и не может быть скопирован или подделан. Это не только гарантирует безопасность билетов для зрителей, но и дает артистам (и организаторам мероприятий) больше возможностей для контроля над распространением, ценообразованием и перепродажей билетов. Использование смарт-контрактов и НФТ даже позволяет создать новые источники дохода, такие как роялти и разделение доходов!
(Если вам нужна справочная информация о любом из этих терминов, технологии blockchain или web3 в целом, ознакомьтесь с этой статьей о том, как стать Web3-разработчиком, изучив стек Web3).
Шаг 1: Установите MetaMask
Первое, что мы собираемся сделать, это установить кошелек MetaMask и добавить в него тестовую сеть Sepolia. MetaMask - это самый популярный в мире, безопасный и простой в использовании цифровой кошелек для самостоятельного хранения средств.
Сначала загрузите расширение MetaMask. После установки расширения MetaMask настроит для вас кошелек. В процессе вам будет предоставлена секретная фраза. Храните ее в безопасности и ни в коем случае не разглашайте.
После того как вы настроите MetaMask, нажмите на вкладку Сеть в правом верхнем углу. Вы увидите опцию показать/скрыть тестовые сети.
Когда вы включите опцию тестовых сетей, вы сможете увидеть тестовую сеть Sepolia в выпадающем меню. Мы хотим использовать сеть Sepolia, чтобы мы могли развернуть и протестировать нашу систему, не тратя реальных денег.
Шаг 2: Получите несколько тестовых ETH
Для того чтобы развернуть наш смарт-контракт и взаимодействовать с ним, нам потребуется немного бесплатных тестовых ETH. Вы можете получить бесплатные ETH Sepolia из крана Sepolia.
Как только вы пополните свой кошелек, вы должны увидеть ненулевой баланс при переключении на тестовую сеть Sepolia на MetaMask.

Шаг 3: Установите NPM и Node
Как и все Ethereum dapps, мы будем собирать наш проект с помощью node и npm. Если они не установлены на вашей локальной машине, вы можете сделать это здесь.
Чтобы убедиться, что все работает правильно, выполните следующую команду:
$ node -v
Если все прошло успешно, вы должны увидеть номер версии Node.
Шаг 4: Зарегистрируйте учетную запись Infura
Для того чтобы развернуть наш контракт в сети Sepolia, нам понадобится учетная запись Infura. Infura предоставляет нам доступ к конечным точкам RPC, которые обеспечивают быстрый, надежный и простой доступ к выбранному нами блокчейну.
Зарегистрируйтесь для получения бесплатной учетной записи. После создания учетной записи перейдите на приборную панель и выберите ”Создать новый ключ”.

Для сети выберите Web3 API и назовите ее Ticketing System или как-нибудь по своему усмотрению.
После того как вы нажмете кнопку Create, Infura сгенерирует для вас ключ API и автоматически предоставит вам конечные точки RPC для Ethereum, Goerli, Sepolia, L2 и не-EVM L1 (и соответствующих им тестовых сетей).
В данном руководстве нас интересует только конечная точка RPC Sepolia. Этот URL имеет вид https://sepolia.infura.io/v3/←API KEY→.
Шаг 5: Создайте проект Node и установите необходимые пакеты
Давайте создадим пустой репозиторий проекта, выполнив следующие команды:
$ mkdir nft-ticketing && cd nft-ticketing
$ npm init -y
Для создания и развертывания нашего криптовалютного смарт-контракта мы будем использовать Truffle, среду разработки мирового класса и механизм тестирования смарт-контрактов EVM. Установите Truffle, выполнив:
$ npm install —save truffle
Теперь мы можем создать пустой проект Truffle, выполнив следующую команду:
$ npx truffle init
Чтобы проверить, все ли работает правильно, выполните
$ npx truffle test
Теперь мы успешно настроили Truffle. Далее установим пакет контрактов OpenZeppelin. Этот пакет даст нам доступ к базовой реализации ERC-721 (стандарт для неиграбельных токенов), а также к нескольким полезным дополнительным функциям.
$ npm install @openzeppelin/contracts
Чтобы позволить Truffle использовать наш кошелек MetaMask, подписывать транзакции и оплачивать газ от нашего имени, нам потребуется еще один пакет под названием hdwalletprovider. Установите его с помощью следующей команды:
$ npm install @truffle/hdwallet-provider
Наконец, чтобы сохранить конфиденциальную информацию о нашем кошельке в безопасности, мы будем использовать пакет dotenv.
$ npm install dotenv
Шаг 6: Создайте смарт-контракт тикетинга для NFT
Откройте репозиторий проекта в редакторе кода (например: VS Code). В папке contracts создайте новый файл под названием NftTicketing.sol.
Наш билетный контракт будет наследовать все функциональные возможности, предлагаемые ERC721Enumerable реализацией OpenZeppelin. Сюда входят переводы, отслеживание метаданных, данные о владельцах и т.д.
Мы реализуем следующие функции с нуля:
- Публичная первичная продажа: Наш контракт даст своему владельцу право продавать билеты по определенной цене. Владелец будет иметь право открывать и закрывать продажи, обновлять цены на билеты и снимать деньги, отправленные на контракт для покупки билетов. У публики будет возможность купить билеты по цене продажи, когда продажа открыта и билеты еще есть в наличии.
- Аэродроппинг: Владелец сможет рассылать билеты по списку адресов кошельков.
- Резервирование: Владелец также сможет резервировать билеты для себя без необходимости платить цену публичной продажи.
Добавьте следующий код в файл 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);
}
}
Убедитесь, что контракт компилируется правильно, выполнив команду:
npx truffle compile
Наш контракт уже довольно сложный, но можно добавить некоторые дополнительные функции по вашему усмотрению.
Например, вы можете реализовать механизм защиты от скальпирования в вашем контракте. Это можно сделать следующим образом:
- Определите связку Solidity, которая действует как список разрешений для кошельков, на которых может храниться более одного билета.
- Создайте функцию, позволяющую владельцу добавлять адреса в этот список.
- Ввести проверку в _beforeTokenTransfer, которая разрешает майнить или переводить деньги на кошелек, уже имеющий билет, только если он находится в списке разрешенных адресов.
Добавьте следующий фрагмент ниже конструктора контракта:
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;
}
}
Наконец, измените функцию _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);
}
Скомпилируйте контракт еще раз, используя приведенную выше команду Truffle.
Шаг 7: Обновление конфигурации Truffle и создание файла .env
Создайте новый файл в корневом каталоге проекта под названием .env и добавьте в него следующее содержимое:
INFURA_API_KEY = "https://sepolia.infura.io/v3/<Your-API-Key>"
MNEMONIC = "<Your-MetaMask-Secret-Recovery-Phrase>"
Далее добавим информацию о нашем кошельке, конечной точке RPC Infura и сети Sepolia в файл конфигурации Truffle. Замените содержимое файла 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',
}
}
};
Шаг 8: Развертывание смарт-контракта NFT
Теперь давайте напишем скрипт для развертывания нашего контракта на блокчейне Sepolia.
В папке 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!")
};
Все готово! Разверните контракт, выполнив следующую команду:
truffle migrate --network sepolia
Если все прошло успешно, вы должны увидеть результат (содержащий адрес контракта), который выглядит примерно так:
Starting migrations…
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
Вы можете найти адрес вашего контракта на сайте Sepolia etherscan и увидеть его в реальном времени.
Поздравляем! Вы успешно развернули контракт в Sepolia.
Шаг 9: Взаимодействие с умным контрактом
У нас есть наш смарт-контракт! Следующим шагом будет развертывание фронтендов, которые будут взаимодействовать с контрактом и позволят любому человеку вызвать функцию mint, чтобы сделать пожертвование и отчеканить билет для себя.
Для полнофункциональной службы продажи билетов вам, как правило, понадобятся следующие фронтенды:
- Веб-сайт (с отличным пользовательским интерфейсом), где публичные пользователи могут оплачивать и чеканить свои билеты.
- Портал администратора, где владелец может резервировать билеты, обновлять цены, передавать роль администратора на другой кошелек, снимать доходы от продаж, открывать и закрывать продажи и т.д.
- Инструмент, который проверяет наличие у человека конкретного билета как онлайн, так и IRL.
Создание этих систем с нуля не входит в задачи данного руководства, но мы оставим вам несколько ресурсов и советов.
- В качестве отправной точки для сайта майнинга посмотрите фронтенд, который я создал в руководстве "Спасибо NFT".
- Если вы верифицируете свой контракт на Etherscan, он автоматически предоставит вам административный портал, где вы сможете вызвать любую функцию на вашем контракте. Это хороший первый шаг, прежде чем вы решите создать собственное решение.
- Проверить, что в кошельке есть билет из вашей коллекции, очень просто с помощью функции balanceOf. Если кто-то может доказать, что он владеет кошельком, содержащим один из наших билетов, то это, по сути, является доказательством того, что у него есть билет. Этого можно достичь с помощью цифровых подписей.
Верификация с помощью API Infura NFT
Еще один совет: как только у вас есть смарт-контракт и фронтенд (или даже до того, как фронтенд завершен, и вы хотите доказать, что все работает), вы можете использовать Infura NFT API для проверки того, что ваш новый NFT существует. Infura NFT API - это быстрый способ заменить множество кода, связанного с NFT, одним вызовом API.
Например, информация, необходимая для подтверждения права собственности на наш NFT, легко доступна нам через API. Все, что нам нужно, это адрес кошелька. Код будет выглядеть следующим образом:
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
И вы должны увидеть что-то вроде этого:
{
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]
},
…
]
}
Заключение
В этом руководстве мы развернули полностью функциональный сервис продажи билетов NFT, используя Truffle, Infura и Infura NFT API. Это, конечно, не все, что вам понадобится для того, чтобы разорить Ticketmaster, но это надежное начало и отличное доказательство концепции! Даже если вы не возьмете этот код и не создадите свою собственную платформу для продажи билетов NFT, надеюсь, вы узнали немного о web3 в процессе.