Подготовка перед деплоем в mainnet

This commit is contained in:
AidarKC
2026-07-01 12:23:30 +04:00
parent ab4dab34aa
commit 0240db59ee
52 changed files with 1018 additions and 473 deletions
+128 -33
View File
@@ -1,6 +1,6 @@
# create-SHiNE-DAO
Чистый актуальный комплект для создания SHiNE DAO с governance NFT и для работы с NFT, которые:
Чистый актуальный комплект для создания SHiNE DAO с переносимым governance mint (`Liquid`) и для работы с NFT, которые:
1. можно выпускать через голосование DAO;
2. можно сжигать через голосование DAO;
@@ -15,16 +15,18 @@
## Что внутри
- `dao.config.env.example` - шаблон конфига для создания самого DAO.
- `dao.config.env.example` - шаблон конфига для создания mint и DAO в три шага.
- `scripts/01_create_governance_mint.js` - создание governance mint и metadata без депозита в Realm.
- `scripts/02_create_dao_with_governance_nfts.js` - создание `Realm / TokenOwnerRecord / Governance / Treasury` без deposit и без передачи authority.
- `scripts/03_transfer_dao_authorities.js` - передача `mint authority`, `freeze authority`, `realm authority` и `metadata update authority` на DAO.
- `nft-flow.config.env.example` - шаблон конфига для выпуска и burn NFT через уже созданное DAO.
- `scripts/01_check_dao_config.sh` - предстаровая проверка конфига DAO.
- `scripts/02_create_dao_with_governance_nfts.js` - создание DAO с governance NFT.
- `scripts/03_prepare_second_voter.js` - подготовка второго голосующего.
- `scripts/04_create_empty_nft_template_with_metadata.js` - создание NFT mint-шаблона с metadata и картинкой.
- `scripts/05_propose_vote_mint_nft.js` - proposal/sign/vote на mint NFT.
- `scripts/06_execute_mint_nft.js` - execute mint proposal.
- `scripts/07_propose_vote_burn_nft.js` - proposal/sign/vote на burn NFT.
- `scripts/08_execute_burn_nft.js` - execute burn proposal.
- `scripts/01_check_dao_config.sh` - проверка конфига перед созданием DAO из существующего mint.
- `scripts/04_prepare_second_voter.js` - подготовка второго голосующего.
- `scripts/05_create_empty_nft_template_with_metadata.js` - создание NFT mint-шаблона с metadata и картинкой.
- `scripts/06_propose_vote_mint_nft.js` - proposal/sign/vote на mint NFT.
- `scripts/07_execute_mint_nft.js` - execute mint proposal.
- `scripts/08_propose_vote_burn_nft.js` - proposal/sign/vote на burn NFT.
- `scripts/09_execute_burn_nft.js` - execute burn proposal.
- `docs/FUTURE_FAST_BURN_EXECUTOR.md` - описание будущей программы для быстрого burn одного конкретного `mint`.
## Подтвержденная база, на которой собран комплект
@@ -74,9 +76,9 @@ npm install
- `solana-keygen`
- `node`
## Быстрый сценарий: создать DAO и запустить NFT-flow
## Быстрый сценарий: создать mint, раздать токены, создать DAO и затем передать все права на DAO
### 1. Создать конфиг DAO
### 1. Создать конфиг
```bash
cp create-SHiNE-DAO/dao.config.env.example create-SHiNE-DAO/dao.config.env
@@ -86,29 +88,116 @@ cp create-SHiNE-DAO/dao.config.env.example create-SHiNE-DAO/dao.config.env
- имя DAO;
- путь к keypair создателя;
- имя и символ governance NFT;
- `DAO_GOV_NFT_SUPPLY` (обычно `10`);
- имя и символ governance mint;
- при необходимости `DAO_GOV_MINT_KEYPAIR` для красивого или фиксированного mint address;
- `DAO_GOV_TOKEN_METADATA_URI` и `DAO_GOV_TOKEN_IMAGE_URL`.
### 2. Проверить конфиг DAO
Обычно используются два keypair-файла:
- `DAO_ISSUER_KEYPAIR` - временный кошелёк создателя/оператора;
- `DAO_GOV_MINT_KEYPAIR` - keypair самого mint, если нужен красивый адрес governance mint.
Сгенерировать их можно так:
```bash
solana-keygen new --outfile ./dao-issuer.json
solana-keygen new --outfile ./dao-gov-mint.json
```
Публичные ключи посмотреть так:
```bash
solana-keygen pubkey ./dao-issuer.json
solana-keygen pubkey ./dao-gov-mint.json
```
Практическое правило:
- первый keypair обычно временный, его красивый адрес не важен;
- красивым обычно имеет смысл делать именно `DAO_GOV_MINT_KEYPAIR`, потому что это постоянный публичный адрес governance mint.
### 2. Создать governance mint
```bash
node create-SHiNE-DAO/scripts/01_create_governance_mint.js ./create-SHiNE-DAO/dao.config.env
```
Этот шаг:
1. создаёт mint;
2. прикрепляет metadata на Arweave;
3. оставляет у кошелька создателя:
- `mint authority`
- `freeze authority`
- `metadata update authority`
Актуальные Arweave-ссылки для governance mint сейчас такие:
- image: `https://arweave.net/rPGo-uYIFoLxMBVTGyPr5K6fuF0y9xL-8TlAaZyBJgY`
- metadata: `https://arweave.net/PdupvtRhj_DTBjTLcjD7Z5GSiEcFUf01GHaYMDs6gGw`
### 3. Раздать токены вручную
```bash
spl-token mint <GOVERNANCE_MINT> 1 <WALLET_1>
spl-token mint <GOVERNANCE_MINT> 1 <WALLET_2>
```
После этого можно руками наминтить управляющие токены на нужные кошельки.
На этом этапе токен ещё обычный и переносимый.
### 4. Проверить конфиг DAO
```bash
./create-SHiNE-DAO/scripts/01_check_dao_config.sh ./create-SHiNE-DAO/dao.config.env
```
### 3. Создать DAO
Перед проверкой в `dao.config.env` нужно указать:
- `DAO_GOV_TOKEN_MINT` - уже созданный mint;
- `DAO_REALM_NAME` - новое имя DAO;
- `DAO_ISSUER_KEYPAIR` - кошелёк создателя;
- `DAO_VOTING_TIME_SEC` - по умолчанию `86400` секунд (1 день);
- `DAO_APPROVAL_THRESHOLD_PERCENT`.
### 5. Создать DAO
```bash
node create-SHiNE-DAO/scripts/02_create_dao_with_governance_nfts.js ./create-SHiNE-DAO/dao.config.env
```
Этот шаг:
- создаёт `Realm`;
- создаёт `TokenOwnerRecord` для кошелька создателя без deposit;
- создаёт `Governance` и `Native Treasury`;
- задаёт community token type = `Liquid`;
- не делает deposit токенов;
- не передаёт authority.
После этого из отчета взять:
- `realm`
- `governance`
- `communityMint`
- `tokenOwnerRecord`
### 4. Создать конфиг NFT-flow
### 6. Передать все authority на DAO
```bash
node create-SHiNE-DAO/scripts/03_transfer_dao_authorities.js ./create-SHiNE-DAO/dao.config.env
```
Этот шаг передаёт на DAO:
- `mint authority`
- `freeze authority`
- `realm authority`
- `metadata update authority`
После этого у создателя не должно остаться административных прав на governance mint и Realm.
### 7. Создать конфиг NFT-flow
```bash
cp create-SHiNE-DAO/nft-flow.config.env.example create-SHiNE-DAO/nft-flow.config.env
@@ -123,33 +212,33 @@ cp create-SHiNE-DAO/nft-flow.config.env.example create-SHiNE-DAO/nft-flow.config
- `VOTER2_KEYPAIR`
- metadata URI и картинку для целевого NFT.
### 5. Подготовить второго голосующего
### 8. Подготовить второго голосующего
```bash
node create-SHiNE-DAO/scripts/03_prepare_second_voter.js ./create-SHiNE-DAO/nft-flow.config.env
node create-SHiNE-DAO/scripts/04_prepare_second_voter.js ./create-SHiNE-DAO/nft-flow.config.env
```
### 6. Создать NFT mint-шаблон с metadata
### 9. Создать NFT mint-шаблон с metadata
```bash
node create-SHiNE-DAO/scripts/04_create_empty_nft_template_with_metadata.js ./create-SHiNE-DAO/nft-flow.config.env
node create-SHiNE-DAO/scripts/05_create_empty_nft_template_with_metadata.js ./create-SHiNE-DAO/nft-flow.config.env
```
Скрипт вернет `mint` будущего NFT.
### 7. Поднять proposal на mint NFT
### 10. Поднять proposal на mint NFT
```bash
node create-SHiNE-DAO/scripts/05_propose_vote_mint_nft.js \
node create-SHiNE-DAO/scripts/06_propose_vote_mint_nft.js \
./create-SHiNE-DAO/nft-flow.config.env \
<TARGET_WALLET> \
<NFT_MINT>
```
### 8. Выполнить mint proposal
### 11. Выполнить mint proposal
```bash
node create-SHiNE-DAO/scripts/06_execute_mint_nft.js \
node create-SHiNE-DAO/scripts/07_execute_mint_nft.js \
./create-SHiNE-DAO/nft-flow.config.env \
<PROPOSAL> \
<PROPOSAL_TX> \
@@ -157,19 +246,19 @@ node create-SHiNE-DAO/scripts/06_execute_mint_nft.js \
<NFT_MINT>
```
### 9. Поднять proposal на burn NFT
### 12. Поднять proposal на burn NFT
```bash
node create-SHiNE-DAO/scripts/07_propose_vote_burn_nft.js \
node create-SHiNE-DAO/scripts/08_propose_vote_burn_nft.js \
./create-SHiNE-DAO/nft-flow.config.env \
<CURRENT_OWNER_WALLET> \
<NFT_MINT>
```
### 10. Выполнить burn proposal
### 13. Выполнить burn proposal
```bash
node create-SHiNE-DAO/scripts/08_execute_burn_nft.js \
node create-SHiNE-DAO/scripts/09_execute_burn_nft.js \
./create-SHiNE-DAO/nft-flow.config.env \
<PROPOSAL> \
<PROPOSAL_TX> \
@@ -190,12 +279,18 @@ node create-SHiNE-DAO/scripts/08_execute_burn_nft.js \
## Что важно про стартовые governance NFT
На старте governance NFT mint'ятся создателю DAO, а затем депонируются в Realm.
В новом трёхшаговом flow governance mint создается отдельно, токены можно распределить вручную до создания DAO, а затем отдельным шагом DAO получает права на:
Практически это означает:
- `mint authority`
- `freeze authority`
- `realm authority`
- `metadata update authority`
- первоначальный voting power принадлежит создателю DAO;
- затем governance NFT можно дополнительно распределять между другими участниками.
Это сделано специально, чтобы:
1. сначала спокойно распределить токены;
2. затем создать Realm без блокировки стартовых токенов;
3. и только потом окончательно передать контроль DAO.
## Future
@@ -4,28 +4,33 @@
DAO_CLUSTER="devnet"
# Для каждого нового запуска должен быть новый realm name.
DAO_REALM_NAME="SHiNE NFT DAO 2026-06-30"
DAO_REALM_NAME="SHiNE DAO2"
# Governance NFT коллекция DAO
DAO_GOV_NFT_NAME="SHiNE Governance NFT"
DAO_GOV_NFT_SYMBOL="SHINE-GOV"
DAO_GOV_TOKEN_DESCRIPTION="Governance NFT for SHiNE DAO"
DAO_GOV_TOKEN_DESCRIPTION="Токен для голосования в SHiNE DAO. Voting token for SHiNE DAO."
# Metadata JSON governance mint
DAO_GOV_TOKEN_METADATA_URI="https://arweave.net/CHANGE_ME_METADATA_JSON"
DAO_GOV_TOKEN_IMAGE_URL="https://arweave.net/CHANGE_ME_IMAGE"
# Шаг 1: создание mint
# Первый keypair временный: он нужен только как оператор для старта.
# Красивым обычно имеет смысл делать именно mint keypair.
DAO_GOV_MINT_KEYPAIR="/home/ai/work/SHiNE/keys_for_create_DAO/dao-gov-mint.json"
# Стартовое число governance NFT
DAO_GOV_NFT_SUPPLY="10"
# Metadata для governance mint
DAO_GOV_TOKEN_METADATA_URI="https://arweave.net/PdupvtRhj_DTBjTLcjD7Z5GSiEcFUf01GHaYMDs6gGw"
DAO_GOV_TOKEN_IMAGE_URL="https://arweave.net/rPGo-uYIFoLxMBVTGyPr5K6fuF0y9xL-8TlAaZyBJgY"
# Минимум для тестов Realms - 3600
DAO_VOTING_TIME_SEC="3600"
# Шаг 2: создание DAO из уже существующего mint
DAO_GOV_TOKEN_MINT=""
# Дефолт для новых DAO - 86400 секунд (1 день)
DAO_VOTING_TIME_SEC="86400"
# 51..100
DAO_APPROVAL_THRESHOLD_PERCENT="51"
# Создатель DAO
DAO_ISSUER_KEYPAIR="$HOME/.config/solana/phantomWallet.json"
# Создатель DAO / временный оператор до передачи authority на DAO
DAO_ISSUER_KEYPAIR="/home/ai/work/SHiNE/keys_for_create_DAO/dao-issuer.json"
# Governance program (Realms)
SPL_GOVERNANCE_PROGRAM_ID="GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw"
@@ -23,7 +23,7 @@ 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
if [[ -z "${DAO_REALM_NAME:-}" || -z "${DAO_CLUSTER:-}" || -z "${DAO_ISSUER_KEYPAIR:-}" || -z "${SPL_GOVERNANCE_PROGRAM_ID:-}" || -z "${DAO_GOV_TOKEN_MINT:-}" ]]; then
echo "Ошибка: обязательные поля конфига пустые"
exit 1
fi
@@ -33,7 +33,7 @@ if [[ ! -f "$DAO_ISSUER_KEYPAIR" ]]; then
exit 1
fi
if ! [[ "${DAO_VOTING_TIME_SEC}" =~ ^[0-9]+$ ]] || ! [[ "${DAO_GOV_NFT_SUPPLY}" =~ ^[0-9]+$ ]] || ! [[ "${DAO_APPROVAL_THRESHOLD_PERCENT}" =~ ^[0-9]+$ ]]; then
if ! [[ "${DAO_VOTING_TIME_SEC}" =~ ^[0-9]+$ ]] || ! [[ "${DAO_APPROVAL_THRESHOLD_PERCENT}" =~ ^[0-9]+$ ]]; then
echo "Ошибка: числовые параметры заданы некорректно"
exit 1
fi
@@ -45,6 +45,12 @@ fi
ISSUER_PUBKEY="$(solana-keygen pubkey "$DAO_ISSUER_KEYPAIR")"
ISSUER_BALANCE="$(solana balance "$ISSUER_PUBKEY" --url "$DAO_CLUSTER" 2>/dev/null || true)"
MINT_PUBKEY="$DAO_GOV_TOKEN_MINT"
if ! node -e "const { PublicKey } = require('@solana/web3.js'); new PublicKey(process.argv[1])" "$MINT_PUBKEY" >/dev/null 2>&1; then
echo "Ошибка: DAO_GOV_TOKEN_MINT не является валидным pubkey"
exit 1
fi
REALM_PDA="$(node - "$DAO_REALM_NAME" "$SPL_GOVERNANCE_PROGRAM_ID" <<'NODE'
const { PublicKey } = require("@solana/web3.js");
@@ -63,6 +69,11 @@ if solana account "$REALM_PDA" --url "$DAO_CLUSTER" >/dev/null 2>&1; then
REALM_EXISTS="yes"
fi
MINT_EXISTS="no"
if solana account "$MINT_PUBKEY" --url "$DAO_CLUSTER" >/dev/null 2>&1; then
MINT_EXISTS="yes"
fi
cat <<EOF
============================================================
ПРОВЕРКА create-SHiNE-DAO
@@ -72,11 +83,12 @@ Realm name: $DAO_REALM_NAME
Realm PDA: $REALM_PDA
Realm уже существует: $REALM_EXISTS
Governance program: $SPL_GOVERNANCE_PROGRAM_ID
Governance mint: $MINT_PUBKEY
Mint уже существует: $MINT_EXISTS
Эмиттер (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
@@ -0,0 +1,162 @@
#!/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_PROGRAM_ID,
AuthorityType,
getMintLen,
createInitializeMintInstruction,
createSetAuthorityInstruction,
} = 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");
function lamportsToSol(lamports) {
return Number(lamports) / 1_000_000_000;
}
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_GOV_NFT_NAME",
"DAO_GOV_NFT_SYMBOL",
"DAO_GOV_TOKEN_DESCRIPTION",
"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 mintKeypair = cfg.DAO_GOV_MINT_KEYPAIR
? loadKeypair(path.resolve(cfg.DAO_GOV_MINT_KEYPAIR))
: Keypair.generate();
const governanceProgramId = new PublicKey(cfg.SPL_GOVERNANCE_PROGRAM_ID);
const startBalance = await connection.getBalance(issuer.publicKey, "confirmed");
console.log("============================================================");
console.log("CREATE SHiNE GOVERNANCE MINT");
console.log("------------------------------------------------------------");
console.log("Сеть: ", cluster);
console.log("Mint: ", mintKeypair.publicKey.toBase58());
console.log("Issuer: ", issuer.publicKey.toBase58());
console.log("Governance program:", governanceProgramId.toBase58());
console.log("Баланс до: ", `${lamportsToSol(startBalance)} SOL`);
console.log("Name: ", cfg.DAO_GOV_NFT_NAME);
console.log("Symbol: ", cfg.DAO_GOV_NFT_SYMBOL);
console.log("Metadata URI: ", cfg.DAO_GOV_TOKEN_METADATA_URI);
console.log("Image URL: ", cfg.DAO_GOV_TOKEN_IMAGE_URL);
console.log("============================================================");
const mintLen = getMintLen([]);
const mintRent = await connection.getMinimumBalanceForRentExemption(mintLen);
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)
);
const sigMint = await sendAndConfirmTransaction(connection, txMint, [issuer, mintKeypair], {
commitment: "confirmed",
});
const sigMetadata = await attachGovernanceMetadata(cfg, cluster, issuer, mintKeypair.publicKey, mintKeypair);
const report = {
createdAt: new Date().toISOString(),
cluster,
mint: mintKeypair.publicKey.toBase58(),
issuer: issuer.publicKey.toBase58(),
mintAuthority: issuer.publicKey.toBase58(),
freezeAuthority: issuer.publicKey.toBase58(),
metadataUpdateAuthority: issuer.publicKey.toBase58(),
governanceProgramId: governanceProgramId.toBase58(),
metadataUri: cfg.DAO_GOV_TOKEN_METADATA_URI,
imageUrl: cfg.DAO_GOV_TOKEN_IMAGE_URL,
txMint: sigMint,
txMetadata: sigMetadata,
};
const runsDir = path.resolve(__dirname, "..", "local", "runs");
ensureDir(runsDir);
const reportPath = path.join(runsDir, `${nowStamp()}_governance_mint.json`);
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log("Готово. Governance mint создан.");
console.log("mint:", mintKeypair.publicKey.toBase58());
console.log("report:", reportPath);
}
main().catch((e) => {
console.error("CreateMint error:", e?.message || e);
process.exit(1);
});
@@ -0,0 +1,68 @@
#!/usr/bin/env node
"use strict";
const path = require("path");
const { Connection, PublicKey, Transaction, sendAndConfirmTransaction } = require("@solana/web3.js");
const {
TOKEN_PROGRAM_ID,
getAssociatedTokenAddressSync,
createAssociatedTokenAccountIdempotentInstruction,
createMintToInstruction,
getMint,
} = require("@solana/spl-token");
const { clusterUrl, loadKeypair, parseEnvConfig, resolveConfigPath } = require("./js_common");
async function main() {
const targetWallet = process.argv[3];
const amountRaw = process.argv[4] || "1";
if (!targetWallet) {
throw new Error("Usage: node 01a_mint_governance_token.js <config.env> <target_wallet> [amount]");
}
const cfg = parseEnvConfig(resolveConfigPath(process.argv[2], "dao.config.env"));
if (!cfg.DAO_GOV_TOKEN_MINT) throw new Error("В конфиге пустой DAO_GOV_TOKEN_MINT");
if (!cfg.DAO_ISSUER_KEYPAIR) throw new Error("В конфиге пустой DAO_ISSUER_KEYPAIR");
const conn = new Connection(clusterUrl(cfg.DAO_CLUSTER || "devnet"), "confirmed");
const issuer = loadKeypair(path.resolve(cfg.DAO_ISSUER_KEYPAIR));
const mint = new PublicKey(cfg.DAO_GOV_TOKEN_MINT);
const target = new PublicKey(targetWallet);
const amount = BigInt(amountRaw);
const mintInfo = await getMint(conn, mint, "confirmed", TOKEN_PROGRAM_ID);
if (mintInfo.decimals !== 0) {
throw new Error(`Ожидался governance mint с decimals=0, а получено ${mintInfo.decimals}`);
}
const ata = getAssociatedTokenAddressSync(mint, target, false, TOKEN_PROGRAM_ID);
const tx = new Transaction().add(
createAssociatedTokenAccountIdempotentInstruction(
issuer.publicKey,
ata,
target,
mint,
TOKEN_PROGRAM_ID
),
createMintToInstruction(
mint,
ata,
issuer.publicKey,
amount,
[],
TOKEN_PROGRAM_ID
)
);
const sig = await sendAndConfirmTransaction(conn, tx, [issuer], { commitment: "confirmed" });
console.log("Mint done.");
console.log("target:", target.toBase58());
console.log("ata:", ata.toBase58());
console.log("amount:", amount.toString());
console.log("signature:", sig);
}
main().catch((e) => {
console.error("MintGovernanceToken error:", e?.message || e);
process.exit(1);
});
@@ -7,20 +7,14 @@ const readline = require("readline");
const BN = require("bn.js");
const {
Connection,
Keypair,
SystemProgram,
PublicKey,
Transaction,
sendAndConfirmTransaction,
} = require("@solana/web3.js");
const {
TOKEN_PROGRAM_ID,
AuthorityType,
getMintLen,
createInitializeMintInstruction,
getAssociatedTokenAddressSync,
createAssociatedTokenAccountIdempotentInstruction,
createMintToInstruction,
createSetAuthorityInstruction,
getAccount,
} = require("@solana/spl-token");
const {
MintMaxVoteWeightSource,
@@ -32,17 +26,11 @@ const {
GoverningTokenConfigAccountArgs,
GoverningTokenType,
withCreateRealm,
withDepositGoverningTokens,
withCreateTokenOwnerRecord,
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");
const { clusterUrl, ensureDir, loadKeypair, parseEnvConfig, resolveConfigPath, nowStamp } = require("./js_common");
function lamportsToSol(lamports) {
return Number(lamports) / 1_000_000_000;
@@ -57,43 +45,6 @@ async function askYes() {
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}`);
@@ -104,26 +55,31 @@ async function main() {
"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));
"DAO_GOV_TOKEN_MINT",
].forEach((k) => {
if (!cfg[k]) throw new Error(`В конфиге отсутствует обязательный параметр: ${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);
const mint = new PublicKey(cfg.DAO_GOV_TOKEN_MINT);
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 mintInfoBefore = await connection.getParsedAccountInfo(mint, "confirmed");
if (!mintInfoBefore.value) throw new Error(`Governing mint не найден: ${mint.toBase58()}`);
const mintAccountBefore = await connection.getAccountInfo(mint, "confirmed");
if (!mintAccountBefore || !mintAccountBefore.owner.equals(TOKEN_PROGRAM_ID)) {
throw new Error(`Mint должен быть классическим SPL Token mint: ${mint.toBase58()}`);
}
const mintParsed = mintInfoBefore.value.data.parsed.info;
const mintAuthorityBefore = mintParsed.mintAuthority || null;
const freezeAuthorityBefore = mintParsed.freezeAuthority || null;
const issuerAta = getAssociatedTokenAddressSync(mint, issuer.publicKey, false, TOKEN_PROGRAM_ID);
const issuerAtaInfo = await getAccount(connection, issuerAta, "confirmed").catch(() => null);
const [realmPda] = PublicKey.findProgramAddressSync(
[Buffer.from("governance"), Buffer.from(cfg.DAO_REALM_NAME, "utf8")],
@@ -135,18 +91,19 @@ async function main() {
const startBalance = await connection.getBalance(issuer.publicKey, "confirmed");
console.log("============================================================");
console.log("CREATE SHiNE DAO WITH GOVERNANCE NFT");
console.log("CREATE SHiNE DAO FROM EXISTING GOVERNANCE MINT");
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("Governance mint: ", mint.toBase58());
console.log("Issuer ATA: ", issuerAta.toBase58());
console.log("Issuer ATA есть: ", issuerAtaInfo ? "yes" : "no");
console.log("Issuer ATA amount:", issuerAtaInfo ? issuerAtaInfo.amount.toString() : "0");
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("Mint authority: ", mintAuthorityBefore || "null");
console.log("Freeze authority:", freezeAuthorityBefore || "null");
console.log("============================================================");
if (!(await askYes())) {
@@ -154,40 +111,11 @@ async function main() {
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,
tokenType: GoverningTokenType.Liquid,
});
const realmPk = await withCreateRealm(
ixRealm,
@@ -195,7 +123,7 @@ async function main() {
PROGRAM_VERSION_V3,
cfg.DAO_REALM_NAME,
issuer.publicKey,
mintKeypair.publicKey,
mint,
issuer.publicKey,
undefined,
MintMaxVoteWeightSource.FULL_SUPPLY_FRACTION,
@@ -203,29 +131,32 @@ async function main() {
communityTokenConfig,
undefined
);
const sigRealm = await sendAndConfirmTransaction(connection, new Transaction().add(...ixRealm), [issuer], { commitment: "confirmed" });
const sigRealm = await sendAndConfirmTransaction(connection, new Transaction().add(...ixRealm), [issuer], {
commitment: "confirmed",
});
const ixDeposit = [];
const tokenOwnerRecordPk = await withDepositGoverningTokens(
ixDeposit,
const ixTokenOwnerRecord = [];
const tokenOwnerRecordPk = await withCreateTokenOwnerRecord(
ixTokenOwnerRecord,
governanceProgramId,
PROGRAM_VERSION_V3,
realmPk,
issuerAta,
mintKeypair.publicKey,
issuer.publicKey,
issuer.publicKey,
issuer.publicKey,
new BN(supply),
true
mint,
issuer.publicKey
);
const sigTokenOwnerRecord = await sendAndConfirmTransaction(
connection,
new Transaction().add(...ixTokenOwnerRecord),
[issuer],
{ commitment: "confirmed" }
);
const sigDeposit = await sendAndConfirmTransaction(connection, new Transaction().add(...ixDeposit), [issuer], { commitment: "confirmed" });
const governanceConfig = new GovernanceConfig({
communityVoteThreshold: new VoteThreshold({ type: VoteThresholdType.YesVotePercentage, value: thresholdPct }),
communityVoteThreshold: new VoteThreshold({ type: VoteThresholdType.YesVotePercentage, value: Number(cfg.DAO_APPROVAL_THRESHOLD_PERCENT || 51) }),
minCommunityTokensToCreateProposal: new BN(1),
minInstructionHoldUpTime: 0,
baseVotingTime: votingTimeSec,
baseVotingTime: Number(cfg.DAO_VOTING_TIME_SEC || 86400),
communityVoteTipping: VoteTipping.Early,
minCouncilTokensToCreateProposal: new BN(0),
councilVoteThreshold: new VoteThreshold({ type: VoteThresholdType.Disabled }),
@@ -249,82 +180,45 @@ async function main() {
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 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(),
governingMint: mint.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,
tokenOwnerRecord: tokenOwnerRecordPk.toBase58(),
mintAuthorityBefore,
freezeAuthorityBefore,
txRealm: sigRealm,
txDeposit: sigDeposit,
txTokenOwnerRecord: sigTokenOwnerRecord,
txGovernanceTreasury: sigGov,
txSetMintAuthorityToGovernance: sigSetMintAuthority,
txSetRealmAuthority: sigSetRealmAuthority,
tokenSupply: supply,
votingTimeSec,
thresholdPercent: thresholdPct,
startBalanceSol: lamportsToSol(startBalance),
endBalanceSol: lamportsToSol(endBalance),
spentSol: lamportsToSol(spentLamports),
communityTokenType: "Liquid",
};
const runsDir = path.resolve(__dirname, "..", "local", "runs");
ensureDir(runsDir);
const reportPath = path.join(runsDir, `${nowStamp()}_create_dao_from_existing_mint.json`);
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
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);
console.log("DAO created.");
console.log("communityMint:", mint.toBase58());
console.log("realm:", realmPk.toBase58());
console.log("governance:", governancePk.toBase58());
console.log("nativeTreasury:", treasuryPk.toBase58());
console.log("tokenOwnerRecord:", tokenOwnerRecordPk.toBase58());
console.log("spentSol:", "see report via tx fees + rent");
console.log("report:", reportPath);
}
main().catch((e) => {
console.error("Ошибка создания DAO:", e?.message || e);
console.error("CreateDAO error:", e?.message || e);
process.exit(1);
});
@@ -0,0 +1,268 @@
#!/usr/bin/env node
"use strict";
const fs = require("fs");
const path = require("path");
const readline = require("readline");
const {
Connection,
PublicKey,
Transaction,
sendAndConfirmTransaction,
} = require("@solana/web3.js");
const {
TOKEN_PROGRAM_ID,
AuthorityType,
createSetAuthorityInstruction,
} = require("@solana/spl-token");
const {
PROGRAM_VERSION_V3,
getRealm,
withSetRealmAuthority,
SetRealmAuthorityAction,
} = require("@solana/spl-governance");
const { createUmi } = require("@metaplex-foundation/umi-bundle-defaults");
const {
createSignerFromKeypair,
signerIdentity,
none,
some,
} = require("@metaplex-foundation/umi");
const { fromWeb3JsKeypair, fromWeb3JsPublicKey } = require("@metaplex-foundation/umi-web3js-adapters");
const {
mplTokenMetadata,
fetchMetadataFromSeeds,
updateV1,
} = require("@metaplex-foundation/mpl-token-metadata");
const {
clusterUrl,
ensureDir,
loadKeypair,
nowStamp,
parseEnvConfig,
resolveConfigPath,
} = 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 для передачи authority на DAO: ", resolve)
);
rl.close();
return answer.trim() === "YES";
}
async function fetchMetadataState(cluster, issuer, mint) {
const umi = createUmi(clusterUrl(cluster));
const issuerSigner = createSignerFromKeypair(umi, fromWeb3JsKeypair(issuer));
umi.use(signerIdentity(issuerSigner));
umi.use(mplTokenMetadata());
const metadata = await fetchMetadataFromSeeds(umi, { mint: fromWeb3JsPublicKey(mint) });
return {
umi,
issuerSigner,
metadata,
};
}
async function fetchMetadataUpdateAuthority(cluster, mint) {
const umi = createUmi(clusterUrl(cluster));
umi.use(mplTokenMetadata());
const metadata = await fetchMetadataFromSeeds(umi, { mint: fromWeb3JsPublicKey(mint) });
return metadata.updateAuthority.toString();
}
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_ISSUER_KEYPAIR",
"SPL_GOVERNANCE_PROGRAM_ID",
"DAO_GOV_TOKEN_MINT",
].forEach((k) => {
if (!cfg[k]) throw new Error(`В конфиге отсутствует обязательный параметр: ${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 mint = new PublicKey(cfg.DAO_GOV_TOKEN_MINT);
const [realmPk] = PublicKey.findProgramAddressSync(
[Buffer.from("governance"), Buffer.from(cfg.DAO_REALM_NAME, "utf8")],
governanceProgramId
);
const [governancePk] = PublicKey.findProgramAddressSync(
[Buffer.from("account-governance"), realmPk.toBuffer(), realmPk.toBuffer()],
governanceProgramId
);
const realmInfo = await connection.getAccountInfo(realmPk, "confirmed");
if (!realmInfo) throw new Error(`Realm не найден: ${realmPk.toBase58()}. Сначала выполните шаг 2.`);
const governanceInfo = await connection.getAccountInfo(governancePk, "confirmed");
if (!governanceInfo) {
throw new Error(`Governance PDA не найден: ${governancePk.toBase58()}. Сначала выполните шаг 2.`);
}
const realmBefore = await getRealm(connection, realmPk);
const realmAuthorityBefore = realmBefore.account.authority?.toBase58?.() || null;
if (realmAuthorityBefore !== issuer.publicKey.toBase58()) {
throw new Error(`Realm authority уже не у issuer: ${realmAuthorityBefore}`);
}
const mintInfoBefore = await connection.getParsedAccountInfo(mint, "confirmed");
if (!mintInfoBefore.value) throw new Error(`Mint не найден: ${mint.toBase58()}`);
const mintParsedBefore = mintInfoBefore.value.data.parsed.info;
const mintAuthorityBefore = mintParsedBefore.mintAuthority || null;
const freezeAuthorityBefore = mintParsedBefore.freezeAuthority || null;
if (mintAuthorityBefore && mintAuthorityBefore !== issuer.publicKey.toBase58()) {
throw new Error(`Mint authority уже не у issuer: ${mintAuthorityBefore}`);
}
if (freezeAuthorityBefore && freezeAuthorityBefore !== issuer.publicKey.toBase58()) {
throw new Error(`Freeze authority уже не у issuer: ${freezeAuthorityBefore}`);
}
const { umi, metadata, issuerSigner } = await fetchMetadataState(cluster, issuer, mint);
const metadataUpdateAuthorityBefore = metadata.updateAuthority.toString();
if (metadataUpdateAuthorityBefore !== issuer.publicKey.toBase58()) {
throw new Error(`Metadata update authority уже не у issuer: ${metadataUpdateAuthorityBefore}`);
}
const startBalance = await connection.getBalance(issuer.publicKey, "confirmed");
console.log("============================================================");
console.log("TRANSFER DAO AUTHORITIES");
console.log("------------------------------------------------------------");
console.log("Сеть: ", cluster);
console.log("Realm: ", realmPk.toBase58());
console.log("Governance PDA: ", governancePk.toBase58());
console.log("Issuer: ", issuer.publicKey.toBase58());
console.log("Governance mint: ", mint.toBase58());
console.log("Баланс до: ", `${lamportsToSol(startBalance)} SOL`);
console.log("Mint authority до: ", mintAuthorityBefore || "null");
console.log("Freeze authority до: ", freezeAuthorityBefore || "null");
console.log("Realm authority до: ", realmAuthorityBefore || "null");
console.log("Metadata authority до: ", metadataUpdateAuthorityBefore);
console.log("============================================================");
if (!(await askYes())) {
console.log("Отменено.");
return;
}
const ixTransferAuthorities = [
createSetAuthorityInstruction(
mint,
issuer.publicKey,
AuthorityType.MintTokens,
governancePk,
[],
TOKEN_PROGRAM_ID
),
createSetAuthorityInstruction(
mint,
issuer.publicKey,
AuthorityType.FreezeAccount,
governancePk,
[],
TOKEN_PROGRAM_ID
),
];
const sigTransferAuthorities = await sendAndConfirmTransaction(
connection,
new Transaction().add(...ixTransferAuthorities),
[issuer],
{ commitment: "confirmed" }
);
const ixSetRealmAuthority = [];
withSetRealmAuthority(
ixSetRealmAuthority,
governanceProgramId,
PROGRAM_VERSION_V3,
realmPk,
issuer.publicKey,
governancePk,
SetRealmAuthorityAction.SetChecked
);
const sigSetRealmAuthority = await sendAndConfirmTransaction(
connection,
new Transaction().add(...ixSetRealmAuthority),
[issuer],
{ commitment: "confirmed" }
);
const metadataTx = await updateV1(umi, {
authority: issuerSigner,
mint: fromWeb3JsPublicKey(mint),
newUpdateAuthority: some(fromWeb3JsPublicKey(governancePk)),
data: none(),
primarySaleHappened: none(),
isMutable: none(),
collection: { __kind: "None" },
collectionDetails: { __kind: "None" },
uses: { __kind: "None" },
ruleSet: { __kind: "None" },
authorizationData: none(),
}).sendAndConfirm(umi);
const sigMetadataAuthority = Buffer.from(metadataTx.signature).toString("base64");
const mintInfoAfter = await connection.getParsedAccountInfo(mint, "confirmed");
const mintParsedAfter = mintInfoAfter.value.data.parsed.info;
const mintAuthorityAfter = mintParsedAfter.mintAuthority || null;
const freezeAuthorityAfter = mintParsedAfter.freezeAuthority || null;
const realmAfter = await getRealm(connection, realmPk);
const realmAuthorityAfter = realmAfter.account.authority?.toBase58?.() || null;
const metadataUpdateAuthorityAfter = await fetchMetadataUpdateAuthority(cluster, mint);
const report = {
createdAt: new Date().toISOString(),
cluster,
realmName: cfg.DAO_REALM_NAME,
governanceProgramId: governanceProgramId.toBase58(),
issuer: issuer.publicKey.toBase58(),
governingMint: mint.toBase58(),
realm: realmPk.toBase58(),
governance: governancePk.toBase58(),
mintAuthorityBefore,
freezeAuthorityBefore,
metadataUpdateAuthorityBefore,
realmAuthorityBefore,
mintAuthorityAfter,
freezeAuthorityAfter,
realmAuthorityAfter,
metadataUpdateAuthorityAfter,
txTransferAuthorities: sigTransferAuthorities,
txSetRealmAuthority: sigSetRealmAuthority,
txSetMetadataUpdateAuthority: sigMetadataAuthority,
};
const runsDir = path.resolve(__dirname, "..", "local", "runs");
ensureDir(runsDir);
const reportPath = path.join(runsDir, `${nowStamp()}_transfer_dao_authorities.json`);
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log("Все authority переданы на DAO.");
console.log("realm:", realmPk.toBase58());
console.log("governance:", governancePk.toBase58());
console.log("mintAuthority после:", mintAuthorityAfter || "null");
console.log("freezeAuthority после:", freezeAuthorityAfter || "null");
console.log("realmAuthority после:", realmAuthorityAfter || "null");
console.log("metadataAuthority после:", metadataUpdateAuthorityAfter);
console.log("report:", reportPath);
}
main().catch((e) => {
console.error("TransferAuthorities error:", e?.message || e);
process.exit(1);
});
@@ -22,7 +22,7 @@ const { clusterUrl, ensureDir, loadKeypair, nowStamp, parseEnvConfig, resolveCon
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>");
if (!targetWallet || !nftMintStr) throw new Error("Usage: node 06_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");
@@ -13,7 +13,7 @@ async function main() {
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>");
if (!process.argv[6]) throw new Error("Usage: node 07_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));
@@ -21,7 +21,7 @@ const { clusterUrl, ensureDir, loadKeypair, nowStamp, parseEnvConfig, resolveCon
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>");
if (!currentOwnerWallet || !nftMintStr) throw new Error("Usage: node 08_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");
@@ -13,7 +13,7 @@ async function main() {
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>");
if (!process.argv[6]) throw new Error("Usage: node 09_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));