@vechain/vebetterdao-contracts
Version:
Open-source repository that houses the smart contracts powering the decentralized VeBetterDAO on the VeChain Thor blockchain.
203 lines (202 loc) • 10.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.castVotesToXDapps = exports.assignAppCategories = exports.endorseXAppsWithExistingNodes = exports.endorseXApps = exports.registerXDapps = void 0;
const hardhat_1 = require("hardhat");
const typechain_types_1 = require("../../typechain-types");
const sdk_core_1 = require("@vechain/sdk-core");
const utils_1 = require("@repo/utils");
const chunk_1 = require("./chunk");
const config_1 = require("@repo/config");
const config_2 = require("@repo/config");
const sdk_network_1 = require("@vechain/sdk-network");
const ipfs_1 = require("./ipfs");
const thorClient = sdk_network_1.ThorClient.at((0, config_2.getConfig)().nodeUrl);
const registerXDapps = async (contractAddress, accounts, apps) => {
console.log("Adding x-apps...");
const appChunks = (0, chunk_1.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 = sdk_core_1.Clause.callFunction(sdk_core_1.Address.of(contractAddress), sdk_core_1.ABIContract.ofAbi(typechain_types_1.X2EarnApps__factory.abi).getFunction("submitApp"), [app.teamWalletAddress, app.admin, app.name, app.metadataURI]);
await utils_1.TransactionUtils.sendTx(thorClient, [clause], account.pk);
}
}
};
exports.registerXDapps = registerXDapps;
const endorseXApps = async (endorsers, x2EarnApps, apps, stargateMock) => {
const contractsConfig = (0, config_1.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 hardhat_1.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);
};
exports.endorseXApps = endorseXApps;
const endorseXAppsWithExistingNodes = async (endorsers, x2EarnApps, apps, nodeManagement) => {
const contractsConfig = (0, config_1.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);
};
exports.endorseXAppsWithExistingNodes = endorseXAppsWithExistingNodes;
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 hardhat_1.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`);
};
const assignAppCategories = async (x2EarnApps, deployer, apps) => {
const config = (0, config_2.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 (0, ipfs_1.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`);
};
exports.assignAppCategories = assignAppCategories;
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 = (0, chunk_1.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(sdk_core_1.Clause.callFunction(sdk_core_1.Address.of(contractAddress), sdk_core_1.ABIContract.ofAbi(typechain_types_1.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 utils_1.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.");
};
exports.castVotesToXDapps = castVotesToXDapps;