clean up tests
1. Replace all instances of `now()` with `await currentTime()` to get a more accurate representation of time from the block timestamp. Update examples.js to be async. 2. Move `RequestState` to `marketplace.js` 3. Delete `TestStorage` as `slashAmount` function no longer needed.
This commit is contained in:
parent
ad040cfee6
commit
321132b6fa
|
@ -49,7 +49,6 @@ contract AccountLocks {
|
||||||
function _extendLockExpiryTo(bytes32 lockId, uint256 expiry) internal {
|
function _extendLockExpiryTo(bytes32 lockId, uint256 expiry) internal {
|
||||||
Lock storage lock = locks[lockId];
|
Lock storage lock = locks[lockId];
|
||||||
require(lock.owner != address(0), "Lock does not exist");
|
require(lock.owner != address(0), "Lock does not exist");
|
||||||
// require(lock.owner == msg.sender, "Only lock creator can extend expiry");
|
|
||||||
require(lock.expiry >= block.timestamp, "Lock already expired");
|
require(lock.expiry >= block.timestamp, "Lock already expired");
|
||||||
lock.expiry = expiry;
|
lock.expiry = expiry;
|
||||||
}
|
}
|
||||||
|
|
|
@ -232,7 +232,7 @@ contract Marketplace is Collateral, Proofs {
|
||||||
|
|
||||||
function proofEnd(bytes32 slotId) public view returns (uint256) {
|
function proofEnd(bytes32 slotId) public view returns (uint256) {
|
||||||
Slot memory slot = _slot(slotId);
|
Slot memory slot = _slot(slotId);
|
||||||
uint256 end = _end(slotId);
|
uint256 end = _end(slot.requestId);
|
||||||
if (!_slotAcceptsProofs(slotId)) {
|
if (!_slotAcceptsProofs(slotId)) {
|
||||||
return end < block.timestamp ? end : block.timestamp - 1;
|
return end < block.timestamp ? end : block.timestamp - 1;
|
||||||
}
|
}
|
||||||
|
@ -263,7 +263,7 @@ contract Marketplace is Collateral, Proofs {
|
||||||
// TODO: add check for _isFinished
|
// TODO: add check for _isFinished
|
||||||
if (_isCancelled(requestId)) {
|
if (_isCancelled(requestId)) {
|
||||||
return RequestState.Cancelled;
|
return RequestState.Cancelled;
|
||||||
else if (_isFinished(requestId) {
|
} else if (_isFinished(requestId)) {
|
||||||
return RequestState.Finished;
|
return RequestState.Finished;
|
||||||
} else {
|
} else {
|
||||||
RequestContext storage context = _context(requestId);
|
RequestContext storage context = _context(requestId);
|
||||||
|
@ -361,15 +361,6 @@ contract Marketplace is Collateral, Proofs {
|
||||||
assert(funds.received == funds.balance + funds.sent);
|
assert(funds.received == funds.balance + funds.sent);
|
||||||
}
|
}
|
||||||
|
|
||||||
function acceptsProofs(bytes32 requestId) private view {
|
|
||||||
RequestState s = state(requestId);
|
|
||||||
require(s == RequestState.New || s == RequestState.Started, "Invalid state");
|
|
||||||
// must test these states separately as they handle cases where the state hasn't
|
|
||||||
// yet been updated by a transaction
|
|
||||||
require(!_isCancelled(requestId), "Request cancelled");
|
|
||||||
require(!_isFinished(requestId), "Request finished");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Modifier that requires the request state to be that which is accepting proof submissions from hosts occupying slots.
|
/// @notice Modifier that requires the request state to be that which is accepting proof submissions from hosts occupying slots.
|
||||||
/// @dev Request state must be new or started, and must not be cancelled, finished, or failed.
|
/// @dev Request state must be new or started, and must not be cancelled, finished, or failed.
|
||||||
/// @param slotId id of the slot, that is mapped to a request, for which to obtain state info
|
/// @param slotId id of the slot, that is mapped to a request, for which to obtain state info
|
||||||
|
|
|
@ -82,6 +82,7 @@ contract Storage is Collateral, Marketplace {
|
||||||
|
|
||||||
function markProofAsMissing(bytes32 slotId, uint256 period)
|
function markProofAsMissing(bytes32 slotId, uint256 period)
|
||||||
public
|
public
|
||||||
|
slotMustAcceptProofs(slotId)
|
||||||
{
|
{
|
||||||
_markProofAsMissing(slotId, period);
|
_markProofAsMissing(slotId, period);
|
||||||
address host = _host(slotId);
|
address host = _host(slotId);
|
||||||
|
|
|
@ -37,7 +37,7 @@ contract TestMarketplace is Marketplace {
|
||||||
function testAcceptsProofs(bytes32 slotId)
|
function testAcceptsProofs(bytes32 slotId)
|
||||||
public
|
public
|
||||||
view
|
view
|
||||||
slotAcceptsProofs(slotId)
|
slotMustAcceptProofs(slotId)
|
||||||
// solhint-disable-next-line no-empty-blocks
|
// solhint-disable-next-line no-empty-blocks
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
pragma solidity ^0.8.0;
|
|
||||||
|
|
||||||
import "./Storage.sol";
|
|
||||||
|
|
||||||
// exposes internal functions of Storage for testing
|
|
||||||
contract TestStorage is Storage {
|
|
||||||
constructor(
|
|
||||||
IERC20 token,
|
|
||||||
uint256 _proofPeriod,
|
|
||||||
uint256 _proofTimeout,
|
|
||||||
uint8 _proofDowntime,
|
|
||||||
uint256 _collateralAmount,
|
|
||||||
uint256 _slashMisses,
|
|
||||||
uint256 _slashPercentage,
|
|
||||||
uint256 _minCollateralThreshold
|
|
||||||
)
|
|
||||||
Storage(
|
|
||||||
token,
|
|
||||||
_proofPeriod,
|
|
||||||
_proofTimeout,
|
|
||||||
_proofDowntime,
|
|
||||||
_collateralAmount,
|
|
||||||
_slashMisses,
|
|
||||||
_slashPercentage,
|
|
||||||
_minCollateralThreshold
|
|
||||||
)
|
|
||||||
// solhint-disable-next-line no-empty-blocks
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function slashAmount(address account, uint256 percentage) public view returns (uint256) {
|
|
||||||
return _slashAmount(account, percentage);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -18,7 +18,7 @@ async function deployStorage({ deployments, getNamedAccounts }) {
|
||||||
minCollateralThreshold,
|
minCollateralThreshold,
|
||||||
]
|
]
|
||||||
const { deployer } = await getNamedAccounts()
|
const { deployer } = await getNamedAccounts()
|
||||||
await deployments.deploy("TestStorage", { args, from: deployer })
|
await deployments.deploy("Storage", { args, from: deployer })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mine256blocks({ network, ethers }) {
|
async function mine256blocks({ network, ethers }) {
|
||||||
|
@ -32,5 +32,5 @@ module.exports = async (environment) => {
|
||||||
await deployStorage(environment)
|
await deployStorage(environment)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.tags = ["TestStorage"]
|
module.exports.tags = ["Storage"]
|
||||||
module.exports.dependencies = ["TestToken"]
|
module.exports.dependencies = ["TestToken"]
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
const { ethers } = require("hardhat")
|
const { ethers } = require("hardhat")
|
||||||
const { expect } = require("chai")
|
const { expect } = require("chai")
|
||||||
const { hexlify, randomBytes, toHexString } = ethers.utils
|
const { hexlify, randomBytes, toHexString } = ethers.utils
|
||||||
const { advanceTimeTo, snapshot, revert, advanceTime } = require("./evm")
|
const {
|
||||||
|
advanceTimeTo,
|
||||||
|
snapshot,
|
||||||
|
revert,
|
||||||
|
advanceTime,
|
||||||
|
currentTime,
|
||||||
|
} = require("./evm")
|
||||||
const { exampleLock } = require("./examples")
|
const { exampleLock } = require("./examples")
|
||||||
const { now, hours } = require("./time")
|
const { hours } = require("./time")
|
||||||
const { waitUntilExpired } = require("./marketplace")
|
const { waitUntilCancelled } = require("./marketplace")
|
||||||
|
|
||||||
describe("Account Locks", function () {
|
describe("Account Locks", function () {
|
||||||
let locks
|
let locks
|
||||||
|
@ -16,12 +22,12 @@ describe("Account Locks", function () {
|
||||||
|
|
||||||
describe("creating a lock", function () {
|
describe("creating a lock", function () {
|
||||||
it("allows creation of a lock with an expiry time", async function () {
|
it("allows creation of a lock with an expiry time", async function () {
|
||||||
let { id, expiry } = exampleLock()
|
let { id, expiry } = await exampleLock()
|
||||||
await locks.createLock(id, expiry)
|
await locks.createLock(id, expiry)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("fails to create a lock with an existing id", async function () {
|
it("fails to create a lock with an existing id", async function () {
|
||||||
let { id, expiry } = exampleLock()
|
let { id, expiry } = await exampleLock()
|
||||||
await locks.createLock(id, expiry)
|
await locks.createLock(id, expiry)
|
||||||
await expect(locks.createLock(id, expiry + 1)).to.be.revertedWith(
|
await expect(locks.createLock(id, expiry + 1)).to.be.revertedWith(
|
||||||
"Lock already exists"
|
"Lock already exists"
|
||||||
|
@ -33,7 +39,7 @@ describe("Account Locks", function () {
|
||||||
let lock
|
let lock
|
||||||
|
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
lock = exampleLock()
|
lock = await exampleLock()
|
||||||
await locks.createLock(lock.id, lock.expiry)
|
await locks.createLock(lock.id, lock.expiry)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -44,7 +50,7 @@ describe("Account Locks", function () {
|
||||||
|
|
||||||
it("fails to lock account when lock does not exist", async function () {
|
it("fails to lock account when lock does not exist", async function () {
|
||||||
let [account] = await ethers.getSigners()
|
let [account] = await ethers.getSigners()
|
||||||
let nonexistent = exampleLock().id
|
let nonexistent = (await exampleLock()).id
|
||||||
await expect(locks.lock(account.address, nonexistent)).to.be.revertedWith(
|
await expect(locks.lock(account.address, nonexistent)).to.be.revertedWith(
|
||||||
"Lock does not exist"
|
"Lock does not exist"
|
||||||
)
|
)
|
||||||
|
@ -55,7 +61,7 @@ describe("Account Locks", function () {
|
||||||
let lock
|
let lock
|
||||||
|
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
lock = exampleLock()
|
lock = await exampleLock()
|
||||||
await locks.createLock(lock.id, lock.expiry)
|
await locks.createLock(lock.id, lock.expiry)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -64,7 +70,7 @@ describe("Account Locks", function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("fails to unlock a lock that does not exist", async function () {
|
it("fails to unlock a lock that does not exist", async function () {
|
||||||
let nonexistent = exampleLock().id
|
let nonexistent = (await exampleLock()).id
|
||||||
await expect(locks.unlock(nonexistent)).to.be.revertedWith(
|
await expect(locks.unlock(nonexistent)).to.be.revertedWith(
|
||||||
"Lock does not exist"
|
"Lock does not exist"
|
||||||
)
|
)
|
||||||
|
@ -85,7 +91,7 @@ describe("Account Locks", function () {
|
||||||
|
|
||||||
it("unlocks an account whose locks have been unlocked", async function () {
|
it("unlocks an account whose locks have been unlocked", async function () {
|
||||||
let [account] = await ethers.getSigners()
|
let [account] = await ethers.getSigners()
|
||||||
let lock = exampleLock()
|
let lock = await exampleLock()
|
||||||
await locks.createLock(lock.id, lock.expiry)
|
await locks.createLock(lock.id, lock.expiry)
|
||||||
await locks.lock(account.address, lock.id)
|
await locks.lock(account.address, lock.id)
|
||||||
await locks.unlock(lock.id)
|
await locks.unlock(lock.id)
|
||||||
|
@ -94,7 +100,7 @@ describe("Account Locks", function () {
|
||||||
|
|
||||||
it("unlocks an account whose locks have expired", async function () {
|
it("unlocks an account whose locks have expired", async function () {
|
||||||
let [account] = await ethers.getSigners()
|
let [account] = await ethers.getSigners()
|
||||||
let lock = { ...exampleLock(), expiry: now() }
|
let lock = { ...(await exampleLock()), expiry: currentTime() }
|
||||||
await locks.createLock(lock.id, lock.expiry)
|
await locks.createLock(lock.id, lock.expiry)
|
||||||
await locks.lock(account.address, lock.id)
|
await locks.lock(account.address, lock.id)
|
||||||
await locks.unlockAccount()
|
await locks.unlockAccount()
|
||||||
|
@ -102,7 +108,7 @@ describe("Account Locks", function () {
|
||||||
|
|
||||||
it("unlocks multiple accounts tied to the same lock", async function () {
|
it("unlocks multiple accounts tied to the same lock", async function () {
|
||||||
let [account0, account1] = await ethers.getSigners()
|
let [account0, account1] = await ethers.getSigners()
|
||||||
let lock = exampleLock()
|
let lock = await exampleLock()
|
||||||
await locks.createLock(lock.id, lock.expiry)
|
await locks.createLock(lock.id, lock.expiry)
|
||||||
await locks.lock(account0.address, lock.id)
|
await locks.lock(account0.address, lock.id)
|
||||||
await locks.lock(account1.address, lock.id)
|
await locks.lock(account1.address, lock.id)
|
||||||
|
@ -113,7 +119,7 @@ describe("Account Locks", function () {
|
||||||
|
|
||||||
it("fails to unlock when some locks are still locked", async function () {
|
it("fails to unlock when some locks are still locked", async function () {
|
||||||
let [account] = await ethers.getSigners()
|
let [account] = await ethers.getSigners()
|
||||||
let [lock1, lock2] = [exampleLock(), exampleLock()]
|
let [lock1, lock2] = [await exampleLock(), await exampleLock()]
|
||||||
await locks.createLock(lock1.id, lock1.expiry)
|
await locks.createLock(lock1.id, lock1.expiry)
|
||||||
await locks.createLock(lock2.id, lock2.expiry)
|
await locks.createLock(lock2.id, lock2.expiry)
|
||||||
await locks.lock(account.address, lock1.id)
|
await locks.lock(account.address, lock1.id)
|
||||||
|
@ -133,7 +139,7 @@ describe("Account Locks", function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
async function addLock() {
|
async function addLock() {
|
||||||
let { id, expiry } = exampleLock()
|
let { id, expiry } = await exampleLock()
|
||||||
await locks.createLock(id, expiry)
|
await locks.createLock(id, expiry)
|
||||||
await locks.lock(account.address, id)
|
await locks.lock(account.address, id)
|
||||||
return id
|
return id
|
||||||
|
@ -168,15 +174,18 @@ describe("Account Locks", function () {
|
||||||
|
|
||||||
describe("extend lock expiry", function () {
|
describe("extend lock expiry", function () {
|
||||||
let expiry
|
let expiry
|
||||||
|
let newExpiry
|
||||||
let id
|
let id
|
||||||
|
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
await snapshot()
|
await snapshot()
|
||||||
|
|
||||||
let lock = exampleLock()
|
let lock = await exampleLock()
|
||||||
id = lock.id
|
id = lock.id
|
||||||
expiry = lock.expiry
|
expiry = lock.expiry
|
||||||
await locks.createLock(id, expiry)
|
await locks.createLock(id, expiry)
|
||||||
|
newExpiry = (await currentTime()) + hours(1)
|
||||||
|
|
||||||
let [account] = await ethers.getSigners()
|
let [account] = await ethers.getSigners()
|
||||||
await locks.lock(account.address, id)
|
await locks.lock(account.address, id)
|
||||||
})
|
})
|
||||||
|
@ -186,30 +195,21 @@ describe("Account Locks", function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("fails when lock id doesn't exist", async function () {
|
it("fails when lock id doesn't exist", async function () {
|
||||||
let other = exampleLock()
|
let other = await exampleLock()
|
||||||
await expect(
|
await expect(
|
||||||
locks.extendLockExpiryTo(other.id, now() + hours(1))
|
locks.extendLockExpiryTo(other.id, newExpiry)
|
||||||
).to.be.revertedWith("Lock does not exist")
|
).to.be.revertedWith("Lock does not exist")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("fails when lock is already expired", async function () {
|
it("fails when lock is already expired", async function () {
|
||||||
waitUntilExpired(expiry)
|
await waitUntilCancelled(expiry)
|
||||||
await expect(locks.extendLockExpiry(id, hours(1))).to.be.revertedWith(
|
await expect(locks.extendLockExpiryTo(id, newExpiry)).to.be.revertedWith(
|
||||||
"Lock already expired"
|
"Lock already expired"
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("successfully updates lock expiry", async function () {
|
it("successfully updates lock expiry", async function () {
|
||||||
await expect(locks.extendLockExpiryTo(id, now() + hours(1))).not.to.be
|
await expect(locks.extendLockExpiryTo(id, newExpiry)).not.to.be.reverted
|
||||||
.reverted
|
|
||||||
})
|
|
||||||
|
|
||||||
it("unlocks account after expiry", async function () {
|
|
||||||
await expect(locks.extendLockExpiryTo(id, now() + hours(1))).not.to.be
|
|
||||||
.reverted
|
|
||||||
await expect(locks.unlockAccount()).to.be.revertedWith("Account locked")
|
|
||||||
advanceTime(hours(1))
|
|
||||||
await expect(locks.unlockAccount()).not.to.be.reverted
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -97,7 +97,7 @@ describe("Collateral", function () {
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
await token.approve(collateral.address, 42)
|
await token.approve(collateral.address, 42)
|
||||||
await collateral.deposit(42)
|
await collateral.deposit(42)
|
||||||
lock = exampleLock()
|
lock = await exampleLock()
|
||||||
await collateral.createLock(lock.id, lock.expiry)
|
await collateral.createLock(lock.id, lock.expiry)
|
||||||
await collateral.lock(account0.address, lock.id)
|
await collateral.lock(account0.address, lock.id)
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,13 +2,14 @@ const { ethers } = require("hardhat")
|
||||||
const { hexlify, randomBytes } = ethers.utils
|
const { hexlify, randomBytes } = ethers.utils
|
||||||
const { expect } = require("chai")
|
const { expect } = require("chai")
|
||||||
const { exampleRequest } = require("./examples")
|
const { exampleRequest } = require("./examples")
|
||||||
const { now, hours, minutes } = require("./time")
|
const { hours, minutes } = require("./time")
|
||||||
const { requestId, slotId, askToArray } = require("./ids")
|
const { requestId, slotId, askToArray } = require("./ids")
|
||||||
const { waitUntilExpired, waitUntilAllSlotsFilled } = require("./marketplace")
|
const {
|
||||||
waitUntilCancelled,
|
waitUntilCancelled,
|
||||||
waitUntilStarted,
|
waitUntilStarted,
|
||||||
waitUntilFinished,
|
waitUntilFinished,
|
||||||
waitUntilFailed,
|
waitUntilFailed,
|
||||||
|
RequestState
|
||||||
} = require("./marketplace")
|
} = require("./marketplace")
|
||||||
const { price, pricePerSlot } = require("./price")
|
const { price, pricePerSlot } = require("./price")
|
||||||
const {
|
const {
|
||||||
|
@ -16,7 +17,7 @@ const {
|
||||||
revert,
|
revert,
|
||||||
ensureMinimumBlockHeight,
|
ensureMinimumBlockHeight,
|
||||||
advanceTime,
|
advanceTime,
|
||||||
currentTime
|
currentTime,
|
||||||
} = require("./evm")
|
} = require("./evm")
|
||||||
|
|
||||||
describe("Marketplace", function () {
|
describe("Marketplace", function () {
|
||||||
|
@ -53,7 +54,7 @@ describe("Marketplace", function () {
|
||||||
proofDowntime
|
proofDowntime
|
||||||
)
|
)
|
||||||
|
|
||||||
request = exampleRequest()
|
request = await exampleRequest()
|
||||||
request.client = client.address
|
request.client = client.address
|
||||||
|
|
||||||
slot = {
|
slot = {
|
||||||
|
@ -108,80 +109,6 @@ describe("Marketplace", function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("freeing a slot", function () {
|
|
||||||
var id
|
|
||||||
beforeEach(async function () {
|
|
||||||
slot.index = 0
|
|
||||||
id = slotId(slot)
|
|
||||||
|
|
||||||
switchAccount(client)
|
|
||||||
await token.approve(marketplace.address, price(request))
|
|
||||||
await marketplace.requestStorage(request)
|
|
||||||
switchAccount(host)
|
|
||||||
await token.approve(marketplace.address, collateral)
|
|
||||||
await marketplace.deposit(collateral)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("fails to free slot when slot not filled", async function () {
|
|
||||||
slot.index = 5
|
|
||||||
let nonExistentId = slotId(slot)
|
|
||||||
await expect(marketplace.freeSlot(nonExistentId)).to.be.revertedWith(
|
|
||||||
"Slot empty"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("fails to free slot when not started", async function () {
|
|
||||||
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
||||||
await expect(marketplace.freeSlot(id)).to.be.revertedWith("Invalid state")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("fails to free slot when finished", async function () {
|
|
||||||
await waitUntilStarted(
|
|
||||||
marketplace,
|
|
||||||
request.ask.slots,
|
|
||||||
slot.request,
|
|
||||||
proof
|
|
||||||
)
|
|
||||||
await waitUntilFinished(marketplace, slotId(slot))
|
|
||||||
await expect(marketplace.freeSlot(id)).to.be.revertedWith(
|
|
||||||
"Request finished"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("successfully frees slot", async function () {
|
|
||||||
await waitUntilStarted(
|
|
||||||
marketplace,
|
|
||||||
request.ask.slots,
|
|
||||||
slot.request,
|
|
||||||
proof
|
|
||||||
)
|
|
||||||
await expect(marketplace.freeSlot(id)).not.to.be.reverted
|
|
||||||
})
|
|
||||||
|
|
||||||
it("emits event once slot is freed", async function () {
|
|
||||||
await waitUntilStarted(
|
|
||||||
marketplace,
|
|
||||||
request.ask.slots,
|
|
||||||
slot.request,
|
|
||||||
proof
|
|
||||||
)
|
|
||||||
await expect(await marketplace.freeSlot(id))
|
|
||||||
.to.emit(marketplace, "SlotFreed")
|
|
||||||
.withArgs(slot.request, id)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("cannot get slot once freed", async function () {
|
|
||||||
await waitUntilStarted(
|
|
||||||
marketplace,
|
|
||||||
request.ask.slots,
|
|
||||||
slot.request,
|
|
||||||
proof
|
|
||||||
)
|
|
||||||
await marketplace.freeSlot(id)
|
|
||||||
await expect(marketplace.slot(id)).to.be.revertedWith("Slot empty")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("filling a slot", function () {
|
describe("filling a slot", function () {
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
switchAccount(client)
|
switchAccount(client)
|
||||||
|
@ -233,7 +160,7 @@ describe("Marketplace", function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("is rejected when request is unknown", async function () {
|
it("is rejected when request is unknown", async function () {
|
||||||
let unknown = exampleRequest()
|
let unknown = await exampleRequest()
|
||||||
await expect(
|
await expect(
|
||||||
marketplace.fillSlot(requestId(unknown), 0, proof)
|
marketplace.fillSlot(requestId(unknown), 0, proof)
|
||||||
).to.be.revertedWith("Unknown request")
|
).to.be.revertedWith("Unknown request")
|
||||||
|
@ -241,7 +168,7 @@ describe("Marketplace", function () {
|
||||||
|
|
||||||
it("is rejected when request is cancelled", async function () {
|
it("is rejected when request is cancelled", async function () {
|
||||||
switchAccount(client)
|
switchAccount(client)
|
||||||
let expired = { ...request, expiry: now() - hours(1) }
|
let expired = { ...request, expiry: (await currentTime()) - hours(1) }
|
||||||
await token.approve(marketplace.address, price(request))
|
await token.approve(marketplace.address, price(request))
|
||||||
await marketplace.requestStorage(expired)
|
await marketplace.requestStorage(expired)
|
||||||
switchAccount(host)
|
switchAccount(host)
|
||||||
|
@ -260,7 +187,7 @@ describe("Marketplace", function () {
|
||||||
await waitUntilFinished(marketplace, slotId(slot))
|
await waitUntilFinished(marketplace, slotId(slot))
|
||||||
await expect(
|
await expect(
|
||||||
marketplace.fillSlot(slot.request, slot.index, proof)
|
marketplace.fillSlot(slot.request, slot.index, proof)
|
||||||
).to.be.revertedWith("Request finished")
|
).to.be.revertedWith("Request not accepting proofs")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("is rejected when request is failed", async function () {
|
it("is rejected when request is failed", async function () {
|
||||||
|
@ -273,7 +200,7 @@ describe("Marketplace", function () {
|
||||||
await waitUntilFailed(marketplace, slot, request.ask.maxSlotLoss)
|
await waitUntilFailed(marketplace, slot, request.ask.maxSlotLoss)
|
||||||
await expect(
|
await expect(
|
||||||
marketplace.fillSlot(slot.request, slot.index, proof)
|
marketplace.fillSlot(slot.request, slot.index, proof)
|
||||||
).to.be.revertedWith("Invalid state")
|
).to.be.revertedWith("Request not accepting proofs")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("is rejected when slot index not in range", async function () {
|
it("is rejected when slot index not in range", async function () {
|
||||||
|
@ -330,8 +257,21 @@ describe("Marketplace", function () {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("fails to free slot when finished", async function () {
|
||||||
|
await waitUntilStarted(
|
||||||
|
marketplace,
|
||||||
|
request.ask.slots,
|
||||||
|
slot.request,
|
||||||
|
proof
|
||||||
|
)
|
||||||
|
await waitUntilFinished(marketplace, slotId(slot))
|
||||||
|
await expect(marketplace.freeSlot(id)).to.be.revertedWith(
|
||||||
|
"Slot not accepting proofs"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it("successfully frees slot", async function () {
|
it("successfully frees slot", async function () {
|
||||||
await waitUntilAllSlotsFilled(
|
await waitUntilStarted(
|
||||||
marketplace,
|
marketplace,
|
||||||
request.ask.slots,
|
request.ask.slots,
|
||||||
slot.request,
|
slot.request,
|
||||||
|
@ -341,7 +281,7 @@ describe("Marketplace", function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("emits event once slot is freed", async function () {
|
it("emits event once slot is freed", async function () {
|
||||||
await waitUntilAllSlotsFilled(
|
await waitUntilStarted(
|
||||||
marketplace,
|
marketplace,
|
||||||
request.ask.slots,
|
request.ask.slots,
|
||||||
slot.request,
|
slot.request,
|
||||||
|
@ -353,7 +293,7 @@ describe("Marketplace", function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("cannot get slot once freed", async function () {
|
it("cannot get slot once freed", async function () {
|
||||||
await waitUntilAllSlotsFilled(
|
await waitUntilStarted(
|
||||||
marketplace,
|
marketplace,
|
||||||
request.ask.slots,
|
request.ask.slots,
|
||||||
slot.request,
|
slot.request,
|
||||||
|
@ -388,12 +328,6 @@ describe("Marketplace", function () {
|
||||||
expect(endBalance - startBalance).to.equal(pricePerSlot(request))
|
expect(endBalance - startBalance).to.equal(pricePerSlot(request))
|
||||||
})
|
})
|
||||||
|
|
||||||
it("is only allowed when the contract is finished", async function () {
|
|
||||||
await expect(
|
|
||||||
marketplace.payoutSlot(slot.request, slot.index)
|
|
||||||
).to.be.revertedWith("Contract not ended")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("is only allowed when the contract has ended", async function () {
|
it("is only allowed when the contract has ended", async function () {
|
||||||
await marketplace.fillSlot(slot.request, slot.index, proof)
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
||||||
await expect(
|
await expect(
|
||||||
|
@ -454,6 +388,8 @@ describe("Marketplace", function () {
|
||||||
await marketplace.fillSlot(slot.request, i, proof)
|
await marketplace.fillSlot(slot.request, i, proof)
|
||||||
}
|
}
|
||||||
await expect(await marketplace.state(slot.request)).to.equal(
|
await expect(await marketplace.state(slot.request)).to.equal(
|
||||||
|
RequestState.Started
|
||||||
|
)
|
||||||
})
|
})
|
||||||
it("fails when all slots are already filled", async function () {
|
it("fails when all slots are already filled", async function () {
|
||||||
const lastSlot = request.ask.slots - 1
|
const lastSlot = request.ask.slots - 1
|
||||||
|
@ -531,7 +467,7 @@ describe("Marketplace", function () {
|
||||||
await marketplace.deposit(collateral)
|
await marketplace.deposit(collateral)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("state is Cancelled when client withdraws funds", async function () {
|
it("changes state to Cancelled when client withdraws funds", async function () {
|
||||||
await expect(await marketplace.state(slot.request)).to.equal(
|
await expect(await marketplace.state(slot.request)).to.equal(
|
||||||
RequestState.New
|
RequestState.New
|
||||||
)
|
)
|
||||||
|
@ -599,7 +535,7 @@ describe("Marketplace", function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("changes state to Cancelled once request is cancelled", async function () {
|
it("changes state to Cancelled once request is cancelled", async function () {
|
||||||
await waitUntilExpired(request.expiry)
|
await waitUntilCancelled(request.expiry)
|
||||||
await expect(await marketplace.state(slot.request)).to.equal(
|
await expect(await marketplace.state(slot.request)).to.equal(
|
||||||
RequestState.Cancelled
|
RequestState.Cancelled
|
||||||
)
|
)
|
||||||
|
@ -607,7 +543,7 @@ describe("Marketplace", function () {
|
||||||
|
|
||||||
it("changes isCancelled to true once request is cancelled", async function () {
|
it("changes isCancelled to true once request is cancelled", async function () {
|
||||||
await expect(await marketplace.isCancelled(slot.request)).to.be.false
|
await expect(await marketplace.isCancelled(slot.request)).to.be.false
|
||||||
await waitUntilExpired(request.expiry)
|
await waitUntilCancelled(request.expiry)
|
||||||
await expect(await marketplace.isCancelled(slot.request)).to.be.true
|
await expect(await marketplace.isCancelled(slot.request)).to.be.true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -620,7 +556,7 @@ describe("Marketplace", function () {
|
||||||
it("changes isSlotCancelled to true once request is cancelled", async function () {
|
it("changes isSlotCancelled to true once request is cancelled", async function () {
|
||||||
await marketplace.fillSlot(slot.request, slot.index, proof)
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
||||||
await expect(await marketplace.isSlotCancelled(slotId(slot))).to.be.false
|
await expect(await marketplace.isSlotCancelled(slotId(slot))).to.be.false
|
||||||
await waitUntilExpired(request.expiry)
|
await waitUntilCancelled(request.expiry)
|
||||||
await expect(await marketplace.isSlotCancelled(slotId(slot))).to.be.true
|
await expect(await marketplace.isSlotCancelled(slotId(slot))).to.be.true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -629,7 +565,7 @@ describe("Marketplace", function () {
|
||||||
await expect(await marketplace.proofEnd(slotId(slot))).to.be.gt(
|
await expect(await marketplace.proofEnd(slotId(slot))).to.be.gt(
|
||||||
await currentTime()
|
await currentTime()
|
||||||
)
|
)
|
||||||
await waitUntilExpired(request.expiry)
|
await waitUntilCancelled(request.expiry)
|
||||||
await expect(await marketplace.proofEnd(slotId(slot))).to.be.lt(
|
await expect(await marketplace.proofEnd(slotId(slot))).to.be.lt(
|
||||||
await currentTime()
|
await currentTime()
|
||||||
)
|
)
|
||||||
|
@ -651,7 +587,7 @@ describe("Marketplace", function () {
|
||||||
await waitUntilCancelled(request.expiry)
|
await waitUntilCancelled(request.expiry)
|
||||||
await expect(
|
await expect(
|
||||||
marketplace.testAcceptsProofs(slotId(slot))
|
marketplace.testAcceptsProofs(slotId(slot))
|
||||||
).to.be.revertedWith("Request cancelled")
|
).to.be.revertedWith("Slot not accepting proofs")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("fails when request Cancelled (state set to Cancelled)", async function () {
|
it("fails when request Cancelled (state set to Cancelled)", async function () {
|
||||||
|
@ -661,7 +597,7 @@ describe("Marketplace", function () {
|
||||||
await marketplace.withdrawFunds(slot.request)
|
await marketplace.withdrawFunds(slot.request)
|
||||||
await expect(
|
await expect(
|
||||||
marketplace.testAcceptsProofs(slotId(slot))
|
marketplace.testAcceptsProofs(slotId(slot))
|
||||||
).to.be.revertedWith("Invalid state")
|
).to.be.revertedWith("Slot not accepting proofs")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("fails when request Finished (isFinished is true)", async function () {
|
it("fails when request Finished (isFinished is true)", async function () {
|
||||||
|
@ -674,7 +610,7 @@ describe("Marketplace", function () {
|
||||||
await waitUntilFinished(marketplace, slotId(slot))
|
await waitUntilFinished(marketplace, slotId(slot))
|
||||||
await expect(
|
await expect(
|
||||||
marketplace.testAcceptsProofs(slotId(slot))
|
marketplace.testAcceptsProofs(slotId(slot))
|
||||||
).to.be.revertedWith("Request finished")
|
).to.be.revertedWith("Slot not accepting proofs")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("fails when request Finished (state set to Finished)", async function () {
|
it("fails when request Finished (state set to Finished)", async function () {
|
||||||
|
@ -688,7 +624,7 @@ describe("Marketplace", function () {
|
||||||
await marketplace.payoutSlot(slot.request, slot.index)
|
await marketplace.payoutSlot(slot.request, slot.index)
|
||||||
await expect(
|
await expect(
|
||||||
marketplace.testAcceptsProofs(slotId(slot))
|
marketplace.testAcceptsProofs(slotId(slot))
|
||||||
).to.be.revertedWith("Invalid state")
|
).to.be.revertedWith("Slot not accepting proofs")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("fails when request Failed", async function () {
|
it("fails when request Failed", async function () {
|
||||||
|
@ -708,6 +644,5 @@ describe("Marketplace", function () {
|
||||||
).to.be.revertedWith("Slot empty")
|
).to.be.revertedWith("Slot empty")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,7 +10,7 @@ const {
|
||||||
advanceTime,
|
advanceTime,
|
||||||
advanceTimeTo,
|
advanceTimeTo,
|
||||||
} = require("./evm")
|
} = require("./evm")
|
||||||
const { periodic, hours, now, minutes } = require("./time")
|
const { periodic, hours, minutes } = require("./time")
|
||||||
|
|
||||||
describe("Proofs", function () {
|
describe("Proofs", function () {
|
||||||
const id = hexlify(randomBytes(32))
|
const id = hexlify(randomBytes(32))
|
||||||
|
|
|
@ -32,9 +32,9 @@ describe("Storage", function () {
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
;[client, host] = await ethers.getSigners()
|
;[client, host] = await ethers.getSigners()
|
||||||
|
|
||||||
await deployments.fixture(["TestToken", "TestStorage"])
|
await deployments.fixture(["TestToken", "Storage"])
|
||||||
token = await ethers.getContract("TestToken")
|
token = await ethers.getContract("TestToken")
|
||||||
storage = await ethers.getContract("TestStorage")
|
storage = await ethers.getContract("Storage")
|
||||||
|
|
||||||
await token.mint(client.address, 1_000_000_000)
|
await token.mint(client.address, 1_000_000_000)
|
||||||
await token.mint(host.address, 1_000_000_000)
|
await token.mint(host.address, 1_000_000_000)
|
||||||
|
@ -44,7 +44,7 @@ describe("Storage", function () {
|
||||||
slashPercentage = await storage.slashPercentage()
|
slashPercentage = await storage.slashPercentage()
|
||||||
minCollateralThreshold = await storage.minCollateralThreshold()
|
minCollateralThreshold = await storage.minCollateralThreshold()
|
||||||
|
|
||||||
request = exampleRequest()
|
request = await exampleRequest()
|
||||||
request.client = client.address
|
request.client = client.address
|
||||||
slot = {
|
slot = {
|
||||||
request: requestId(request),
|
request: requestId(request),
|
||||||
|
@ -121,12 +121,7 @@ describe("Storage", function () {
|
||||||
it("frees slot when collateral slashed below minimum threshold", async function () {
|
it("frees slot when collateral slashed below minimum threshold", async function () {
|
||||||
const id = slotId(slot)
|
const id = slotId(slot)
|
||||||
|
|
||||||
await waitUntilAllSlotsFilled(
|
await waitUntilStarted(storage, request.ask.slots, slot.request, proof)
|
||||||
storage,
|
|
||||||
request.ask.slots,
|
|
||||||
slot.request,
|
|
||||||
proof
|
|
||||||
)
|
|
||||||
|
|
||||||
// max slashes before dropping below collateral threshold
|
// max slashes before dropping below collateral threshold
|
||||||
const maxSlashes = 10
|
const maxSlashes = 10
|
||||||
|
@ -193,7 +188,6 @@ describe("Storage", function () {
|
||||||
await expect(await storage.willProofBeRequired(id)).to.be.false
|
await expect(await storage.willProofBeRequired(id)).to.be.false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
it("does not require proofs once cancelled", async function () {
|
it("does not require proofs once cancelled", async function () {
|
||||||
const id = slotId(slot)
|
const id = slotId(slot)
|
||||||
await storage.fillSlot(slot.request, slot.index, proof)
|
await storage.fillSlot(slot.request, slot.index, proof)
|
||||||
|
@ -225,89 +219,6 @@ describe("Storage", function () {
|
||||||
expect(BigNumber.from(challenge2).isZero())
|
expect(BigNumber.from(challenge2).isZero())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe("freeing a slot", function () {
|
|
||||||
beforeEach(async function () {
|
|
||||||
period = (await storage.proofPeriod()).toNumber()
|
|
||||||
;({ periodOf, periodEnd } = periodic(period))
|
|
||||||
})
|
|
||||||
|
|
||||||
async function waitUntilProofIsRequired(id) {
|
|
||||||
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
|
|
||||||
while (
|
|
||||||
!(
|
|
||||||
(await storage.isProofRequired(id)) &&
|
|
||||||
(await storage.getPointer(id)) < 250
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
await advanceTime(period)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function markProofAsMissing(slotId, onMarkAsMissing) {
|
|
||||||
for (let i = 0; i < slashMisses; i++) {
|
|
||||||
await waitUntilProofIsRequired(slotId)
|
|
||||||
let missedPeriod = periodOf(await currentTime())
|
|
||||||
await advanceTime(period)
|
|
||||||
if (i === slashMisses - 1 && typeof onMarkAsMissing === "function") {
|
|
||||||
onMarkAsMissing(missedPeriod)
|
|
||||||
} else await storage.markProofAsMissing(slotId, missedPeriod)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
it("frees slot when collateral slashed below minimum threshold", async function () {
|
|
||||||
const id = slotId(slot)
|
|
||||||
|
|
||||||
await waitUntilStarted(storage, request.ask.slots, slot.request, proof)
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
await markProofAsMissing(id)
|
|
||||||
let balance = await storage.balanceOf(host.address)
|
|
||||||
let slashAmount = await storage.slashAmount(
|
|
||||||
host.address,
|
|
||||||
slashPercentage
|
|
||||||
)
|
|
||||||
if (balance - slashAmount < minCollateralThreshold) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let onMarkAsMissing = async function (missedPeriod) {
|
|
||||||
await expect(
|
|
||||||
await storage.markProofAsMissing(id, missedPeriod)
|
|
||||||
).to.emit(storage, "SlotFreed")
|
|
||||||
}
|
|
||||||
await markProofAsMissing(id, onMarkAsMissing)
|
|
||||||
await expect(storage.getSlot(id)).to.be.revertedWith("Slot empty")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
describe("contract state", function () {
|
|
||||||
it("isCancelled is true once request is cancelled", async function () {
|
|
||||||
await expect(await storage.isCancelled(slot.request)).to.equal(false)
|
|
||||||
await waitUntilCancelled(request.expiry)
|
|
||||||
await expect(await storage.isCancelled(slot.request)).to.equal(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("isSlotCancelled fails when slot is empty", async function () {
|
|
||||||
await expect(storage.isSlotCancelled(slotId(slot))).to.be.revertedWith(
|
|
||||||
"Slot empty"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("isSlotCancelled is true once request is cancelled", async function () {
|
|
||||||
await storage.fillSlot(slot.request, slot.index, proof)
|
|
||||||
await waitUntilCancelled(request.expiry)
|
|
||||||
await expect(await storage.isSlotCancelled(slotId(slot))).to.equal(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("isFinished is true once started and contract duration lapses", async function () {
|
|
||||||
await expect(await storage.isFinished(slot.request)).to.be.false
|
|
||||||
// fill all slots, should change state to RequestState.Started
|
|
||||||
await waitUntilStarted(storage, request.ask.slots, slot.request, proof)
|
|
||||||
await expect(await storage.isFinished(slot.request)).to.be.false
|
|
||||||
advanceTime(request.ask.duration + 1)
|
|
||||||
await expect(await storage.isFinished(slot.request)).to.be.true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: implement checking of actual proofs of storage, instead of dummy bool
|
// TODO: implement checking of actual proofs of storage, instead of dummy bool
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
const { ethers } = require("hardhat")
|
const { ethers } = require("hardhat")
|
||||||
const { now, hours } = require("./time")
|
const { hours } = require("./time")
|
||||||
|
const { currentTime } = require("./evm")
|
||||||
const { hexlify, randomBytes } = ethers.utils
|
const { hexlify, randomBytes } = ethers.utils
|
||||||
|
|
||||||
const exampleRequest = () => ({
|
const exampleRequest = async () => {
|
||||||
|
const now = await currentTime()
|
||||||
|
return {
|
||||||
client: hexlify(randomBytes(20)),
|
client: hexlify(randomBytes(20)),
|
||||||
ask: {
|
ask: {
|
||||||
slots: 4,
|
slots: 4,
|
||||||
|
@ -23,13 +26,16 @@ const exampleRequest = () => ({
|
||||||
name: Array.from(randomBytes(512)),
|
name: Array.from(randomBytes(512)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expiry: now() + hours(1),
|
expiry: now + hours(1),
|
||||||
nonce: hexlify(randomBytes(32)),
|
nonce: hexlify(randomBytes(32)),
|
||||||
})
|
}
|
||||||
|
}
|
||||||
const exampleLock = () => ({
|
const exampleLock = async () => {
|
||||||
|
const now = await currentTime()
|
||||||
|
return {
|
||||||
id: hexlify(randomBytes(32)),
|
id: hexlify(randomBytes(32)),
|
||||||
expiry: now() + hours(1),
|
expiry: now + hours(1),
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = { exampleRequest, exampleLock }
|
module.exports = { exampleRequest, exampleLock }
|
||||||
|
|
|
@ -25,9 +25,18 @@ async function waitUntilFailed(contract, slot, maxSlotLoss) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RequestState = {
|
||||||
|
New: 0,
|
||||||
|
Started: 1,
|
||||||
|
Cancelled: 2,
|
||||||
|
Finished: 3,
|
||||||
|
Failed: 4,
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
waitUntilCancelled,
|
waitUntilCancelled,
|
||||||
waitUntilStarted,
|
waitUntilStarted,
|
||||||
waitUntilFinished,
|
waitUntilFinished,
|
||||||
waitUntilFailed,
|
waitUntilFailed,
|
||||||
|
RequestState,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
const now = () => Math.round(Date.now() / 1000)
|
|
||||||
const hours = (amount) => amount * minutes(60)
|
const hours = (amount) => amount * minutes(60)
|
||||||
const minutes = (amount) => amount * seconds(60)
|
const minutes = (amount) => amount * seconds(60)
|
||||||
const seconds = (amount) => amount
|
const seconds = (amount) => amount
|
||||||
|
@ -9,4 +8,4 @@ const periodic = (length) => ({
|
||||||
periodEnd: (period) => (period + 1) * length,
|
periodEnd: (period) => (period + 1) * length,
|
||||||
})
|
})
|
||||||
|
|
||||||
module.exports = { now, hours, minutes, seconds, periodic }
|
module.exports = { hours, minutes, seconds, periodic }
|
||||||
|
|
Loading…
Reference in New Issue