#!/usr/bin/env node "use strict"; const fs = require("fs"); const path = require("path"); const readline = require("readline"); const { Connection, PublicKey, Transaction, sendAndConfirmTransaction, } = require("@solana/web3.js"); const { TOKEN_PROGRAM_ID, AuthorityType, createSetAuthorityInstruction, } = require("@solana/spl-token"); const { PROGRAM_VERSION_V3, getRealm, withSetRealmAuthority, SetRealmAuthorityAction, } = require("@solana/spl-governance"); const { createUmi } = require("@metaplex-foundation/umi-bundle-defaults"); const { createSignerFromKeypair, signerIdentity, none, some, } = require("@metaplex-foundation/umi"); const { fromWeb3JsKeypair, fromWeb3JsPublicKey } = require("@metaplex-foundation/umi-web3js-adapters"); const { mplTokenMetadata, fetchMetadataFromSeeds, updateV1, } = require("@metaplex-foundation/mpl-token-metadata"); const { clusterUrl, ensureDir, loadKeypair, nowStamp, parseEnvConfig, resolveConfigPath, } = require("./js_common"); function lamportsToSol(lamports) { return Number(lamports) / 1_000_000_000; } async function askYes() { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const answer = await new Promise((resolve) => rl.question("Введите YES для передачи authority на DAO: ", resolve) ); rl.close(); return answer.trim() === "YES"; } async function fetchMetadataState(cluster, issuer, mint) { const umi = createUmi(clusterUrl(cluster)); const issuerSigner = createSignerFromKeypair(umi, fromWeb3JsKeypair(issuer)); umi.use(signerIdentity(issuerSigner)); umi.use(mplTokenMetadata()); const metadata = await fetchMetadataFromSeeds(umi, { mint: fromWeb3JsPublicKey(mint) }); return { umi, issuerSigner, metadata, }; } async function fetchMetadataUpdateAuthority(cluster, mint) { const umi = createUmi(clusterUrl(cluster)); umi.use(mplTokenMetadata()); const metadata = await fetchMetadataFromSeeds(umi, { mint: fromWeb3JsPublicKey(mint) }); return metadata.updateAuthority.toString(); } async function main() { const configPath = resolveConfigPath(process.argv[2], "dao.config.env"); if (!fs.existsSync(configPath)) throw new Error(`Конфиг не найден: ${configPath}`); const cfg = parseEnvConfig(configPath); [ "DAO_CLUSTER", "DAO_REALM_NAME", "DAO_ISSUER_KEYPAIR", "SPL_GOVERNANCE_PROGRAM_ID", "DAO_GOV_TOKEN_MINT", ].forEach((k) => { if (!cfg[k]) throw new Error(`В конфиге отсутствует обязательный параметр: ${k}`); }); const cluster = cfg.DAO_CLUSTER; const connection = new Connection(clusterUrl(cluster), "confirmed"); const issuer = loadKeypair(path.resolve(cfg.DAO_ISSUER_KEYPAIR)); const governanceProgramId = new PublicKey(cfg.SPL_GOVERNANCE_PROGRAM_ID); const mint = new PublicKey(cfg.DAO_GOV_TOKEN_MINT); const [realmPk] = PublicKey.findProgramAddressSync( [Buffer.from("governance"), Buffer.from(cfg.DAO_REALM_NAME, "utf8")], governanceProgramId ); const [governancePk] = PublicKey.findProgramAddressSync( [Buffer.from("account-governance"), realmPk.toBuffer(), realmPk.toBuffer()], governanceProgramId ); const realmInfo = await connection.getAccountInfo(realmPk, "confirmed"); if (!realmInfo) throw new Error(`Realm не найден: ${realmPk.toBase58()}. Сначала выполните шаг 2.`); const governanceInfo = await connection.getAccountInfo(governancePk, "confirmed"); if (!governanceInfo) { throw new Error(`Governance PDA не найден: ${governancePk.toBase58()}. Сначала выполните шаг 2.`); } const realmBefore = await getRealm(connection, realmPk); const realmAuthorityBefore = realmBefore.account.authority?.toBase58?.() || null; if (realmAuthorityBefore !== issuer.publicKey.toBase58()) { throw new Error(`Realm authority уже не у issuer: ${realmAuthorityBefore}`); } const mintInfoBefore = await connection.getParsedAccountInfo(mint, "confirmed"); if (!mintInfoBefore.value) throw new Error(`Mint не найден: ${mint.toBase58()}`); const mintParsedBefore = mintInfoBefore.value.data.parsed.info; const mintAuthorityBefore = mintParsedBefore.mintAuthority || null; const freezeAuthorityBefore = mintParsedBefore.freezeAuthority || null; if (mintAuthorityBefore && mintAuthorityBefore !== issuer.publicKey.toBase58()) { throw new Error(`Mint authority уже не у issuer: ${mintAuthorityBefore}`); } if (freezeAuthorityBefore && freezeAuthorityBefore !== issuer.publicKey.toBase58()) { throw new Error(`Freeze authority уже не у issuer: ${freezeAuthorityBefore}`); } const { umi, metadata, issuerSigner } = await fetchMetadataState(cluster, issuer, mint); const metadataUpdateAuthorityBefore = metadata.updateAuthority.toString(); if (metadataUpdateAuthorityBefore !== issuer.publicKey.toBase58()) { throw new Error(`Metadata update authority уже не у issuer: ${metadataUpdateAuthorityBefore}`); } const startBalance = await connection.getBalance(issuer.publicKey, "confirmed"); console.log("============================================================"); console.log("TRANSFER DAO AUTHORITIES"); console.log("------------------------------------------------------------"); console.log("Сеть: ", cluster); console.log("Realm: ", realmPk.toBase58()); console.log("Governance PDA: ", governancePk.toBase58()); console.log("Issuer: ", issuer.publicKey.toBase58()); console.log("Governance mint: ", mint.toBase58()); console.log("Баланс до: ", `${lamportsToSol(startBalance)} SOL`); console.log("Mint authority до: ", mintAuthorityBefore || "null"); console.log("Freeze authority до: ", freezeAuthorityBefore || "null"); console.log("Realm authority до: ", realmAuthorityBefore || "null"); console.log("Metadata authority до: ", metadataUpdateAuthorityBefore); console.log("============================================================"); if (!(await askYes())) { console.log("Отменено."); return; } const ixTransferAuthorities = [ createSetAuthorityInstruction( mint, issuer.publicKey, AuthorityType.MintTokens, governancePk, [], TOKEN_PROGRAM_ID ), createSetAuthorityInstruction( mint, issuer.publicKey, AuthorityType.FreezeAccount, governancePk, [], TOKEN_PROGRAM_ID ), ]; const sigTransferAuthorities = await sendAndConfirmTransaction( connection, new Transaction().add(...ixTransferAuthorities), [issuer], { commitment: "confirmed" } ); const ixSetRealmAuthority = []; withSetRealmAuthority( ixSetRealmAuthority, governanceProgramId, PROGRAM_VERSION_V3, realmPk, issuer.publicKey, governancePk, SetRealmAuthorityAction.SetChecked ); const sigSetRealmAuthority = await sendAndConfirmTransaction( connection, new Transaction().add(...ixSetRealmAuthority), [issuer], { commitment: "confirmed" } ); const metadataTx = await updateV1(umi, { authority: issuerSigner, mint: fromWeb3JsPublicKey(mint), newUpdateAuthority: some(fromWeb3JsPublicKey(governancePk)), data: none(), primarySaleHappened: none(), isMutable: none(), collection: { __kind: "None" }, collectionDetails: { __kind: "None" }, uses: { __kind: "None" }, ruleSet: { __kind: "None" }, authorizationData: none(), }).sendAndConfirm(umi); const sigMetadataAuthority = Buffer.from(metadataTx.signature).toString("base64"); const mintInfoAfter = await connection.getParsedAccountInfo(mint, "confirmed"); const mintParsedAfter = mintInfoAfter.value.data.parsed.info; const mintAuthorityAfter = mintParsedAfter.mintAuthority || null; const freezeAuthorityAfter = mintParsedAfter.freezeAuthority || null; const realmAfter = await getRealm(connection, realmPk); const realmAuthorityAfter = realmAfter.account.authority?.toBase58?.() || null; const metadataUpdateAuthorityAfter = await fetchMetadataUpdateAuthority(cluster, mint); const report = { createdAt: new Date().toISOString(), cluster, realmName: cfg.DAO_REALM_NAME, governanceProgramId: governanceProgramId.toBase58(), issuer: issuer.publicKey.toBase58(), governingMint: mint.toBase58(), realm: realmPk.toBase58(), governance: governancePk.toBase58(), mintAuthorityBefore, freezeAuthorityBefore, metadataUpdateAuthorityBefore, realmAuthorityBefore, mintAuthorityAfter, freezeAuthorityAfter, realmAuthorityAfter, metadataUpdateAuthorityAfter, txTransferAuthorities: sigTransferAuthorities, txSetRealmAuthority: sigSetRealmAuthority, txSetMetadataUpdateAuthority: sigMetadataAuthority, }; const runsDir = path.resolve(__dirname, "..", "local", "runs"); ensureDir(runsDir); const reportPath = path.join(runsDir, `${nowStamp()}_transfer_dao_authorities.json`); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2)); console.log("Все authority переданы на DAO."); console.log("realm:", realmPk.toBase58()); console.log("governance:", governancePk.toBase58()); console.log("mintAuthority после:", mintAuthorityAfter || "null"); console.log("freezeAuthority после:", freezeAuthorityAfter || "null"); console.log("realmAuthority после:", realmAuthorityAfter || "null"); console.log("metadataAuthority после:", metadataUpdateAuthorityAfter); console.log("report:", reportPath); } main().catch((e) => { console.error("TransferAuthorities error:", e?.message || e); process.exit(1); });