shine-solana/shine/scripts/dao_legacy/create_realm_dao_build_exec.js

380 lines
13 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
"use strict";
const fs = require("fs");
const path = require("path");
const readline = require("readline");
const BN = require("bn.js");
const {
Connection,
Keypair,
PublicKey,
SystemProgram,
Transaction,
sendAndConfirmTransaction,
clusterApiUrl,
} = require("@solana/web3.js");
const {
TOKEN_PROGRAM_ID,
getMintLen,
createInitializeMintInstruction,
getAssociatedTokenAddressSync,
createAssociatedTokenAccountIdempotentInstruction,
createMintToInstruction,
} = require("@solana/spl-token");
const {
MintMaxVoteWeightSource,
VoteThreshold,
VoteThresholdType,
VoteTipping,
GovernanceConfig,
withCreateRealm,
withDepositGoverningTokens,
withCreateGovernance,
withCreateNativeTreasury,
PROGRAM_VERSION_V3,
} = require("@solana/spl-governance");
function parseEnvConfig(configPath) {
const raw = fs.readFileSync(configPath, "utf8");
const out = {};
for (const line of raw.split("\n")) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eq = trimmed.indexOf("=");
if (eq === -1) continue;
const key = trimmed.slice(0, eq).trim();
let val = trimmed.slice(eq + 1).trim();
if (
(val.startsWith('"') && val.endsWith('"')) ||
(val.startsWith("'") && val.endsWith("'"))
) {
val = val.slice(1, -1);
}
val = val.replace(/\$HOME/g, process.env.HOME || "");
out[key] = val;
}
return out;
}
function assertRequired(cfg, key) {
if (!cfg[key]) {
throw new Error(`В конфиге отсутствует обязательный параметр: ${key}`);
}
}
function loadKeypair(filePath) {
const arr = JSON.parse(fs.readFileSync(filePath, "utf8"));
return Keypair.fromSecretKey(Uint8Array.from(arr));
}
function lamportsToSol(lamports) {
return Number(lamports) / 1_000_000_000;
}
function nowStamp() {
const d = new Date();
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}_${p(d.getHours())}-${p(
d.getMinutes()
)}-${p(d.getSeconds())}`;
}
async function askYes() {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise((resolve) =>
rl.question("Введите YES для реального создания DAO: ", resolve)
);
rl.close();
return answer.trim() === "YES";
}
async function main() {
const configPath = process.argv[2]
? path.resolve(process.argv[2])
: path.resolve(__dirname, "dao.config.env");
if (!fs.existsSync(configPath)) {
throw new Error(`Конфиг не найден: ${configPath}`);
}
const cfg = parseEnvConfig(configPath);
[
"DAO_CLUSTER",
"DAO_REALM_NAME",
"DAO_GOV_NFT_NAME",
"DAO_GOV_NFT_SYMBOL",
"DAO_GOV_NFT_SUPPLY",
"DAO_VOTING_TIME_SEC",
"DAO_APPROVAL_THRESHOLD_PERCENT",
"DAO_ISSUER_KEYPAIR",
"SPL_GOVERNANCE_PROGRAM_ID",
].forEach((k) => assertRequired(cfg, k));
const cluster = cfg.DAO_CLUSTER;
const connection = new Connection(clusterApiUrl(cluster), "confirmed");
const issuer = loadKeypair(path.resolve(cfg.DAO_ISSUER_KEYPAIR));
const governanceProgramId = new PublicKey(cfg.SPL_GOVERNANCE_PROGRAM_ID);
const supply = Number(cfg.DAO_GOV_NFT_SUPPLY);
const votingTimeSec = Number(cfg.DAO_VOTING_TIME_SEC);
const thresholdPct = Number(cfg.DAO_APPROVAL_THRESHOLD_PERCENT);
if (!Number.isInteger(supply) || supply <= 0) {
throw new Error("DAO_GOV_NFT_SUPPLY должен быть целым > 0");
}
if (!Number.isInteger(votingTimeSec) || votingTimeSec <= 0) {
throw new Error("DAO_VOTING_TIME_SEC должен быть целым > 0");
}
if (!Number.isInteger(thresholdPct) || thresholdPct < 51 || thresholdPct > 100) {
throw new Error("DAO_APPROVAL_THRESHOLD_PERCENT должен быть в диапазоне 51..100");
}
const [realmPda] = PublicKey.findProgramAddressSync(
[Buffer.from("governance"), Buffer.from(cfg.DAO_REALM_NAME, "utf8")],
governanceProgramId
);
const realmExists = (await connection.getAccountInfo(realmPda)) !== null;
if (realmExists) {
throw new Error(
`Realm уже существует для имени '${cfg.DAO_REALM_NAME}': ${realmPda.toBase58()}`
);
}
const startBalance = await connection.getBalance(issuer.publicKey, "confirmed");
console.log("============================================================");
console.log("СОЗДАНИЕ DAO (EXEC)");
console.log("------------------------------------------------------------");
console.log("Сеть: ", cluster);
console.log("Realm name: ", cfg.DAO_REALM_NAME);
console.log("Realm PDA: ", realmPda.toBase58());
console.log("Governance program: ", governanceProgramId.toBase58());
console.log("Issuer: ", issuer.publicKey.toBase58());
console.log("Баланс до старта: ", `${lamportsToSol(startBalance)} SOL`);
console.log("NFT symbol/name: ", `${cfg.DAO_GOV_NFT_SYMBOL} / ${cfg.DAO_GOV_NFT_NAME}`);
console.log("NFT supply: ", supply);
console.log("Voting time sec: ", votingTimeSec);
console.log("Threshold %: ", thresholdPct);
console.log("Конфиг: ", configPath);
console.log("============================================================");
const ok = await askYes();
if (!ok) {
console.log("Отменено пользователем.");
return;
}
const mintKeypair = Keypair.generate();
const mintLen = getMintLen([]);
const mintRent = await connection.getMinimumBalanceForRentExemption(mintLen);
const issuerAta = getAssociatedTokenAddressSync(
mintKeypair.publicKey,
issuer.publicKey,
false,
TOKEN_PROGRAM_ID
);
const txMint = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: issuer.publicKey,
newAccountPubkey: mintKeypair.publicKey,
space: mintLen,
lamports: mintRent,
programId: TOKEN_PROGRAM_ID,
}),
createInitializeMintInstruction(
mintKeypair.publicKey,
0,
issuer.publicKey,
issuer.publicKey,
TOKEN_PROGRAM_ID
),
createAssociatedTokenAccountIdempotentInstruction(
issuer.publicKey,
issuerAta,
issuer.publicKey,
mintKeypair.publicKey,
TOKEN_PROGRAM_ID
),
createMintToInstruction(
mintKeypair.publicKey,
issuerAta,
issuer.publicKey,
supply,
[],
TOKEN_PROGRAM_ID
)
);
const sigMint = await sendAndConfirmTransaction(connection, txMint, [issuer, mintKeypair], {
commitment: "confirmed",
});
const programVersion = PROGRAM_VERSION_V3;
const ixRealm = [];
const realmPk = await withCreateRealm(
ixRealm,
governanceProgramId,
programVersion,
cfg.DAO_REALM_NAME,
issuer.publicKey,
mintKeypair.publicKey,
issuer.publicKey,
undefined,
MintMaxVoteWeightSource.FULL_SUPPLY_FRACTION,
new BN(1)
);
const txRealm = new Transaction().add(...ixRealm);
const sigRealm = await sendAndConfirmTransaction(connection, txRealm, [issuer], {
commitment: "confirmed",
});
const ixDeposit = [];
const tokenOwnerRecordPk = await withDepositGoverningTokens(
ixDeposit,
governanceProgramId,
programVersion,
realmPk,
issuerAta,
mintKeypair.publicKey,
issuer.publicKey,
issuer.publicKey,
issuer.publicKey,
new BN(supply),
true
);
const txDeposit = new Transaction().add(...ixDeposit);
const sigDeposit = await sendAndConfirmTransaction(connection, txDeposit, [issuer], {
commitment: "confirmed",
});
const governanceConfig = new GovernanceConfig({
communityVoteThreshold: new VoteThreshold({
type: VoteThresholdType.YesVotePercentage,
value: thresholdPct,
}),
minCommunityTokensToCreateProposal: new BN(1),
minInstructionHoldUpTime: 0,
baseVotingTime: votingTimeSec,
communityVoteTipping: VoteTipping.Strict,
minCouncilTokensToCreateProposal: new BN(0),
councilVoteThreshold: new VoteThreshold({ type: VoteThresholdType.Disabled }),
councilVetoVoteThreshold: new VoteThreshold({ type: VoteThresholdType.Disabled }),
communityVetoVoteThreshold: new VoteThreshold({ type: VoteThresholdType.Disabled }),
councilVoteTipping: VoteTipping.Disabled,
votingCoolOffTime: 0,
depositExemptProposalCount: 0,
});
const ixGov = [];
const governancePk = await withCreateGovernance(
ixGov,
governanceProgramId,
programVersion,
realmPk,
realmPk,
governanceConfig,
tokenOwnerRecordPk,
issuer.publicKey,
issuer.publicKey
);
const treasuryPk = await withCreateNativeTreasury(
ixGov,
governanceProgramId,
programVersion,
governancePk,
issuer.publicKey
);
const txGov = new Transaction().add(...ixGov);
const sigGov = await sendAndConfirmTransaction(connection, txGov, [issuer], {
commitment: "confirmed",
});
const endBalance = await connection.getBalance(issuer.publicKey, "confirmed");
const spentLamports = startBalance - endBalance;
const report = {
createdAt: new Date().toISOString(),
cluster,
configPath,
realmName: cfg.DAO_REALM_NAME,
governanceProgramId: governanceProgramId.toBase58(),
issuer: issuer.publicKey.toBase58(),
communityMint: mintKeypair.publicKey.toBase58(),
issuerAta: issuerAta.toBase58(),
realm: realmPk.toBase58(),
tokenOwnerRecord: tokenOwnerRecordPk.toBase58(),
governance: governancePk.toBase58(),
nativeTreasury: treasuryPk.toBase58(),
txMint: sigMint,
txRealm: sigRealm,
txDeposit: sigDeposit,
txGovernanceTreasury: sigGov,
votingTimeSec,
thresholdPercent: thresholdPct,
nftSupply: supply,
startBalanceLamports: startBalance,
endBalanceLamports: endBalance,
spentLamports,
startBalanceSol: lamportsToSol(startBalance),
endBalanceSol: lamportsToSol(endBalance),
spentSol: lamportsToSol(spentLamports),
};
const reportDir = path.resolve(__dirname, "runs");
fs.mkdirSync(reportDir, { recursive: true });
const reportBaseName = `${nowStamp()}_${cfg.DAO_REALM_NAME
.replace(/[^a-zA-Z0-9._-]+/g, "_")
.slice(0, 80)}`;
const reportJsonPath = path.join(reportDir, `${reportBaseName}.json`);
const reportTxtPath = path.join(reportDir, `${reportBaseName}.txt`);
fs.writeFileSync(reportJsonPath, JSON.stringify(report, null, 2));
fs.writeFileSync(
reportTxtPath,
[
`createdAt: ${report.createdAt}`,
`cluster: ${report.cluster}`,
`realmName: ${report.realmName}`,
`governanceProgramId: ${report.governanceProgramId}`,
`issuer: ${report.issuer}`,
`communityMint: ${report.communityMint}`,
`issuerAta: ${report.issuerAta}`,
`realm: ${report.realm}`,
`tokenOwnerRecord: ${report.tokenOwnerRecord}`,
`governance: ${report.governance}`,
`nativeTreasury: ${report.nativeTreasury}`,
`txMint: ${report.txMint}`,
`txRealm: ${report.txRealm}`,
`txDeposit: ${report.txDeposit}`,
`txGovernanceTreasury: ${report.txGovernanceTreasury}`,
`nftSupply: ${report.nftSupply}`,
`votingTimeSec: ${report.votingTimeSec}`,
`thresholdPercent: ${report.thresholdPercent}`,
`startBalanceSol: ${report.startBalanceSol}`,
`endBalanceSol: ${report.endBalanceSol}`,
`spentSol: ${report.spentSol}`,
`configPath: ${report.configPath}`,
].join("\n") + "\n"
);
console.log("============================================================");
console.log("DAO СОЗДАНО");
console.log("------------------------------------------------------------");
console.log("Community mint (SPL Token, transferable): ", mintKeypair.publicKey.toBase58());
console.log("Issuer ATA: ", issuerAta.toBase58());
console.log("Realm: ", realmPk.toBase58());
console.log("TokenOwnerRecord: ", tokenOwnerRecordPk.toBase58());
console.log("Governance: ", governancePk.toBase58());
console.log("Native treasury PDA: ", treasuryPk.toBase58());
console.log("Tx mint: ", sigMint);
console.log("Tx realm: ", sigRealm);
console.log("Tx deposit: ", sigDeposit);
console.log("Tx governance+treasury: ", sigGov);
console.log("Баланс после: ", `${lamportsToSol(endBalance)} SOL`);
console.log("Потрачено: ", `${lamportsToSol(spentLamports)} SOL`);
console.log("Отчёт JSON: ", reportJsonPath);
console.log("Отчёт TXT: ", reportTxtPath);
console.log("============================================================");
}
main().catch((e) => {
console.error("Ошибка создания DAO:", e?.message || e);
process.exit(1);
});