Отключить репосты и добавить Solana-модуль

This commit is contained in:
AidarKC
2026-05-24 12:16:39 +03:00
parent abdce05136
commit 56cd90a197
95 changed files with 24261 additions and 64 deletions
@@ -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); });
@@ -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 };