Давайте нарушим работу Ticketmaster, создав систему продажи билетов Web3

Давайте нарушим работу Ticketmaster, создав систему продажи билетов Web3

30 апреля 2023 г.

Популярным и практичным вариантом использования NFT является создание билетов на прямые трансляции. Блокчейны, такие как Ethereum, могут гарантировать право собственности, автора и подлинность цифрового элемента, эффективно решая Проблема поддельных билетов. В то время как крупные игроки, такие как Ticketmaster, изо всех сил пытаются смягчить последствия спекуляций (отчаянно пытаясь контролировать, кто может перепродавать билеты, где и за сколько) и мошенничество — у web3 уже есть решение. Билетная индустрия созрела для потрясений.

В этом руководстве мы рассмотрим, как создать такое решение для продажи билетов с помощью ConsenSys Truffle, Infura и Infura NFT API. Мы развернем умный контракт, который действует как служба продажи билетов и создает билеты как невзаимозаменяемые токены ERC-20 (NFT). Мы также рассмотрим несколько архитектур потенциальных внешних интерфейсов, которые могут взаимодействовать с контрактом и вместе функционировать как интегрированная полнофункциональная система продажи билетов Web3.

Давайте строить!

Создайте систему продажи билетов NFT на Ethereum

Базовая архитектура нашей системы предназначена для создания смарт-контракта, который выпускает наши билеты в виде невзаимозаменяемых токенов (NFT). NFT идеально подходят для того, что мы хотим создать. Это доказуемо уникальные цифровые токены, которые позволяют нам гарантировать, что каждый билет уникален и не может быть скопирован или подделан. Это не только гарантирует безопасный опыт продажи билетов для посетителей концертов, но также дает артистам (и организаторам мероприятий) больший контроль над распределением билетов, ценообразованием и перепродажей. Использование смарт-контрактов и NFT даже позволяет создавать новые потоки доходов, такие как выплаты роялти и распределение доходов!

(Если вам нужна справочная информация по любому из этих терминов, технологии блокчейн или web3 в целом, ознакомьтесь с этой статьей на Учимся становиться разработчиком Web3, изучая стек Web3k).

Шаг 1. Установите MetaMask

Первое, что мы собираемся сделать, это настроить кошелек MetaMask и добавить в него тестовую сеть Sepolia. MetaMask — это самый популярный, безопасный и простой в использовании цифровой кошелек с самостоятельным хранением.

Сначала загрузите расширение MetaMask. После установки расширения MetaMask настроит для вас кошелек. В процессе вам будет дана секретная фраза. Храните это в тайне и ни при каких обстоятельствах не публикуйте.

После того, как вы настроили MetaMask, щелкните вкладку «Сеть» в правом верхнем углу. Вы увидите возможность показать/скрыть тестовые сети.

После включения тестовых сетей вы сможете увидеть тестовую сеть Sepolia в раскрывающемся меню. Мы хотим использовать сеть Sepolia, чтобы развертывать и тестировать нашу систему, не тратя реальных денег.

Шаг 2. Получите тестовый ETH

Чтобы развернуть наш смарт-контракт и взаимодействовать с ним, нам потребуется немного бесплатного тестового ETH. Вы можете получить бесплатный ETH Sepolia через кран Sepolia.

После того, как вы пополнили свой кошелек, вы должны увидеть ненулевой баланс при переключении на тестовую сеть Sepolia на MetaMask.

Шаг 3. Установите NPM и Node

Как и все децентрализованные приложения Ethereum, мы создадим наш проект, используя node и npm. Если они не установлены на вашем локальном компьютере, вы можете сделать это здесь.

Чтобы убедиться, что все работает правильно, выполните следующую команду:

$ node -v

Если все пойдет хорошо, вы увидите номер версии Node.

Шаг 4. Зарегистрируйте учетную запись Infura

Чтобы развернуть наш контракт в сети Sepolia, нам потребуется учетная запись Infura. Infura предоставляет нам доступ к конечным точкам RPC, которые обеспечивают быстрый, надежный и простой доступ к выбранной нами цепочке блоков.

Зарегистрируйте бесплатную учетную запись. Создав аккаунт, перейдите на панель инструментов и выберите Создать новый ключ.

Для сети выберите Web3 API и назовите его Система продажи билетов или как вам угодно.

Как только вы нажмете Создать, Infura сгенерирует для вас ключ API и автоматически предоставит конечные точки RPC для Ethereum, Goerli, Sepolia, L2 и L1 без EVM (и соответствующие им тестовые сети).

В этом руководстве нас интересует только конечная точка Sepolia RPC. Этот 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.

Наш контракт на продажу билетов унаследует все функции, предлагаемые реализацией OpenZeppelin ERC721Enumerable. Сюда входят передачи, отслеживание метаданных, данные о праве собственности и т. д.

Мы реализуем следующие функции с нуля:

  1. Публичная первичная продажа: наш контракт дает его владельцу право продавать билеты по определенной цене. Владелец будет иметь право открывать и закрывать продажи, обновлять цены на билеты и снимать любые деньги, отправленные в контракт на покупку билетов. Публика будет иметь возможность чеканить билеты по распродажной цене, когда распродажа открыта, а билеты все еще есть в наличии.
  2. Раздача билетов. Владелец сможет раздавать билеты по списку адресов кошельков.
  3. Бронирование: владелец также сможет зарезервировать билеты для себя без необходимости платить цену публичной продажи.

Добавьте следующий код в 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

Наш контракт уже довольно сложный, но вы можете добавить некоторые дополнительные функции по своему усмотрению.

Например, вы можете внедрить в контракт механизм защиты от скальпинга. Шаги для этого будут следующими:

  1. Определите сопоставление Solidity, которое действует как белый список для кошельков, которые могут содержать более одного билета.
  2. Создайте функцию, позволяющую владельцу добавлять адреса в этот белый список.
  3. Введите проверку _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>"

Затем давайте добавим информацию о нашем кошельке, конечной точке Infura RPC и сети 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...
======================
> 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. Взаимодействие со смарт-контрактом

У нас есть наш смарт-контракт! Следующим шагом является развертывание внешних интерфейсов, которые взаимодействуют с контрактом и позволяют любому вызвать функцию монетного двора, чтобы сделать пожертвование и создать билет для себя.

Для полнофункциональной службы продажи билетов вам обычно потребуются следующие внешние интерфейсы:

  1. Веб-сайт (с отличным пользовательским интерфейсом), на котором обычные пользователи могут оплачивать и выпускать свои билеты.
  2. Портал администратора, на котором владелец может резервировать и раздавать билеты, обновлять цены, передавать роль администратора в другой кошелек, снимать доход от продаж, открывать и закрывать продажи и т. д.
  3. Инструмент, который проверяет, есть ли у человека конкретный билет как онлайн, так и в реальной жизни.

Создание этих систем с нуля выходит за рамки этого руководства, но мы оставим вам несколько ресурсов и советов.

  1. Что касается внешнего веб-сайта чеканки, ознакомьтесь с внешним интерфейсом, который я создал в Спасибо NFT в качестве отправной точки.
  2. Если вы подтвердите свой контракт на Etherscan, он автоматически предоставит вам портал администрирования, где вы сможете вызывать любую функцию вашего контракта. Это хороший первый шаг, прежде чем вы решите создать собственное решение.
  3. Убедиться, что в кошельке есть билет из вашей коллекции, очень просто с помощью функции balanceOf. Если кто-то может доказать, что у него есть бумажник с одним из наших билетов, это, по сути, доказательство того, что у него есть билет. Этого можно добиться с помощью цифровых подписей.

Проверка с помощью Infura NFT API

Еще один совет: когда у вас есть смарт-контракт и внешний интерфейс (или даже до того, как ваш внешний интерфейс будет завершен, и вы хотите доказать, что все работает), вы можете использовать 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));

Запустите его…

$ 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.

Основное изображение для этой статьи было создано с помощью генератора AI-изображений через подсказку "рок-концерт на большом стадионе".


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE