NFT 的一个流行且实用的用例是生成现场活动的门票。以太坊等区块链可以保证数字物品的所有权、来源和真实性,有效解决假票问题。虽然Ticketmaster等主要参与者正在努力减少黄牛(拼命控制谁可以转售门票、在哪里转售以及转售多少)和门票欺诈——web3已经有了解决方案。票务行业颠覆的时机已经成熟。
在本教程中,我们将了解如何使用 ConsenSys Truffle 、 Infura和 Infura NFT API 创建此类票务解决方案。我们将部署一个充当票务服务的智能合约,并将票据创建为 ERC-20 不可替代令牌 (NFT)。我们还将介绍一些可以与合约交互的潜在前端架构,并一起作为一个集成的、全栈的、web3 票务系统。
让我们开始建设吧!
我们系统的基本架构旨在创建一个智能合约,将我们的门票作为不可替代的代币 (NFT) 发行。 NFT 非常适合我们想要构建的东西。它们是可证明的唯一数字代币,使我们能够确保每张票都是独一无二的,无法复制或伪造。这不仅保证了音乐会观众的安全票务体验,而且还使艺术家(和活动组织者)能够更好地控制门票分配、定价和转售。使用智能合约和 NFT 甚至可以带来新的收入来源,例如版税支付和收益分享!
(如果您需要任何这些术语、区块链技术或 web3 的背景信息,请查看这篇关于通过探索 Web3 堆栈学习成为 Web3 开发人员的文章)。
我们要做的第一件事是设置一个 MetaMask 钱包并将 Sepolia 测试网络添加到其中。 MetaMask 是世界上最受欢迎、最安全且易于使用的自托管数字钱包。
首先,下载 MetaMask 扩展。安装扩展程序后,MetaMask 将为您设置钱包。在此过程中,您将获得一个密语。保持安全,在任何情况下都不应将其公开。
设置 MetaMask 后,单击右上角的“网络”选项卡。您将看到一个显示/隐藏测试网络的选项。
打开测试网络后,您应该能够在下拉菜单中看到 Sepolia 测试网络。我们想使用 Sepolia 网络,这样我们就可以部署和测试我们的系统,而无需花费任何真金白银。
为了部署我们的智能合约并与之交互,我们需要一些免费的测试 ETH。您可以从Sepolia 水龙头获取免费的 Sepolia ETH。
一旦你为你的钱包注入资金,当你切换到 MetaMask 上的 Sepolia 测试网络时,你应该会看到一个非零余额。
像所有以太坊 dapps 一样,我们将使用 node 和 npm 构建我们的项目。如果您没有在本地计算机上安装这些,您可以在此处安装。
为确保一切正常,请运行以下命令:
$ node -v
如果一切顺利,您应该会看到 Node.js 的版本号。
为了将我们的合约部署到 Sepolia 网络,我们需要一个 Infura 帐户。 Infura 使我们能够访问 RPC 端点,从而可以快速、可靠且轻松地访问我们选择的区块链。
注册一个免费帐户。创建帐户后,导航到仪表板并选择Create New Key 。
对于网络,选择Web3 API并将其命名为Ticketing System或您选择的名称。
单击创建后,Infura 将为您生成一个 API 密钥,并自动为您提供以太坊、Goerli、Sepolia、L2 和非 EVM L1(及其相应的测试网)的 RPC 端点。
对于本教程,我们只对 Sepolia RPC 端点感兴趣。此 URL 的格式为https://sepolia.infura.io/v3/←API KEY→
让我们通过运行以下命令来设置一个空的项目存储库:
$ mkdir nft-ticketing && cd nft-ticketing $ npm init -y
我们将使用世界一流的 EVM 智能合约开发环境和测试框架 Truffle 来构建和部署我们的加密货币智能合约。通过运行安装松露:
$ npm install —save truffle
我们现在可以通过运行以下命令来创建准系统 Truffle 项目:
$ npx truffle init
要检查一切是否正常,请运行:
$ npx truffle test
我们现在已经成功配置了 Truffle。接下来让我们安装OpenZeppelin合同包。这个包将使我们能够访问 ERC-721 基础实现(不可替代令牌的标准)以及一些有用的附加功能。
$ npm install @openzeppelin/contracts
为了允许 Truffle 代表我们使用我们的 MetaMask 钱包、签署交易和支付 gas,我们将需要另一个名为hdwalletprovider
的包。使用以下命令安装它:
$ npm install @truffle/hdwallet-provider
最后,为了保护我们敏感的钱包信息安全,我们将使用dotenv
包。
$ npm install dotenv
在代码编辑器(例如:VS Code)中打开项目存储库。在contracts
文件夹中,创建一个名为NftTicketing.sol
的新文件。
我们的票务合约将继承 OpenZeppelin 的ERC721Enumerable
实现提供的所有功能。这包括传输、元数据跟踪、所有权数据等。
我们将从头开始实现以下功能:
将以下代码添加到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
我们的合约已经很复杂了,但是可以添加一些你认为合适的额外功能。
例如,您可以在合约中实施反剥头皮机制。这样做的步骤如下:
在合约的构造函数下方添加以下代码片段:
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 命令再次编译合约。
在项目的根目录中创建一个名为.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', } } };
现在让我们编写一个脚本来将我们的合约部署到 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 步:与智能合约接口
我们有我们的智能合约!下一步是部署与合约接口的前端,并允许任何人调用 mint 函数进行捐赠并为自己铸造一张票。
对于功能齐全的票务服务,您通常需要以下前端:
从头开始构建这些系统超出了本教程的范围,但我们将为您提供一些资源和技巧。
对于前端铸币网站,请查看我在Thank You NFT教程中构建的前端作为起点。
如果您在 Etherscan 上验证您的合约,它会自动为您提供一个管理门户,您可以在其中调用合约上的任何功能。这是您决定构建自定义解决方案之前的良好开端。
使用balanceOf
函数验证钱包是否有您收藏的票证非常简单。如果有人能证明他们拥有一个包含我们其中一张门票的钱包,这基本上就是证明他们有一张门票。这可以使用数字签名来实现。
另一个提示:一旦你有了智能合约和前端(或者甚至在你的前端完成并且你想证明一切正常之前),你可以使用Infura NFT API来验证你的新 NFT 是否存在。 Infura NFT API 是一种通过单个 API 调用替换大量 NFT 相关代码的快速方法。
例如,我们可以通过 API 轻松获得我们需要显示 NFT 所有权的信息。我们只需要提供钱包地址。代码看起来像这样:
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] }, … ] }
在本教程中,我们使用Truffle 、 Infura和 Infura NFT API部署了一个功能齐全的 NFT 票务服务。
这显然不是颠覆 Ticketmaster 所需的一切——但它是一个坚实的开始,也是一个很好的概念证明!即使您不使用此代码并启动自己的 NFT 票务平台,也希望您在此过程中对 web3 有所了解。
本文的主图是由 HackerNoon 的AI Image Generator通过提示“大体育场的摇滚音乐会”生成的。