diff --git a/contracts/VenueMint.sol b/contracts/VenueMint.sol index 097a8bc..7fc153c 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 @@ -34,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") { @@ -71,10 +79,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; @@ -119,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; @@ -143,9 +156,10 @@ contract VenueMint is ERC1155Holder, ERC1155 { } } - return result; + return (result, tmp); } + // 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 +199,46 @@ 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; + + setApprovalForAll(self, true); + 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; + } + 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..b45efa9 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 () { @@ -118,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); }) @@ -133,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 () => { @@ -148,6 +149,63 @@ 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(); + + // 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) => { + await holder_instance.disallow_user_to_user_ticket_transfer(); + disallow_ran = true; + }); + + // create the purchaser instance + const purchaser_instance = holder_instance.connect(nftpurchaserwallet); + + 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); + }) + }) + describe("Vendor Payment Functionality", function () { // checks that the vendor recieves the funds from one transaction @@ -467,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