#!/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, Transaction, sendAndConfirmTransaction, clusterApiUrl } = require("@solana/web3.js"); const { PROGRAM_VERSION_V3, withRevokeGoverningTokens } = 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 loadKeypair(filePath) { const arr = JSON.parse(fs.readFileSync(filePath, "utf8")); return Keypair.fromSecretKey(Uint8Array.from(arr)); } async function askYes() { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const answer = await new Promise((resolve) => rl.question("Введите YES для отзыва (burn/revoke) governance токенов: ", 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"); const realmStr = process.argv[3]; const mintStr = process.argv[4]; const targetOwnerStr = process.argv[5]; const amountStr = process.argv[6] || "1"; if (!realmStr || !mintStr || !targetOwnerStr) { throw new Error( "Использование: node scripts/dao/revoke_member_token_full_exec.js [amount]" ); } const cfg = parseEnvConfig(configPath); const cluster = cfg.DAO_CLUSTER || "devnet"; const governanceProgramId = new PublicKey(cfg.SPL_GOVERNANCE_PROGRAM_ID); const revokeKpPath = cfg.DAO_REVOKE_AUTHORITY_KEYPAIR || cfg.DAO_ISSUER_KEYPAIR; if (!revokeKpPath) throw new Error("В конфиге нет DAO_REVOKE_AUTHORITY_KEYPAIR и DAO_ISSUER_KEYPAIR"); const revokeAuthority = loadKeypair(path.resolve(revokeKpPath)); const realm = new PublicKey(realmStr); const mint = new PublicKey(mintStr); const targetOwner = new PublicKey(targetOwnerStr); const amount = new BN(amountStr); if (amount.lten(0)) throw new Error("amount должен быть > 0"); console.log("============================================================"); console.log("REVOKE/BURN GOVERNANCE TOKENS"); console.log("------------------------------------------------------------"); console.log("Сеть: ", cluster); console.log("Governance program: ", governanceProgramId.toBase58()); console.log("Realm: ", realm.toBase58()); console.log("Mint: ", mint.toBase58()); console.log("Target owner: ", targetOwner.toBase58()); console.log("Amount: ", amount.toString()); console.log("Revoke authority: ", revokeAuthority.publicKey.toBase58()); console.log("============================================================"); const ok = await askYes(); if (!ok) { console.log("Отменено пользователем."); return; } const connection = new Connection(clusterApiUrl(cluster), "confirmed"); const ix = []; await withRevokeGoverningTokens( ix, governanceProgramId, PROGRAM_VERSION_V3, realm, targetOwner, mint, revokeAuthority.publicKey, amount ); const sig = await sendAndConfirmTransaction(connection, new Transaction().add(...ix), [revokeAuthority], { commitment: "confirmed", }); console.log("Готово. Tx:", sig); } main().catch((e) => { console.error("Ошибка revoke:", e?.message || e); process.exit(1); });