###### tags: `z-institute` `畢專` # 5/23 5/9 ### Ticket.sol https://dzone.com/articles/build-a-web3-ticketing-system-and-disrupt-ticketma-1 ``` // SPDX-License-Identifier: MIT 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 函數庫來計數 NFT 的 ID //可以先放在Remix上編輯 Counters.Counter private _tokenIds; // 建立一個 _tokenIds 的計數器 uint256 public constant MAX_SUPPLY = 1000; // 定義 MAX_SUPPLY 為 1000 address[MAX_SUPPLY] private _owners; // 建立一個 _owners 的地址數組,長度為 MAX_SUPPLY uint256 public constant MAX_PER_MINT = 1; // 定義 MAX_PER_MINT 為 1 uint256 public constant PRICE = 0.01 ether; // 定義 PRICE 為 0.01 ether mapping(address => Ticket[]) private _ticketsOwned; // struct Ticket { // 建立一個名為 Ticket 的結構 uint256 expireTime; string name; } event TicketUsed(address indexed owner, string name); // 建立一個 TicketUsed 事件 constructor(string memory uri) ERC1155(uri) { _setTokenURI(newTokenID, _tokenURI); //然後使用 _setTokenURI 函數將URI 設置給新的 NFT } function mintNfts(uint256 count) external payable { // 鑄造 NFT 函數 uint256 nextId = _tokenIds.current(); // 取得下一個 token 的 ID require(nextId + count < MAX_SUPPLY, "Supply limit exceeded"); // 判斷是否超過了最大供應量 require(count > 0 && count <= MAX_PER_MINT, "Can only mint one NFT per address"); // 判斷每個地址是否只能鑄造一個 NFT require(msg.value >= PRICE * count, "Insufficient ether sent to purchase tickets"); //判斷是否付了足夠的錢 //這個合約中的mintNfts和_mintSingleNft函數都是用於創建新的NFT。它們的區別在於mintNfts用於一次性創建多個NFT,而_mintSingleNft用於創建一個單獨的NFT。 //具體來說,mintNfts接受一個參數_count,表示要創建的NFT的數量,然後循環_count次調用_mintSingleNft來創建每個NFT 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 _mintSingleNft(address owner, string memory tokenURI) private { //只能被合約內部的其他函數調用,不能被外部調用。 require(totalSupply() == _tokenIds.current, "Indexing has broken down!"); //檢查目前的總供應量是否等於 _tokenIds 的當前值,如果不是,則代表索引已經出現了問題,此時函數將停止執行。 uint newTokenID = _tokenIds.current(); //獲取當前的token ID _tokenIds.increment(); //增加 _tokenIds 的值,以便下次創建新的 NFT 時,其token ID 能夠自動增加。 } function useTicket(string memory name) external { for (uint256 i = 0; i < _ticketsOwned[msg.sender].length; i++) { //先遍歷 _ticketsOwned 數組,找到名稱為 name 的票 if (keccak256(bytes(_ticketsOwned[msg.sender][i].name)) == keccak256(bytes(name))) {//如果找到了與 name 相等的票券 delete _ticketsOwned[msg.sender][i];// 如果找到了,則delete 該票券 //並發出 TicketUsed 事件,表示票券已被使用。然後函數返回。 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");//如果轉移失敗,將會觸發 require 斷言,使函數執行停止 } 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 supportsInterface(bytes4 interfaceId)public view override(ERC1155) returns (bool) { return super.supportsInterface(interfaceId); } } ``` <!-- function _mintSingleNft(address owner, string memory tokenURI) private { //只能被合約內部的其他函數調用,不能被外部調用。 require(totalSupply() == _tokenIds.current, "Indexing has broken down!"); //檢查目前的總供應量是否等於 _tokenIds 的當前值,如果不是,則代表索引已經出現了問題,此時函數將停止執行。 uint newTokenID = _tokenIds.current(); //獲取當前的token ID _setTokenURI(newTokenID, _tokenURI); //然後使用 _setTokenURI 函數將URI 設置給新的 NFT _tokenIds.increment(); //增加 _tokenIds 的值,以便下次創建新的 NFT 時,其token ID 能夠自動增加。 } --> ### test.js //測試時可以分功能測試 ``` const { expect } = require("chai"); describe("Ticket", 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); }); }); ``` 使用ethers.js的getContractFactory和deploy函數創建新的Ticket合約實例,然後獲取其地址並存儲到變數nftContractAddress中。此外,它還獲取簽署者帳戶的地址並存儲到變數owner中。 測試案例檢查 mintNfts 函數是否正確地鑄造(創建)了新的 NFT,並將其發送到指定的地址。該函數期望通過傳遞一個 count 和一個以以太幣為單位的 price 參數,來鑄造指定數量的 NFT,並將它們發送到調用者的地址。 測試的第一部分使用 expect 函數檢查是否會在調用 mintNfts 函數時觸發一個名為 TransferSingle 的事件,這個事件會包含一些參數,其中包括調用者的地址、空地址、接收者的地址、NFT 的 ID 和數量等。這個事件的觸發表示鑄造 NFT 的過程已經正確地完成。如果事件觸發成功,測試就通過了。 測試的第二部分使用 expect 和 await 函數,檢查該合約的餘額是否符合預期。它通過調用 balanceOf 函數檢查調用者的地址在指定 NFT ID 下持有的 NFT 數量是否等於 count。如果餘額等於 count,則測試通過。 supportsInterface: 用於檢查合約是否實現了ERC1155介面。 Q 怎麼寫URI測試? 之後補withdraw/burn/..測試 MINT權限 數量 name --- 5/9 更改版 ``` // SPDX-License-Identifier: MIT 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 { 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 => MyTicket[]) private _ticketsOwned; struct MyTicket { uint256 expireTime; string name; } event TicketUsed(address indexed owner, string name); constructor(string memory uri) ERC1155(uri) { // Mint a ticket and add it to the user's list } function mintNfts(uint256 count) external payable { // mint NFTs uint256 nextId = _tokenIds.current(); // get the next 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); } } //function uri(uint256 tokenId) public pure override returns (string memory) { // return string(abi.encodePacked("https://donexttime.com/", Strings.toString(tokenId), ".json")); //} 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 _mintSingleNft(address owner, string memory tokenURI) private { require(MAX_SUPPLY == _tokenIds.current(), "Indexing has broken down!"); uint newTokenID = _tokenIds.current(); _tokenIds.increment(); } function supportsInterface(bytes4 interfaceId)public view override(ERC1155) returns (bool) { return super.supportsInterface(interfaceId); } } ``` --- 1. 繼續完成畢業專案智能合約與開始做前端介面設計!!並寫一些基本智能合約 function 的測試 💪 2. 將畢業專案上傳至各組的 repository 內,請把目前進度推上去,之後都用這個來追蹤 第二組 : https://github.com/z-institute/Solidity-EVM-Dev-Batch-2-Group-2 第三組 : https://github.com/z-institute/Solidity-EVM-Dev-Batch-2-Group-3 3. 下次上課會檢查畢業專案的整體進度,並在課前準備好程式相關的 debug 問題,可用 Github Issues 方式整理問題與提交,到時候會較有效率 4. --- IPFS安裝筆記(專題) https://circular-seed-c96.notion.site/2d25394237904ca58a5dc0efa443e69d ---- ``` const { expect } = require("chai"); describe("Ticket", async function () { const [owner, user] = await ethers.getSigners(); let nft; let nftContractAddress; const MAX_SUPPLY = 1000; const MAX_PER_MINT = 2; const PRICE = ethers.utils.parseEther("0.01"); beforeEach('Ticket', async () => { const Ticket = await ethers.getContractFactory('Ticket') nft = await Ticket.deploy("https://example.com/{id}.json") 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 use ticket and remove from _ticketsOwned", async function () { await nft.mintNfts(1, {value: ethers.utils.parseEther("0.01")}); const tokenId = await nft.tokenIds(); await nft.connect(owner).useTicket("test"); expect((await nft._ticketsOwned(owner.address)).length).to.equal(0); }); it("Should withdraw ether to owner address", async function () { await nft.mintNfts(1, {value: ethers.utils.parseEther("0.01")}); const balanceBefore = await ethers.provider.getBalance(owner.address); await nft.connect(owner).withdraw(); const balanceAfter = await ethers.provider.getBalance(owner.address); expect(balanceAfter.sub(balanceBefore)).to.equal(ethers.utils.parseEther("0.01")); }); it("Should burn NFT", async function () { await nft.mintNfts(1, {value: ethers.utils.parseEther("0.01")}); const tokenId = await nft.tokenIds(); await expect(nft.burn(tokenId, 1)) .to.emit(nft, "TransferSingle") .withArgs(owner.address, owner.address, ethers.constants.AddressZero, tokenId, 1); expect(await nft.balanceOf(owner.address, tokenId)).to.equal(0); }); it("Should support interface", async function () { expect(await nft.supportsInterface("0xd9b67a26"), true); }); }); ``` user can buy ticket ``` // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "./Ticket.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract BookingSystem is Ownable { Ticket private ticketContract; struct Event { string eventName; uint256 maxSupply; uint256 startTime; uint256 price; } mapping(uint256 => Event) public events; mapping(address => mapping(uint256 => uint256)) public userTickets; uint256 public constant REFUND_PERCENTAGE = 80; uint256 public constant REFUND_TIME = 1200; event TicketPurchased(address indexed buyer, uint256 indexed eventId, uint256 ticketId); event TicketRefunded(address indexed buyer, uint256 indexed eventId, uint256 ticketId); constructor(address _ticketContract) { ticketContract = Ticket(_ticketContract); } function createEvent(uint256 eventId, string memory eventName, uint256 maxSupply, uint256 startTime, uint256 price) external { require(events[eventId].startTime == 0, "Event already exists"); events[eventId] = Event(eventName, maxSupply, startTime, price); } function purchaseTicket(uint256 eventId) external payable { Event memory eventInfo = events[eventId]; require(eventInfo.startTime > 0, "Event does not exist"); require(block.timestamp < eventInfo.startTime, "Event has already started"); require(msg.value == eventInfo.price, "Incorrect payment amount"); uint256 ticketId = ticketContract.ticketIdCounter().current(); ticketContract.ticketIdCounter().increment(); ticketContract.tickets(ticketId) = Ticket.TicketData(1, 0); userTickets[msg.sender][eventId] = ticketId; ticketContract.mint(1); emit TicketPurchased(msg.sender, eventId, ticketId); } function refundTicket(uint256 eventId) external { Event memory eventInfo = events[eventId]; require(eventInfo.startTime > 0, "Event does not exist"); require(block.timestamp < eventInfo.startTime - REFUND_TIME, "Refund time has passed"); uint256 ticketId = userTickets[msg.sender][eventId]; require(ticketId > 0, "User does not have a ticket for this event"); uint256 refundAmount = eventInfo.price * REFUND_PERCENTAGE / 100; uint256 feeAmount = eventInfo.price - refundAmount; delete userTickets[msg.sender][eventId]; payable(msg.sender).transfer(refundAmount); payable(owner()).transfer(feeAmount); ticketContract.safeTransferFrom(address(this), address(0), ticketId, 1, ""); emit TicketRefunded(msg.sender, eventId, ticketId); } } ``` ``` // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; contract Ticket is ERC1155, Ownable { using Counters for Counters.Counter; using Strings for uint256; struct TicketData { uint256 supply; uint256 minted; } mapping(uint256 => TicketData) public tickets; Counters.Counter private ticketIdCounter; uint256 public constant MAX_SUPPLY = 500; uint256 public constant MAX_PER_MINT = 1; uint256 public price = 0.01 ether; constructor() ERC1155("") {} // function buyTicket(uint256 amount) external payable { require(msg.value == price * amount, "Invalid payment amount"); require(amount <= MAX_PER_MINT, "Exceeds maximum tickets per mint"); require(ticketIdCounter.current() + amount <= MAX_SUPPLY, "Exceeds maximum ticket supply"); for (uint256 i = 0; i < amount; i++) { uint256 ticketId = ticketIdCounter.current(); ticketIdCounter.increment(); tickets[ticketId] = TicketData(1, 0); _mint(msg.sender, ticketId, 1, ""); } } // function mint(uint256 amount) external onlyOwner { require(amount <= MAX_SUPPLY - ticketIdCounter.current(), "Exceeds maximum ticket supply"); for (uint256 i = 0; i < amount; i++) { uint256 ticketId = ticketIdCounter.current(); ticketIdCounter.increment(); tickets[ticketId] = TicketData(1, 0); _mint(msg.sender, ticketId, 1, ""); } } } ``` 5/19 ``` // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; contract TicketContract is ERC1155, Ownable { using Counters for Counters.Counter; struct Ticket { uint256 id; address owner; uint256 eventId; uint256 quantity; uint256 price; bool isValid; uint256 startTime; bool refunded; bool isUsed; } mapping(uint256 => Ticket) public tickets; Counters.Counter private ticketIdCounter; uint256 public constant MAX_SUPPLY = 500; uint256 public constant MAX_PER_MINT = 1; uint256 public price = 0.01 ether; uint256 public constant REFUND_PERCENT = 80; event TicketMinted(uint256 indexed ticketId, address indexed owner); event TicketBurned(uint256 indexed ticketId); constructor() ERC1155("") {} function mintTicket(uint256 eventId, uint256 quantity) external payable { require(quantity <= MAX_PER_MINT, "Exceeds maximum per mint"); require(ticketIdCounter.current() + quantity <= MAX_SUPPLY, "Exceeds maximum supply"); require(msg.value == price * quantity, "Insufficient payment"); for (uint256 i = 0; i < quantity; i++) { uint256 ticketId = ticketIdCounter.current(); tickets[ticketId] = Ticket({ id: ticketId, owner: msg.sender, eventId: eventId, quantity: 1, price: price, isValid: true, startTime: block.timestamp, refunded: false, isUsed: false }); _mint(msg.sender, ticketId, 1, ""); ticketIdCounter.increment(); emit TicketMinted(ticketId, msg.sender); } } function burnTicket(uint256 ticketId) external { require(tickets[ticketId].owner == msg.sender, "Not the ticket owner"); require(tickets[ticketId].isValid, "Invalid ticket"); require(!tickets[ticketId].isUsed, "Ticket already used"); _burn(msg.sender, ticketId, 1); tickets[ticketId].isValid = false; tickets[ticketId].isUsed = true; emit TicketBurned(ticketId); } function burnTicketsAfterEvent(uint256 eventId) external onlyOwner { require(block.timestamp > tickets[eventId].startTime, "Event has not ended yet"); for (uint256 i = 0; i < ticketIdCounter.current(); i++) { if (tickets[i].eventId == eventId && tickets[i].isValid) { _burn(tickets[i].owner, i, tickets[i].quantity); tickets[i].isValid = false; emit TicketBurned(i); } } } function refund(uint256 ticketId) external { require(tickets[ticketId].owner == msg.sender, "Not the ticket owner"); require(tickets[ticketId].isValid, "Invalid ticket"); require(block.timestamp < tickets[ticketId].startTime, "Refund period has ended"); if (!tickets[ticketId].refunded) { uint256 refundAmount = (tickets[ticketId].price * REFUND_PERCENT) / 100; uint256 feeAmount = tickets[ticketId].price - refundAmount; tickets[ticketId].refunded = true; payable(tickets[ticketId].owner).transfer(refundAmount); payable(owner()).transfer(feeAmount); } } function getEvent(uint256 eventId) external view returns ( uint256 id, uint256 totalTickets, uint256 ticketPrice, uint256 startTime ) { Ticket storage ticket = tickets[eventId]; require(ticket.owner != address(0), "Event does not exist"); return ( ticket.eventId, ticketIdCounter.current(), ticket.price, ticket.startTime ); } function getTotalTickets() external view returns (uint256) { return ticketIdCounter.current(); } function supportsInterface(bytes4 interfaceId) public view override(ERC1155) returns (bool) { return super.supportsInterface(interfaceId); } function withdraw() external onlyOwner { uint256 balance = address(this).balance; payable(owner()).transfer(balance); } } ``` 5/23 ``` // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; contract TicketContract is ERC1155, Ownable { using Counters for Counters.Counter; struct Ticket { uint256 id; // 票ID address owner; // 擁有者地址 uint256 eventId; // 活動ID uint256 quantity; // 數量(每個NFT代表一張票) uint256 price; // 價格 uint256 startTime; // 活動開始時間 bool refunded; // 是否已退款 bool isUsed; // 是否已使用 } mapping(address => mapping(address => bool)) private _operatorApprovals;//為了使 NFT 可以進行轉移而用於追蹤已授權的操作者。 mapping(uint256 => Ticket) public tickets; // 票ID映射到票的詳細信息 Counters.Counter private ticketIdCounter; // 票ID計數器 uint256 public constant MAX_SUPPLY = 500; // 最大供應量 uint256 public constant MAX_PER_MINT = 1; // 每次購買的最大數量 uint256 public price = 0.01 ether; // 票價 uint256 public constant REFUND_PERCENT = 80; // 退款百分比 event TicketMinted(uint256 indexed ticketId, address indexed owner); // 票被鑄造時觸發的事件 event TicketBurned(uint256 indexed ticketId); // 票被銷毀時觸發的事件 event TicketTransferred(uint256 indexed ticketId, address indexed from, address indexed to); // 票被轉移時觸發的事件 constructor() ERC1155("") {} //鑄造票 function mintTicket(uint256 eventId, uint256 quantity) external payable { require(quantity <= MAX_PER_MINT, "Exceeds maximum per mint"); require(ticketIdCounter.current() + quantity <= MAX_SUPPLY, "Exceeds maximum supply"); require(msg.value == price * quantity, "Insufficient payment"); for (uint256 i = 0; i < quantity; i++) { uint256 ticketId = ticketIdCounter.current(); tickets[ticketId] = Ticket({ id: ticketId, owner: msg.sender, eventId: eventId, quantity: 1, price: price, startTime: block.timestamp, refunded: false, isUsed: false }); _mint(msg.sender, ticketId, 1, ""); ticketIdCounter.increment(); emit TicketMinted(ticketId, msg.sender); } } //銷毀票 function burnTicket(uint256 ticketId) external { require(!tickets[ticketId].isUsed, "Ticket already used"); _burn(msg.sender, ticketId, 1); tickets[ticketId].isUsed = true; emit TicketBurned(ticketId); } //轉移票 function transferTicket(uint256 ticketId, address to) external { require(tickets[ticketId].owner == msg.sender, "Not the ticket owner"); require(!tickets[ticketId].isUsed, "Ticket already used"); tickets[ticketId].owner = to; _safeTransferFrom(msg.sender, to, ticketId, 1, ""); emit TicketTransferred(ticketId, msg.sender, to); } //活動結束後銷毀所有票 function burnTicketsAfterEvent(uint256 eventId) external onlyOwner { require(block.timestamp > tickets[eventId].startTime, "Event has not ended yet"); for (uint256 i = 0; i < ticketIdCounter.current(); i++) { if (tickets[i].eventId == eventId) { _burn(tickets[i].owner, i, tickets[i].quantity); emit TicketBurned(i); } } } //退款 function refund(uint256 ticketId) external { require(tickets[ticketId].owner == msg.sender, "Not the ticket owner"); require(block.timestamp < tickets[ticketId].startTime, "Refund period has ended"); if (!tickets[ticketId].refunded) { uint256 refundAmount = (tickets[ticketId].price * REFUND_PERCENT) / 100; uint256 feeAmount = tickets[ticketId].price - refundAmount; tickets[ticketId].refunded = true; payable(tickets[ticketId].owner).transfer(refundAmount); payable(owner()).transfer(feeAmount); _burn(msg.sender, ticketId, 1); emit TicketBurned(ticketId); } } //設置票務系統中的NFT的URI function setURI(string memory newUri) public onlyOwner { _setURI(newUri); } //查詢總票數,展示網頁中的現在票務銷售情況 function getTotalTickets() external view returns (uint256) { return ticketIdCounter.current(); } //檢查合約是否支持ERC1155介面 function supportsInterface(bytes4 interfaceId) public view override(ERC1155) returns (bool) { return super.supportsInterface(interfaceId); } //提取合約中的資金 function withdraw() external onlyOwner { uint256 balance = address(this).balance; payable(owner()).transfer(balance); } //獲取活動詳細信息 function getEvent(uint256 eventId) external view returns ( uint256 id, uint256 totalTickets, uint256 ticketPrice, uint256 startTime ) { Ticket storage ticket = tickets[eventId]; require(ticket.owner != address(0), "Event does not exist"); return ( ticket.eventId, ticketIdCounter.current(), ticket.price, ticket.startTime ); } } ``` 測試檔 ``` const { expect } = require("chai"); describe("TicketContract", function () { let ticketContract; let owner; let addr1; let addr2; beforeEach(async function () { [owner, addr1, addr2] = await ethers.getSigners(); const TicketContract = await ethers.getContractFactory("TicketContract"); ticketContract = await TicketContract.deploy(); await ticketContract.deployed(); }); it("should mint tickets", async function () { const eventId = 1; const quantity = 1; const mintTicketTx = await ticketContract.mintTicket(eventId, quantity, { value: ethers.utils.parseEther("0.01"), }); await mintTicketTx.wait(); const totalTickets = await ticketContract.getTotalTickets(); expect(totalTickets).to.equal(quantity); }); it("should burn tickets", async function () { const eventId = 1; const quantity = 1; await ticketContract.mintTicket(eventId, quantity, { value: ethers.utils.parseEther("0.01"), }); const ticketId = 0; const burnTicketTx = await ticketContract.burnTicket(ticketId); await burnTicketTx.wait(); const totalTickets = await ticketContract.getTotalTickets(); expect(totalTickets).to.equal(0); }); it("should transfer tickets", async function () { const eventId = 1; const quantity = 1; await ticketContract.mintTicket(eventId, quantity, { value: ethers.utils.parseEther("0.01"), }); const ticketId = 0; const transferTicketTx = await ticketContract.transferTicket(ticketId, addr1.address); await transferTicketTx.wait(); const ownerOfTicket = await ticketContract.ownerOf(ticketId); expect(ownerOfTicket).to.equal(addr1.address); }); it("should refund tickets", async function () { const eventId = 1; const quantity = 1; await ticketContract.mintTicket(eventId, quantity, { value: ethers.utils.parseEther("0.01"), }); const ticketId = 0; const refundTicketTx = await ticketContract.refund(ticketId); await refundTicketTx.wait(); const totalTickets = await ticketContract.getTotalTickets(); expect(totalTickets).to.equal(0); }); }); ``` --- ## 使用Ethers.js或Web3.js與智能合約進行互動 ### Ethers.js ``` const { ethers } = require('ethers'); // 定義智能合約 ABI const ticketContractAbi = [ // 定義智能合約方法和事件 // ... ]; // 定義智能合約地址 const ticketContractAddress = '0x123456789abcdef...'; // 設定以太坊節點提供者 const provider = new ethers.providers.JsonRpcProvider('https://ropsten.infura.io/v3/YOUR_INFURA_PROJECT_ID'); // 連接到智能合約 const ticketContract = new ethers.Contract(ticketContractAddress, ticketContractAbi, provider); // 鑄造票 async function mintTicket(eventId, quantity) { const signer = provider.getSigner(); const price = ethers.utils.parseEther('0.01').mul(quantity); // 獲取鑄造票的交易 const transaction = await ticketContract.connect(signer).mintTicket(eventId, quantity, { value: price, }); // 等待交易被確認 await transaction.wait(); console.log('Ticket minted successfully!'); } // 銷毀票 async function burnTicket(ticketId) { const signer = provider.getSigner(); // 獲取銷毀票的交易 const transaction = await ticketContract.connect(signer).burnTicket(ticketId); // 等待交易被確認 await transaction.wait(); console.log('Ticket burned successfully!'); } // 轉移票 async function transferTicket(ticketId, toAddress) { const signer = provider.getSigner(); // 獲取轉移票的交易 const transaction = await ticketContract.connect(signer).transferTicket(ticketId, toAddress); // 等待交易被確認 await transaction.wait(); console.log('Ticket transferred successfully!'); } // 退款 async function refundTicket(ticketId) { const signer = provider.getSigner(); // 獲取退款的交易 const transaction = await ticketContract.connect(signer).refund(ticketId); // 等待交易被確認 await transaction.wait(); console.log('Ticket refunded successfully!'); } // 使用範例 async function main() { // 鑄造票 await mintTicket(1, 1); // 銷毀票 await burnTicket(1); // 轉移票 await transferTicket(1, '0x123456789abcdef...'); // 退款 await refundTicket(1); } main().catch((error) => { console.error(error); }); ``` ### Web3.js ``` const Web3 = require('web3'); // 定義智能合約 ABI const ticketContractAbi = [ // 定義智能合約方法和事件 // ... ]; // 定義智能合約地址 const ticketContractAddress = '0x123456789abcdef...'; // 設定以太坊節點提供者 const providerUrl = 'https://ropsten const Web3 = require('web3'); // 定義智能合約 ABI const ticketContractAbi = [ // 定義智能合約方法和事件 // ... ]; // 定義智能合約地址 const ticketContractAddress = '0x123456789abcdef...'; // 設定以太坊節點提供者 const providerUrl = 'https://ropsten ``` ### 建立合約的 API ``` npm install express web3 ``` #### app.js ``` const express = require('express'); const Web3 = require('web3'); const app = express(); const web3 = new Web3(new Web3.providers.HttpProvider('https://ropsten.infura.io/v3/YOUR_INFURA_PROJECT_ID')); // 合約 ABI 和地址 const ticketContractAbi = [ // 在這裡插入合約的 ABI ]; const ticketContractAddress = '0x...'; // 在這裡插入合約的地址 // 創建智能合約實例 const ticketContract = new web3.eth.Contract(ticketContractAbi, ticketContractAddress); // 鑄造票的 API 端點 app.post('/api/mintTicket', async (req, res) => { try { const { eventId, quantity } = req.body; // 執行鑄造票的交易 const accounts = await web3.eth.getAccounts(); const price = web3.utils.toWei('0.01', 'ether') * quantity; const transaction = await ticketContract.methods.mintTicket(eventId, quantity).send({ from: accounts[0], value: price, }); res.status(200).json({ message: 'Ticket minted successfully' }); } catch (error) { console.error(error); res.status(500).json({ error: 'Failed to mint ticket' }); } }); // 其他 API 端點... // 啟動服務器 app.listen(3000, () => { console.log('Server is running on port 3000'); }); ``` ### ABI ``` [ { "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "account", "type": "address" }, { "indexed": true, "internalType": "address", "name": "operator", "type": "address" }, { "indexed": false, "internalType": "bool", "name": "approved", "type": "bool" } ], "name": "ApprovalForAll", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "previousOwner", "type": "address" }, { "indexed": true, "internalType": "address", "name": "newOwner", "type": "address" } ], "name": "OwnershipTransferred", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "uint256", "name": "ticketId", "type": "uint256" } ], "name": "TicketBurned", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "uint256", "name": "ticketId", "type": "uint256" }, { "indexed": true, "internalType": "address", "name": "owner", "type": "address" } ], "name": "TicketMinted", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "uint256", "name": "ticketId", "type": "uint256" }, { "indexed": true, "internalType": "address", "name": "from", "type": "address" }, { "indexed": true, "internalType": "address", "name": "to", "type": "address" } ], "name": "TicketTransferred", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "operator", "type": "address" }, { "indexed": true, "internalType": "address", "name": "from", "type": "address" }, { "indexed": true, "internalType": "address", "name": "to", "type": "address" }, { "indexed": false, "internalType": "uint256[]", "name": "ids", "type": "uint256[]" }, { "indexed": false, "internalType": "uint256[]", "name": "values", "type": "uint256[]" } ], "name": "TransferBatch", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "operator", "type": "address" }, { "indexed": true, "internalType": "address", "name": "from", "type": "address" }, { "indexed": true, "internalType": "address", "name": "to", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "id", "type": "uint256" }, { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } ], "name": "TransferSingle", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "string", "name": "value", "type": "string" }, { "indexed": true, "internalType": "uint256", "name": "id", "type": "uint256" } ], "name": "URI", "type": "event" }, { "inputs": [], "name": "MAX_PER_MINT", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "MAX_SUPPLY", "outputs": [ {https://hackmd.io/-nSNZxPmSWmIiJxsVIEwtw# "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "REFUND_PERCENT", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "account", "type": "address" }, { "internalType": "uint256", "name": "id", "type": "uint256" } ], "name": "balanceOf", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "address[]", "name": "accounts", "type": "address[]" }, { "internalType": "uint256[]", "name": "ids", "type": "uint256[]" } ], "name": "balanceOfBatch", "outputs": [ { "internalType": "uint256[]", "name": "", "type": "uint256[]" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint256", "name": "ticketId", "type": "uint256" } ], "name": "burnTicket", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "uint256", "name": "eventId", "type": "uint256" } ], "name": "burnTicketsAfterEvent", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "uint256", "name": "eventId", "type": "uint256" } ], "name": "getEvent", "outputs": [ { "internalType": "uint256", "name": "id", "type": "uint256" }, { "internalType": "uint256", "name": "totalTickets", "type": "uint256" }, { "internalType": "uint256", "name": "ticketPrice", "type": "uint256" }, { "internalType": "uint256", "name": "startTime", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "getTotalTickets", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "account", "type": "address" }, { "internalType": "address", "name": "operator", "type": "address" } ], "name": "isApprovedForAll", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint256", "name": "eventId", "type": "uint256" }, { "internalType": "uint256", "name": "quantity", "type": "uint256" } ], "name": "mintTicket", "outputs": [], "stateMutability": "payable", "type": "function" }, { "inputs": [], "name": "owner", "outputs": [ { "internalType": "address", "name": "", "type": "address" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "price", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint256", "name": "ticketId", "type": "uint256" } ], "name": "refund", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "renounceOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "from", "type": "address" }, { "internalType": "address", "name": "to", "type": "address" }, { "internalType": "uint256[]", "name": "ids", "type": "uint256[]" }, { "internalType": "uint256[]", "name": "amounts", "type": "uint256[]" }, { "internalType": "bytes", "name": "data", "type": "bytes" } ], "name": "safeBatchTransferFrom", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "from", "type": "address" }, { "internalType": "address", "name": "to", "type": "address" }, { "internalType": "uint256", "name": "id", "type": "uint256" }, { "internalType": "uint256", "name": "amount", "type": "uint256" }, { "internalType": "bytes", "name": "data", "type": "bytes" } ], "name": "safeTransferFrom", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "operator", "type": "address" }, { "internalType": "bool", "name": "approved", "type": "bool" } ], "name": "setApprovalForAll", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "string", "name": "newUri", "type": "string" } ], "name": "setURI", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "bytes4", "name": "interfaceId", "type": "bytes4" } ], "name": "supportsInterface", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "name": "tickets", "outputs": [ { "internalType": "uint256", "name": "id", "type": "uint256" }, { "internalType": "address", "name": "owner", "type": "address" }, { "internalType": "uint256", "name": "eventId", "type": "uint256" }, { "internalType": "uint256", "name": "quantity", "type": "uint256" }, { "internalType": "uint256", "name": "price", "type": "uint256" }, { "internalType": "uint256", "name": "startTime", "type": "uint256" }, { "internalType": "bool", "name": "refunded", "type": "bool" }, { "internalType": "bool", "name": "isUsed", "type": "bool" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "newOwner", "type": "address" } ], "name": "transferOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "uint256", "name": "ticketId", "type": "uint256" }, { "internalType": "address", "name": "to", "type": "address" } ], "name": "transferTicket", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "name": "uri", "outputs": [ { "internalType": "string", "name": "", "type": "string" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "withdraw", "outputs": [], "stateMutability": "nonpayable", "type": "function" } ] ``` 5/26 修改 ticket.sol ``` ``` Event ID:可以區分不同的活動。同一合約中舉辦多個活動,可以使用不同的 Event ID 來區分每個活動的票。burnTicketsAfterEvent使用 Event ID 來銷毀特定活動的所有票。查詢活動詳細信息,通過 Event ID,查詢特定活動的詳細信息,例如該活動的票總數、票價和開始時間。