import {
loadFixture as hardhatLoadFixture,
time,
} from "@nomicfoundation/hardhat-network-helpers";
import { assert, expect } from "chai";
import type {
BaseContract,
BigNumberish,
ContractTransactionReceipt,
ContractTransactionResponse,
EventLog,
Signer,
} from "ethers";
import { ethers } from "hardhat";
import type {
CyberValleyEventManager,
CyberValleyEventTicket,
DynamicRevenueSplitter,
MockUSDT,
} from "../../typechain-types";
import {
defaultCancelEventArgs,
defaultCloseEventArgs,
defaultCreateEventPlaceRequest,
defaultEventDepositSize,
defaultSubmitEventRequest,
defaultUpdateEventPlaceRequest,
} from "./data";
import type {
ApproveEventArgs,
ApproveEventPlaceArgs,
CancelEventArgs,
CloseEventArgs,
CreateEventPlaceArgs,
DeclineEventPlaceArgs,
Event,
NewEventRequestEvent,
SubmitEventPlaceRequestArgs,
SubmitEventRequestArgs,
UpdateEventPlaceArgs,
} from "./types";
import {
approveEventArgsToArray,
approveEventPlaceArgsToArray,
baseEventArgsToArray,
cancelEventArgsToArray,
closeEventArgsToArray,
declineEventPlaceArgsToArray,
submitEventPlaceRequestArgsToArray,
submitEventRequestArgsToArray,
updateEventPlaceArgsToArray,
} from "./types";
export { baseEventArgsToArray };
export type ContractsFixture = {
ERC20: MockUSDT & BaseContract;
eventManager: CyberValleyEventManager & BaseContract;
eventTicket: CyberValleyEventTicket & BaseContract;
splitter: DynamicRevenueSplitter & BaseContract;
owner: Signer;
master: Signer;
localProvider: Signer;
verifiedShaman: Signer;
creator: Signer;
staff: Signer;
signer: Signer;
};
const blockchainRestoreDisabled = process.env.DISABLE_BLOCKHAIN_RESTORE != null;
if (blockchainRestoreDisabled) {
console.warn("!!! BLOCKCHAIN SNAPSHOT RESTORATION IS DISABLED !!!");
}
type Fixture<T> = () => Promise<T>;
async function dummyLoadFixture<T>(fn: Fixture<T>): Promise<T> {
return await fn();
}
const loadFixture = blockchainRestoreDisabled
? dummyLoadFixture
: hardhatLoadFixture;
export { loadFixture };
export async function deployContract(): Promise<ContractsFixture> {
const [owner, master, localProvider, verifiedShaman, creator, staff, signer] =
await ethers.getSigners();
const ERC20 = await ethers.deployContract("MockUSDT");
const CyberValleyEventManagerFactory = await ethers.getContractFactory(
"CyberValleyEventManager",
);
const CyberValleyEventTicketFactory = await ethers.getContractFactory(
"CyberValleyEventTicket",
);
const eventTicket = await CyberValleyEventTicketFactory.deploy(
"CyberValleyEventTicket",
"CVET",
master,
"",
);
const DynamicRevenueSplitterFactory = await ethers.getContractFactory(
"DynamicRevenueSplitter",
);
const splitter = await DynamicRevenueSplitterFactory.deploy(
await ERC20.getAddress(),
await master.getAddress(), // CyberiaDAO placeholder
await staff.getAddress(), // CVE PT PMA placeholder
await master.getAddress(), // admin
);
const eventManager = await CyberValleyEventManagerFactory.deploy(
await ERC20.getAddress(),
await eventTicket.getAddress(),
master,
await timestamp(0),
);
await eventTicket
.connect(master)
.setEventManagerAddress(await eventManager.getAddress());
await eventManager
.connect(master)
.setRevenueSplitter(await splitter.getAddress());
// Set the EventManager in the splitter
await splitter
.connect(master)
.setEventManager(await eventManager.getAddress());
// Grant ADMIN_ROLE to EventManager so it can call transferAllProfiles
const ADMIN_ROLE = await splitter.ADMIN_ROLE();
await splitter
.connect(master)
.grantRole(ADMIN_ROLE, await eventManager.getAddress());
// Grant profile manager role with 5% bps to localProvider
await splitter
.connect(master)
.grantProfileManager(await localProvider.getAddress(), 500);
// Create a default profile (ID 1) that tests can use
// Use creator address as recipient since localProvider can't add themselves
await splitter
.connect(localProvider)
.createDistributionProfile([await creator.getAddress()], [8000]);
await eventManager
.connect(master)
.grantLocalProvider(await localProvider.getAddress(), 500);
const BACKEND_ROLE = await eventManager.BACKEND_ROLE();
await eventManager
.connect(master)
.grantRole(BACKEND_ROLE, await master.getAddress());
const VERIFIED_SHAMAN_ROLE = await eventManager.VERIFIED_SHAMAN_ROLE();
await eventManager
.connect(master)
.grantRole(VERIFIED_SHAMAN_ROLE, await verifiedShaman.getAddress());
await eventManager
.connect(master)
.grantRole(VERIFIED_SHAMAN_ROLE, await creator.getAddress());
return {
ERC20,
eventManager,
eventTicket,
splitter,
owner,
master,
localProvider,
verifiedShaman,
creator,
staff,
signer,
};
}
export async function submitEventPlaceRequest(
eventManager: CyberValleyEventManager,
verifiedShaman: Signer,
patch?: Partial<SubmitEventPlaceRequestArgs>,
): Promise<{ eventPlaceId: BigNumberish; tx: ContractTransactionResponse }> {
const tx = await eventManager.connect(verifiedShaman).submitEventPlaceRequest(
...submitEventPlaceRequestArgsToArray({
...defaultCreateEventPlaceRequest,
...patch,
}),
);
const receipt = await tx.wait();
assert(receipt != null);
const event = receipt.logs
.filter((e): e is EventLog => "fragment" in e && "args" in e)
.find((e) => e.fragment?.name === "NewEventPlaceRequest");
assert(event != null, "NewEventPlaceRequest wasn't emitted");
return { tx, eventPlaceId: event.args.id };
}
export async function approveEventPlace(
eventManager: CyberValleyEventManager,
localProvider: Signer,
request: ApproveEventPlaceArgs,
) {
return await eventManager
.connect(localProvider)
.approveEventPlace(...approveEventPlaceArgsToArray(request));
}
export async function declineEventPlace(
eventManager: CyberValleyEventManager,
localProvider: Signer,
request: DeclineEventPlaceArgs,
) {
return await eventManager
.connect(localProvider)
.declineEventPlace(...declineEventPlaceArgsToArray(request));
}
export async function createEventPlace(
eventManager: CyberValleyEventManager,
verifiedShaman: Signer,
localProvider: Signer,
patch?: Partial<SubmitEventPlaceRequestArgs>,
deposit?: BigNumberish,
): Promise<{ eventPlaceId: BigNumberish; tx: ContractTransactionResponse }> {
const { eventPlaceId, tx } = await submitEventPlaceRequest(
eventManager,
verifiedShaman,
patch,
);
await approveEventPlace(eventManager, localProvider, {
eventPlaceId,
eventDepositSize: deposit ?? 100,
});
return { tx, eventPlaceId };
}
export async function createValidEventPlace(
eventManager: CyberValleyEventManager,
verifiedShaman: Signer,
localProvider: Signer,
): ReturnType<typeof createEventPlace> {
return await createEventPlace(
eventManager,
verifiedShaman,
localProvider,
defaultCreateEventPlaceRequest,
);
}
export async function updateEventPlace(
eventManager: CyberValleyEventManager,
localProvider: Signer,
request: UpdateEventPlaceArgs,
) {
return await eventManager
.connect(localProvider)
.updateEventPlace(...updateEventPlaceArgsToArray(request));
}
export async function createAndUpdateEventPlace(
eventManager: CyberValleyEventManager,
verifiedShaman: Signer,
localProvider: Signer,
request: Partial<UpdateEventPlaceArgs>,
) {
const { eventPlaceId } = await createValidEventPlace(
eventManager,
verifiedShaman,
localProvider,
);
return await updateEventPlace(eventManager, localProvider, {
...defaultUpdateEventPlaceRequest,
eventPlaceId,
...request,
});
}
export async function createEvent(
eventManager: CyberValleyEventManager,
ERC20: MockUSDT,
verifiedShaman: Signer,
localProvider: Signer,
creator: Signer,
eventPlacePatch: Partial<CreateEventPlaceArgs>,
submitEventPatch: Partial<Event>,
approveEventPatch: Partial<ApproveEventArgs>,
splitter?: DynamicRevenueSplitter,
): Promise<{
request: SubmitEventRequestArgs;
tx: Promise<ContractTransactionResponse>;
eventId: BigNumberish;
}> {
// Create event place
const { eventPlaceId } = await createEventPlace(
eventManager,
verifiedShaman,
localProvider,
eventPlacePatch,
defaultEventDepositSize,
);
// Mint tokens & approve: event request fee is per-place deposit.
const deposit = defaultEventDepositSize;
await ERC20.connect(creator).mint(deposit);
await ERC20.connect(creator).approve(await eventManager.getAddress(), deposit);
// Submit request (categories are created atomically during submission)
const {
tx: submitEventRequestTx,
request,
getEventId,
} = await submitEventRequest(eventManager, creator, submitEventPatch);
const eventId = await getEventId();
// Create or use existing distribution profile
let distributionProfileId = approveEventPatch.distributionProfileId;
if (distributionProfileId == null && splitter != null) {
// Create a profile for the localProvider - profile manager cannot add themselves
// so we use the creator address as recipient for this test profile
const creatorAddress = await creator.getAddress();
const localProviderAddress = await localProvider.getAddress();
// Create profile and get the ID from the event
const tx = await splitter
.connect(localProvider)
.createDistributionProfile([creatorAddress], [8000]);
const receipt = await tx.wait();
const event = receipt?.logs.find((log) => {
try {
const parsed = splitter.interface.parseLog(log);
return parsed?.name === "DistributionProfileCreated";
} catch {
return false;
}
});
const parsedEvent = event ? splitter.interface.parseLog(event) : null;
distributionProfileId = parsedEvent?.args[0] ?? 1n;
}
// Approve
const tx = eventManager.connect(localProvider).approveEvent(
...approveEventArgsToArray({
eventId,
distributionProfileId: distributionProfileId ?? 1n,
...approveEventPatch,
}),
);
return { request, tx, eventId };
}
export async function createEventForCategories(
eventManager: CyberValleyEventManager,
ERC20: MockUSDT,
verifiedShaman: Signer,
localProvider: Signer,
creator: Signer,
eventPlacePatch: Partial<CreateEventPlaceArgs>,
submitEventPatch: Partial<SubmitEventRequestArgs>,
): Promise<{
request: SubmitEventRequestArgs;
eventId: BigNumberish;
eventPlaceId: BigNumberish;
}> {
// Create event place
const { eventPlaceId } = await createEventPlace(
eventManager,
verifiedShaman,
localProvider,
eventPlacePatch,
defaultEventDepositSize,
);
// Mint tokens & approve: event request fee is per-place deposit.
const deposit = defaultEventDepositSize;
await ERC20.connect(creator).mint(deposit);
await ERC20.connect(creator).approve(await eventManager.getAddress(), deposit);
// Submit request (event is in "Submitted" state, not approved)
const { request, getEventId } = await submitEventRequest(
eventManager,
creator,
{
eventPlaceId,
...submitEventPatch,
},
);
const eventId = await getEventId();
return { request, eventId, eventPlaceId };
}
export async function submitEventRequest(
eventManager: CyberValleyEventManager,
creator: Signer,
patch: Partial<SubmitEventRequestArgs>,
): Promise<{
request: SubmitEventRequestArgs;
tx: Promise<ContractTransactionResponse>;
getEventId: () => Promise<BigNumberish>;
}> {
const request = {
...defaultSubmitEventRequest,
startDate: await timestamp(5),
...patch,
};
assert(request.eventPlaceId != null);
const tx = eventManager
.connect(creator)
.submitEventRequest(...submitEventRequestArgsToArray(request));
return {
request,
tx,
getEventId: async () => {
const { id } = extractEvent<NewEventRequestEvent>(
await (await tx).wait(),
"NewEventRequest",
);
return id;
},
};
}
export async function closeEvent(
eventManager: CyberValleyEventManager,
localProvider: Signer,
patch: Partial<CloseEventArgs>,
): Promise<{
request: CloseEventArgs;
tx: Promise<ContractTransactionResponse>;
}> {
const request = {
...defaultCloseEventArgs,
...patch,
};
const tx = eventManager
.connect(localProvider)
.closeEvent(...closeEventArgsToArray(request));
return { request, tx };
}
export async function createAndCloseEvent(
eventManager: CyberValleyEventManager,
ERC20: MockUSDT,
verifiedShaman: Signer,
localProvider: Signer,
creator: Signer,
patch: Partial<CloseEventArgs>,
splitter?: DynamicRevenueSplitter,
): ReturnType<typeof closeEvent> {
const { tx: createEventTx, eventId } = await createEvent(
eventManager,
ERC20,
verifiedShaman,
localProvider,
creator,
{},
{},
{},
splitter,
);
await createEventTx;
await time.increase(100_000_000);
return await closeEvent(eventManager, localProvider, { eventId, ...patch });
}
export async function cancelEvent(
eventManager: CyberValleyEventManager,
localProvider: Signer,
patch: Partial<CancelEventArgs>,
): Promise<{
request: CancelEventArgs;
tx: Promise<ContractTransactionResponse>;
}> {
const request = {
...defaultCancelEventArgs,
...patch,
};
const tx = eventManager
.connect(localProvider)
.cancelEvent(...cancelEventArgsToArray(request));
return { request, tx };
}
export async function createAndCancelEvent(
eventManager: CyberValleyEventManager,
ERC20: MockUSDT,
verifiedShaman: Signer,
localProvider: Signer,
creator: Signer,
patch: Partial<CancelEventArgs>,
splitter?: DynamicRevenueSplitter,
): ReturnType<typeof cancelEvent> {
const { tx: createEventTx, eventId } = await createEvent(
eventManager,
ERC20,
verifiedShaman,
localProvider,
creator,
{},
{},
{},
splitter,
);
await createEventTx;
await time.increase(100_000_000);
return await cancelEvent(eventManager, localProvider, { eventId, ...patch });
}
export function extractEvent<T>(
receipt: ContractTransactionReceipt | null,
eventName: string,
): T {
assert(receipt != null, "Got null receipt");
const event = receipt.logs
.filter((e): e is EventLog => "fragment" in e && "args" in e)
.find((e) => e.fragment?.name === eventName);
assert(event != null, `${eventName} wasn't emitted`);
return event.args as T;
}
/**
* The fuck do u mean that expect works only inside of `it`
* i.e. it raises `AssertionError`, but the runner doesn't care and ignore it
*/
export function itExpectsOnlyMaster<K extends keyof CyberValleyEventManager>(
methodName: K,
request: Parameters<CyberValleyEventManager[K]>,
) {
it(`${String(methodName)} allowed only to master`, async () => {
const { eventManager, owner } = await loadFixture(deployContract);
const method = eventManager[methodName];
assert(method != null);
const masterRole = await eventManager.MASTER_ROLE();
await expect(method.apply(eventManager, request))
.to.be.revertedWithCustomError(
eventManager,
"AccessControlUnauthorizedAccount",
)
.withArgs(await owner.getAddress(), masterRole);
});
}
export function itExpectsOnlyLocalProvider<
K extends keyof CyberValleyEventManager,
>(methodName: K, request: Parameters<CyberValleyEventManager[K]>) {
it(`${String(methodName)} allowed only to local provider`, async () => {
const { eventManager, owner } = await loadFixture(deployContract);
const method = eventManager[methodName];
assert(method != null);
const localProviderRole = await eventManager.LOCAL_PROVIDER_ROLE();
await expect(method.apply(eventManager, request))
.to.be.revertedWithCustomError(
eventManager,
"AccessControlUnauthorizedAccount",
)
.withArgs(await owner.getAddress(), localProviderRole);
});
}
export function itExpectsOnlyVerifiedShaman<
K extends keyof CyberValleyEventManager,
>(methodName: K, request: Parameters<CyberValleyEventManager[K]>) {
it(`${String(methodName)} allowed only to verified shaman`, async () => {
const { eventManager, owner } = await loadFixture(deployContract);
const method = eventManager[methodName];
assert(method != null);
if (methodName === "submitEventPlaceRequest") {
await expect(method.apply(eventManager, request)).to.be.revertedWith(
"access denied",
);
} else {
const verifiedShamanRole = await eventManager.VERIFIED_SHAMAN_ROLE();
await expect(method.apply(eventManager, request))
.to.be.revertedWithCustomError(
eventManager,
"AccessControlUnauthorizedAccount",
)
.withArgs(await owner.getAddress(), verifiedShamanRole);
}
});
}
export function itExpectsOnlyStaff<K extends keyof CyberValleyEventManager>(
methodName: K,
request: Parameters<CyberValleyEventManager[K]>,
) {
it(`${String(methodName)} allowed only to staff`, async () => {
const { eventManager } = await loadFixture(deployContract);
const method = eventManager[methodName];
assert(method != null);
await expect(method.apply(eventManager, request)).to.be.revertedWith(
"Must have staff role",
);
});
}
export async function timestamp(daysFromNow: number): Promise<BigNumberish> {
// Use genesis block timestamp for consistent results across test runs
const genesisBlock = await ethers.provider.getBlock(0);
const baseTimestamp = genesisBlock?.timestamp || 0;
const days = daysFromNow * 24 * 60 * 60;
return baseTimestamp + days;
}
export function stringify<T>(obj: T): string {
return JSON.stringify(obj, (_, v) =>
typeof v === "bigint" ? v.toString() : v,
);
}