From c314ff3227074d3f3badc80da18f491a46960d53 Mon Sep 17 00:00:00 2001 From: Christopher Canaday Date: Fri, 21 Feb 2025 18:41:14 -0500 Subject: [PATCH 1/5] in progress --- contracts/VenueMint.sol | 42 +++++++++++++++++++++++++++++++++++++++++ test/VenueMint.ts | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/contracts/VenueMint.sol b/contracts/VenueMint.sol index 097a8bc..ebb3347 100644 --- a/contracts/VenueMint.sol +++ b/contracts/VenueMint.sol @@ -20,13 +20,20 @@ struct Ids { bool exists; } +struct Transferable { + bool transferable; + bool exists; +} + contract VenueMint is ERC1155Holder, ERC1155 { address private owner; // Deployer of contract (us) address private self; // The address of the contract (self) mapping (string => address payable) private event_to_vendor; // Mapping event descriptions to vendor wallets mapping (uint256 => uint256) private ticket_costs; // Mapping nft ids to cost + mapping (uint256 => uint256) private original_ticket_costs; // mapping nft ids to their original costs (ticket_costs gets zero'd on purchase) mapping (string => Ids) private event_to_ids; // Mapping event descriptions to NFT ids + mapping (uint256 => Transferable) private id_to_transferable; // Mapping ticket id to whether they are allowed to be transferred to another user uint256 last_id = 0; // The last id that we minted @@ -71,10 +78,15 @@ contract VenueMint is ERC1155Holder, ERC1155 { amounts[i - last_id] = 1; if (i < unique_seats) { ticket_costs[i] = costs[i - last_id]; + original_ticket_costs[i] = costs[i - last_id]; } else { ticket_costs[i] = costs[costs.length - 1]; + original_ticket_costs[i] = costs[costs.length - 1]; } + id_to_transferable[i].exists = true; + + /* if(i < unique_seats) { amounts[i] = 1; @@ -146,6 +158,7 @@ contract VenueMint is ERC1155Holder, ERC1155 { return result; } + // returns true if the description is available. false otherwise function is_description_available(string calldata description) public view returns (bool) { return !event_to_ids[description].exists; } @@ -185,6 +198,35 @@ contract VenueMint is ERC1155Holder, ERC1155 { return (true, total_cost); } + // this enables the input user to transfer the callers tickets + function allow_user_to_user_ticket_transfer(uint256 ticketid) public returns (bool) { + Transferable memory tmp = id_to_transferable[ticketid]; + require(tmp.exists, "The ticket id provided is not valid."); + + id_to_transferable[ticketid].transferable = true; + + console.log(self); + setApprovalForAll(self, true); + return true; + } + + function buy_ticket_from_user(address user, uint256 ticketid) payable public returns (bool) { + Transferable memory tmp = id_to_transferable[ticketid]; + + require(tmp.exists, "The ticket id provided is not valid."); + require(tmp.transferable, "This ticket id provided is not transferable."); + require(msg.value >= original_ticket_costs[ticketid],"You did not send enough money to purchase the ticket."); + + (bool success, ) = user.call{value:original_ticket_costs[ticketid]}(""); + require(success, "Failed to pay the user you are purchasing from."); + + _safeTransferFrom(user, msg.sender, ticketid, 1, ""); + + id_to_transferable[ticketid].transferable = false; + + return true; + } + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155, ERC1155Holder) returns (bool) { return super.supportsInterface(interfaceId); diff --git a/test/VenueMint.ts b/test/VenueMint.ts index f239593..6273967 100644 --- a/test/VenueMint.ts +++ b/test/VenueMint.ts @@ -148,6 +148,39 @@ describe("VenueMint", function () { expect(await contract.is_description_available("test")).to.equal(false); }) + describe("User to User Ticket Transfer", function () { + it("properly transfers tickets from user to user", async () => { + const init_contract = await loadFixture(deployOne); + + // make wallets + const nftholderwallet = ethers.Wallet.createRandom().connect(ethers.provider); + const nftholderaddress = await nftholderwallet.getAddress(); + const nftpurchaserwallet = ethers.Wallet.createRandom().connect(ethers.provider); + const nftpurchaseraddress = await nftpurchaserwallet.getAddress(); + + console.log(nftholderaddress); + console.log(nftpurchaseraddress); + + // give both money + ethers.provider.send("hardhat_setBalance", [nftholderaddress, "0xFFFFFFFFFFFFFFFFFFFFF"]); + ethers.provider.send("hardhat_setBalance", [nftpurchaseraddress, "0xFFFFFFFFFFFFFFFFFFFFF"]); + + const holder_instance = init_contract.connect(nftholderwallet); + const tmp = await holder_instance.create_new_event("test", "cool beans", 1, 0, [2000]); + + const resp = await holder_instance.buy_tickets("test", [0], {value: ethers.parseEther("1")}); + + const resp1 = await holder_instance.allow_user_to_user_ticket_transfer(0); + + const purchaser_instance = holder_instance.connect(nftpurchaserwallet); + + const resp2 = await purchaser_instance.buy_ticket_from_user(nftholderaddress,0, {value: ethers.parseEther("1")}); + + // console.log(resp2); + expect(resp2).to.equal(true); + }) + }) + describe("Vendor Payment Functionality", function () { // checks that the vendor recieves the funds from one transaction From 716a9dfff03c53e3755b3f7ce2279f959d787cee Mon Sep 17 00:00:00 2001 From: Christopher Canaday Date: Fri, 21 Feb 2025 21:01:19 -0500 Subject: [PATCH 2/5] working. need to add ability for seller to take away our approval --- contracts/VenueMint.sol | 1 - test/VenueMint.ts | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/contracts/VenueMint.sol b/contracts/VenueMint.sol index ebb3347..e166317 100644 --- a/contracts/VenueMint.sol +++ b/contracts/VenueMint.sol @@ -205,7 +205,6 @@ contract VenueMint is ERC1155Holder, ERC1155 { id_to_transferable[ticketid].transferable = true; - console.log(self); setApprovalForAll(self, true); return true; } diff --git a/test/VenueMint.ts b/test/VenueMint.ts index 6273967..afbff53 100644 --- a/test/VenueMint.ts +++ b/test/VenueMint.ts @@ -158,9 +158,6 @@ describe("VenueMint", function () { const nftpurchaserwallet = ethers.Wallet.createRandom().connect(ethers.provider); const nftpurchaseraddress = await nftpurchaserwallet.getAddress(); - console.log(nftholderaddress); - console.log(nftpurchaseraddress); - // give both money ethers.provider.send("hardhat_setBalance", [nftholderaddress, "0xFFFFFFFFFFFFFFFFFFFFF"]); ethers.provider.send("hardhat_setBalance", [nftpurchaseraddress, "0xFFFFFFFFFFFFFFFFFFFFF"]); @@ -176,8 +173,7 @@ describe("VenueMint", function () { const resp2 = await purchaser_instance.buy_ticket_from_user(nftholderaddress,0, {value: ethers.parseEther("1")}); - // console.log(resp2); - expect(resp2).to.equal(true); + expect(await purchaser_instance.balanceOf(nftpurchaseraddress, 0)).to.equal(1n); }) }) From 14f3fe093769f0df70578a213d877da74ad530eb Mon Sep 17 00:00:00 2001 From: Christopher Canaday Date: Sun, 23 Feb 2025 02:27:29 -0500 Subject: [PATCH 3/5] Added ability to revoke the contracts ability to control your tokens. Added extra check to make sure that the contract can transfer the NFT before trying to. Added event emission to the end of the buy function so the seller can wait for it and revoke the contracts control over their tokens. Updated testing to check event emission, event emission catching (and then deauthorizing the contract), and that payment is properly sent. --- contracts/VenueMint.sol | 13 ++++++++++++ test/VenueMint.ts | 44 ++++++++++++++++++++++++++++++++++------- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/contracts/VenueMint.sol b/contracts/VenueMint.sol index e166317..3fca1c1 100644 --- a/contracts/VenueMint.sol +++ b/contracts/VenueMint.sol @@ -41,6 +41,7 @@ contract VenueMint is ERC1155Holder, ERC1155 { event Event_Commencement(address indexed from, string description, string venue_URI, uint256 capacity); event Buy_Ticket_Event(string description, uint256 count); + event User_To_User_Transfer_Concluded(address indexed seller, address indexed buyer); // Set the owner to the deployer and self to the address of the contract constructor() ERC1155("https://onlytickets.co/api/tokens/{id}.json") { @@ -209,20 +210,32 @@ contract VenueMint is ERC1155Holder, ERC1155 { return true; } + // disables the contracts control of the senders tokens + function disallow_user_to_user_ticket_transfer() public { + setApprovalForAll(self, false); + } + + // this should be called buy the purchaser of the ticket function buy_ticket_from_user(address user, uint256 ticketid) payable public returns (bool) { Transferable memory tmp = id_to_transferable[ticketid]; + // we need to make sure this is a valid transfer require(tmp.exists, "The ticket id provided is not valid."); + require(isApprovedForAll(user, self), "The seller has not authorized a transfer."); require(tmp.transferable, "This ticket id provided is not transferable."); require(msg.value >= original_ticket_costs[ticketid],"You did not send enough money to purchase the ticket."); + // send the money to the user (bool success, ) = user.call{value:original_ticket_costs[ticketid]}(""); require(success, "Failed to pay the user you are purchasing from."); + // transfer the nft to the purchaser _safeTransferFrom(user, msg.sender, ticketid, 1, ""); id_to_transferable[ticketid].transferable = false; + emit User_To_User_Transfer_Concluded(user, msg.sender); + return true; } diff --git a/test/VenueMint.ts b/test/VenueMint.ts index afbff53..0e449c2 100644 --- a/test/VenueMint.ts +++ b/test/VenueMint.ts @@ -3,6 +3,7 @@ import { contracts } from "../typechain-types"; import { expect } from "chai"; import hre, { ethers } from "hardhat"; import { bigint } from "hardhat/internal/core/params/argumentTypes"; +import { utils } from "../typechain-types/@openzeppelin/contracts"; // overall testing framework describe("VenueMint", function () { @@ -151,29 +152,58 @@ describe("VenueMint", function () { describe("User to User Ticket Transfer", function () { it("properly transfers tickets from user to user", async () => { const init_contract = await loadFixture(deployOne); - + // make wallets const nftholderwallet = ethers.Wallet.createRandom().connect(ethers.provider); const nftholderaddress = await nftholderwallet.getAddress(); const nftpurchaserwallet = ethers.Wallet.createRandom().connect(ethers.provider); const nftpurchaseraddress = await nftpurchaserwallet.getAddress(); - + // give both money ethers.provider.send("hardhat_setBalance", [nftholderaddress, "0xFFFFFFFFFFFFFFFFFFFFF"]); ethers.provider.send("hardhat_setBalance", [nftpurchaseraddress, "0xFFFFFFFFFFFFFFFFFFFFF"]); - + + // make the holder instance and create the event const holder_instance = init_contract.connect(nftholderwallet); const tmp = await holder_instance.create_new_event("test", "cool beans", 1, 0, [2000]); - + + // buy the tickets and give the contract permission to control the holders tokens const resp = await holder_instance.buy_tickets("test", [0], {value: ethers.parseEther("1")}); - const resp1 = await holder_instance.allow_user_to_user_ticket_transfer(0); - + + // variable to check that we removed the contracts ability to control our tokens + // a filter that allows us to only execute the function based on the holder and purchaser address + var disallow_ran = false; + const holderfilter = init_contract.filters.User_To_User_Transfer_Concluded(nftholderaddress, nftpurchaseraddress); + + // this is here purely for example (it is not necessary for our tests) + // setup a listener that removes the contracts ability to manage the holders tokens + holder_instance.on(holderfilter, async (response) => { + console.log("Revoking contract access."); + await holder_instance.disallow_user_to_user_ticket_transfer(); + disallow_ran = true; + }); + + // create the purchaser instance const purchaser_instance = holder_instance.connect(nftpurchaserwallet); - const resp2 = await purchaser_instance.buy_ticket_from_user(nftholderaddress,0, {value: ethers.parseEther("1")}); + const holder_balance_before = await ethers.provider.getBalance(nftholderaddress); + // make sure that the ticket buying function emits the correct events + // make sure that the ticket was properly transfered from the two wallets + expect(await purchaser_instance.buy_ticket_from_user(nftholderaddress,0, {value: ethers.parseEther("1")})).to.emit(purchaser_instance, "User_To_User_Transfer_Concluded").withArgs(nftholderaddress, nftpurchaseraddress); expect(await purchaser_instance.balanceOf(nftpurchaseraddress, 0)).to.equal(1n); + + // make sure the seller got the correct amount of money + const holder_balance_after = await ethers.provider.getBalance(nftholderaddress); + expect(holder_balance_after - holder_balance_before).to.equal(2000); + + // wait 5 seconds this is required for this to work right + // explanation here: https://stackoverflow.com/questions/68432609/contract-event-listener-is-not-firing-when-running-hardhat-tests-with-ethers-js + await new Promise(res => setTimeout(() => res(null), 5000)); + + // make sure the listener caught the event + expect(disallow_ran).to.equal(true); }) }) From 4ca0a93c82f54afcd8d7b846d705e7e16007c1eb Mon Sep 17 00:00:00 2001 From: Christopher Canaday Date: Mon, 24 Feb 2025 17:13:28 -0500 Subject: [PATCH 4/5] get_event_ids now returns extra data --- contracts/VenueMint.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/VenueMint.sol b/contracts/VenueMint.sol index 3fca1c1..7fc153c 100644 --- a/contracts/VenueMint.sol +++ b/contracts/VenueMint.sol @@ -132,7 +132,7 @@ contract VenueMint is ERC1155Holder, ERC1155 { } // returns a list of all valid NFT ids for the event - function get_event_ids(string calldata description) public view returns (uint256[] memory) { + function get_event_ids(string calldata description) public view returns (uint256[] memory, Ids memory) { Ids memory tmp = event_to_ids[description]; uint256 count = 0; @@ -156,7 +156,7 @@ contract VenueMint is ERC1155Holder, ERC1155 { } } - return result; + return (result, tmp); } // returns true if the description is available. false otherwise From da6e4037d323a3bb3a4f281c18783fa4a76b9a3f Mon Sep 17 00:00:00 2001 From: Christopher Canaday Date: Mon, 24 Feb 2025 17:29:51 -0500 Subject: [PATCH 5/5] fixing some testing issues --- test/VenueMint.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/VenueMint.ts b/test/VenueMint.ts index 0e449c2..b45efa9 100644 --- a/test/VenueMint.ts +++ b/test/VenueMint.ts @@ -119,7 +119,7 @@ describe("VenueMint", function () { const tmp = await contract.create_new_event("test", "0xblahblah", 5, 0, [5,5,5,5,5]); // check that it returns properly for a valid and non valid event - expect(await contract.get_event_ids("test")).to.eql(Array(0n,1n,2n,3n,4n)); + expect((await contract.get_event_ids("test"))[0]).to.eql(Array(0n,1n,2n,3n,4n)); expect(contract.get_event_ids("")).to.rejectedWith(Error); }) @@ -134,7 +134,7 @@ describe("VenueMint", function () { const user_contract_instance = contract.connect(user); await user_contract_instance.buy_tickets("test", [0], {value: ethers.parseEther("1")}); - expect(await contract.get_event_ids("test")).to.eql(Array(1n,2n,3n,4n)) + expect((await contract.get_event_ids("test"))[0]).to.eql(Array(1n,2n,3n,4n)) }) it("will validate the description", async () => { @@ -179,7 +179,6 @@ describe("VenueMint", function () { // this is here purely for example (it is not necessary for our tests) // setup a listener that removes the contracts ability to manage the holders tokens holder_instance.on(holderfilter, async (response) => { - console.log("Revoking contract access."); await holder_instance.disallow_user_to_user_ticket_transfer(); disallow_ran = true; }); @@ -526,7 +525,7 @@ describe("VenueMint", function () { //console.log(event_data); let num_tickets_to_purchase = genRandom(1, event_data['number_tickets']); - let tickets_to_purchase = [...(await client_contract.get_event_ids(event_data['event_name'])).slice(0, num_tickets_to_purchase)]; + let tickets_to_purchase = [...(await client_contract.get_event_ids(event_data['event_name']))[0].slice(0, num_tickets_to_purchase)]; let local_cost = num_tickets_to_purchase * event_data['ticket_cost']; // update total for total money vendor should have