diff --git a/contracts/Marketplace.sol b/contracts/Marketplace.sol index 21aa466..f7a083a 100644 --- a/contracts/Marketplace.sol +++ b/contracts/Marketplace.sol @@ -7,11 +7,12 @@ import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import "./Configuration.sol"; import "./Requests.sol"; import "./Proofs.sol"; +import "./SlotReservations.sol"; import "./StateRetrieval.sol"; import "./Endian.sol"; import "./Groth16.sol"; -contract Marketplace is Proofs, StateRetrieval, Endian { +contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian { using EnumerableSet for EnumerableSet.Bytes32Set; using Requests for Request; diff --git a/contracts/SlotReservations.sol b/contracts/SlotReservations.sol new file mode 100644 index 0000000..d1bfd12 --- /dev/null +++ b/contracts/SlotReservations.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import "./Requests.sol"; + +contract SlotReservations { + using EnumerableSet for EnumerableSet.AddressSet; + + mapping(SlotId => EnumerableSet.AddressSet) private _reservations; + + uint8 private constant _MAX_RESERVATIONS = 3; + + function reserveSlot(SlotId slotId, address host) public returns (bool) { + require(canReserveSlot(slotId, host), "Reservation not allowed"); + // returns false if set already contains address + return _reservations[slotId].add(host); + } + + function canReserveSlot( + SlotId slotId, + address host + ) public view returns (bool) { + return + // TODO: add in check for address inside of expanding window + (_reservations[slotId].length() < _MAX_RESERVATIONS) && + (!_reservations[slotId].contains(host)); + } +} diff --git a/test/SlotReservations.test.js b/test/SlotReservations.test.js new file mode 100644 index 0000000..d82f9f1 --- /dev/null +++ b/test/SlotReservations.test.js @@ -0,0 +1,73 @@ +const { expect } = require("chai") +const { ethers } = require("hardhat") +const { exampleRequest, exampleAddress } = require("./examples") +const { requestId, slotId } = require("./ids") + +describe("SlotReservations", function () { + let reservations + let provider, address1, address2, address3 + let request + let slot + + beforeEach(async function () { + let SlotReservations = await ethers.getContractFactory("SlotReservations") + reservations = await SlotReservations.deploy() + + provider = exampleAddress() + address1 = exampleAddress() + address2 = exampleAddress() + address3 = exampleAddress() + + request = await exampleRequest() + request.client = exampleAddress() + + slot = { + request: requestId(request), + index: request.ask.slots / 2, + } + }) + + it("allows a slot to be reserved", async function () { + let reserved = await reservations.callStatic.reserveSlot( + slotId(slot), + provider + ) + expect(reserved).to.be.true + }) + + it("reports a slot can be reserved", async function () { + expect(await reservations.canReserveSlot(slotId(slot), provider)).to.be.true + }) + + it("cannot reserve a slot more than once", async function () { + let id = slotId(slot) + await reservations.reserveSlot(id, provider) + await expect(reservations.reserveSlot(id, provider)).to.be.revertedWith( + "Reservation not allowed" + ) + }) + + it("reports a slot cannot be reserved if already reserved", async function () { + let id = slotId(slot) + await reservations.reserveSlot(id, provider) + expect(await reservations.canReserveSlot(id, provider)).to.be.false + }) + + it("cannot reserve a slot if reservations are at capacity", async function () { + let id = slotId(slot) + await reservations.reserveSlot(id, address1) + await reservations.reserveSlot(id, address2) + await reservations.reserveSlot(id, address3) + await expect(reservations.reserveSlot(id, provider)).to.be.revertedWith( + "Reservation not allowed" + ) + }) + + it("reports a slot cannot be reserved if reservations are at capacity", async function () { + let id = slotId(slot) + await reservations.reserveSlot(id, address1) + await reservations.reserveSlot(id, address2) + await reservations.reserveSlot(id, address3) + expect(await reservations.canReserveSlot(id, provider)).to.be.false + }) +}) diff --git a/test/examples.js b/test/examples.js index 06d8428..73900a3 100644 --- a/test/examples.js +++ b/test/examples.js @@ -51,9 +51,12 @@ const invalidProof = () => ({ c: { x: 0, y: 0 }, }) +const exampleAddress = () => hexlify(randomBytes(20)) + module.exports = { exampleConfiguration, exampleRequest, exampleProof, invalidProof, + exampleAddress, }