const fs = require("node:fs");
const path = require("node:path");
const hre = require("hardhat");
const { network } = hre;
const bs58 = reqDefault("bs58");
const namehash = require("eth-ens-namehash");
function reqDefault(p) {
const m = require(p);
return m?.default ?? m;
}
const ERC20Module = reqDefault("../ignition/modules/ERC20");
const EventManagerModule = reqDefault("../ignition/modules/EventManager");
const EventTicketModule = reqDefault("../ignition/modules/EventTicket");
const ReferralRewardsModule = reqDefault("../ignition/modules/ReferralRewards");
const RevenueSplitterModule = reqDefault("../ignition/modules/RevenueSplitter");
const { deployENS } = require("./deploy-ens");
const MASTER_EOA = "0x2789023F36933E208675889869c7d3914A422921";
const DEV_TEAM_EOA = MASTER_EOA;
const API_HOST = process.env.PUBLIC_API_HOST;
const BACKEND_PORT = process.env.BACKEND_PORT || "8000";
const BACKEND_HOST = `http://127.0.0.1:${BACKEND_PORT}`;
const IPFS_HOST = process.env.IPFS_PUBLIC_HOST;
const USDT_DECIMALS = 6;
function usdt(amount) {
return hre.ethers.parseUnits(String(amount), USDT_DECIMALS);
}
function usage() {
console.log(`
Usage:
pnpm exec hardhat run scripts/deploy-dev.js --network cvlandDev
pnpm exec hardhat run scripts/deploy-dev.js --network cvlandDev -- --validate
Manual flow (dev):
1) Start infra/services (systemd or tmux): ./cvland start
2) Ensure backend is reachable: ${BACKEND_HOST}/api/health/
3) Deploy + seed chain data: make -C ethereum deploy-dev
4) Start indexer to sync on-chain -> DB: make -C backend run-indexer
5) Open UI: http://127.0.0.1:5173
Notes:
- ERC20 amounts are micro-USDT (6 decimals). Use parseUnits(x, 6) everywhere.
- Event request fee is per-place deposit (eventDepositSize).
`);
}
async function validateOnly() {
const errors = [];
if (!IPFS_HOST) errors.push("Missing env var: IPFS_PUBLIC_HOST");
if (!BACKEND_PORT) errors.push("Missing/empty BACKEND_PORT");
if (typeof fetch !== "function") {
errors.push("Node.js global fetch is not available (need Node 18+).");
}
if (errors.length > 0) {
for (const e of errors) console.error(`โ ${e}`);
process.exit(1);
}
try {
const resp = await fetch(`${BACKEND_HOST}/api/health/`);
if (!resp.ok) {
throw new Error(`HTTP ${resp.status}`);
}
} catch (e) {
console.error(
`โ Backend is not reachable at ${BACKEND_HOST}/api/health/ (${String(e)})`,
);
process.exit(1);
}
console.log("โ deploy-dev validation passed");
}
async function setBlockchainTime(targetDate) {
const timestamp = Math.floor(targetDate.getTime() / 1000);
await network.provider.send("evm_mine", [timestamp]);
console.log(`Blockchain time set to: ${targetDate.toISOString()}`);
}
function getWeekDates(weeksAgo = 0) {
const now = new Date();
const dayOfWeek = now.getDay();
const daysSinceMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
const currentMonday = new Date(now);
currentMonday.setDate(now.getDate() - daysSinceMonday);
currentMonday.setHours(0, 0, 0, 0);
const targetMonday = new Date(currentMonday);
targetMonday.setDate(currentMonday.getDate() - weeksAgo * 7);
return targetMonday;
}
function getBytes32FromMultiash(multihash) {
const decoded = bs58.decode(multihash);
return {
digest: `0x${Buffer.from(decoded.slice(2)).toString("hex")}`,
hashFunction: decoded[0],
size: decoded[1],
};
}
async function registerEnsNames({
ensRegistry,
publicResolver,
reverseRegistrar,
signers,
}) {
const { ethers } = hre;
console.log("\nRegistering ENS names...");
const ensNames = [
{ name: "master.eth", signer: signers.master },
{ name: "creator.eth", signer: signers.creatorSlave },
{ name: "customer1.eth", signer: signers.completeSlave },
{ name: "verifiedshaman.eth", signer: signers.verifiedShaman },
{ name: "localprovider.eth", signer: signers.localProvider },
{ name: "backend.eth", signer: signers.backend },
];
const ethNode = namehash.hash("eth");
const resolverAddress = await publicResolver.getAddress();
const addrReverseNode = await reverseRegistrar.ADDR_REVERSE_NODE();
for (const { name, signer } of ensNames) {
const owner = signer.address;
const label = name.split(".")[0];
const node = namehash.hash(name);
const currentOwner = await ensRegistry.owner(node);
if (currentOwner === ethers.ZeroAddress) {
await (
await ensRegistry
.connect(signers.master)
.setSubnodeRecord(
ethNode,
ethers.keccak256(ethers.toUtf8Bytes(label)),
owner,
resolverAddress,
0,
)
).wait();
await (await publicResolver.connect(signer).setAddr(node, owner)).wait();
console.log(` Registered ${name} -> ${owner}`);
} else {
console.log(` ${name} already registered to ${currentOwner}`);
}
const addrHex = owner.slice(2).toLowerCase();
const addrHash = ethers.keccak256(ethers.toUtf8Bytes(addrHex));
const reverseNode = ethers.keccak256(
ethers.solidityPacked(
["bytes32", "bytes32"],
[addrReverseNode, addrHash],
),
);
const currentReverseName = await publicResolver.name(reverseNode);
if (currentReverseName && currentReverseName.length > 0) {
console.log(` Reverse already set: ${owner} -> ${currentReverseName}`);
continue;
}
await (await reverseRegistrar.connect(signer).setName(name)).wait();
await (
await publicResolver.connect(signer).setName(reverseNode, name)
).wait();
console.log(` Set reverse: ${owner} -> ${name}`);
}
}
async function deployContracts() {
console.log("\n=== PHASE 1: Deploying Contracts ===\n");
const { ensRegistry, publicResolver, reverseRegistrar } =
await deployENS(hre);
const prevWeekStart = getWeekDates(2);
const { erc20 } = await hre.ignition.deploy(ERC20Module, {});
const { eventTicket } = await hre.ignition.deploy(EventTicketModule, {
parameters: { EventTicket: { masterAddress: MASTER_EOA } },
});
const { eventManager } = await hre.ignition.deploy(EventManagerModule, {
parameters: {
EventManager: {
masterAddress: MASTER_EOA,
eventTicket: await eventTicket.getAddress(),
erc20: await erc20.getAddress(),
initialOffset: Math.floor(prevWeekStart.getTime() / 1000),
},
},
});
await eventTicket.setEventManagerAddress(await eventManager.getAddress());
const { splitter } = await hre.ignition.deploy(RevenueSplitterModule, {
parameters: {
DynamicRevenueSplitter: {
usdt: await erc20.getAddress(),
cyberiaDAO: MASTER_EOA,
cvePtPma: DEV_TEAM_EOA,
admin: MASTER_EOA,
},
},
});
const [
master,
creatorSlave,
completeSlave,
verifiedShaman,
localProvider,
backend,
] = await hre.ethers.getSigners();
await registerEnsNames({
ensRegistry,
publicResolver,
reverseRegistrar,
signers: {
master,
creatorSlave,
completeSlave,
verifiedShaman,
localProvider,
backend,
},
});
await eventManager
.connect(master)
.setRevenueSplitter(await splitter.getAddress());
const { referralRewards } = await hre.ignition.deploy(ReferralRewardsModule, {
parameters: {
ReferralRewards: {
rewardToken: await erc20.getAddress(),
admin: MASTER_EOA,
operator: await eventManager.getAddress(),
},
},
});
await eventManager
.connect(master)
.setReferralRewards(await referralRewards.getAddress());
await splitter
.connect(master)
.setEventManager(await eventManager.getAddress());
await splitter
.connect(master)
.grantProfileManager(localProvider.address, 500);
await splitter
.connect(localProvider)
.createDistributionProfile([master.address], [8000]);
await splitter.connect(master).grantProfileManager(master.address, 8500);
await splitter.connect(master).createDistributionProfile([], []);
await eventManager
.connect(master)
.grantLocalProvider(localProvider.address, 500);
const BACKEND_ROLE = await eventManager.BACKEND_ROLE();
await eventManager.connect(master).grantRole(BACKEND_ROLE, backend.address);
const VERIFIED_SHAMAN_ROLE = await eventManager.VERIFIED_SHAMAN_ROLE();
await eventManager
.connect(backend)
.grantRole(VERIFIED_SHAMAN_ROLE, verifiedShaman.address);
console.log("Contracts deployed successfully");
console.log(` EventManager: ${await eventManager.getAddress()}`);
console.log(` EventTicket: ${await eventTicket.getAddress()}`);
console.log(` ERC20: ${await erc20.getAddress()}`);
console.log(` ReferralRewards: ${await referralRewards.getAddress()}`);
return {
eventManager,
eventTicket,
erc20,
splitter,
referralRewards,
ensRegistry,
publicResolver,
reverseRegistrar,
signers: {
master,
creatorSlave,
completeSlave,
verifiedShaman,
localProvider,
backend,
},
};
}
async function createPlaces(
eventManager,
master,
verifiedShaman,
localProvider,
) {
console.log("\n=== PHASE 2: Creating Event Places ===\n");
const prevWeekStart = getWeekDates(2);
await setBlockchainTime(prevWeekStart);
const placeConfigs = [
{
title: "Beach Venue",
deposit: "10",
geometry: {
type: "Point",
coordinates: [{ lat: -8.291059, lng: 115.0841631 }],
},
},
{
title: "Mountain Retreat",
deposit: "25",
geometry: {
type: "Point",
coordinates: [{ lat: -8.299827, lng: 115.098407 }],
},
},
{
title: "City Center Hall",
deposit: "50",
geometry: {
type: "Point",
coordinates: [{ lat: -8.285, lng: 115.09 }],
},
},
{
title: "Riverside Garden",
deposit: "15",
geometry: {
type: "Point",
coordinates: [{ lat: -8.295, lng: 115.08 }],
},
},
];
const places = [];
for (const cfg of placeConfigs) {
const depositUnits = usdt(cfg.deposit);
const body = new FormData();
body.append("title", cfg.title);
body.append("description", "A beautiful venue for events");
body.append("geometry", JSON.stringify(cfg.geometry));
body.append("eventDepositSize", depositUnits.toString());
const resp = await fetch(`${BACKEND_HOST}/api/ipfs/places/meta`, {
body,
method: "PUT",
headers: { "X-User-Address": master.address },
});
if (!resp.ok) {
throw new Error(`Failed to upload place metadata: ${await resp.text()}`);
}
const result = await resp.json();
const mh = getBytes32FromMultiash(result.cid);
const submitTx = await eventManager
.connect(verifiedShaman)
.submitEventPlaceRequest(
100,
20,
usdt("5"),
1,
1,
true,
mh.digest,
mh.hashFunction,
mh.size,
0, );
const submitReceipt = await submitTx.wait();
const newEventPlaceRequestEvent = submitReceipt.logs
.filter((e) => e.fragment?.name === "NewEventPlaceRequest")
.find((e) => e);
if (!newEventPlaceRequestEvent) {
throw new Error("NewEventPlaceRequest event not found");
}
const placeId = newEventPlaceRequestEvent.args.id;
await eventManager
.connect(localProvider)
.approveEventPlace(placeId, depositUnits);
places.push({ id: placeId, ...cfg, cid: result.cid });
console.log(
`Created place: ${cfg.title} (ID: ${placeId}, Deposit: ${cfg.deposit} USDT)`,
);
}
return places;
}
async function createEvents(eventManager, erc20, places, signers) {
console.log("\n=== PHASE 3: Creating Events ===\n");
const { master, creatorSlave, verifiedShaman, localProvider } = signers;
const prevWeekStart = getWeekDates(2);
const currentWeekStart = getWeekDates(0);
const prevWeekEventConfigs = [
{
title: "Summer Festival - Prev Week",
description: "Previous week festival",
website: "https://example.com/festival",
placeIndex: 0,
price: "100",
startDate: new Date(prevWeekStart.getTime() + 7 * 24 * 60 * 60 * 1000),
daysAmount: 3,
cover: "seed-data/event-covers/event-1-onepiece.jpg",
creator: creatorSlave,
socials: { network: "telegram", value: "@prev-week-creator" },
categories: [{ name: "General", discount: 0, quota: 0, unlimited: true }],
},
{
title: "Tech Conference - Prev Week",
description: "Previous week conference",
website: "https://example.com/conference",
placeIndex: 1,
price: "50",
startDate: new Date(prevWeekStart.getTime() + 3 * 24 * 60 * 60 * 1000),
daysAmount: 2,
cover: "seed-data/event-covers/event-2-game.jpg",
creator: creatorSlave,
socials: { network: "telegram", value: "@prev-week-conf" },
categories: [{ name: "General", discount: 0, quota: 0, unlimited: true }],
},
];
const currentWeekEventConfigs = [
{
title: "ThE fIrSt eVeNt",
description: "GuR sVeFg rIrAg",
website: "https://example.com/first",
placeIndex: 0,
price: "100",
startDate: new Date(currentWeekStart.getTime() + 7 * 24 * 60 * 60 * 1000),
daysAmount: 3,
cover: "seed-data/event-covers/event-1-onepiece.jpg",
creator: creatorSlave,
socials: { network: "telegram", value: "@john-doe" },
categories: [
{ name: "Women", discount: 2000, quota: 30, unlimited: false },
{ name: "Locals", discount: 1000, quota: 50, unlimited: false },
{ name: "Families", discount: 5000, quota: 0, unlimited: true },
],
},
{
title: "let it be the other one",
description: "yrg vg or gur bgure bar",
website: "https://example.com/other",
placeIndex: 1,
price: "50",
startDate: new Date(currentWeekStart.getTime() + 8 * 24 * 60 * 60 * 1000),
daysAmount: 2,
cover: "seed-data/event-covers/event-2-game.jpg",
creator: creatorSlave,
socials: { network: "telegram", value: "@clare-doe" },
categories: [
{ name: "Early Bird", discount: 1500, quota: 25, unlimited: false },
{ name: "Students", discount: 2500, quota: 0, unlimited: true },
],
},
{
title: "pending pending pending",
description: "la lala lalalala",
website: "https://example.com/pending",
placeIndex: 1,
price: "69",
startDate: new Date(
currentWeekStart.getTime() + 10 * 24 * 60 * 60 * 1000,
),
daysAmount: 5,
cover: "seed-data/event-covers/event-3-sample.png",
creator: creatorSlave,
socials: { network: "telegram", value: "@michael-doe" },
categories: [{ name: "General", discount: 0, quota: 0, unlimited: true }],
skipApproval: true,
},
];
const events = [];
console.log("Creating previous week events...");
for (const cfg of prevWeekEventConfigs) {
const event = await createSingleEvent(
eventManager,
erc20,
places,
cfg,
events.length,
verifiedShaman,
localProvider,
);
events.push(event);
}
await setBlockchainTime(currentWeekStart);
console.log("Creating current week events...");
for (const cfg of currentWeekEventConfigs) {
const event = await createSingleEvent(
eventManager,
erc20,
places,
cfg,
events.length,
verifiedShaman,
localProvider,
);
events.push(event);
}
return events;
}
async function createSingleEvent(
eventManager,
erc20,
places,
cfg,
eventId,
verifiedShaman,
localProvider,
) {
const placeId = places[cfg.placeIndex].id;
const socialsResponse = await fetch(
`${BACKEND_HOST}/api/ipfs/users/socials`,
{
method: "PUT",
body: JSON.stringify(cfg.socials),
headers: {
"Content-Type": "application/json",
"X-User-Address": cfg.creator.address,
},
},
);
if (!socialsResponse.ok) {
throw new Error(
`Failed to upload socials: ${await socialsResponse.text()}`,
);
}
const socials = await socialsResponse.json();
const imgBuffer = fs.readFileSync(path.join(__dirname, cfg.cover));
const imgBlob = new Blob([imgBuffer]);
const body = new FormData();
body.set("title", cfg.title);
body.set("description", cfg.description);
body.set("website", cfg.website || "");
body.set("cover", imgBlob, "image.jpg");
body.set("socials_cid", socials.cid);
body.set("address", cfg.creator.address);
const eventMetaResponse = await fetch(
`${BACKEND_HOST}/api/ipfs/events/meta`,
{
body,
method: "PUT",
headers: { "X-User-Address": cfg.creator.address },
},
);
if (!eventMetaResponse.ok) {
throw new Error(
`Failed to upload event metadata: ${await eventMetaResponse.text()}`,
);
}
const eventMeta = await eventMetaResponse.json();
const mh = getBytes32FromMultiash(eventMeta.cid);
const seedBudget = usdt("1000");
await erc20.connect(cfg.creator).mint(seedBudget);
await erc20.connect(cfg.creator).approve(await eventManager.getAddress(), seedBudget);
const tx = await eventManager.connect(cfg.creator).submitEventRequest(
placeId,
usdt(cfg.price),
Math.floor(cfg.startDate / 1000),
cfg.daysAmount,
mh.digest,
mh.hashFunction,
mh.size,
cfg.categories.map((cat) => ({
name: cat.name,
discountPercentage: cat.discount,
quota: cat.quota,
hasQuota: !cat.unlimited,
})),
);
const receipt = await tx.wait();
const newEventRequestEvent = receipt.logs.find(
(log) => log.fragment?.name === "NewEventRequest",
);
const createdEventId = newEventRequestEvent
? Number(newEventRequestEvent.args[0])
: null;
if (createdEventId === null) {
throw new Error("Failed to get event ID from transaction receipt");
}
if (!cfg.skipApproval) {
await eventManager.connect(localProvider).approveEvent(createdEventId, 1);
}
console.log(
`Created event: ${cfg.title} (ID: ${createdEventId}, Place: ${placeId})`,
);
return {
id: createdEventId,
...cfg,
placeId,
cid: eventMeta.cid,
};
}
const IPFS_API_HOST = process.env.IPFS_API_HOST || "http://127.0.0.1:5001";
async function uploadToIpfsDirect(data) {
const blob = new Blob([JSON.stringify(data)], { type: "application/json" });
const formData = new FormData();
formData.append("file", blob, "data.json");
const response = await fetch(`${IPFS_API_HOST}/api/v0/add`, {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error(`IPFS upload failed: ${await response.text()}`);
}
const result = await response.json();
return result.Hash;
}
async function mintTickets(eventManager, erc20, events, signers) {
console.log("\n=== PHASE 4: Minting Tickets ===\n");
const { completeSlave } = signers;
const ticketConfigs = [
{
eventIndex: 0,
categoryId: 0,
socials: { network: "instagram", value: "@buyer1_event0" },
},
{
eventIndex: 0,
categoryId: 0,
socials: { network: "telegram", value: "@buyer2_event0" },
},
{
eventIndex: 0,
categoryId: 0,
socials: { network: "discord", value: "@buyer3_event0" },
},
{
eventIndex: 1,
categoryId: 1,
socials: { network: "discord", value: "@buyer_event1" },
},
{
eventIndex: 2,
categoryId: 2,
socials: { network: "instagram", value: "@women_buyer1" },
},
{
eventIndex: 2,
categoryId: 3,
socials: { network: "telegram", value: "@locals_buyer1" },
},
{
eventIndex: 2,
categoryId: 4,
socials: { network: "discord", value: "@family_buyer1" },
},
{
eventIndex: 3,
categoryId: 5,
socials: { network: "instagram", value: "@earlybird_buyer" },
},
{
eventIndex: 3,
categoryId: 6,
socials: { network: "telegram", value: "@student_buyer" },
},
];
for (const cfg of ticketConfigs) {
const event = events[cfg.eventIndex];
const unitPrice = usdt(event.price);
const orderData = {
event_id: event.id,
buyer_address: completeSlave.address,
socials: cfg.socials,
tickets: [
{
categoryId: cfg.categoryId,
categoryName: "Standard",
price: unitPrice.toString(),
quantity: 1,
},
],
total_tickets: 1,
total_price: unitPrice.toString(),
currency: "USDC",
referral_data: "",
};
const cid = await uploadToIpfsDirect(orderData);
console.log(` Order metadata uploaded: ${cid}`);
const mh = getBytes32FromMultiash(cid);
await (await erc20.connect(completeSlave).mint(unitPrice)).wait();
await (
await erc20
.connect(completeSlave)
.approve(await eventManager.getAddress(), unitPrice)
).wait();
const mintData = eventManager.interface.encodeFunctionData("mintTickets", [
event.id,
cfg.categoryId,
1,
mh.digest,
mh.hashFunction,
mh.size,
"0x0000000000000000000000000000000000000000",
]);
await (
await completeSlave.sendTransaction({
to: await eventManager.getAddress(),
data: mintData,
gasLimit: 1_000_000n,
})
).wait();
console.log(
`Minted ticket for event: ${event.title} (Event ID: ${event.id})`,
);
}
console.log("\nAll tickets minted successfully");
}
async function main() {
const args = process.argv.slice(2);
if (args.includes("--help") || args.includes("-h")) {
usage();
return;
}
if (args.includes("--validate")) {
await validateOnly();
return;
}
console.log("============================================================");
console.log("Cyber Valley Tickets - Deploy and Seed");
console.log("============================================================");
console.log("\nArchitecture:");
console.log(" 1. Deploy contracts");
console.log(" 2. Create places & events on-chain");
console.log(" 3. Mint tickets on-chain (using returned event IDs)");
console.log(" 4. Indexer (started separately) processes all events");
console.log("\nNote: Contract is source of truth. Indexer syncs to DB.");
console.log(" Tickets can be minted immediately after event creation.");
console.log("============================================================\n");
if (!API_HOST) throw new Error("PUBLIC_API_HOST env var is missing");
if (!IPFS_HOST) throw new Error("IPFS_PUBLIC_HOST env var is missing");
const health = await fetch(`${BACKEND_HOST}/api/health/`);
if (!health.ok) {
throw new Error(`Backend is not reachable at ${BACKEND_HOST}/api/health/ (HTTP ${health.status})`);
}
const {
eventManager,
eventTicket,
erc20,
splitter,
referralRewards,
publicResolver,
signers,
} = await deployContracts();
const places = await createPlaces(
eventManager,
signers.master,
signers.verifiedShaman,
signers.localProvider,
);
const events = await createEvents(eventManager, erc20, places, signers);
await mintTickets(eventManager, erc20, events, signers);
console.log("\n=== Deployment Complete ===\n");
console.log("Contract Addresses:");
console.log(`export PUBLIC_ERC20_ADDRESS=${await erc20.getAddress()}`);
console.log(
`export PUBLIC_EVENT_TICKET_ADDRESS=${await eventTicket.getAddress()}`,
);
console.log(
`export PUBLIC_EVENT_MANAGER_ADDRESS=${await eventManager.getAddress()}`,
);
console.log(
`export PUBLIC_REVENUE_SPLITTER_ADDRESS=${await splitter.getAddress()}`,
);
console.log(
`export PUBLIC_REFERRAL_REWARDS_ADDRESS=${await referralRewards.getAddress()}`,
);
console.log(
`export PUBLIC_ENS_RESOLVER_ADDRESS=${await publicResolver.getAddress()}`,
);
console.log(
"\nNext step: Start the indexer to sync on-chain state to database",
);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});