###### 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,查詢特定活動的詳細信息,例如該活動的票總數、票價和開始時間。