SHA256
Отключить репосты и добавить Solana-модуль
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
const path = require("path");
|
||||
const BN = require("bn.js");
|
||||
const { Connection, Transaction, sendAndConfirmTransaction } = require("@solana/web3.js");
|
||||
const { getAssociatedTokenAddressSync, createAssociatedTokenAccountIdempotentInstruction, createMintToInstruction, TOKEN_PROGRAM_ID } = require("@solana/spl-token");
|
||||
const { withDepositGoverningTokens, PROGRAM_VERSION_V3, getTokenOwnerRecordAddress } = require("@solana/spl-governance");
|
||||
const { parseEnvConfig, resolveConfigPath, loadKeypair, clusterUrl, PublicKey } = require("./js_common");
|
||||
|
||||
async function main() {
|
||||
const cfg = parseEnvConfig(resolveConfigPath(process.argv[2]));
|
||||
const conn = new Connection(clusterUrl(cfg.CLUSTER), "confirmed");
|
||||
const main = loadKeypair(path.resolve(__dirname, cfg.MAIN_KEYPAIR));
|
||||
const voter2 = loadKeypair(path.resolve(__dirname, cfg.VOTER2_KEYPAIR));
|
||||
const realm = new PublicKey(cfg.REALM);
|
||||
const mint = new PublicKey(cfg.GOVERNING_MINT);
|
||||
const govPid = new PublicKey(cfg.SPL_GOVERNANCE_PROGRAM_ID);
|
||||
|
||||
const ata = getAssociatedTokenAddressSync(mint, voter2.publicKey, false, TOKEN_PROGRAM_ID);
|
||||
const ix1 = [
|
||||
createAssociatedTokenAccountIdempotentInstruction(main.publicKey, ata, voter2.publicKey, mint, TOKEN_PROGRAM_ID),
|
||||
createMintToInstruction(mint, ata, main.publicKey, 1n, [], TOKEN_PROGRAM_ID),
|
||||
];
|
||||
const sigMint = await sendAndConfirmTransaction(conn, new Transaction().add(...ix1), [main], { commitment: "confirmed" });
|
||||
|
||||
const tor = await getTokenOwnerRecordAddress(govPid, realm, mint, voter2.publicKey);
|
||||
const ai = await conn.getAccountInfo(tor, "confirmed");
|
||||
let sigDeposit = null;
|
||||
if (!ai) {
|
||||
const ix2 = [];
|
||||
await withDepositGoverningTokens(ix2, govPid, PROGRAM_VERSION_V3, realm, ata, mint, voter2.publicKey, main.publicKey, voter2.publicKey, new BN(1), true);
|
||||
sigDeposit = await sendAndConfirmTransaction(conn, new Transaction().add(...ix2), [main, voter2], { commitment: "confirmed" });
|
||||
}
|
||||
console.log("prepare done");
|
||||
console.log("mint tx:", sigMint);
|
||||
console.log("deposit tx:", sigDeposit || "already exists");
|
||||
}
|
||||
main().catch((e) => { console.error(e?.message || e); process.exit(1); });
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const BN = require("bn.js");
|
||||
const { Connection, Keypair, SystemProgram, Transaction, sendAndConfirmTransaction } = require("@solana/web3.js");
|
||||
const {
|
||||
TOKEN_2022_PROGRAM_ID,
|
||||
ExtensionType,
|
||||
getMintLen,
|
||||
createInitializeMintInstruction,
|
||||
createInitializePermanentDelegateInstruction,
|
||||
createInitializeNonTransferableMintInstruction,
|
||||
getAssociatedTokenAddressSync,
|
||||
createAssociatedTokenAccountIdempotentInstruction,
|
||||
createMintToInstruction,
|
||||
createSetAuthorityInstruction,
|
||||
AuthorityType,
|
||||
} = require("@solana/spl-token");
|
||||
const { parseEnvConfig, resolveConfigPath, loadKeypair, clusterUrl, nowStamp, PublicKey } = require("./js_common");
|
||||
|
||||
async function main() {
|
||||
const target = process.argv[3];
|
||||
if (!target) throw new Error("Usage: node 01_create_nft_for_wallet_admin.js <config.env> <target_wallet>");
|
||||
const cfg = parseEnvConfig(resolveConfigPath(process.argv[2]));
|
||||
const conn = new Connection(clusterUrl(cfg.CLUSTER), "confirmed");
|
||||
const main = loadKeypair(path.resolve(__dirname, cfg.MAIN_KEYPAIR));
|
||||
const governance = new PublicKey(cfg.GOVERNANCE);
|
||||
const targetPk = new PublicKey(target);
|
||||
const mint = Keypair.generate();
|
||||
const mintLen = getMintLen([ExtensionType.NonTransferable, ExtensionType.PermanentDelegate]);
|
||||
const rentMint = await conn.getMinimumBalanceForRentExemption(mintLen, "confirmed");
|
||||
const ata = getAssociatedTokenAddressSync(mint.publicKey, targetPk, false, TOKEN_2022_PROGRAM_ID);
|
||||
const tx = new Transaction().add(
|
||||
SystemProgram.createAccount({ fromPubkey: main.publicKey, newAccountPubkey: mint.publicKey, space: mintLen, lamports: rentMint, programId: TOKEN_2022_PROGRAM_ID }),
|
||||
createInitializeNonTransferableMintInstruction(mint.publicKey, TOKEN_2022_PROGRAM_ID),
|
||||
createInitializePermanentDelegateInstruction(mint.publicKey, main.publicKey, TOKEN_2022_PROGRAM_ID),
|
||||
createInitializeMintInstruction(mint.publicKey, 0, main.publicKey, main.publicKey, TOKEN_2022_PROGRAM_ID),
|
||||
createAssociatedTokenAccountIdempotentInstruction(main.publicKey, ata, targetPk, mint.publicKey, TOKEN_2022_PROGRAM_ID),
|
||||
createMintToInstruction(mint.publicKey, ata, main.publicKey, 1n, [], TOKEN_2022_PROGRAM_ID),
|
||||
createSetAuthorityInstruction(mint.publicKey, main.publicKey, AuthorityType.MintTokens, governance, [], TOKEN_2022_PROGRAM_ID),
|
||||
createSetAuthorityInstruction(mint.publicKey, main.publicKey, AuthorityType.FreezeAccount, governance, [], TOKEN_2022_PROGRAM_ID),
|
||||
createSetAuthorityInstruction(mint.publicKey, main.publicKey, AuthorityType.PermanentDelegate, governance, [], TOKEN_2022_PROGRAM_ID)
|
||||
);
|
||||
const sig = await sendAndConfirmTransaction(conn, tx, [main, mint], { commitment: "confirmed" });
|
||||
const runs = path.resolve(__dirname, cfg.RUNS_DIR || "./runs");
|
||||
fs.mkdirSync(runs, { recursive: true });
|
||||
const report = { createdAt: new Date().toISOString(), mint: mint.publicKey.toBase58(), owner: targetPk.toBase58(), ata: ata.toBase58(), tx: sig };
|
||||
const rp = path.join(runs, `${nowStamp()}_admin_create_nft_${targetPk.toBase58().slice(0,8)}.json`);
|
||||
fs.writeFileSync(rp, JSON.stringify(report, null, 2));
|
||||
console.log("NFT created and delegated to governance");
|
||||
console.log("mint:", report.mint);
|
||||
console.log("owner:", report.owner);
|
||||
console.log("report:", rp);
|
||||
}
|
||||
main().catch((e)=>{console.error(e?.message||e);process.exit(1);});
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { Connection, Keypair, SystemProgram, Transaction, sendAndConfirmTransaction } = require("@solana/web3.js");
|
||||
const {
|
||||
TOKEN_2022_PROGRAM_ID,
|
||||
ExtensionType,
|
||||
getMintLen,
|
||||
createInitializeMintInstruction,
|
||||
createInitializePermanentDelegateInstruction,
|
||||
createInitializeNonTransferableMintInstruction,
|
||||
createSetAuthorityInstruction,
|
||||
AuthorityType,
|
||||
} = require("@solana/spl-token");
|
||||
const { parseEnvConfig, resolveConfigPath, loadKeypair, clusterUrl, nowStamp, PublicKey } = require("./js_common");
|
||||
|
||||
async function main() {
|
||||
const cfg = parseEnvConfig(resolveConfigPath(process.argv[2]));
|
||||
const conn = new Connection(clusterUrl(cfg.CLUSTER), "confirmed");
|
||||
const main = loadKeypair(path.resolve(__dirname, cfg.MAIN_KEYPAIR));
|
||||
const governance = new PublicKey(cfg.GOVERNANCE);
|
||||
const mint = Keypair.generate();
|
||||
const mintLen = getMintLen([ExtensionType.NonTransferable, ExtensionType.PermanentDelegate]);
|
||||
const rentMint = await conn.getMinimumBalanceForRentExemption(mintLen, "confirmed");
|
||||
const tx = new Transaction().add(
|
||||
SystemProgram.createAccount({ fromPubkey: main.publicKey, newAccountPubkey: mint.publicKey, space: mintLen, lamports: rentMint, programId: TOKEN_2022_PROGRAM_ID }),
|
||||
createInitializeNonTransferableMintInstruction(mint.publicKey, TOKEN_2022_PROGRAM_ID),
|
||||
createInitializePermanentDelegateInstruction(mint.publicKey, main.publicKey, TOKEN_2022_PROGRAM_ID),
|
||||
createInitializeMintInstruction(mint.publicKey, 0, main.publicKey, main.publicKey, TOKEN_2022_PROGRAM_ID),
|
||||
createSetAuthorityInstruction(mint.publicKey, main.publicKey, AuthorityType.MintTokens, governance, [], TOKEN_2022_PROGRAM_ID),
|
||||
createSetAuthorityInstruction(mint.publicKey, main.publicKey, AuthorityType.FreezeAccount, governance, [], TOKEN_2022_PROGRAM_ID),
|
||||
createSetAuthorityInstruction(mint.publicKey, main.publicKey, AuthorityType.PermanentDelegate, governance, [], TOKEN_2022_PROGRAM_ID)
|
||||
);
|
||||
const sig = await sendAndConfirmTransaction(conn, tx, [main, mint], { commitment: "confirmed" });
|
||||
const runs = path.resolve(__dirname, cfg.RUNS_DIR || "./runs");
|
||||
fs.mkdirSync(runs, { recursive: true });
|
||||
const rp = path.join(runs, `${nowStamp()}_empty_nft_template.json`);
|
||||
fs.writeFileSync(rp, JSON.stringify({ mint: mint.publicKey.toBase58(), tx: sig, createdAt: new Date().toISOString() }, null, 2));
|
||||
console.log("EMPTY NFT template created");
|
||||
console.log("mint:", mint.publicKey.toBase58());
|
||||
console.log("report:", rp);
|
||||
}
|
||||
main().catch((e)=>{console.error(e?.message||e);process.exit(1);});
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/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 { TOKEN_2022_PROGRAM_ID, getAssociatedTokenAddressSync, createMintToInstruction } = require("@solana/spl-token");
|
||||
const {
|
||||
PROGRAM_VERSION_V3, Vote, YesNoVote, VoteType,
|
||||
withCreateProposal, withInsertTransaction, withSignOffProposal, withCastVote,
|
||||
getTokenOwnerRecordAddress, getProposalTransactionAddress
|
||||
} = require("@solana/spl-governance");
|
||||
const { parseEnvConfig, resolveConfigPath, loadKeypair, clusterUrl, nowStamp, toInstructionData } = require("./js_common");
|
||||
|
||||
async function main() {
|
||||
const targetWallet = process.argv[3];
|
||||
const nftMintStr = process.argv[4];
|
||||
if (!targetWallet || !nftMintStr) throw new Error("Usage: node 02_propose_vote_mint_nft.js <config.env> <target_wallet> <nft_mint>");
|
||||
const cfg = parseEnvConfig(resolveConfigPath(process.argv[2]));
|
||||
const conn = new Connection(clusterUrl(cfg.CLUSTER), "confirmed");
|
||||
const main = loadKeypair(path.resolve(__dirname, cfg.MAIN_KEYPAIR));
|
||||
const realm = new PublicKey(cfg.REALM); const governance = new PublicKey(cfg.GOVERNANCE);
|
||||
const governingMint = new PublicKey(cfg.GOVERNING_MINT); const govPid = new PublicKey(cfg.SPL_GOVERNANCE_PROGRAM_ID);
|
||||
const nftMint = new PublicKey(nftMintStr); const target = new PublicKey(targetWallet);
|
||||
const mainTor = await getTokenOwnerRecordAddress(govPid, realm, governingMint, main.publicKey);
|
||||
const targetAta = getAssociatedTokenAddressSync(nftMint, target, false, TOKEN_2022_PROGRAM_ID);
|
||||
const ataExists = (await conn.getAccountInfo(targetAta, "confirmed")) !== null;
|
||||
if (!ataExists) throw new Error(`Target ATA not found. Create it first: ${targetAta.toBase58()}`);
|
||||
|
||||
const ixCreate = [];
|
||||
const proposal = await withCreateProposal(ixCreate, govPid, PROGRAM_VERSION_V3, realm, governance, mainTor, `Mint NFT to ${target.toBase58().slice(0,8)}`, "https://arweave.net/", governingMint, main.publicKey, undefined, VoteType.SINGLE_CHOICE, ["Approve"], true, main.publicKey);
|
||||
const txCreate = await sendAndConfirmTransaction(conn, new Transaction().add(...ixCreate), [main], { commitment: "confirmed" });
|
||||
|
||||
const mintIx = [createMintToInstruction(nftMint, targetAta, governance, 1n, [], TOKEN_2022_PROGRAM_ID)];
|
||||
const insertData = mintIx.map(toInstructionData);
|
||||
const ixInsert = [];
|
||||
const proposalTx = await withInsertTransaction(ixInsert, govPid, PROGRAM_VERSION_V3, governance, proposal, mainTor, main.publicKey, 0, 0, 0, insertData, main.publicKey);
|
||||
const txInsert = await sendAndConfirmTransaction(conn, new Transaction().add(...ixInsert), [main], { commitment: "confirmed" });
|
||||
|
||||
const ixSign = [];
|
||||
withSignOffProposal(ixSign, govPid, PROGRAM_VERSION_V3, realm, governance, proposal, main.publicKey, undefined, mainTor);
|
||||
const txSign = await sendAndConfirmTransaction(conn, new Transaction().add(...ixSign), [main], { commitment: "confirmed" });
|
||||
|
||||
const vote = Vote.fromYesNoVote(YesNoVote.Yes);
|
||||
const ixVote1 = [];
|
||||
await withCastVote(ixVote1, govPid, PROGRAM_VERSION_V3, realm, governance, proposal, mainTor, mainTor, main.publicKey, governingMint, vote, main.publicKey);
|
||||
const txVote1 = await sendAndConfirmTransaction(conn, new Transaction().add(...ixVote1), [main], { commitment: "confirmed" });
|
||||
|
||||
const computedTx = await getProposalTransactionAddress(govPid, PROGRAM_VERSION_V3, proposal, 0, 0);
|
||||
if (!computedTx.equals(proposalTx)) throw new Error("proposal tx mismatch");
|
||||
const runs = path.resolve(__dirname, cfg.RUNS_DIR || "./runs"); fs.mkdirSync(runs, { recursive: true });
|
||||
const report = { type: "mint_nft", realm: realm.toBase58(), governance: governance.toBase58(), proposal: proposal.toBase58(), proposalTransaction: proposalTx.toBase58(), nftMint: nftMint.toBase58(), targetWallet: target.toBase58(), targetAta: targetAta.toBase58(), txCreate, txInsert, txSign, txVote1 };
|
||||
const rp = path.join(runs, `${nowStamp()}_proposal_mint_${target.toBase58().slice(0,8)}.json`);
|
||||
fs.writeFileSync(rp, JSON.stringify(report, null, 2));
|
||||
console.log("proposal mint created and voted");
|
||||
console.log("report:", rp);
|
||||
console.log("execute command:");
|
||||
console.log(`node 03_execute_mint_nft.js ${resolveConfigPath(process.argv[2])} ${proposal.toBase58()} ${proposalTx.toBase58()} ${nftMint.toBase58()} ${target.toBase58()}`);
|
||||
}
|
||||
main().catch((e)=>{console.error(e?.message||e);process.exit(1);});
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
const { Connection, PublicKey, Transaction, sendAndConfirmTransaction } = require("@solana/web3.js");
|
||||
const { TOKEN_2022_PROGRAM_ID, getAssociatedTokenAddressSync, createMintToInstruction } = require("@solana/spl-token");
|
||||
const { PROGRAM_VERSION_V3, withExecuteTransaction } = require("@solana/spl-governance");
|
||||
const { parseEnvConfig, resolveConfigPath, loadKeypair, clusterUrl, toInstructionData } = require("./js_common");
|
||||
const path = require("path");
|
||||
|
||||
async function main() {
|
||||
const cfg = parseEnvConfig(resolveConfigPath(process.argv[2]));
|
||||
const proposal = new PublicKey(process.argv[3]);
|
||||
const proposalTx = new PublicKey(process.argv[4]);
|
||||
const nftMint = new PublicKey(process.argv[5]);
|
||||
const target = new PublicKey(process.argv[6]);
|
||||
if (!process.argv[6]) throw new Error("Usage: node 03_execute_mint_nft.js <config.env> <proposal> <proposalTx> <nftMint> <targetWallet>");
|
||||
const conn = new Connection(clusterUrl(cfg.CLUSTER), "confirmed");
|
||||
const main = loadKeypair(path.resolve(__dirname, cfg.MAIN_KEYPAIR));
|
||||
const governance = new PublicKey(cfg.GOVERNANCE); const govPid = new PublicKey(cfg.SPL_GOVERNANCE_PROGRAM_ID);
|
||||
const targetAta = getAssociatedTokenAddressSync(nftMint, target, false, TOKEN_2022_PROGRAM_ID);
|
||||
const ataExists = (await conn.getAccountInfo(targetAta, "confirmed")) !== null;
|
||||
if (!ataExists) throw new Error(`Target ATA not found. Create it first: ${targetAta.toBase58()}`);
|
||||
const mintIx = [
|
||||
createMintToInstruction(nftMint, targetAta, governance, 1n, [], TOKEN_2022_PROGRAM_ID),
|
||||
].map(toInstructionData);
|
||||
const ix = [];
|
||||
await withExecuteTransaction(ix, govPid, PROGRAM_VERSION_V3, governance, proposal, proposalTx, mintIx);
|
||||
const sig = await sendAndConfirmTransaction(conn, new Transaction().add(...ix), [main], { commitment: "confirmed" });
|
||||
console.log("execute mint done:", sig);
|
||||
}
|
||||
main().catch((e)=>{console.error(e?.message||e);process.exit(1);});
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { Connection, PublicKey, Transaction, sendAndConfirmTransaction } = require("@solana/web3.js");
|
||||
const { TOKEN_2022_PROGRAM_ID, getAssociatedTokenAddressSync, createBurnCheckedInstruction } = require("@solana/spl-token");
|
||||
const {
|
||||
PROGRAM_VERSION_V3, Vote, YesNoVote, VoteType,
|
||||
withCreateProposal, withInsertTransaction, withSignOffProposal, withCastVote,
|
||||
getTokenOwnerRecordAddress
|
||||
} = require("@solana/spl-governance");
|
||||
const { parseEnvConfig, resolveConfigPath, loadKeypair, clusterUrl, nowStamp, toInstructionData } = require("./js_common");
|
||||
|
||||
async function main() {
|
||||
const targetWallet = process.argv[3];
|
||||
const nftMintStr = process.argv[4];
|
||||
if (!targetWallet || !nftMintStr) throw new Error("Usage: node 04_propose_vote_burn_nft.js <config.env> <target_wallet> <nft_mint>");
|
||||
const cfg = parseEnvConfig(resolveConfigPath(process.argv[2]));
|
||||
const conn = new Connection(clusterUrl(cfg.CLUSTER), "confirmed");
|
||||
const main = loadKeypair(path.resolve(__dirname, cfg.MAIN_KEYPAIR));
|
||||
const realm = new PublicKey(cfg.REALM); const governance = new PublicKey(cfg.GOVERNANCE);
|
||||
const governingMint = new PublicKey(cfg.GOVERNING_MINT); const govPid = new PublicKey(cfg.SPL_GOVERNANCE_PROGRAM_ID);
|
||||
const nftMint = new PublicKey(nftMintStr); const target = new PublicKey(targetWallet);
|
||||
const mainTor = await getTokenOwnerRecordAddress(govPid, realm, governingMint, main.publicKey);
|
||||
const sourceAta = getAssociatedTokenAddressSync(nftMint, target, false, TOKEN_2022_PROGRAM_ID);
|
||||
|
||||
const ixCreate = [];
|
||||
const proposal = await withCreateProposal(ixCreate, govPid, PROGRAM_VERSION_V3, realm, governance, mainTor, `Burn NFT ${nftMint.toBase58().slice(0,8)}`, "https://arweave.net/", governingMint, main.publicKey, undefined, VoteType.SINGLE_CHOICE, ["Approve"], true, main.publicKey);
|
||||
const txCreate = await sendAndConfirmTransaction(conn, new Transaction().add(...ixCreate), [main], { commitment: "confirmed" });
|
||||
|
||||
const burnIx = [toInstructionData(createBurnCheckedInstruction(sourceAta, nftMint, governance, 1n, 0, [], TOKEN_2022_PROGRAM_ID))];
|
||||
const ixInsert = [];
|
||||
const proposalTx = await withInsertTransaction(ixInsert, govPid, PROGRAM_VERSION_V3, governance, proposal, mainTor, main.publicKey, 0, 0, 0, burnIx, main.publicKey);
|
||||
const txInsert = await sendAndConfirmTransaction(conn, new Transaction().add(...ixInsert), [main], { commitment: "confirmed" });
|
||||
|
||||
const ixSign = [];
|
||||
withSignOffProposal(ixSign, govPid, PROGRAM_VERSION_V3, realm, governance, proposal, main.publicKey, undefined, mainTor);
|
||||
const txSign = await sendAndConfirmTransaction(conn, new Transaction().add(...ixSign), [main], { commitment: "confirmed" });
|
||||
|
||||
const vote = Vote.fromYesNoVote(YesNoVote.Yes);
|
||||
const ixVote1 = [];
|
||||
await withCastVote(ixVote1, govPid, PROGRAM_VERSION_V3, realm, governance, proposal, mainTor, mainTor, main.publicKey, governingMint, vote, main.publicKey);
|
||||
const txVote1 = await sendAndConfirmTransaction(conn, new Transaction().add(...ixVote1), [main], { commitment: "confirmed" });
|
||||
const runs = path.resolve(__dirname, cfg.RUNS_DIR || "./runs"); fs.mkdirSync(runs, { recursive: true });
|
||||
const report = { type: "burn_nft", realm: realm.toBase58(), governance: governance.toBase58(), proposal: proposal.toBase58(), proposalTransaction: proposalTx.toBase58(), nftMint: nftMint.toBase58(), targetWallet: target.toBase58(), sourceAta: sourceAta.toBase58(), txCreate, txInsert, txSign, txVote1 };
|
||||
const rp = path.join(runs, `${nowStamp()}_proposal_burn_${target.toBase58().slice(0,8)}.json`);
|
||||
fs.writeFileSync(rp, JSON.stringify(report, null, 2));
|
||||
console.log("proposal burn created and voted");
|
||||
console.log("report:", rp);
|
||||
console.log("execute command:");
|
||||
console.log(`node 05_execute_burn_nft.js ${resolveConfigPath(process.argv[2])} ${proposal.toBase58()} ${proposalTx.toBase58()} ${nftMint.toBase58()} ${target.toBase58()}`);
|
||||
}
|
||||
main().catch((e)=>{console.error(e?.message||e);process.exit(1);});
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
const path = require("path");
|
||||
const { Connection, PublicKey, Transaction, sendAndConfirmTransaction } = require("@solana/web3.js");
|
||||
const { TOKEN_2022_PROGRAM_ID, getAssociatedTokenAddressSync, createBurnCheckedInstruction } = require("@solana/spl-token");
|
||||
const { PROGRAM_VERSION_V3, withExecuteTransaction } = require("@solana/spl-governance");
|
||||
const { parseEnvConfig, resolveConfigPath, loadKeypair, clusterUrl, toInstructionData } = require("./js_common");
|
||||
|
||||
async function main() {
|
||||
const cfg = parseEnvConfig(resolveConfigPath(process.argv[2]));
|
||||
const proposal = new PublicKey(process.argv[3]);
|
||||
const proposalTx = new PublicKey(process.argv[4]);
|
||||
const nftMint = new PublicKey(process.argv[5]);
|
||||
const target = new PublicKey(process.argv[6]);
|
||||
if (!process.argv[6]) throw new Error("Usage: node 05_execute_burn_nft.js <config.env> <proposal> <proposalTx> <nftMint> <targetWallet>");
|
||||
const conn = new Connection(clusterUrl(cfg.CLUSTER), "confirmed");
|
||||
const main = loadKeypair(path.resolve(__dirname, cfg.MAIN_KEYPAIR));
|
||||
const governance = new PublicKey(cfg.GOVERNANCE); const govPid = new PublicKey(cfg.SPL_GOVERNANCE_PROGRAM_ID);
|
||||
const sourceAta = getAssociatedTokenAddressSync(nftMint, target, false, TOKEN_2022_PROGRAM_ID);
|
||||
const burnIx = [toInstructionData(createBurnCheckedInstruction(sourceAta, nftMint, governance, 1n, 0, [], TOKEN_2022_PROGRAM_ID))];
|
||||
const ix = [];
|
||||
await withExecuteTransaction(ix, govPid, PROGRAM_VERSION_V3, governance, proposal, proposalTx, burnIx);
|
||||
const sig = await sendAndConfirmTransaction(conn, new Transaction().add(...ix), [main], { commitment: "confirmed" });
|
||||
console.log("execute burn done:", sig);
|
||||
}
|
||||
main().catch((e)=>{console.error(e?.message||e);process.exit(1);});
|
||||
@@ -0,0 +1,66 @@
|
||||
# CreateGovernmentNFTAndDAO
|
||||
|
||||
## RU
|
||||
|
||||
Скрипты для Devnet, чтобы управлять NFT через DAO (Realms/SPL Governance):
|
||||
1) создать предложение на выпуск NFT (`mint`) и выполнить его;
|
||||
2) создать предложение на сжигание NFT (`burn`) и выполнить его.
|
||||
|
||||
### Что лежит в папке
|
||||
|
||||
- `config.env` — параметры кластера, DAO, ключей.
|
||||
- `keypairs/` — ключи оператора и второго участника.
|
||||
- `runs/` — отчёты запусков (proposal, tx и т.д.).
|
||||
- `00_prepare_voter2_deposit.js` — депонирование governance-токена для второго голосующего.
|
||||
- `01_create_nft_for_wallet_admin.js` — создать NFT на кошелёк и делегировать право governance PDA.
|
||||
- `01b_create_empty_nft_template.js` — создать пустой NFT mint-шаблон (supply=0) для будущего DAO mint.
|
||||
- `02_propose_vote_mint_nft.js` — создать+подписать+проголосовать за proposal на mint.
|
||||
- `03_execute_mint_nft.js` — выполнить proposal mint.
|
||||
- `04_propose_vote_burn_nft.js` — создать+подписать+проголосовать за proposal на burn.
|
||||
- `05_execute_burn_nft.js` — выполнить proposal burn.
|
||||
|
||||
### Важно перед запуском
|
||||
|
||||
1. Нужен `node`, `@solana/web3.js`, `@solana/spl-token`, `@solana/spl-governance`.
|
||||
2. В `config.env` должен быть корректный `REALM`, `GOVERNANCE`, `GOVERNING_MINT`, `MAIN_KEYPAIR`.
|
||||
3. Для `mint via DAO` целевой ATA должен существовать заранее (скрипт `02` это проверяет).
|
||||
|
||||
### Быстрый полный тест (mint + burn)
|
||||
|
||||
1. Создать NFT-шаблон (куда DAO будет минтить):
|
||||
- `node 01b_create_empty_nft_template.js ./config.env`
|
||||
2. Создать ATA для целевого кошелька и этого mint (если ещё нет).
|
||||
3. Поднять proposal на mint:
|
||||
- `node 02_propose_vote_mint_nft.js ./config.env <target_wallet> <nft_mint>`
|
||||
4. Выполнить proposal (команду берёшь из консоли шага 3):
|
||||
- `node 03_execute_mint_nft.js ./config.env <proposal> <proposal_tx> <nft_mint> <target_wallet>`
|
||||
5. Создать NFT для burn-теста:
|
||||
- `node 01_create_nft_for_wallet_admin.js ./config.env <wallet_with_nft>`
|
||||
6. Поднять proposal на burn:
|
||||
- `node 04_propose_vote_burn_nft.js ./config.env <wallet_with_nft> <nft_mint>`
|
||||
7. Выполнить proposal burn (команда из шага 6):
|
||||
- `node 05_execute_burn_nft.js ./config.env <proposal> <proposal_tx> <nft_mint> <wallet_with_nft>`
|
||||
|
||||
### Как проверить результат
|
||||
|
||||
Смотри JSON-отчёты в `runs/`: там есть `proposal`, `proposalTransaction`, tx подписи и mint/кошельки.
|
||||
|
||||
Для проверки через час:
|
||||
1) поднимаешь proposal (скрипт `02` или `04`);
|
||||
2) ждёшь;
|
||||
3) запускаешь соответствующий `execute` скрипт с параметрами из отчёта.
|
||||
|
||||
### Проверка DAO
|
||||
|
||||
В текущем `config.env`:
|
||||
- Realm: `2DTh1ivaekAW8kRYzGPsL2taFLJFFkBjEwqPisebxsS7`
|
||||
- Governance PDA: `EMZ8vmr1xB4HZBDCFL9rHB98m1C5cYrGnRA8ZHayyGwD`
|
||||
- Governing mint: `F1KctLRvVzqwcBYNGsivnjR39gY8Uvq5U3uyaqEBNASg`
|
||||
|
||||
## EN
|
||||
|
||||
Devnet scripts for DAO-governed NFT flow (Realms/SPL Governance):
|
||||
- propose/sign/vote/execute NFT mint to a wallet;
|
||||
- propose/sign/vote/execute NFT burn from a wallet.
|
||||
|
||||
Main idea: first script in each pair creates proposal and vote, second script executes proposal later.
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { PublicKey, Keypair, clusterApiUrl } = require("@solana/web3.js");
|
||||
const { InstructionData, AccountMetaData } = require("@solana/spl-governance");
|
||||
|
||||
function parseEnvConfig(configPath) {
|
||||
const raw = fs.readFileSync(configPath, "utf8");
|
||||
const out = {};
|
||||
for (const line of raw.split("\n")) {
|
||||
const t = line.trim();
|
||||
if (!t || t.startsWith("#")) continue;
|
||||
const i = t.indexOf("=");
|
||||
if (i < 0) continue;
|
||||
const k = t.slice(0, i).trim();
|
||||
let v = t.slice(i + 1).trim();
|
||||
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
|
||||
out[k] = v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function resolveConfigPath(argvPath) {
|
||||
return argvPath ? path.resolve(argvPath) : path.resolve(__dirname, "config.env");
|
||||
}
|
||||
|
||||
function loadKeypair(fp) {
|
||||
return Keypair.fromSecretKey(Uint8Array.from(JSON.parse(fs.readFileSync(fp, "utf8"))));
|
||||
}
|
||||
|
||||
function clusterUrl(cluster) {
|
||||
if (cluster === "devnet" || cluster === "mainnet-beta" || cluster === "testnet") return clusterApiUrl(cluster);
|
||||
return cluster;
|
||||
}
|
||||
|
||||
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())}`;
|
||||
}
|
||||
|
||||
function toInstructionData(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),
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { parseEnvConfig, resolveConfigPath, loadKeypair, clusterUrl, nowStamp, toInstructionData, PublicKey };
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#
|
||||
# RU: Создает governance token (Token-2022, NonTransferable + PermanentDelegate)
|
||||
# с настройками из governance_token.config.env.
|
||||
# EN: Creates governance token (Token-2022, NonTransferable + PermanentDelegate)
|
||||
# using settings from governance_token.config.env.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_PATH="$SCRIPT_DIR/governance_token.config.env"
|
||||
node "$SCRIPT_DIR/js/01_create_governance_token_exec.js" "$CONFIG_PATH"
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#
|
||||
# RU: Выпускает ровно 1 membership-токен на указанный кошелек.
|
||||
# Если у кошелька уже есть >=1 токен, скрипт завершится ошибкой.
|
||||
# EN: Mints exactly 1 membership token to the given wallet.
|
||||
# If wallet already has >=1 token, script exits with error.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_PATH="$SCRIPT_DIR/governance_token.config.env"
|
||||
WALLET="${1:-}"
|
||||
|
||||
if [[ -z "$WALLET" ]]; then
|
||||
echo "Использование:"
|
||||
echo " $0 <wallet>"
|
||||
echo "Usage:"
|
||||
echo " $0 <wallet>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
node "$SCRIPT_DIR/js/02_mint_membership_to_wallet_exec.js" "$CONFIG_PATH" "$WALLET"
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#
|
||||
# RU: Принудительно сжигает 1 membership-токен на указанном кошельке.
|
||||
# EN: Force-burns exactly 1 membership token from the given wallet.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_PATH="$SCRIPT_DIR/governance_token.config.env"
|
||||
WALLET="${1:-}"
|
||||
|
||||
if [[ -z "$WALLET" ]]; then
|
||||
echo "Использование:"
|
||||
echo " $0 <wallet>"
|
||||
echo "Usage:"
|
||||
echo " $0 <wallet>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
node "$SCRIPT_DIR/js/03_force_burn_from_wallet_exec.js" "$CONFIG_PATH" "$WALLET"
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#
|
||||
# RU: Создает DAO (Realm + Governance + Treasury) на уже существующем governance mint.
|
||||
# EN: Creates DAO (Realm + Governance + Treasury) using existing governance mint.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_PATH="$SCRIPT_DIR/governance_token.config.env"
|
||||
|
||||
node "$SCRIPT_DIR/js/05_create_dao_exec.js" "$CONFIG_PATH"
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#
|
||||
# RU: Передает права Mint/Freeze/PermanentDelegate на Governance PDA из конфига.
|
||||
# Перед отправкой транзакции внутри JS будет подтверждение "yes".
|
||||
# EN: Transfers Mint/Freeze/PermanentDelegate authorities to Governance PDA
|
||||
# from config. JS script asks for "yes" confirmation before sending.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_PATH="$SCRIPT_DIR/governance_token.config.env"
|
||||
node "$SCRIPT_DIR/js/04_transfer_rights_to_governance_pda_exec.js" "$CONFIG_PATH"
|
||||
@@ -0,0 +1,74 @@
|
||||
# CreateGovernmentTokenAndDAO
|
||||
|
||||
## RU
|
||||
|
||||
Единый набор скриптов для:
|
||||
1. создания governance token,
|
||||
2. выдачи/сжигания membership токенов,
|
||||
3. передачи прав на Governance PDA,
|
||||
4. создания DAO (Realm/Governance/Treasury).
|
||||
|
||||
### Важная структура ключей
|
||||
|
||||
Используются две папки:
|
||||
- `keypairs/dao_creator/` — ключ инициатора DAO и плательщика (ровно 1 `*.json`).
|
||||
- `keypairs/government_token/` — ключ mint governance token (ровно 1 `*.json`).
|
||||
|
||||
Скрипты автоматически берут единственный файл из этих папок.
|
||||
Если в папке `government_token` 0 файлов или больше 1 — скрипт завершится ошибкой.
|
||||
|
||||
### Скрипты
|
||||
|
||||
```bash
|
||||
./01_create_governance_token.sh
|
||||
./02_mint_token_to_wallet.sh <WALLET>
|
||||
./03_force_burn_from_wallet.sh <WALLET>
|
||||
./04_create_dao.sh
|
||||
./05_transfer_rights_to_governance_pda.sh
|
||||
./grind_vanity_mint.sh [PREFIX] [COUNT] [ignore-case]
|
||||
```
|
||||
|
||||
### Базовый порядок
|
||||
|
||||
1. (Опционально) `grind_vanity_mint.sh`, затем ОБЯЗАТЕЛЬНО скопировать выбранный json в `keypairs/government_token/`.
|
||||
Пример:
|
||||
```bash
|
||||
cp ./runs/<FOUND_KEYPAIR>.json ./keypairs/government_token/selected_mint.json
|
||||
```
|
||||
2. `01_create_governance_token.sh`
|
||||
3. В `governance_token.config.env` указать `GT_MINT_ADDRESS`.
|
||||
4. `02_mint_token_to_wallet.sh <WALLET>`
|
||||
5. `03_force_burn_from_wallet.sh <WALLET>`
|
||||
6. `04_create_dao.sh`
|
||||
7. Внести полученный Governance PDA в `GT_GOVERNANCE_PDA`.
|
||||
8. `05_transfer_rights_to_governance_pda.sh`
|
||||
|
||||
---
|
||||
|
||||
## EN
|
||||
|
||||
Unified scripts for:
|
||||
1. governance token creation,
|
||||
2. membership mint/burn,
|
||||
3. authority transfer to Governance PDA,
|
||||
4. DAO creation (Realm/Governance/Treasury).
|
||||
|
||||
### Required keypair layout
|
||||
|
||||
Two folders are used:
|
||||
- `keypairs/dao_creator/` — DAO creator/payer keypair (exactly 1 `*.json`).
|
||||
- `keypairs/government_token/` — governance token mint keypair (exactly 1 `*.json`).
|
||||
|
||||
Scripts auto-detect the single file in each folder.
|
||||
If `government_token` has 0 files or more than 1 file, script fails with error.
|
||||
|
||||
### Scripts
|
||||
|
||||
```bash
|
||||
./01_create_governance_token.sh
|
||||
./02_mint_token_to_wallet.sh <WALLET>
|
||||
./03_force_burn_from_wallet.sh <WALLET>
|
||||
./04_create_dao.sh
|
||||
./05_transfer_rights_to_governance_pda.sh
|
||||
./grind_vanity_mint.sh [PREFIX] [COUNT] [ignore-case]
|
||||
```
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#
|
||||
# RU: Подбирает vanity mint keypair через `solana-keygen grind`.
|
||||
# Параметры: [PREFIX] [COUNT] [ignore-case]
|
||||
# EN: Finds vanity mint keypair using `solana-keygen grind`.
|
||||
# Args: [PREFIX] [COUNT] [ignore-case]
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_PATH="$SCRIPT_DIR/governance_token.config.env"
|
||||
PREFIX="${1:-}"
|
||||
COUNT="${2:-1}"
|
||||
IGNORE_CASE="${3:-}"
|
||||
|
||||
if [[ -n "$PREFIX" ]]; then
|
||||
node "$SCRIPT_DIR/js/grind_vanity_mint_exec.js" "$CONFIG_PATH" "$PREFIX" "$COUNT" "$IGNORE_CASE"
|
||||
else
|
||||
node "$SCRIPT_DIR/js/grind_vanity_mint_exec.js" "$CONFIG_PATH"
|
||||
fi
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { Connection, SystemProgram, Transaction, sendAndConfirmTransaction } = require("@solana/web3.js");
|
||||
const { TOKEN_PROGRAM_ID, getMintLen, createInitializeMintInstruction } = require("@solana/spl-token");
|
||||
const { parseEnvConfig, assertRequired, resolveConfigPath, loadKeypair, findSingleJsonFile, saveKeypair, parseCluster, nowStamp, ui, getOperatorKeypairFromConfig } = require("./_common");
|
||||
|
||||
async function main() {
|
||||
const cfg = parseEnvConfig(resolveConfigPath(process.argv[2]));
|
||||
assertRequired(cfg, "GT_CLUSTER"); assertRequired(cfg, "GT_RUNS_DIR");
|
||||
const operator = getOperatorKeypairFromConfig(cfg);
|
||||
const connection = new Connection(parseCluster(cfg.GT_CLUSTER), "confirmed");
|
||||
const gtDir = path.resolve(cfg.GT_GOVERNMENT_TOKEN_KEYPAIR_DIR || path.join(__dirname, "..", "keypairs", "government_token"));
|
||||
fs.mkdirSync(gtDir, { recursive: true });
|
||||
const mintKeypairPath = findSingleJsonFile(gtDir);
|
||||
const mint = loadKeypair(mintKeypairPath);
|
||||
const mintLen = getMintLen([]);
|
||||
const rent = await connection.getMinimumBalanceForRentExemption(mintLen, "confirmed");
|
||||
ui.title("=== Создание governance token (SPL classic) / Create governance token (SPL classic) ===");
|
||||
const tx = new Transaction().add(
|
||||
SystemProgram.createAccount({ fromPubkey: operator.publicKey, newAccountPubkey: mint.publicKey, space: mintLen, lamports: rent, programId: TOKEN_PROGRAM_ID }),
|
||||
createInitializeMintInstruction(mint.publicKey, 0, operator.publicKey, operator.publicKey, TOKEN_PROGRAM_ID)
|
||||
);
|
||||
const sig = await sendAndConfirmTransaction(connection, tx, [operator, mint], { commitment: "confirmed" });
|
||||
const runsDir = path.resolve(cfg.GT_RUNS_DIR); fs.mkdirSync(runsDir, { recursive: true });
|
||||
const outMintPath = mintKeypairPath;
|
||||
saveKeypair(outMintPath, mint);
|
||||
fs.writeFileSync(path.join(runsDir, `${nowStamp()}_create_token.json`), JSON.stringify({ mint: mint.publicKey.toBase58(), txCreateMint: sig }, null, 2));
|
||||
ui.ok(`OK: Mint ${mint.publicKey.toBase58()}`);
|
||||
ui.info(`RU: Использован keypair: ${mintKeypairPath}`);
|
||||
ui.info(`EN: Used keypair: ${mintKeypairPath}`);
|
||||
ui.info(`RU: Вставьте этот mint в файл: ${path.resolve(__dirname, "..", "governance_token.config.env")}`);
|
||||
ui.info(`RU: Строка: GT_MINT_ADDRESS="${mint.publicKey.toBase58()}"`);
|
||||
ui.info(`EN: Put this mint into file: ${path.resolve(__dirname, "..", "governance_token.config.env")}`);
|
||||
ui.info(`EN: Line: GT_MINT_ADDRESS="${mint.publicKey.toBase58()}"`);
|
||||
ui.info(`Mint keypair: ${outMintPath}`);
|
||||
ui.info(`Tx: ${sig}`);
|
||||
}
|
||||
main().catch((e) => { ui.err(`Ошибка / Error: ${e?.message || e}`); process.exit(1); });
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
const { Connection, PublicKey, Transaction, sendAndConfirmTransaction } = require("@solana/web3.js");
|
||||
const { TOKEN_2022_PROGRAM_ID, getAssociatedTokenAddressSync, createAssociatedTokenAccountIdempotentInstruction, createMintToInstruction, getAccount } = require("@solana/spl-token");
|
||||
const { parseEnvConfig, assertRequired, resolveConfigPath, parseCluster, ui, getMintPublicKeyFromConfig, getOperatorKeypairFromConfig } = require("./_common");
|
||||
|
||||
async function main() {
|
||||
const cfg = parseEnvConfig(resolveConfigPath(process.argv[2]));
|
||||
const receiver = new PublicKey(process.argv[3]);
|
||||
if (!process.argv[3]) throw new Error("Использование / Usage: node .../02...js <config.env> <receiver_wallet>");
|
||||
assertRequired(cfg, "GT_CLUSTER");
|
||||
const mint = getMintPublicKeyFromConfig(cfg);
|
||||
const operator = getOperatorKeypairFromConfig(cfg);
|
||||
const connection = new Connection(parseCluster(cfg.GT_CLUSTER), "confirmed");
|
||||
const ata = getAssociatedTokenAddressSync(mint, receiver, false, TOKEN_2022_PROGRAM_ID);
|
||||
const ataInfo = await connection.getAccountInfo(ata, "confirmed");
|
||||
if (ataInfo) {
|
||||
const tokenAcc = await getAccount(connection, ata, "confirmed", TOKEN_2022_PROGRAM_ID);
|
||||
if (tokenAcc.amount >= 1n) {
|
||||
throw new Error(
|
||||
`На кошельке уже есть membership token / Wallet already has membership token. wallet=${receiver.toBase58()} amount=${tokenAcc.amount.toString()}`
|
||||
);
|
||||
}
|
||||
}
|
||||
const ix = [
|
||||
createAssociatedTokenAccountIdempotentInstruction(operator.publicKey, ata, receiver, mint, TOKEN_2022_PROGRAM_ID),
|
||||
createMintToInstruction(mint, ata, operator.publicKey, 1n, [], TOKEN_2022_PROGRAM_ID),
|
||||
];
|
||||
ui.title("=== Выпуск 1 membership токена / Mint 1 membership token ===");
|
||||
const sig = await sendAndConfirmTransaction(connection, new Transaction().add(...ix), [operator], { commitment: "confirmed" });
|
||||
ui.ok("Успешно / Success");
|
||||
ui.info(`Mint: ${mint.toBase58()}`); ui.info(`Wallet: ${receiver.toBase58()}`); ui.info(`Tx: ${sig}`);
|
||||
}
|
||||
main().catch((e) => { ui.err(`Ошибка / Error: ${e?.message || e}`); process.exit(1); });
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
const { Connection, PublicKey, Transaction, sendAndConfirmTransaction } = require("@solana/web3.js");
|
||||
const { TOKEN_2022_PROGRAM_ID, getAssociatedTokenAddressSync, createBurnCheckedInstruction } = require("@solana/spl-token");
|
||||
const { parseEnvConfig, assertRequired, resolveConfigPath, parseCluster, ui, getMintPublicKeyFromConfig, getOperatorKeypairFromConfig } = require("./_common");
|
||||
|
||||
async function main() {
|
||||
const cfg = parseEnvConfig(resolveConfigPath(process.argv[2]));
|
||||
const targetOwner = new PublicKey(process.argv[3]);
|
||||
if (!process.argv[3]) throw new Error("Использование / Usage: node .../03...js <config.env> <target_owner_wallet>");
|
||||
assertRequired(cfg, "GT_CLUSTER");
|
||||
const mint = getMintPublicKeyFromConfig(cfg);
|
||||
const operator = getOperatorKeypairFromConfig(cfg);
|
||||
const connection = new Connection(parseCluster(cfg.GT_CLUSTER), "confirmed");
|
||||
const targetAta = getAssociatedTokenAddressSync(mint, targetOwner, false, TOKEN_2022_PROGRAM_ID);
|
||||
const ix = createBurnCheckedInstruction(targetAta, mint, operator.publicKey, 1n, 0, [], TOKEN_2022_PROGRAM_ID);
|
||||
ui.title("=== Принудительное сжигание 1 токена / Force burn 1 token ===");
|
||||
const sig = await sendAndConfirmTransaction(connection, new Transaction().add(ix), [operator], { commitment: "confirmed" });
|
||||
ui.ok("Успешно / Success");
|
||||
ui.info(`Mint: ${mint.toBase58()}`); ui.info(`Wallet: ${targetOwner.toBase58()}`); ui.info(`Tx: ${sig}`);
|
||||
}
|
||||
main().catch((e) => { ui.err(`Ошибка / Error: ${e?.message || e}`); process.exit(1); });
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
const { Connection, PublicKey, Transaction, sendAndConfirmTransaction } = require("@solana/web3.js");
|
||||
const { TOKEN_2022_PROGRAM_ID, AuthorityType, createSetAuthorityInstruction } = require("@solana/spl-token");
|
||||
const { parseEnvConfig, assertRequired, resolveConfigPath, parseCluster, askYes, ui, getMintPublicKeyFromConfig, getOperatorKeypairFromConfig } = require("./_common");
|
||||
|
||||
async function main() {
|
||||
const cfg = parseEnvConfig(resolveConfigPath(process.argv[2]));
|
||||
assertRequired(cfg, "GT_CLUSTER"); assertRequired(cfg, "GT_GOVERNANCE_PDA");
|
||||
const mint = getMintPublicKeyFromConfig(cfg);
|
||||
const operator = getOperatorKeypairFromConfig(cfg);
|
||||
const governancePda = new PublicKey(cfg.GT_GOVERNANCE_PDA);
|
||||
const connection = new Connection(parseCluster(cfg.GT_CLUSTER), "confirmed");
|
||||
ui.title("=== Передача прав DAO / Transfer rights to DAO ===");
|
||||
ui.warn(`RU: Будут переданы права Mint/Freeze/PermanentDelegate от ${operator.publicKey.toBase58()} на ${governancePda.toBase58()}`);
|
||||
ui.warn(`EN: Mint/Freeze/PermanentDelegate authorities will be transferred to governance PDA.`);
|
||||
const ok = await askYes("Введите yes / Type yes to continue: ");
|
||||
if (!ok) return ui.warn("Отменено / Cancelled");
|
||||
const ixs = [
|
||||
createSetAuthorityInstruction(mint, operator.publicKey, AuthorityType.MintTokens, governancePda, [], TOKEN_2022_PROGRAM_ID),
|
||||
createSetAuthorityInstruction(mint, operator.publicKey, AuthorityType.FreezeAccount, governancePda, [], TOKEN_2022_PROGRAM_ID),
|
||||
createSetAuthorityInstruction(mint, operator.publicKey, AuthorityType.PermanentDelegate, governancePda, [], TOKEN_2022_PROGRAM_ID),
|
||||
];
|
||||
const sig = await sendAndConfirmTransaction(connection, new Transaction().add(...ixs), [operator], { commitment: "confirmed" });
|
||||
ui.ok("Успешно / Success");
|
||||
ui.info(`Mint: ${mint.toBase58()}`); ui.info(`DAO PDA: ${governancePda.toBase58()}`); ui.info(`Tx: ${sig}`);
|
||||
}
|
||||
main().catch((e) => { ui.err(`Ошибка / Error: ${e?.message || e}`); process.exit(1); });
|
||||
@@ -0,0 +1,191 @@
|
||||
#!/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);
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const readline = require("readline");
|
||||
const { Keypair, PublicKey, clusterApiUrl } = require("@solana/web3.js");
|
||||
|
||||
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(`В конфиге отсутствует обязательный параметр / Missing config key: ${key}`);
|
||||
}
|
||||
|
||||
function resolveConfigPath(argvPath) {
|
||||
return argvPath ? path.resolve(argvPath) : path.resolve(__dirname, "..", "governance_token.config.env");
|
||||
}
|
||||
|
||||
function loadKeypair(filePath) {
|
||||
const arr = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
return Keypair.fromSecretKey(Uint8Array.from(arr));
|
||||
}
|
||||
|
||||
function findSingleJsonFile(dirPath) {
|
||||
const abs = path.resolve(dirPath);
|
||||
if (!fs.existsSync(abs)) throw new Error(`Папка не найдена / Directory not found: ${abs}`);
|
||||
const files = fs.readdirSync(abs).filter((f) => {
|
||||
const p = path.join(abs, f);
|
||||
return fs.statSync(p).isFile() && f.endsWith(".json");
|
||||
});
|
||||
if (files.length !== 1) {
|
||||
throw new Error(`В папке должен быть ровно 1 json-файл / Directory must contain exactly 1 json file: ${abs}. Сейчас: ${files.length}`);
|
||||
}
|
||||
return path.join(abs, files[0]);
|
||||
}
|
||||
|
||||
function saveKeypair(filePath, keypair) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, JSON.stringify(Array.from(keypair.secretKey)));
|
||||
}
|
||||
|
||||
function parseCluster(cluster) {
|
||||
if (cluster === "devnet" || cluster === "mainnet-beta" || cluster === "testnet") return clusterApiUrl(cluster);
|
||||
return cluster;
|
||||
}
|
||||
|
||||
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(prompt) {
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||
const answer = await new Promise((resolve) => rl.question(prompt, resolve));
|
||||
rl.close();
|
||||
return answer.trim() === "yes";
|
||||
}
|
||||
|
||||
function colors(s, code) { return `\x1b[${code}m${s}\x1b[0m`; }
|
||||
const ui = {
|
||||
info: (s) => console.log(colors(s, "36")),
|
||||
ok: (s) => console.log(colors(s, "32")),
|
||||
warn: (s) => console.log(colors(s, "33")),
|
||||
err: (s) => console.log(colors(s, "31")),
|
||||
title: (s) => console.log(colors(s, "1;35")),
|
||||
};
|
||||
|
||||
function getMintPublicKeyFromConfig(cfg) {
|
||||
if (cfg.GT_MINT_ADDRESS && cfg.GT_MINT_ADDRESS.trim()) return new PublicKey(cfg.GT_MINT_ADDRESS.trim());
|
||||
if (cfg.GT_GOVERNMENT_TOKEN_KEYPAIR_DIR && cfg.GT_GOVERNMENT_TOKEN_KEYPAIR_DIR.trim()) {
|
||||
const kpPath = findSingleJsonFile(path.resolve(cfg.GT_GOVERNMENT_TOKEN_KEYPAIR_DIR));
|
||||
return loadKeypair(kpPath).publicKey;
|
||||
}
|
||||
if (cfg.GT_MINT_KEYPAIR_PATH && cfg.GT_MINT_KEYPAIR_PATH.trim()) return loadKeypair(path.resolve(cfg.GT_MINT_KEYPAIR_PATH)).publicKey;
|
||||
throw new Error("Не задан mint: укажите GT_MINT_ADDRESS или положите 1 keypair в GT_GOVERNMENT_TOKEN_KEYPAIR_DIR");
|
||||
}
|
||||
|
||||
function getOperatorKeypairFromConfig(cfg) {
|
||||
if (cfg.GT_DAO_CREATOR_KEYPAIR_DIR && cfg.GT_DAO_CREATOR_KEYPAIR_DIR.trim()) {
|
||||
const kpPath = findSingleJsonFile(path.resolve(cfg.GT_DAO_CREATOR_KEYPAIR_DIR));
|
||||
return loadKeypair(kpPath);
|
||||
}
|
||||
if (cfg.GT_OPERATOR_KEYPAIR_PATH && cfg.GT_OPERATOR_KEYPAIR_PATH.trim()) {
|
||||
return loadKeypair(path.resolve(cfg.GT_OPERATOR_KEYPAIR_PATH));
|
||||
}
|
||||
throw new Error("Не задан ключ оператора: укажите GT_DAO_CREATOR_KEYPAIR_DIR или GT_OPERATOR_KEYPAIR_PATH");
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseEnvConfig,
|
||||
assertRequired,
|
||||
resolveConfigPath,
|
||||
loadKeypair,
|
||||
findSingleJsonFile,
|
||||
saveKeypair,
|
||||
parseCluster,
|
||||
nowStamp,
|
||||
askYes,
|
||||
ui,
|
||||
getMintPublicKeyFromConfig,
|
||||
getOperatorKeypairFromConfig,
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { spawn } = require("child_process");
|
||||
const { parseEnvConfig, resolveConfigPath, nowStamp, ui } = require("./_common");
|
||||
const DEFAULT_PREFIX = "SHi";
|
||||
|
||||
async function main() {
|
||||
const cfg = parseEnvConfig(resolveConfigPath(process.argv[2]));
|
||||
const runsDir = path.resolve(cfg.GT_RUNS_DIR || path.join(__dirname, "..", "runs"));
|
||||
fs.mkdirSync(runsDir, { recursive: true });
|
||||
const prefix = process.argv[3] || cfg.GT_VANITY_PREFIX || DEFAULT_PREFIX;
|
||||
if (!/^[1-9A-HJ-NP-Za-km-z]+$/.test(prefix)) throw new Error("Префикс Base58 без 0/O/I/l");
|
||||
ui.title("=== Vanity подбор mint keypair / Vanity mint keypair grind ===");
|
||||
ui.info(`Prefix: ${prefix}`);
|
||||
const args = ["grind", "--starts-with", `${prefix}:1`];
|
||||
const p = spawn("solana-keygen", args, { cwd: runsDir, stdio: ["ignore", "pipe", "pipe"] });
|
||||
const lines = [];
|
||||
const on = (d) => {
|
||||
for (const l of String(d).split("\n")) {
|
||||
const line = l.trim(); if (!line) continue;
|
||||
lines.push(line); console.log(line);
|
||||
}
|
||||
};
|
||||
p.stdout.on("data", on); p.stderr.on("data", on);
|
||||
const code = await new Promise((resolve) => p.on("close", resolve));
|
||||
if (code !== 0) throw new Error(`solana-keygen grind exit code ${code}`);
|
||||
const rp = path.join(runsDir, `${nowStamp()}_vanity_grind_report.json`);
|
||||
fs.writeFileSync(rp, JSON.stringify({ createdAt: new Date().toISOString(), prefix, command: `solana-keygen ${args.join(" ")}`, outputLog: lines }, null, 2));
|
||||
ui.ok("Готово / Done");
|
||||
ui.info(`Report: ${rp}`);
|
||||
ui.info(`RU: Скопируйте выбранный keypair из runs в keypairs/government_token/ (один json-файл).`);
|
||||
ui.info(`EN: Copy selected keypair from runs to keypairs/government_token/ (single json file).`);
|
||||
}
|
||||
main().catch((e) => { ui.err(`Ошибка / Error: ${e?.message || e}`); process.exit(1); });
|
||||
@@ -0,0 +1,60 @@
|
||||
# DAO scripts (актуальные)
|
||||
|
||||
## 1) Проверка конфигурации
|
||||
|
||||
```bash
|
||||
scripts/dao/create_realm_dao_full_test.sh scripts/dao/dao.config.env
|
||||
```
|
||||
|
||||
## 2) Реальное создание FULL DAO
|
||||
|
||||
```bash
|
||||
node scripts/dao/create_realm_dao_full_build_exec.js scripts/dao/dao.config.env
|
||||
```
|
||||
|
||||
Что делает:
|
||||
|
||||
1. Создает governance mint (SPL, decimals=0, supply из конфига).
|
||||
2. Добавляет on-chain metadata для mint (URI и картинка из Arweave).
|
||||
3. Создает Realm / Governance / Native Treasury.
|
||||
4. Депозитит governance токены в Realm.
|
||||
5. Пишет отчеты в `scripts/dao/runs/*.json` и `*.txt`.
|
||||
|
||||
## 3) Revoke/Burn membership токенов
|
||||
|
||||
### Вариант A (рекомендуется): через DAO голосование
|
||||
|
||||
```bash
|
||||
node scripts/dao/propose_vote_execute_revoke_full_exec.js \
|
||||
scripts/dao/dao.config.env \
|
||||
<REALM_PUBKEY> \
|
||||
<GOVERNANCE_PUBKEY> \
|
||||
<MINT_PUBKEY> \
|
||||
<TARGET_OWNER_PUBKEY> \
|
||||
[AMOUNT]
|
||||
```
|
||||
|
||||
Скрипт делает полный цикл:
|
||||
|
||||
1. `create proposal`
|
||||
2. `insert revoke instruction`
|
||||
3. `sign off`
|
||||
4. `cast vote`
|
||||
5. `execute`
|
||||
|
||||
### Вариант B (технический/админский): прямой revoke
|
||||
|
||||
```bash
|
||||
node scripts/dao/revoke_member_token_full_exec.js \
|
||||
scripts/dao/dao.config.env \
|
||||
<REALM_PUBKEY> \
|
||||
<MINT_PUBKEY> \
|
||||
<TARGET_OWNER_PUBKEY> \
|
||||
[AMOUNT]
|
||||
```
|
||||
|
||||
Важное:
|
||||
|
||||
1. Для `RevokeGoverningTokens` токен должен быть membership-типом (в full-скрипте это уже так).
|
||||
2. Для сценария “только DAO голосованием” используйте вариант A.
|
||||
3. Вариант B оставлен как технический инструмент.
|
||||
@@ -0,0 +1,456 @@
|
||||
#!/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);
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_PATH="${1:-$SCRIPT_DIR/dao.config.env}"
|
||||
|
||||
if [[ ! -f "$CONFIG_PATH" ]]; then
|
||||
echo "Ошибка: не найден конфиг $CONFIG_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
source "$CONFIG_PATH"
|
||||
|
||||
require_cmd() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "Ошибка: команда '$1' не найдена"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_cmd solana
|
||||
require_cmd solana-keygen
|
||||
require_cmd node
|
||||
|
||||
if [[ -z "${DAO_REALM_NAME:-}" || -z "${DAO_CLUSTER:-}" || -z "${DAO_ISSUER_KEYPAIR:-}" || -z "${SPL_GOVERNANCE_PROGRAM_ID:-}" ]]; then
|
||||
echo "Ошибка: обязательные поля конфига пустые"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$DAO_ISSUER_KEYPAIR" ]]; then
|
||||
echo "Ошибка: keypair не найден: $DAO_ISSUER_KEYPAIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${DAO_REALM_NAME}" == *"TEMPLATE"* || "${DAO_REALM_NAME}" == *"CHANGE_ME"* ]]; then
|
||||
echo "Ошибка: похоже, не заменили тестовое имя DAO_REALM_NAME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [[ "${DAO_VOTING_TIME_SEC}" =~ ^[0-9]+$ ]] || ! [[ "${DAO_GOV_NFT_SUPPLY}" =~ ^[0-9]+$ ]] || ! [[ "${DAO_APPROVAL_THRESHOLD_PERCENT}" =~ ^[0-9]+$ ]]; then
|
||||
echo "Ошибка: числовые параметры заданы некорректно"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if (( DAO_APPROVAL_THRESHOLD_PERCENT < 51 || DAO_APPROVAL_THRESHOLD_PERCENT > 100 )); then
|
||||
echo "Ошибка: DAO_APPROVAL_THRESHOLD_PERCENT должен быть в диапазоне 51..100"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ISSUER_PUBKEY="$(solana-keygen pubkey "$DAO_ISSUER_KEYPAIR")"
|
||||
ISSUER_BALANCE="$(solana balance "$ISSUER_PUBKEY" --url "$DAO_CLUSTER" 2>/dev/null || true)"
|
||||
|
||||
REALM_PDA="$(node - "$DAO_REALM_NAME" "$SPL_GOVERNANCE_PROGRAM_ID" <<'NODE'
|
||||
const { PublicKey } = require("@solana/web3.js");
|
||||
const realmName = process.argv[2];
|
||||
const programId = new PublicKey(process.argv[3]);
|
||||
const [pda] = PublicKey.findProgramAddressSync(
|
||||
[Buffer.from("governance"), Buffer.from(realmName, "utf8")],
|
||||
programId
|
||||
);
|
||||
console.log(pda.toBase58());
|
||||
NODE
|
||||
)"
|
||||
|
||||
if [[ -z "$REALM_PDA" ]]; then
|
||||
echo "Ошибка: не удалось вычислить PDA realm."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REALM_EXISTS="no"
|
||||
if solana account "$REALM_PDA" --url "$DAO_CLUSTER" >/dev/null 2>&1; then
|
||||
REALM_EXISTS="yes"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
============================================================
|
||||
ПРЕДСТАРТОВАЯ ПРОВЕРКА DAO (Realms)
|
||||
------------------------------------------------------------
|
||||
Сеть: $DAO_CLUSTER
|
||||
Realm name: $DAO_REALM_NAME
|
||||
Realm PDA: $REALM_PDA
|
||||
Realm уже существует: $REALM_EXISTS
|
||||
Governance program: $SPL_GOVERNANCE_PROGRAM_ID
|
||||
Эмиттер (issuer): $ISSUER_PUBKEY
|
||||
Баланс эмиттера: ${ISSUER_BALANCE:-unknown}
|
||||
NFT name: $DAO_GOV_NFT_NAME
|
||||
NFT symbol: $DAO_GOV_NFT_SYMBOL
|
||||
NFT supply: $DAO_GOV_NFT_SUPPLY
|
||||
Voting time (sec): $DAO_VOTING_TIME_SEC
|
||||
Threshold %: $DAO_APPROVAL_THRESHOLD_PERCENT
|
||||
Конфиг: $CONFIG_PATH
|
||||
============================================================
|
||||
EOF
|
||||
|
||||
if [[ "$REALM_EXISTS" == "yes" ]]; then
|
||||
echo "Стоп: realm с таким именем уже существует в этой сети."
|
||||
echo "Поменяйте DAO_REALM_NAME в конфиге и запустите снова."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Проверка пройдена."
|
||||
echo "Этот скрипт делает только preflight-валидацию."
|
||||
echo "Для реального создания DAO запускайте исполняющий скрипт:"
|
||||
echo "node scripts/dao/create_realm_dao_full_build_exec.js scripts/dao/dao.config.env"
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const BN = require("bn.js");
|
||||
const { Connection, Keypair, PublicKey, Transaction, sendAndConfirmTransaction, clusterApiUrl } = require("@solana/web3.js");
|
||||
const {
|
||||
PROGRAM_VERSION_V3,
|
||||
InstructionData,
|
||||
AccountMetaData,
|
||||
withRevokeGoverningTokens,
|
||||
withExecuteTransaction,
|
||||
} = 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 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),
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const configPath = process.argv[2] ? path.resolve(process.argv[2]) : path.resolve(__dirname, "dao.config.env");
|
||||
const realm = new PublicKey(process.argv[3]);
|
||||
const governance = new PublicKey(process.argv[4]);
|
||||
const proposal = new PublicKey(process.argv[5]);
|
||||
const proposalTx = new PublicKey(process.argv[6]);
|
||||
const mint = new PublicKey(process.argv[7]);
|
||||
const targetOwner = new PublicKey(process.argv[8]);
|
||||
const amount = new BN(process.argv[9] || "1");
|
||||
if (!process.argv[8]) {
|
||||
throw new Error(
|
||||
"Использование: node scripts/dao/execute_revoke_transaction_full_exec.js <config.env> <realm> <governance> <proposal> <proposal_tx> <mint> <target_owner> [amount]"
|
||||
);
|
||||
}
|
||||
const cfg = parseEnvConfig(configPath);
|
||||
const cluster = cfg.DAO_CLUSTER || "devnet";
|
||||
const governanceProgramId = new PublicKey(cfg.SPL_GOVERNANCE_PROGRAM_ID);
|
||||
const signer = loadKeypair(path.resolve(cfg.DAO_ISSUER_KEYPAIR));
|
||||
const connection = new Connection(clusterApiUrl(cluster), "confirmed");
|
||||
|
||||
const ixRawRevoke = [];
|
||||
await withRevokeGoverningTokens(
|
||||
ixRawRevoke,
|
||||
governanceProgramId,
|
||||
PROGRAM_VERSION_V3,
|
||||
realm,
|
||||
targetOwner,
|
||||
mint,
|
||||
governance,
|
||||
amount
|
||||
);
|
||||
const revokeInstructionData = toGovernanceInstructionData(ixRawRevoke[0]);
|
||||
|
||||
const ixExecute = [];
|
||||
await withExecuteTransaction(
|
||||
ixExecute,
|
||||
governanceProgramId,
|
||||
PROGRAM_VERSION_V3,
|
||||
governance,
|
||||
proposal,
|
||||
proposalTx,
|
||||
[revokeInstructionData]
|
||||
);
|
||||
const sig = await sendAndConfirmTransaction(connection, new Transaction().add(...ixExecute), [signer], {
|
||||
commitment: "confirmed",
|
||||
});
|
||||
console.log("Execute success. Tx:", sig);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("Ошибка execute revoke:", e?.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,399 @@
|
||||
#!/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);
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/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 <config.env> <realm> <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 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);
|
||||
});
|
||||
Reference in New Issue
Block a user