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

400 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,
Transaction,
sendAndConfirmTransaction,
clusterApiUrl,
} = require("@solana/web3.js");
const {
PROGRAM_VERSION_V3,
Vote,
YesNoVote,
VoteType,
InstructionData,
AccountMetaData,
withRevokeGoverningTokens,
withCreateProposal,
withInsertTransaction,
withSignOffProposal,
withCastVote,
withExecuteTransaction,
withFinalizeVote,
getTokenOwnerRecordAddress,
getProposalTransactionAddress,
} = 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));
}
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 для proposal->vote->execute revoke: ", resolve)
);
rl.close();
return answer.trim() === "YES";
}
function toGovernanceInstructionData(ix) {
return new InstructionData({
programId: ix.programId,
accounts: ix.keys.map(
(k) =>
new AccountMetaData({
pubkey: k.pubkey,
isSigner: !!k.isSigner,
isWritable: !!k.isWritable,
})
),
data: Uint8Array.from(ix.data),
});
}
function classifyExecuteError(msg) {
const s = String(msg || "").toLowerCase();
if (s.includes("0x20d") || s.includes("hold up time")) {
return "HOLD_UP_TIME";
}
if (s.includes("0x21d") || s.includes("invalid mint authority")) {
return "INVALID_MINT_AUTHORITY";
}
return "OTHER";
}
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 governanceStr = process.argv[4];
const mintStr = process.argv[5];
const targetOwnerStr = process.argv[6];
const amountStr = process.argv[7] || "1";
if (!realmStr || !governanceStr || !mintStr || !targetOwnerStr) {
throw new Error(
"Использование: node scripts/dao/propose_vote_execute_revoke_full_exec.js <config.env> <realm> <governance> <mint> <target_owner_pubkey> [amount]"
);
}
const cfg = parseEnvConfig(configPath);
const cluster = cfg.DAO_CLUSTER || "devnet";
const governanceProgramId = new PublicKey(cfg.SPL_GOVERNANCE_PROGRAM_ID);
const proposerKpPath = cfg.DAO_ISSUER_KEYPAIR;
if (!proposerKpPath) throw new Error("В конфиге нет DAO_ISSUER_KEYPAIR");
const proposer = loadKeypair(path.resolve(proposerKpPath));
const realm = new PublicKey(realmStr);
const governance = new PublicKey(governanceStr);
const mint = new PublicKey(mintStr);
const targetOwner = new PublicKey(targetOwnerStr);
const amount = new BN(amountStr);
if (amount.lten(0)) throw new Error("amount должен быть > 0");
const connection = new Connection(clusterApiUrl(cluster), "confirmed");
const proposerRecord = await getTokenOwnerRecordAddress(
governanceProgramId,
realm,
mint,
proposer.publicKey
);
console.log("============================================================");
console.log("DAO REVOKE THROUGH VOTE");
console.log("------------------------------------------------------------");
console.log("Сеть: ", cluster);
console.log("Governance program: ", governanceProgramId.toBase58());
console.log("Realm: ", realm.toBase58());
console.log("Governance: ", governance.toBase58());
console.log("Mint: ", mint.toBase58());
console.log("Target owner: ", targetOwner.toBase58());
console.log("Amount: ", amount.toString());
console.log("Proposer: ", proposer.publicKey.toBase58());
console.log("Proposer record: ", proposerRecord.toBase58());
console.log("============================================================");
const ok = await askYes();
if (!ok) {
console.log("Отменено пользователем.");
return;
}
const proposalName = `Revoke ${amount.toString()} from ${targetOwner
.toBase58()
.slice(0, 8)}...`;
const proposalDescription = cfg.DAO_REVOKE_PROPOSAL_URI || cfg.DAO_GOV_TOKEN_METADATA_URI || "https://arweave.net/";
const ixCreateProposal = [];
const proposalPk = await withCreateProposal(
ixCreateProposal,
governanceProgramId,
PROGRAM_VERSION_V3,
realm,
governance,
proposerRecord,
proposalName,
proposalDescription,
mint,
proposer.publicKey,
undefined,
VoteType.SINGLE_CHOICE,
["Approve"],
true,
proposer.publicKey
);
const sigCreateProposal = await sendAndConfirmTransaction(
connection,
new Transaction().add(...ixCreateProposal),
[proposer],
{ commitment: "confirmed" }
);
const ixRawRevoke = [];
await withRevokeGoverningTokens(
ixRawRevoke,
governanceProgramId,
PROGRAM_VERSION_V3,
realm,
targetOwner,
mint,
governance,
amount
);
if (ixRawRevoke.length !== 1) throw new Error("Ожидалась одна инструкция revoke");
const revokeInstructionData = toGovernanceInstructionData(ixRawRevoke[0]);
const ixInsert = [];
const proposalTxPk = await withInsertTransaction(
ixInsert,
governanceProgramId,
PROGRAM_VERSION_V3,
governance,
proposalPk,
proposerRecord,
proposer.publicKey,
0,
0,
0,
[revokeInstructionData],
proposer.publicKey
);
const sigInsert = await sendAndConfirmTransaction(connection, new Transaction().add(...ixInsert), [proposer], {
commitment: "confirmed",
});
const ixSignOff = [];
withSignOffProposal(
ixSignOff,
governanceProgramId,
PROGRAM_VERSION_V3,
realm,
governance,
proposalPk,
proposer.publicKey,
undefined,
proposerRecord
);
const sigSignOff = await sendAndConfirmTransaction(connection, new Transaction().add(...ixSignOff), [proposer], {
commitment: "confirmed",
});
const ixVote = [];
const vote = Vote.fromYesNoVote(YesNoVote.Yes);
const voteRecordPk = await withCastVote(
ixVote,
governanceProgramId,
PROGRAM_VERSION_V3,
realm,
governance,
proposalPk,
proposerRecord,
proposerRecord,
proposer.publicKey,
mint,
vote,
proposer.publicKey
);
const sigVote = await sendAndConfirmTransaction(connection, new Transaction().add(...ixVote), [proposer], {
commitment: "confirmed",
});
const computedProposalTxPk = await getProposalTransactionAddress(
governanceProgramId,
PROGRAM_VERSION_V3,
proposalPk,
0,
0
);
if (!computedProposalTxPk.equals(proposalTxPk)) {
throw new Error("Несовпадение адреса proposal transaction");
}
let sigFinalize = null;
try {
const ixFinalize = [];
await withFinalizeVote(
ixFinalize,
governanceProgramId,
PROGRAM_VERSION_V3,
realm,
governance,
proposalPk,
proposerRecord,
mint
);
sigFinalize = await sendAndConfirmTransaction(connection, new Transaction().add(...ixFinalize), [proposer], {
commitment: "confirmed",
});
} catch (_) {
// Может быть уже tipped/succeeded без finalize.
}
let sigExecute = null;
let executeError = null;
let executeErrorKind = null;
try {
const ixExecute = [];
await withExecuteTransaction(
ixExecute,
governanceProgramId,
PROGRAM_VERSION_V3,
governance,
proposalPk,
proposalTxPk,
[revokeInstructionData]
);
sigExecute = await sendAndConfirmTransaction(connection, new Transaction().add(...ixExecute), [proposer], {
commitment: "confirmed",
});
} catch (e) {
executeError = e?.message || String(e);
executeErrorKind = classifyExecuteError(executeError);
}
const report = {
createdAt: new Date().toISOString(),
cluster,
configPath,
governanceProgramId: governanceProgramId.toBase58(),
realm: realm.toBase58(),
governance: governance.toBase58(),
mint: mint.toBase58(),
targetOwner: targetOwner.toBase58(),
amount: amount.toString(),
proposer: proposer.publicKey.toBase58(),
proposerRecord: proposerRecord.toBase58(),
proposal: proposalPk.toBase58(),
proposalTransaction: proposalTxPk.toBase58(),
voteRecord: voteRecordPk.toBase58(),
txCreateProposal: sigCreateProposal,
txInsertTransaction: sigInsert,
txSignOff: sigSignOff,
txVote: sigVote,
txFinalize: sigFinalize,
txExecute: sigExecute,
executeError,
executeErrorKind,
};
const reportDir = path.resolve(__dirname, "runs");
fs.mkdirSync(reportDir, { recursive: true });
const reportBaseName = `${nowStamp()}_revoke_${targetOwner.toBase58().slice(0, 10)}`;
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}`,
`realm: ${report.realm}`,
`governance: ${report.governance}`,
`mint: ${report.mint}`,
`targetOwner: ${report.targetOwner}`,
`amount: ${report.amount}`,
`proposer: ${report.proposer}`,
`proposal: ${report.proposal}`,
`proposalTransaction: ${report.proposalTransaction}`,
`voteRecord: ${report.voteRecord}`,
`txCreateProposal: ${report.txCreateProposal}`,
`txInsertTransaction: ${report.txInsertTransaction}`,
`txSignOff: ${report.txSignOff}`,
`txVote: ${report.txVote}`,
`txFinalize: ${report.txFinalize || "-"}`,
`txExecute: ${report.txExecute || "-"}`,
`executeError: ${report.executeError || "-"}`,
`executeErrorKind: ${report.executeErrorKind || "-"}`,
].join("\n") + "\n"
);
console.log("============================================================");
console.log("REVOKE ЧЕРЕЗ DAO ГОЛОСОВАНИЕ ВЫПОЛНЕН");
console.log("------------------------------------------------------------");
console.log("Proposal: ", proposalPk.toBase58());
console.log("Proposal Tx: ", proposalTxPk.toBase58());
console.log("Tx create proposal: ", sigCreateProposal);
console.log("Tx insert revoke instruction: ", sigInsert);
console.log("Tx sign off: ", sigSignOff);
console.log("Tx cast vote: ", sigVote);
if (sigFinalize) console.log("Tx finalize vote: ", sigFinalize);
if (sigExecute) {
console.log("Tx execute: ", sigExecute);
} else {
console.log("Execute сейчас не прошел (ожидание voting/hold-up):");
console.log("Ошибка execute: ", executeError);
if (executeErrorKind === "HOLD_UP_TIME") {
console.log("Причина: ", "слишком рано для execute (hold-up / окно голосования еще не завершено)");
} else if (executeErrorKind === "INVALID_MINT_AUTHORITY") {
console.log("Причина: ", "community mint authority не передан на governance PDA при создании DAO");
}
console.log("Повтор execute через время этой командой:");
console.log(
`node scripts/dao/execute_revoke_transaction_full_exec.js ${configPath} ${realm.toBase58()} ${governance.toBase58()} ${proposalPk.toBase58()} ${proposalTxPk.toBase58()} ${mint.toBase58()} ${targetOwner.toBase58()} ${amount.toString()}`
);
}
console.log("Отчёт JSON: ", reportJsonPath);
console.log("Отчёт TXT: ", reportTxtPath);
console.log("============================================================");
}
main().catch((e) => {
console.error("Ошибка proposal/vote/execute revoke:", e?.message || e);
process.exit(1);
});