#!/usr/bin/env node "use strict"; const fs = require("fs"); const path = require("path"); const BN = require("bn.js"); const { Connection, PublicKey, Transaction, sendAndConfirmTransaction, } = require("@solana/web3.js"); const { getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, } = 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 { parseEnvConfig, assertRequired, resolveConfigPath, parseCluster, nowStamp, getOperatorKeypairFromConfig, getMintPublicKeyFromConfig, ui } = require("./_common"); async function main() { const configPath = resolveConfigPath(process.argv[2]); const cfg = parseEnvConfig(configPath); [ "GT_CLUSTER", "DAO_REALM_NAME", "SPL_GOVERNANCE_PROGRAM_ID", "DAO_VOTING_TIME_SEC", "DAO_APPROVAL_THRESHOLD_PERCENT" ].forEach((k) => assertRequired(cfg, k)); const cluster = cfg.GT_CLUSTER; const connection = new Connection(parseCluster(cluster), "confirmed"); const operator = getOperatorKeypairFromConfig(cfg); const governanceProgramId = new PublicKey(cfg.SPL_GOVERNANCE_PROGRAM_ID); const mint = getMintPublicKeyFromConfig(cfg); const votingTimeSec = Number(cfg.DAO_VOTING_TIME_SEC); const thresholdPct = Number(cfg.DAO_APPROVAL_THRESHOLD_PERCENT); const runsDir = path.resolve(cfg.DAO_RUNS_DIR || path.join(__dirname, "runs")); fs.mkdirSync(runsDir, { recursive: true }); const mintAi = await connection.getAccountInfo(mint, "confirmed"); if (!mintAi) throw new Error(`Governing mint not found: ${mint.toBase58()}`); if (!mintAi.owner.equals(TOKEN_PROGRAM_ID)) { throw new Error( `Этот CreateDAO ожидает governing mint под классическим SPL Token (${TOKEN_PROGRAM_ID.toBase58()}). ` + `Текущий mint owner: ${mintAi.owner.toBase58()}` ); } 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 already exists: ${realmPda.toBase58()}`); const ownerAtaToken2022 = getAssociatedTokenAddressSync(mint, operator.publicKey, false, TOKEN_2022_PROGRAM_ID); const ownerAtaToken = getAssociatedTokenAddressSync(mint, operator.publicKey, false, TOKEN_PROGRAM_ID); let ownerAta = ownerAtaToken2022; let ownerAtaInfo = await connection.getAccountInfo(ownerAtaToken2022, "confirmed"); let tokenProgramId = TOKEN_2022_PROGRAM_ID; if (!ownerAtaInfo) { ownerAta = ownerAtaToken; ownerAtaInfo = await connection.getAccountInfo(ownerAtaToken, "confirmed"); tokenProgramId = TOKEN_PROGRAM_ID; } if (!ownerAtaInfo) throw new Error("Operator ATA for governing mint not found. Mint at least 1 token to operator first."); 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, operator.publicKey, mint, operator.publicKey, undefined, MintMaxVoteWeightSource.FULL_SUPPLY_FRACTION, new BN(1), communityTokenConfig, undefined ); const sigRealm = await sendAndConfirmTransaction(connection, new Transaction().add(...ixRealm), [operator], { commitment: "confirmed" }); const ixDeposit = []; const tokenOwnerRecordPk = await withDepositGoverningTokens( ixDeposit, governanceProgramId, programVersion, realmPk, ownerAta, mint, operator.publicKey, operator.publicKey, operator.publicKey, new BN(1), true, tokenProgramId ); const sigDeposit = await sendAndConfirmTransaction(connection, new Transaction().add(...ixDeposit), [operator], { 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, operator.publicKey, operator.publicKey ); const treasuryPk = await withCreateNativeTreasury(ixGov, governanceProgramId, programVersion, governancePk, operator.publicKey); const sigGov = await sendAndConfirmTransaction(connection, new Transaction().add(...ixGov), [operator], { commitment: "confirmed" }); const ixRealmAuthority = []; withSetRealmAuthority( ixRealmAuthority, governanceProgramId, programVersion, realmPk, operator.publicKey, governancePk, SetRealmAuthorityAction.SetChecked ); const sigSetRealmAuthority = await sendAndConfirmTransaction(connection, new Transaction().add(...ixRealmAuthority), [operator], { commitment: "confirmed" }); const report = { createdAt: new Date().toISOString(), cluster, realmName: cfg.DAO_REALM_NAME, governanceProgramId: governanceProgramId.toBase58(), governingMint: mint.toBase58(), operator: operator.publicKey.toBase58(), realm: realmPk.toBase58(), governance: governancePk.toBase58(), nativeTreasury: treasuryPk.toBase58(), tokenOwnerRecord: tokenOwnerRecordPk.toBase58(), txRealm: sigRealm, txDeposit: sigDeposit, txGovernanceTreasury: sigGov, txSetRealmAuthority: sigSetRealmAuthority, }; const reportPath = path.join(runsDir, `${nowStamp()}_create_dao.json`); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2)); ui.ok("DAO created successfully / DAO успешно создан"); ui.info(`Realm: ${realmPk.toBase58()}`); ui.info(`Governance PDA: ${governancePk.toBase58()}`); ui.info(`Treasury: ${treasuryPk.toBase58()}`); ui.info(`Report: ${reportPath}`); } main().catch((e) => { console.error("CreateDAO error:", e?.message || e); process.exit(1); });