Обновить документацию и словари

This commit is contained in:
AidarKC
2026-06-30 11:39:31 +04:00
parent 5720f1cb50
commit b0887648f7
89 changed files with 1297 additions and 8 deletions
@@ -0,0 +1,92 @@
#!/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_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
)"
REALM_EXISTS="no"
if solana account "$REALM_PDA" --url "$DAO_CLUSTER" >/dev/null 2>&1; then
REALM_EXISTS="yes"
fi
cat <<EOF
============================================================
ПРОВЕРКА create-SHiNE-DAO
------------------------------------------------------------
Сеть: $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}
Governance NFT name: $DAO_GOV_NFT_NAME
Governance NFT symbol: $DAO_GOV_NFT_SYMBOL
Governance 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 "Проверка пройдена."
@@ -0,0 +1,330 @@
#!/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,
SystemProgram,
Transaction,
sendAndConfirmTransaction,
} = 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");
const { PublicKey, assertRequired, clusterUrl, ensureArweaveUri, ensureDir, loadKeypair, parseEnvConfig, resolveConfigPath, nowStamp } = require("./js_common");
function lamportsToSol(lamports) {
return Number(lamports) / 1_000_000_000;
}
async function askYes() {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise((resolve) =>
rl.question("Введите YES для создания DAO: ", resolve)
);
rl.close();
return answer.trim() === "YES";
}
async function attachGovernanceMetadata(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(clusterUrl(cluster));
const issuerSigner = createSignerFromKeypair(umi, fromWeb3JsKeypair(issuer));
const mintSigner = createSignerFromKeypair(umi, fromWeb3JsKeypair(mintKeypair));
umi.use(signerIdentity(issuerSigner));
umi.use(mplTokenMetadata());
const builder = createV1(umi, {
mint: mintSigner,
authority: issuerSigner,
payer: issuerSigner,
updateAuthority: issuerSigner,
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 result = await builder.sendAndConfirm(umi);
return Buffer.from(result.signature).toString("base64");
}
async function main() {
const configPath = resolveConfigPath(process.argv[2], "dao.config.env");
if (!fs.existsSync(configPath)) throw new Error(`Конфиг не найден: ${configPath}`);
const cfg = parseEnvConfig(configPath);
[
"DAO_CLUSTER",
"DAO_REALM_NAME",
"DAO_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(clusterUrl(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");
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
);
if ((await connection.getAccountInfo(realmPda)) !== null) {
throw new Error(`Realm уже существует: ${realmPda.toBase58()}`);
}
const startBalance = await connection.getBalance(issuer.publicKey, "confirmed");
console.log("============================================================");
console.log("CREATE SHiNE DAO WITH GOVERNANCE NFT");
console.log("------------------------------------------------------------");
console.log("Сеть: ", cluster);
console.log("Realm name: ", cfg.DAO_REALM_NAME);
console.log("Realm PDA: ", realmPda.toBase58());
console.log("Issuer: ", issuer.publicKey.toBase58());
console.log("Supply: ", supply);
console.log("Voting time: ", votingTimeSec);
console.log("Threshold %: ", thresholdPct);
console.log("Баланс до: ", `${lamportsToSol(startBalance)} SOL`);
console.log("Metadata URI: ", cfg.DAO_GOV_TOKEN_METADATA_URI);
console.log("Image URL: ", cfg.DAO_GOV_TOKEN_IMAGE_URL);
console.log("============================================================");
if (!(await askYes())) {
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 attachGovernanceMetadata(cfg, cluster, issuer, mintKeypair.publicKey, mintKeypair);
const ixRealm = [];
const communityTokenConfig = new GoverningTokenConfigAccountArgs({
voterWeightAddin: undefined,
maxVoterWeightAddin: undefined,
tokenType: GoverningTokenType.Membership,
});
const realmPk = await withCreateRealm(
ixRealm,
governanceProgramId,
PROGRAM_VERSION_V3,
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,
PROGRAM_VERSION_V3,
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,
PROGRAM_VERSION_V3,
realmPk,
realmPk,
governanceConfig,
tokenOwnerRecordPk,
issuer.publicKey,
issuer.publicKey
);
const treasuryPk = await withCreateNativeTreasury(ixGov, governanceProgramId, PROGRAM_VERSION_V3, governancePk, issuer.publicKey);
const sigGov = await sendAndConfirmTransaction(connection, new Transaction().add(...ixGov), [issuer], { commitment: "confirmed" });
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,
PROGRAM_VERSION_V3,
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,
tokenSupply: supply,
votingTimeSec,
thresholdPercent: thresholdPct,
startBalanceSol: lamportsToSol(startBalance),
endBalanceSol: lamportsToSol(endBalance),
spentSol: lamportsToSol(spentLamports),
};
const reportsDir = path.resolve(__dirname, "..", "local", "runs");
ensureDir(reportsDir);
const base = `${nowStamp()}_${cfg.DAO_REALM_NAME.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 80)}_dao`;
const jsonPath = path.join(reportsDir, `${base}.json`);
const txtPath = path.join(reportsDir, `${base}.txt`);
fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2));
fs.writeFileSync(txtPath, Object.entries(report).map(([k, v]) => `${k}: ${v}`).join("\n") + "\n");
console.log("DAO создан.");
console.log("communityMint:", report.communityMint);
console.log("realm:", report.realm);
console.log("governance:", report.governance);
console.log("nativeTreasury:", report.nativeTreasury);
console.log("spentSol:", report.spentSol);
console.log("report:", jsonPath);
}
main().catch((e) => {
console.error("Ошибка создания DAO:", e?.message || e);
process.exit(1);
});
@@ -0,0 +1,44 @@
#!/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 { PublicKey, clusterUrl, loadKeypair, parseEnvConfig, resolveConfigPath } = require("./js_common");
async function main() {
const cfg = parseEnvConfig(resolveConfigPath(process.argv[2], "nft-flow.config.env"));
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 mintTx = new Transaction().add(
createAssociatedTokenAccountIdempotentInstruction(main.publicKey, ata, voter2.publicKey, mint, TOKEN_PROGRAM_ID),
createMintToInstruction(mint, ata, main.publicKey, 1n, [], TOKEN_PROGRAM_ID)
);
const sigMint = await sendAndConfirmTransaction(conn, mintTx, [main], { commitment: "confirmed" });
const tor = await getTokenOwnerRecordAddress(govPid, realm, mint, voter2.publicKey);
const ai = await conn.getAccountInfo(tor, "confirmed");
let sigDeposit = "already_exists";
if (!ai) {
const ix = [];
await withDepositGoverningTokens(ix, govPid, PROGRAM_VERSION_V3, realm, ata, mint, voter2.publicKey, main.publicKey, voter2.publicKey, new BN(1), true);
sigDeposit = await sendAndConfirmTransaction(conn, new Transaction().add(...ix), [main, voter2], { commitment: "confirmed" });
}
console.log("Второй голосующий подготовлен.");
console.log("mintTx:", sigMint);
console.log("depositTx:", sigDeposit);
}
main().catch((e) => {
console.error(e?.message || e);
process.exit(1);
});
@@ -0,0 +1,117 @@
#!/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 { 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");
const { PublicKey, assertRequired, clusterUrl, ensureArweaveUri, ensureDir, loadKeypair, nowStamp, parseEnvConfig, resolveConfigPath } = require("./js_common");
async function attachNftMetadata(cfg, cluster, payer, mintKeypair) {
ensureArweaveUri("NFT_METADATA_URI", cfg.NFT_METADATA_URI);
ensureArweaveUri("NFT_IMAGE_URL", cfg.NFT_IMAGE_URL);
const umi = createUmi(clusterUrl(cluster));
const payerSigner = createSignerFromKeypair(umi, fromWeb3JsKeypair(payer));
const mintSigner = createSignerFromKeypair(umi, fromWeb3JsKeypair(mintKeypair));
umi.use(signerIdentity(payerSigner));
umi.use(mplTokenMetadata());
const builder = createV1(umi, {
mint: mintSigner,
authority: payerSigner,
payer: payerSigner,
updateAuthority: payerSigner,
name: cfg.NFT_NAME,
symbol: cfg.NFT_SYMBOL,
uri: cfg.NFT_METADATA_URI,
sellerFeeBasisPoints: percentAmount(0),
tokenStandard: TokenStandard.NonFungible,
decimals: some(0),
creators: none(),
collection: none(),
uses: none(),
collectionDetails: none(),
ruleSet: none(),
printSupply: none(),
primarySaleHappened: false,
isMutable: true,
isCollection: false,
splTokenProgram: fromWeb3JsPublicKey(TOKEN_2022_PROGRAM_ID),
});
const result = await builder.sendAndConfirm(umi);
return Buffer.from(result.signature).toString("base64");
}
async function main() {
const cfg = parseEnvConfig(resolveConfigPath(process.argv[2], "nft-flow.config.env"));
["CLUSTER", "MAIN_KEYPAIR", "GOVERNANCE", "NFT_NAME", "NFT_SYMBOL", "NFT_METADATA_URI", "NFT_IMAGE_URL"].forEach((k) => assertRequired(cfg, k));
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 metadataSig = await attachNftMetadata(cfg, cfg.CLUSTER, main, mint);
const runsDir = path.resolve(__dirname, "..", cfg.RUNS_DIR || "./local/runs");
ensureDir(runsDir);
const report = {
createdAt: new Date().toISOString(),
mint: mint.publicKey.toBase58(),
governance: governance.toBase58(),
metadataUri: cfg.NFT_METADATA_URI,
imageUrl: cfg.NFT_IMAGE_URL,
txMintTemplate: sig,
txMetadata: metadataSig,
};
const reportPath = path.join(runsDir, `${nowStamp()}_nft_template.json`);
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log("NFT mint-шаблон с metadata создан.");
console.log("mint:", report.mint);
console.log("report:", reportPath);
}
main().catch((e) => {
console.error(e?.message || e);
process.exit(1);
});
@@ -0,0 +1,107 @@
#!/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, createAssociatedTokenAccountIdempotentInstruction, createMintToInstruction } = require("@solana/spl-token");
const {
PROGRAM_VERSION_V3,
Vote,
YesNoVote,
VoteType,
withCreateProposal,
withInsertTransaction,
withSignOffProposal,
withCastVote,
getTokenOwnerRecordAddress,
getProposalTransactionAddress,
} = require("@solana/spl-governance");
const { clusterUrl, ensureDir, loadKeypair, nowStamp, parseEnvConfig, resolveConfigPath, 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 05_propose_vote_mint_nft.js <config.env> <target_wallet> <nft_mint>");
const cfg = parseEnvConfig(resolveConfigPath(process.argv[2], "nft-flow.config.env"));
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 ixCreate = [];
const proposal = await withCreateProposal(
ixCreate,
govPid,
PROGRAM_VERSION_V3,
realm,
governance,
mainTor,
`Mint NFT to ${target.toBase58().slice(0, 8)}`,
cfg.NFT_METADATA_URI || "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 = [
createAssociatedTokenAccountIdempotentInstruction(main.publicKey, targetAta, target, nftMint, TOKEN_2022_PROGRAM_ID),
createMintToInstruction(nftMint, targetAta, governance, 1n, [], TOKEN_2022_PROGRAM_ID),
].map(toInstructionData);
const ixInsert = [];
const proposalTx = await withInsertTransaction(ixInsert, govPid, PROGRAM_VERSION_V3, governance, proposal, mainTor, main.publicKey, 0, 0, 0, mintIx, 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 || "./local/runs");
ensureDir(runs);
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 reportPath = path.join(runs, `${nowStamp()}_proposal_mint_${target.toBase58().slice(0, 8)}.json`);
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log("Mint proposal создан и проголосован.");
console.log("proposal:", report.proposal);
console.log("proposalTransaction:", report.proposalTransaction);
console.log("report:", reportPath);
}
main().catch((e) => {
console.error(e?.message || e);
process.exit(1);
});
@@ -0,0 +1,38 @@
#!/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, createAssociatedTokenAccountIdempotentInstruction, createMintToInstruction } = require("@solana/spl-token");
const { PROGRAM_VERSION_V3, withExecuteTransaction } = require("@solana/spl-governance");
const { clusterUrl, loadKeypair, parseEnvConfig, resolveConfigPath, toInstructionData } = require("./js_common");
async function main() {
const cfg = parseEnvConfig(resolveConfigPath(process.argv[2], "nft-flow.config.env"));
const proposal = new PublicKey(process.argv[3]);
const proposalTx = new PublicKey(process.argv[4]);
const target = new PublicKey(process.argv[5]);
const nftMint = new PublicKey(process.argv[6]);
if (!process.argv[6]) throw new Error("Usage: node 06_execute_mint_nft.js <config.env> <proposal> <proposalTx> <targetWallet> <nftMint>");
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 mintIx = [
createAssociatedTokenAccountIdempotentInstruction(main.publicKey, targetAta, target, nftMint, TOKEN_2022_PROGRAM_ID),
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("Mint execute done:", sig);
}
main().catch((e) => {
console.error(e?.message || e);
process.exit(1);
});
@@ -0,0 +1,101 @@
#!/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 { clusterUrl, ensureDir, loadKeypair, nowStamp, parseEnvConfig, resolveConfigPath, toInstructionData } = require("./js_common");
async function main() {
const currentOwnerWallet = process.argv[3];
const nftMintStr = process.argv[4];
if (!currentOwnerWallet || !nftMintStr) throw new Error("Usage: node 07_propose_vote_burn_nft.js <config.env> <current_owner_wallet> <nft_mint>");
const cfg = parseEnvConfig(resolveConfigPath(process.argv[2], "nft-flow.config.env"));
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 currentOwner = new PublicKey(currentOwnerWallet);
const mainTor = await getTokenOwnerRecordAddress(govPid, realm, governingMint, main.publicKey);
const sourceAta = getAssociatedTokenAddressSync(nftMint, currentOwner, 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)}`,
cfg.NFT_METADATA_URI || "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 || "./local/runs");
ensureDir(runs);
const report = {
type: "burn_nft",
realm: realm.toBase58(),
governance: governance.toBase58(),
proposal: proposal.toBase58(),
proposalTransaction: proposalTx.toBase58(),
nftMint: nftMint.toBase58(),
currentOwnerWallet: currentOwner.toBase58(),
sourceAta: sourceAta.toBase58(),
txCreate,
txInsert,
txSign,
txVote1,
};
const reportPath = path.join(runs, `${nowStamp()}_proposal_burn_${currentOwner.toBase58().slice(0, 8)}.json`);
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log("Burn proposal создан и проголосован.");
console.log("proposal:", report.proposal);
console.log("proposalTransaction:", report.proposalTransaction);
console.log("sourceAta:", report.sourceAta);
console.log("report:", reportPath);
}
main().catch((e) => {
console.error(e?.message || e);
process.exit(1);
});
@@ -0,0 +1,35 @@
#!/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 { clusterUrl, loadKeypair, parseEnvConfig, resolveConfigPath, toInstructionData } = require("./js_common");
async function main() {
const cfg = parseEnvConfig(resolveConfigPath(process.argv[2], "nft-flow.config.env"));
const proposal = new PublicKey(process.argv[3]);
const proposalTx = new PublicKey(process.argv[4]);
const currentOwner = new PublicKey(process.argv[5]);
const nftMint = new PublicKey(process.argv[6]);
if (!process.argv[6]) throw new Error("Usage: node 08_execute_burn_nft.js <config.env> <proposal> <proposalTx> <currentOwnerWallet> <nftMint>");
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, currentOwner, 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("Burn execute done:", sig);
}
main().catch((e) => {
console.error(e?.message || e);
process.exit(1);
});
@@ -0,0 +1,91 @@
#!/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 trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eq = trimmed.indexOf("=");
if (eq < 0) continue;
const key = trimmed.slice(0, eq).trim();
let value = trimmed.slice(eq + 1).trim();
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
value = value.replace(/\$HOME/g, process.env.HOME || "");
out[key] = value;
}
return out;
}
function resolveConfigPath(argvPath, fallbackName) {
return argvPath ? path.resolve(argvPath) : path.resolve(__dirname, "..", fallbackName);
}
function loadKeypair(filePath) {
const arr = JSON.parse(fs.readFileSync(filePath, "utf8"));
return Keypair.fromSecretKey(Uint8Array.from(arr));
}
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 ensureArweaveUri(name, uri) {
if (!uri) throw new Error(`${name} пустой`);
if (!(uri.startsWith("https://arweave.net/") || uri.startsWith("ar://"))) {
throw new Error(`${name} должен указывать на Arweave`);
}
}
function assertRequired(cfg, key) {
if (!cfg[key]) throw new Error(`В конфиге отсутствует обязательный параметр: ${key}`);
}
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),
});
}
function ensureDir(dirPath) {
fs.mkdirSync(dirPath, { recursive: true });
}
module.exports = {
PublicKey,
assertRequired,
clusterUrl,
ensureArweaveUri,
ensureDir,
loadKeypair,
nowStamp,
parseEnvConfig,
resolveConfigPath,
toInstructionData,
};