# 5/1 團體作業:目標做出“一個合約&一個測試” ticket.sol ``` // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/utils/math/SafeMath.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "./Ticket.sol"; contract Ticket is Ownable { // Ticket struct struct Ticket { uint expireTime;// the time when the ticket expires string name; // the name of the ticket } mapping(address => Ticket[]) public ticketsOwned; constructor() { // Every time new ticket and push to the list ticketsOwned[msg.sender].push(Ticket (block.timestamp + 1 days, "1 day tickect")); // record a list of tickets for each user ticketsOwned[msg.sender].pop(); // remove the ticket due to expiration or use for(uint256 i = 0; i < ticketsOwned[msg.sender].length; i++) { //checkOwner if the user has the ticket if(keccak256(bytes[ticketsOwned[msg.sender][i].name]) == keccak256(bytes["myTickect"])) {// use string compare library // if the user has the ticket // do something break; } } } } ``` -- 實行MVP:看哪部分要先完成最少需要完成的功能 1.先for一個活動 2. --- erc1155 setUri --- ticket.sol <!-- function mint(address _to, uint256 _quantity) external payable --> ``` import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; contract Ticket is Ownable, ERC1155("xxx.json") { uint public constant MAX_SUPPLY = 1000; //NFT 的最大發行量 uint public constant MAX_PER_MINT = 1; //每次最多可以買幾張 uint public constant PRICE = 0.01 ether; //每張的價格 using Counters for Counters.Counter; //使用 Counters library Counters.Counter private _tokenIds; mapping(address => Ticket[]) public ticketsOwned; // Ticket struct struct Ticket { uint expireTime;// the time when the ticket expires string name; // the name of the ticket } <!-- function --> // 購買 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(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); _tokenIds.increment(); } // 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."); } // 兌換 function _burn(uint256 tokenId) internal override(ERC1155) { super._burn(tokenId); } function supportsInterface(bytes4 interfaceId) public view override(ERC1155) returns (bool) { return super.supportsInterface(interfaceId); } } ``` test.js <!-- it("Name should be as expected", async function () { expect(await token.name()).to.equal("MyTicket"); }); it("Should be able to mint to specific account", async function () { await token.mint(owner.address, 0, { value: ethers.utils.parseEther("0.1"), }); expect(await token.ownerOf(0)).to.equal(owner.address); await token.batchMint(acc1.address, [1, 2], { value: ethers.utils.parseEther("0.2"), }); expect(await token.ownerOf(1)).to.equal(acc1.address); expect(await token.ownerOf(2)).to.equal(acc1.address); }); it("Should revert when not enough funds", async function () { expect(token.mint(owner.address, 0)).to.be.revertedWith( "Not enough funds to mint." ); expect(token.batchMint(acc2.address, [0, 1])).to.be.revertedWith( "Not enough funds to mint." ); }); --> ``` const { expect } = require("chai"); describe("MyTicket", async function () { const [owner,] = await ethers.getSigners(); let token; beforeEach('Setup Contract', async () => { const Ticket = await ethers.getContractFactory('Ticket') nft = await Ticket.deploy() await nft.deployed() nftContractAddress = await nft.address }) it("Should support interface", async function () { expect(await token.supportsInterface("0x00000001"), false); }); }); ``` -- ### Ticket.sol <!-- // Mint a ticket and add it to the user's list _mintSingleNft(msg.sender, "1 day ticket"); _ticketsOwned[msg.sender].push(Ticket(block.timestamp + 1 days, "1 day ticket")); } --> ``` pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; contract Ticket is Ownable, ERC1155("https://donexttime.com/{id}.json") { using Counters for Counters.Counter; Counters.Counter private _tokenIds; // 建立一個 _tokenIds 的計數器 uint256 public constant MAX_SUPPLY = 1000; address[MAX_SUPPLY] private _owners; uint256 public constant MAX_PER_MINT = 2; uint256 public constant PRICE = 0.01 ether; mapping(address => Ticket[]) private _ticketsOwned; struct Ticket { uint256 expireTime; string name; } event TicketUsed(address indexed owner, string name); constructor(string memory uri) ERC1155(uri) { setTokenURI(newTokenID, _tokenURI); } function mintNfts(uint256 count) external payable { uint256 nextId = _tokenIds.current(); // get the next token id require(nextId + count < MAX_SUPPLY, "Supply limit exceeded"); require(count > 0 && count <= MAX_PER_MINT, "Can only mint one NFT per address"); require(msg.value >= PRICE * count, "Insufficient ether sent to purchase tickets"); for (uint256 i = 0; i < count; i++) { string memory metadata = uri(nextId + i); _mintSingleNft(msg.sender, metadata); _ticketsOwned[msg.sender].push(Ticket(block.timestamp + 1 days, "1 day ticket")); } } function useTicket(string memory name) external { for (uint256 i = 0; i < _ticketsOwned[msg.sender].length; i++) { if (keccak256(bytes(_ticketsOwned[msg.sender][i].name)) == keccak256(bytes(name))) { delete _ticketsOwned[msg.sender][i]; emit TicketUsed(msg.sender, name); return; } } revert("No ticket found"); } function withdraw() external onlyOwner { uint256 balance = address(this).balance; require(balance > 0, "No ether left to withdraw"); (bool success, ) = payable(msg.sender).call{value: balance}(""); require(success, "Transfer failed"); } function _burn(address account, uint256 id, uint256 amount) internal virtual override { super._burn(account, id, amount); } function uri(uint256 tokenId) public pure override returns (string memory) { return string(abi.encodePacked("https://donexttime.com/", Strings.toString(tokenId), ".json")); } function _mintSingleNft(address owner, string memory tokenURI) private { require(totalSupply() == _tokenIds.current, "Indexing has broken down!"); uint newTokenID = _tokenIds.current(); _setTokenURI(newTokenID, _tokenURI); _tokenIds.increment(); } function supportsInterface(bytes4 interfaceId)public view override(ERC1155) returns (bool) { return super.supportsInterface(interfaceId); } } ``` ### test.js ``` const { expect } = require("chai"); describe("MyTicket", async function () { const [owner,] = await ethers.getSigners(); let nft; let nftContractAddress; beforeEach('Ticket', async () => { const Ticket = await ethers.getContractFactory('Ticket') nft = await Ticket.deploy() await nft.deployed() nftContractAddress = await nft.address }) it("Should mint NFTs", async function () { const count = 1; const price = ethers.utils.parseEther("0.01"); const tokenId = await nft.tokenIds(); await expect(nft.mintNfts(count, {value: price})) .to.emit(nft, "TransferSingle") .withArgs(owner.address, ethers.constants.AddressZero, owner.address, tokenId, count); expect(await nft.balanceOf(owner.address, tokenId)).to.equal(count); }); it("Should support interface", async function () { expect(await nft.supportsInterface("ERC1155 "), false); }); }); ``` 測試案例中,使用了Mocha的beforeEach函數在每個測試執行之前設置智能合約。它使用ethers.js的getContractFactory和deploy函數創建新的Ticket合約實例,然後獲取其地址並存儲到變數nftContractAddress中。此外,它還獲取簽署者帳戶的地址並存儲到變數owner中。另外還驗證兩個功能: 1. mintNfts: 用於創建和發行新的NFT代幣,並將它們分配給指定的帳戶。此測試案例使用ethers.js的parseEther將0.01 ETH轉換為相應的wei數量,並在呼叫mintNfts時將其作為value參數傳遞。接著,使用Chai的expect語法檢查呼叫mintNfts是否觸發了一個名為TransferSingle的事件,並且檢查事件觸發時的引數是否符合預期。withArgs語法用於確保事件引數與指定的值匹配。 1. supportsInterface: 用於檢查合約是否實現了指定的介面。此測試案例使用Chai的expect語法檢查合約是否未實現ERC1155介面。 --- 待辦清單: 1.把 struct 加進去 code 2.測試再新增一些 --- 參考資料: https://docs.openzeppelin.com/test-helpers/0.5/api#expectEvent https://github.com/z-institute/SBF-BONK/blob/main/contracts/CyberBonk.sol https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC1155/ERC1155.sol https://zhuanlan.zhihu.com/p/561647445