shine-solana/shine/scripts/dao/create_realm_dao_full_build_exec.js

457 lines
17 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,
AuthorityType,
getMintLen,
createInitializeMintInstruction,
getAssociatedTokenAddressSync,
createAssociatedTokenAccountIdempotentInstruction,
createMintToInstruction,
createSetAuthorityInstruction,
} = require("@solana/spl-token");
const {
MintMaxVoteWeightSource,
VoteThreshold,
VoteThresholdType,
VoteTipping,
GovernanceConfig,
PROGRAM_VERSION_V3,
GoverningTokenConfigAccountArgs,
GoverningTokenType,
withCreateRealm,
withDepositGoverningTokens,
withCreateGovernance,
withCreateNativeTreasury,
withSetRealmAuthority,
SetRealmAuthorityAction,
} = require("@solana/spl-governance");
const { createUmi } = require("@metaplex-foundation/umi-bundle-defaults");
const {
createSignerFromKeypair,
signerIdentity,
percentAmount,
none,
some,
} = require("@metaplex-foundation/umi");
const { fromWeb3JsKeypair, fromWeb3JsPublicKey } = require("@metaplex-foundation/umi-web3js-adapters");
const { mplTokenMetadata, createV1, TokenStandard } = require("@metaplex-foundation/mpl-token-metadata");
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";
}
function ensureArweaveUri(name, uri) {
if (!uri) throw new Error(`${name} пустой`);
if (!(uri.startsWith("https://arweave.net/") || uri.startsWith("ar://"))) {
throw new Error(`${name} должен указывать на Arweave (https://arweave.net/... или ar://...)`);
}
}
async function attachTokenMetadataViaUmi(cfg, cluster, issuer, mintPubkey, mintKeypair) {
ensureArweaveUri("DAO_GOV_TOKEN_METADATA_URI", cfg.DAO_GOV_TOKEN_METADATA_URI);
ensureArweaveUri("DAO_GOV_TOKEN_IMAGE_URL", cfg.DAO_GOV_TOKEN_IMAGE_URL);
const umi = createUmi(clusterApiUrl(cluster));
const umiSigner = createSignerFromKeypair(umi, fromWeb3JsKeypair(issuer));
const umiMintSigner = createSignerFromKeypair(umi, fromWeb3JsKeypair(mintKeypair));
umi.use(signerIdentity(umiSigner));
umi.use(mplTokenMetadata());
const builder = createV1(umi, {
mint: umiMintSigner,
authority: umiSigner,
payer: umiSigner,
updateAuthority: umiSigner,
name: cfg.DAO_GOV_NFT_NAME,
symbol: cfg.DAO_GOV_NFT_SYMBOL,
uri: cfg.DAO_GOV_TOKEN_METADATA_URI,
sellerFeeBasisPoints: percentAmount(0),
tokenStandard: TokenStandard.Fungible,
decimals: some(0),
creators: none(),
collection: none(),
uses: none(),
collectionDetails: none(),
ruleSet: none(),
printSupply: none(),
primarySaleHappened: false,
isMutable: true,
isCollection: false,
splTokenProgram: fromWeb3JsPublicKey(TOKEN_PROGRAM_ID),
});
const res = await builder.sendAndConfirm(umi);
const sig = Buffer.from(res.signature).toString("base64");
return sig;
}
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",
"DAO_GOV_TOKEN_METADATA_URI",
"DAO_GOV_TOKEN_IMAGE_URL",
].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 < 3600)
throw new Error("DAO_VOTING_TIME_SEC должен быть >= 3600 (ограничение Realms)");
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 уже существует: ${realmPda.toBase58()}`);
const startBalance = await connection.getBalance(issuer.publicKey, "confirmed");
console.log("============================================================");
console.log("СОЗДАНИЕ DAO (FULL)");
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("Token name/symbol: ", `${cfg.DAO_GOV_NFT_NAME} / ${cfg.DAO_GOV_NFT_SYMBOL}`);
console.log("Token supply: ", supply);
console.log("Voting time sec: ", votingTimeSec);
console.log("Threshold %: ", thresholdPct);
console.log("Arweave metadata URI:", cfg.DAO_GOV_TOKEN_METADATA_URI);
console.log("Arweave image URL: ", cfg.DAO_GOV_TOKEN_IMAGE_URL);
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 sigMetadata = await attachTokenMetadataViaUmi(
cfg,
cluster,
issuer,
mintKeypair.publicKey,
mintKeypair
);
const programVersion = PROGRAM_VERSION_V3;
const ixRealm = [];
const communityTokenConfig = new GoverningTokenConfigAccountArgs({
voterWeightAddin: undefined,
maxVoterWeightAddin: undefined,
tokenType: GoverningTokenType.Membership,
});
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),
communityTokenConfig,
undefined
);
const sigRealm = await sendAndConfirmTransaction(connection, new Transaction().add(...ixRealm), [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 sigDeposit = await sendAndConfirmTransaction(connection, new Transaction().add(...ixDeposit), [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.Early,
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 sigGov = await sendAndConfirmTransaction(connection, new Transaction().add(...ixGov), [issuer], {
commitment: "confirmed",
});
// Для DAO revoke governing tokens mint authority должен быть у governance PDA.
const ixSetMintAuthority = [
createSetAuthorityInstruction(
mintKeypair.publicKey,
issuer.publicKey,
AuthorityType.MintTokens,
governancePk,
[],
TOKEN_PROGRAM_ID
),
];
const sigSetMintAuthority = await sendAndConfirmTransaction(
connection,
new Transaction().add(...ixSetMintAuthority),
[issuer],
{ commitment: "confirmed" }
);
const ixRealmAuthority = [];
withSetRealmAuthority(
ixRealmAuthority,
governanceProgramId,
programVersion,
realmPk,
issuer.publicKey,
governancePk,
SetRealmAuthorityAction.SetChecked
);
const sigSetRealmAuthority = await sendAndConfirmTransaction(
connection,
new Transaction().add(...ixRealmAuthority),
[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(),
metadataUri: cfg.DAO_GOV_TOKEN_METADATA_URI,
imageUrl: cfg.DAO_GOV_TOKEN_IMAGE_URL,
txMint: sigMint,
txMetadata: sigMetadata,
txRealm: sigRealm,
txDeposit: sigDeposit,
txGovernanceTreasury: sigGov,
txSetMintAuthorityToGovernance: sigSetMintAuthority,
txSetRealmAuthority: sigSetRealmAuthority,
votingTimeSec,
thresholdPercent: thresholdPct,
tokenSupply: 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)}_full`;
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}`,
`metadataUri: ${report.metadataUri}`,
`imageUrl: ${report.imageUrl}`,
`txMint: ${report.txMint}`,
`txMetadata: ${report.txMetadata}`,
`txRealm: ${report.txRealm}`,
`txDeposit: ${report.txDeposit}`,
`txGovernanceTreasury: ${report.txGovernanceTreasury}`,
`txSetMintAuthorityToGovernance: ${report.txSetMintAuthorityToGovernance}`,
`txSetRealmAuthority: ${report.txSetRealmAuthority}`,
`tokenSupply: ${report.tokenSupply}`,
`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 FULL СОЗДАНО");
console.log("------------------------------------------------------------");
console.log("Community mint (SPL + metadata): ", mintKeypair.publicKey.toBase58());
console.log("Realm: ", realmPk.toBase58());
console.log("Governance: ", governancePk.toBase58());
console.log("Native treasury PDA: ", treasuryPk.toBase58());
console.log("Tx mint: ", sigMint);
console.log("Tx metadata: ", sigMetadata);
console.log("Tx realm: ", sigRealm);
console.log("Tx deposit: ", sigDeposit);
console.log("Tx governance+treasury: ", sigGov);
console.log("Tx set mint authority -> governance: ", sigSetMintAuthority);
console.log("Tx set realm authority -> governance: ", sigSetRealmAuthority);
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 FULL:", e?.message || e);
process.exit(1);
});