@vechain/vebetterdao-contracts
Version:
Open-source repository that houses the smart contracts powering the decentralized VeBetterDAO on the VeChain Thor blockchain.
195 lines (194 loc) • 10.3 kB
JavaScript
import { ethers } from "hardhat";
import { X2EarnApps__factory, XAllocationVoting__factory, } from "../../typechain-types";
import { Clause, Address, ABIContract } from "@vechain/sdk-core";
import { TransactionUtils } from "@repo/utils";
import { chunk } from "./chunk";
import { getContractsConfig } from "@repo/config";
import { getConfig } from "@repo/config";
import { ThorClient } from "@vechain/sdk-network";
import { uploadBlobToIPFS } from "./ipfs";
const thorClient = ThorClient.at(getConfig().nodeUrl);
export const registerXDapps = async (contractAddress, accounts, apps) => {
console.log("Adding x-apps...");
const appChunks = chunk(apps, 50);
for (const appChunk of appChunks) {
// For each apps
for (let i = 0; i < appChunk.length; i++) {
const app = appChunk[i];
const account = accounts[i % accounts.length]; // Unique signer for each app
const clause = Clause.callFunction(Address.of(contractAddress), ABIContract.ofAbi(X2EarnApps__factory.abi).getFunction("submitApp"), [app.teamWalletAddress, app.admin, app.name, app.metadataURI]);
await TransactionUtils.sendTx(thorClient, [clause], account.pk);
}
}
};
export const endorseXApps = async (endorsers, x2EarnApps, apps, stargateMock) => {
const contractsConfig = getContractsConfig(process.env.NEXT_PUBLIC_APP_ENV);
if (contractsConfig.X2EARN_NODE_COOLDOWN_PERIOD > 1) {
return console.warn("Endorsement cooldown period is greater than 1. Skipping endorsement.");
}
const maxPointsPerNode = Number(await x2EarnApps.maxPointsPerNodePerApp());
const threshold = Number(await x2EarnApps.endorsementScoreThreshold());
console.log(`\n========== Endorsing ${apps.length} x-apps ==========`);
console.log(` Threshold: ${threshold} pts | Max per node per app: ${maxPointsPerNode} pts`);
console.log(` Endorsers available: ${endorsers.length}`);
const stargateNFTAddress = await stargateMock.stargateNFT();
const stargateNFT = await ethers.getContractAt("StargateNFT", stargateNFTAddress);
const endorserNodes = [];
for (const endorser of endorsers) {
const nodeIds = await stargateNFT.idsOwnedBy(endorser.address);
if (nodeIds.length > 0) {
endorserNodes.push({ signer: endorser, nodeIds: [...nodeIds] });
}
}
await endorseAppsWithNodeIds(x2EarnApps, apps, threshold, maxPointsPerNode, endorserNodes);
};
export const endorseXAppsWithExistingNodes = async (endorsers, x2EarnApps, apps, nodeManagement) => {
const contractsConfig = getContractsConfig(process.env.NEXT_PUBLIC_APP_ENV);
if (contractsConfig.X2EARN_NODE_COOLDOWN_PERIOD > 1) {
return console.warn("Endorsement cooldown period is greater than 1. Skipping endorsement.");
}
const maxPointsPerNode = Number(await x2EarnApps.maxPointsPerNodePerApp());
const threshold = Number(await x2EarnApps.endorsementScoreThreshold());
console.log(`\n========== Endorsing ${apps.length} x-apps ==========`);
console.log(` Threshold: ${threshold} pts | Max per node per app: ${maxPointsPerNode} pts`);
console.log(` Endorsers available: ${endorsers.length}`);
const endorserNodes = [];
for (const endorser of endorsers) {
const userNodes = await nodeManagement.getUserNodes(endorser.address);
const nodeIds = userNodes.map(node => node.nodeId);
if (nodeIds.length > 0) {
endorserNodes.push({ signer: endorser, nodeIds });
}
}
await endorseAppsWithNodeIds(x2EarnApps, apps, threshold, maxPointsPerNode, endorserNodes);
};
const endorseAppsWithNodeIds = async (x2EarnApps, apps, threshold, maxPointsPerNode, endorserNodes) => {
const x2EarnAppsAddress = await x2EarnApps.getAddress();
console.log(` Endorsers with nodes: ${endorserNodes.length} (total nodes: ${endorserNodes.reduce((sum, e) => sum + e.nodeIds.length, 0)})`);
if (endorserNodes.length === 0) {
console.log(`\n========== Endorsement complete ==========\n`);
return;
}
// For each app, stagger starting endorser to avoid exhausting the same
// nodes' total budget (each MjolnirX node has 100 pts across ALL apps).
for (let appIdx = 0; appIdx < apps.length; appIdx++) {
const appId = apps[appIdx];
const appInfo = await x2EarnApps.app(appId);
const appName = appInfo.name || appId.slice(0, 10) + "...";
const endorsements = [];
let lastError = null;
for (let j = 0; j < endorserNodes.length; j++) {
const endorser = endorserNodes[(appIdx + j) % endorserNodes.length];
const actualScore = Number(await x2EarnApps.getScore(appId));
if (actualScore >= threshold)
break;
for (const nodeId of endorser.nodeIds) {
const scoreBefore = Number(await x2EarnApps.getScore(appId));
if (scoreBefore >= threshold)
break;
const remaining = threshold - scoreBefore;
const points = Math.min(remaining, maxPointsPerNode);
try {
const freshX2EarnApps = (await ethers.getContractAt("X2EarnApps", x2EarnAppsAddress));
await freshX2EarnApps.connect(endorser.signer).endorseApp(appId, nodeId, points);
const scoreAfter = Number(await x2EarnApps.getScore(appId));
const applied = scoreAfter - scoreBefore;
if (applied > 0) {
endorsements.push(` node #${nodeId} (${endorser.signer.address.slice(0, 8)}...) -> ${applied} pts`);
}
}
catch (error) {
// Node may have hit its cap or budget, skip
lastError = error;
}
}
}
const finalScore = Number(await x2EarnApps.getScore(appId));
const status = finalScore >= threshold ? "ENDORSED" : "PARTIAL";
console.log(`\n [${status}] ${appName} — ${finalScore}/${threshold} pts`);
endorsements.forEach(line => console.log(line));
if (status === "PARTIAL" && lastError instanceof Error) {
console.log(` last error: ${lastError.message}`);
}
}
console.log(`\n========== Endorsement complete ==========\n`);
};
export const assignAppCategories = async (x2EarnApps, deployer, apps) => {
const config = getConfig();
const baseURI = await x2EarnApps.baseURI();
const allAppIds = await x2EarnApps.apps();
const appsWithCategories = apps.filter(a => a.categories && a.categories.length > 0);
if (appsWithCategories.length === 0) {
console.log("No apps with categories to assign, skipping...");
return;
}
console.log(`\n========== Assigning categories to ${appsWithCategories.length} apps ==========`);
for (const app of appsWithCategories) {
const appEntry = allAppIds.find((a) => a.name === app.name);
if (!appEntry) {
console.log(` [SKIP] ${app.name} — not found on-chain`);
continue;
}
const appId = appEntry.id;
// Fetch current metadata from IPFS
const metadataUri = await x2EarnApps.metadataURI(appId);
const fullUri = `${baseURI}${metadataUri}`;
const fetchUrl = fullUri.replace("ipfs://", `${config.ipfsFetchingService}/`);
let metadata = {};
try {
const response = await fetch(fetchUrl);
if (response.ok) {
metadata = (await response.json());
}
}
catch {
// If fetch fails, start with minimal metadata
metadata = { name: app.name };
}
// Add categories
metadata.categories = app.categories;
// Upload updated metadata to IPFS
const blob = new Blob([JSON.stringify(metadata)], { type: "application/json" });
const newCid = await uploadBlobToIPFS(blob, `${app.name}-metadata.json`);
// Update metadata on-chain (deployer has DEFAULT_ADMIN_ROLE)
await x2EarnApps.connect(deployer).updateAppMetadata(appId, newCid);
console.log(` [OK] ${app.name} — categories: [${app.categories.join(", ")}] — CID: ${newCid}`);
}
console.log(`\n========== Categories assigned ==========\n`);
};
export const castVotesToXDapps = async (vot3, xAllocationVoting, accounts, roundId, apps, ignoreErrors = false) => {
console.log("Casting votes to xDapps...");
if (apps.length === 0) {
throw new Error("No xDapps to vote for.");
}
const chunks = chunk(accounts, 50);
const contractAddress = await xAllocationVoting.getAddress();
for (const chunk of chunks) {
await Promise.all(chunk.map(async (account) => {
try {
const clauses = [];
const votePower = BigInt(await vot3.balanceOf(account.key.address.toString()));
const splits = [];
// eslint-disable-next-line no-unused-vars
let randomDappsToVote = apps.filter(_ => Math.floor(Math.random() * 2) == 0);
if (!randomDappsToVote.length)
randomDappsToVote = apps;
// Get the vote power per xDapp rounding down
const votePowerPerApp = votePower / BigInt(randomDappsToVote.length);
randomDappsToVote.forEach(app => splits.push({ app: app, weight: votePowerPerApp }));
clauses.push(Clause.callFunction(Address.of(contractAddress), ABIContract.ofAbi(XAllocationVoting__factory.abi).getFunction("castVote"), [roundId, splits.map(split => split.app), splits.map(split => split.weight)]));
console.log(`Casting round ${roundId} votes for ${account.key.address} with ${splits.map(split => split.weight)} votes to ${splits.map(split => split.app)}`);
await TransactionUtils.sendTx(thorClient, clauses, account.key.pk);
}
catch (e) {
if (ignoreErrors) {
console.error(`Error casting vote for account ${account.key.address}:`, e);
}
else {
throw e;
}
}
}));
}
console.log("Votes cast.");
};