Add governance token scripts with shell wrappers and vanity grind

This commit is contained in:
AidarKC 2026-05-15 15:15:40 +03:00
parent c680b16e58
commit 34c1142173
15 changed files with 3745 additions and 6 deletions

3021
shine/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,16 +5,23 @@
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
},
"dependencies": {
"@coral-xyz/anchor": "^0.31.1"
"@coral-xyz/anchor": "^0.31.1",
"@metaplex-foundation/mpl-token-metadata": "^3.4.0",
"@metaplex-foundation/mpl-toolbox": "^0.10.0",
"@metaplex-foundation/umi": "^1.5.1",
"@metaplex-foundation/umi-bundle-defaults": "^1.5.1",
"@metaplex-foundation/umi-web3js-adapters": "^1.5.1",
"@solana/spl-token": "^0.4.14",
"@solana/spl-governance": "^0.3.28"
},
"devDependencies": {
"chai": "^4.3.4",
"mocha": "^9.0.3",
"ts-mocha": "^10.0.0",
"@types/bn.js": "^5.1.0",
"@types/chai": "^4.3.0",
"@types/mocha": "^9.0.0",
"typescript": "^5.7.3",
"prettier": "^2.6.2"
"chai": "^4.3.4",
"mocha": "^9.0.3",
"prettier": "^2.6.2",
"ts-mocha": "^10.0.0",
"typescript": "^5.7.3"
}
}

View File

@ -0,0 +1,114 @@
#!/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,
createInitializeNonTransferableMintInstruction,
createInitializePermanentDelegateInstruction,
} = require("@solana/spl-token");
const {
parseEnvConfig,
assertRequired,
resolveConfigPath,
loadKeypair,
saveKeypair,
parseCluster,
nowStamp,
} = require("./_common");
async function main() {
const configPath = resolveConfigPath(process.argv[2]);
const cfg = parseEnvConfig(configPath);
assertRequired(cfg, "GT_CLUSTER");
assertRequired(cfg, "GT_OPERATOR_KEYPAIR_PATH");
assertRequired(cfg, "GT_RUNS_DIR");
const operator = loadKeypair(path.resolve(cfg.GT_OPERATOR_KEYPAIR_PATH));
const connection = new Connection(parseCluster(cfg.GT_CLUSTER), "confirmed");
let mint;
if (cfg.GT_MINT_KEYPAIR_PATH) {
mint = loadKeypair(path.resolve(cfg.GT_MINT_KEYPAIR_PATH));
} else {
mint = Keypair.generate();
}
const extensions = [ExtensionType.NonTransferable, ExtensionType.PermanentDelegate];
const mintLen = getMintLen(extensions);
const rent = await connection.getMinimumBalanceForRentExemption(mintLen, "confirmed");
const tx = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: operator.publicKey,
newAccountPubkey: mint.publicKey,
space: mintLen,
lamports: rent,
programId: TOKEN_2022_PROGRAM_ID,
}),
createInitializeNonTransferableMintInstruction(mint.publicKey, TOKEN_2022_PROGRAM_ID),
createInitializePermanentDelegateInstruction(
mint.publicKey,
operator.publicKey,
TOKEN_2022_PROGRAM_ID
),
createInitializeMintInstruction(
mint.publicKey,
0,
operator.publicKey,
operator.publicKey,
TOKEN_2022_PROGRAM_ID
)
);
const sig = await sendAndConfirmTransaction(connection, tx, [operator, mint], {
commitment: "confirmed",
});
const runsDir = path.resolve(cfg.GT_RUNS_DIR);
fs.mkdirSync(runsDir, { recursive: true });
const outMintPath =
cfg.GT_MINT_KEYPAIR_PATH && cfg.GT_MINT_KEYPAIR_PATH.trim()
? path.resolve(cfg.GT_MINT_KEYPAIR_PATH)
: path.join(runsDir, `${nowStamp()}_mint-keypair.json`);
saveKeypair(outMintPath, mint);
const report = {
createdAt: new Date().toISOString(),
cluster: cfg.GT_CLUSTER,
operator: operator.publicKey.toBase58(),
mint: mint.publicKey.toBase58(),
tokenProgram: TOKEN_2022_PROGRAM_ID.toBase58(),
nonTransferable: true,
permanentDelegate: operator.publicKey.toBase58(),
mintAuthority: operator.publicKey.toBase58(),
freezeAuthority: operator.publicKey.toBase58(),
mintKeypairPath: outMintPath,
txCreateMint: sig,
};
const reportPath = path.join(runsDir, `${nowStamp()}_create_token.json`);
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log("Governance token создан.");
console.log("Mint:", mint.publicKey.toBase58());
console.log("Mint keypair:", outMintPath);
console.log("Tx:", sig);
console.log("Report:", reportPath);
}
main().catch((e) => {
console.error("Ошибка создания governance token:", e?.message || e);
process.exit(1);
});

View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_PATH="${1:-$SCRIPT_DIR/governance_token.config.env}"
node "$SCRIPT_DIR/01_create_governance_token_exec.js" "$CONFIG_PATH"

View File

@ -0,0 +1,82 @@
#!/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 {
parseEnvConfig,
assertRequired,
resolveConfigPath,
loadKeypair,
parseCluster,
} = require("./_common");
async function main() {
const configPath = resolveConfigPath(process.argv[2]);
const mint = new PublicKey(process.argv[3]);
const receiver = new PublicKey(process.argv[4]);
const amount = BigInt(process.argv[5] || "1");
if (!process.argv[4]) {
throw new Error(
"Использование: node 02_mint_membership_to_wallet_exec.js <config.env> <mint> <receiver_wallet> [amount]"
);
}
if (amount <= 0n) throw new Error("amount должен быть > 0");
const cfg = parseEnvConfig(configPath);
assertRequired(cfg, "GT_CLUSTER");
assertRequired(cfg, "GT_OPERATOR_KEYPAIR_PATH");
const operator = loadKeypair(path.resolve(cfg.GT_OPERATOR_KEYPAIR_PATH));
const connection = new Connection(parseCluster(cfg.GT_CLUSTER), "confirmed");
const ata = getAssociatedTokenAddressSync(
mint,
receiver,
false,
TOKEN_2022_PROGRAM_ID
);
const ix = [
createAssociatedTokenAccountIdempotentInstruction(
operator.publicKey,
ata,
receiver,
mint,
TOKEN_2022_PROGRAM_ID
),
createMintToInstruction(
mint,
ata,
operator.publicKey,
amount,
[],
TOKEN_2022_PROGRAM_ID
),
];
const sig = await sendAndConfirmTransaction(connection, new Transaction().add(...ix), [operator], {
commitment: "confirmed",
});
console.log("Mint выполнен.");
console.log("Receiver:", receiver.toBase58());
console.log("ATA:", ata.toBase58());
console.log("Amount:", amount.toString());
console.log("Tx:", sig);
}
main().catch((e) => {
console.error("Ошибка mint membership:", e?.message || e);
process.exit(1);
});

View File

@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_PATH="${1:-$SCRIPT_DIR/governance_token.config.env}"
MINT="${2:-}"
WALLET="${3:-}"
AMOUNT="${4:-1}"
if [[ -z "$MINT" || -z "$WALLET" ]]; then
echo "Использование:"
echo " $0 [config.env] <mint> <wallet> [amount]"
exit 1
fi
node "$SCRIPT_DIR/02_mint_membership_to_wallet_exec.js" "$CONFIG_PATH" "$MINT" "$WALLET" "$AMOUNT"

View File

@ -0,0 +1,73 @@
#!/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 {
parseEnvConfig,
assertRequired,
resolveConfigPath,
loadKeypair,
parseCluster,
} = require("./_common");
async function main() {
const configPath = resolveConfigPath(process.argv[2]);
const mint = new PublicKey(process.argv[3]);
const targetOwner = new PublicKey(process.argv[4]);
const amount = BigInt(process.argv[5] || "1");
if (!process.argv[4]) {
throw new Error(
"Использование: node 03_force_burn_from_wallet_exec.js <config.env> <mint> <target_owner_wallet> [amount]"
);
}
if (amount <= 0n) throw new Error("amount должен быть > 0");
const cfg = parseEnvConfig(configPath);
assertRequired(cfg, "GT_CLUSTER");
assertRequired(cfg, "GT_OPERATOR_KEYPAIR_PATH");
const operator = loadKeypair(path.resolve(cfg.GT_OPERATOR_KEYPAIR_PATH));
const connection = new Connection(parseCluster(cfg.GT_CLUSTER), "confirmed");
const targetAta = getAssociatedTokenAddressSync(
mint,
targetOwner,
false,
TOKEN_2022_PROGRAM_ID
);
const ix = createBurnCheckedInstruction(
targetAta,
mint,
operator.publicKey,
amount,
0,
[],
TOKEN_2022_PROGRAM_ID
);
const sig = await sendAndConfirmTransaction(connection, new Transaction().add(ix), [operator], {
commitment: "confirmed",
});
console.log("Force burn выполнен.");
console.log("Target owner:", targetOwner.toBase58());
console.log("Target ATA:", targetAta.toBase58());
console.log("Amount:", amount.toString());
console.log("Tx:", sig);
}
main().catch((e) => {
console.error("Ошибка force burn:", e?.message || e);
process.exit(1);
});

View File

@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_PATH="${1:-$SCRIPT_DIR/governance_token.config.env}"
MINT="${2:-}"
WALLET="${3:-}"
AMOUNT="${4:-1}"
if [[ -z "$MINT" || -z "$WALLET" ]]; then
echo "Использование:"
echo " $0 [config.env] <mint> <wallet> [amount]"
exit 1
fi
node "$SCRIPT_DIR/03_force_burn_from_wallet_exec.js" "$CONFIG_PATH" "$MINT" "$WALLET" "$AMOUNT"

View File

@ -0,0 +1,99 @@
#!/usr/bin/env node
"use strict";
const path = require("path");
const {
Connection,
PublicKey,
Transaction,
sendAndConfirmTransaction,
} = require("@solana/web3.js");
const {
TOKEN_2022_PROGRAM_ID,
AuthorityType,
createSetAuthorityInstruction,
} = require("@solana/spl-token");
const {
parseEnvConfig,
assertRequired,
resolveConfigPath,
loadKeypair,
parseCluster,
askYes,
} = require("./_common");
async function main() {
const configPath = resolveConfigPath(process.argv[2]);
const mint = new PublicKey(process.argv[3]);
if (!process.argv[3]) {
throw new Error(
"Использование: node 04_transfer_rights_to_governance_pda_exec.js <config.env> <mint>"
);
}
const cfg = parseEnvConfig(configPath);
assertRequired(cfg, "GT_CLUSTER");
assertRequired(cfg, "GT_OPERATOR_KEYPAIR_PATH");
assertRequired(cfg, "GT_GOVERNANCE_PDA");
const operator = loadKeypair(path.resolve(cfg.GT_OPERATOR_KEYPAIR_PATH));
const governancePda = new PublicKey(cfg.GT_GOVERNANCE_PDA);
const connection = new Connection(parseCluster(cfg.GT_CLUSTER), "confirmed");
console.log("============================================================");
console.log("ПЕРЕДАЧА ПРАВ УПРАВЛЕНИЯ ТОКЕНОМ НА GOVERNANCE PDA");
console.log("------------------------------------------------------------");
console.log("Сеть: ", cfg.GT_CLUSTER);
console.log("Mint: ", mint.toBase58());
console.log("Текущий оператор: ", operator.publicKey.toBase58());
console.log("Новый authority (DAO PDA): ", governancePda.toBase58());
console.log("Будет передано:");
console.log(" 1) MintTokens authority");
console.log(" 2) FreezeAccount authority");
console.log(" 3) PermanentDelegate authority");
console.log("После этого текущий оператор утратит эти права.");
console.log("============================================================");
const ok = await askYes("Введите yes для подтверждения: ");
if (!ok) {
console.log("Отменено пользователем.");
return;
}
const ixs = [
createSetAuthorityInstruction(
mint,
operator.publicKey,
AuthorityType.MintTokens,
governancePda,
[],
TOKEN_2022_PROGRAM_ID
),
createSetAuthorityInstruction(
mint,
operator.publicKey,
AuthorityType.FreezeAccount,
governancePda,
[],
TOKEN_2022_PROGRAM_ID
),
createSetAuthorityInstruction(
mint,
operator.publicKey,
AuthorityType.PermanentDelegate,
governancePda,
[],
TOKEN_2022_PROGRAM_ID
),
];
const sig = await sendAndConfirmTransaction(connection, new Transaction().add(...ixs), [operator], {
commitment: "confirmed",
});
console.log("Готово. Tx:", sig);
}
main().catch((e) => {
console.error("Ошибка передачи прав:", e?.message || e);
process.exit(1);
});

View File

@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_PATH="${1:-$SCRIPT_DIR/governance_token.config.env}"
MINT="${2:-}"
if [[ -z "$MINT" ]]; then
echo "Использование:"
echo " $0 [config.env] <mint>"
exit 1
fi
node "$SCRIPT_DIR/04_transfer_rights_to_governance_pda_exec.js" "$CONFIG_PATH" "$MINT"

View File

@ -0,0 +1,118 @@
#!/usr/bin/env node
"use strict";
const fs = require("fs");
const path = require("path");
const { spawn } = require("child_process");
const {
parseEnvConfig,
resolveConfigPath,
nowStamp,
} = require("./_common");
// Можно править прямо в начале файла
const DEFAULT_PREFIX = "SH";
const DEFAULT_MATCH_COUNT = 1;
const DEFAULT_IGNORE_CASE = false;
function expectedTries(prefixLen) {
return Math.pow(58, prefixLen);
}
function ensureSolanaKeygen() {
return new Promise((resolve, reject) => {
const p = spawn("solana-keygen", ["--version"], { stdio: ["ignore", "pipe", "pipe"] });
p.on("error", reject);
p.on("close", (code) => {
if (code === 0) resolve();
else reject(new Error("solana-keygen не найден или недоступен"));
});
});
}
async function main() {
const configPath = resolveConfigPath(process.argv[2]);
const cfg = parseEnvConfig(configPath);
const runsDir = path.resolve(cfg.GT_RUNS_DIR || path.join(__dirname, "runs"));
fs.mkdirSync(runsDir, { recursive: true });
const prefix = process.argv[3] || cfg.GT_VANITY_PREFIX || DEFAULT_PREFIX;
const count = Number(process.argv[4] || DEFAULT_MATCH_COUNT);
const ignoreCase = (process.argv[5] || "").toLowerCase() === "ignore-case" || DEFAULT_IGNORE_CASE;
if (!/^[1-9A-HJ-NP-Za-km-z]+$/.test(prefix)) {
throw new Error("Префикс должен быть в Base58 (без 0 O I l)");
}
if (!Number.isInteger(count) || count <= 0) throw new Error("count должен быть целым > 0");
await ensureSolanaKeygen();
const expected = expectedTries(prefix.length);
console.log("============================================================");
console.log("VANITY MINT KEYPAIR GRIND");
console.log("------------------------------------------------------------");
console.log("Prefix:", prefix);
console.log("Count:", count);
console.log("Runs dir:", runsDir);
console.log("Среднее ожидание на 1 совпадение:", expected.toLocaleString("en-US"), "попыток");
console.log("============================================================");
const args = ["grind", "--starts-with", `${prefix}:${count}`];
if (ignoreCase) args.push("--ignore-case");
const child = spawn("solana-keygen", args, {
cwd: runsDir,
stdio: ["ignore", "pipe", "pipe"],
});
const lines = [];
let attempts = 0;
let found = 0;
const onLine = (raw) => {
const line = String(raw).trim();
if (!line) return;
lines.push(line);
console.log(line);
const mAttempts = line.match(/Searched\s+([0-9,]+)\s+keypairs/i);
if (mAttempts) {
attempts = Number(mAttempts[1].replace(/,/g, "")) || attempts;
const pct = Math.min(100, (attempts / expected) * 100);
console.log(
`[progress] attempts=${attempts.toLocaleString("en-US")} ~${pct.toFixed(2)}% от среднего ожидания`
);
}
if (/Wrote keypair to/i.test(line) || /Found matching key/i.test(line)) found += 1;
};
child.stdout.on("data", (d) => String(d).split("\n").forEach(onLine));
child.stderr.on("data", (d) => String(d).split("\n").forEach(onLine));
const code = await new Promise((resolve) => child.on("close", resolve));
if (code !== 0) {
throw new Error(`solana-keygen grind завершился с кодом ${code}`);
}
const report = {
createdAt: new Date().toISOString(),
prefix,
count,
ignoreCase,
runsDir,
avgExpectedTriesPerMatch: expected,
attemptsObserved: attempts,
foundHintsInOutput: found,
command: `solana-keygen ${args.join(" ")}`,
outputLog: lines,
};
const reportPath = path.join(runsDir, `${nowStamp()}_vanity_grind_report.json`);
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log("Report:", reportPath);
console.log("Готово. Keypair-файлы сохранены в:", runsDir);
}
main().catch((e) => {
console.error("Ошибка vanity grind:", e?.message || e);
process.exit(1);
});

View File

@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_PATH="${1:-$SCRIPT_DIR/governance_token.config.env}"
PREFIX="${2:-}"
COUNT="${3:-1}"
IGNORE_CASE="${4:-}"
if [[ -n "$PREFIX" ]]; then
node "$SCRIPT_DIR/05_vanity_mint_keypair_grind_exec.js" "$CONFIG_PATH" "$PREFIX" "$COUNT" "$IGNORE_CASE"
else
node "$SCRIPT_DIR/05_vanity_mint_keypair_grind_exec.js" "$CONFIG_PATH"
fi

View File

@ -0,0 +1,51 @@
# Governance Token Scripts
Скрипты для управления governance token на Token-2022:
- `NonTransferable`
- `PermanentDelegate`
## Конфиг
Файл: `scripts/governance_token/governance_token.config.env`
Ключи:
- `GT_CLUSTER` (`devnet` / `mainnet-beta`)
- `GT_OPERATOR_KEYPAIR_PATH`
- `GT_GOVERNANCE_PDA`
- `GT_MINT_KEYPAIR_PATH` (опционально)
- `GT_RUNS_DIR`
- `GT_VANITY_PREFIX`
## Скрипты
1. Создать новый governance token:
```bash
node scripts/governance_token/01_create_governance_token_exec.js scripts/governance_token/governance_token.config.env
```
2. Выпустить токен участнику:
```bash
node scripts/governance_token/02_mint_membership_to_wallet_exec.js scripts/governance_token/governance_token.config.env <MINT> <WALLET> [AMOUNT]
```
3. Принудительно сжечь токен у участника:
```bash
node scripts/governance_token/03_force_burn_from_wallet_exec.js scripts/governance_token/governance_token.config.env <MINT> <WALLET> [AMOUNT]
```
4. Передать права на Governance PDA (с подтверждением `yes`):
```bash
node scripts/governance_token/04_transfer_rights_to_governance_pda_exec.js scripts/governance_token/governance_token.config.env <MINT>
```
5. Vanity-подбор mint keypair через `solana-keygen grind`:
```bash
node scripts/governance_token/05_vanity_mint_keypair_grind_exec.js scripts/governance_token/governance_token.config.env [PREFIX] [COUNT] [ignore-case]
```
Результаты сохраняются в `GT_RUNS_DIR`.

View File

@ -0,0 +1,88 @@
#!/usr/bin/env node
"use strict";
const fs = require("fs");
const path = require("path");
const readline = require("readline");
const { Keypair, PublicKey, clusterApiUrl } = require("@solana/web3.js");
function parseEnvConfig(configPath) {
const raw = fs.readFileSync(configPath, "utf8");
const out = {};
for (const line of raw.split("\n")) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eq = trimmed.indexOf("=");
if (eq === -1) continue;
const key = trimmed.slice(0, eq).trim();
let val = trimmed.slice(eq + 1).trim();
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
val = val.slice(1, -1);
}
val = val.replace(/\$HOME/g, process.env.HOME || "");
out[key] = val;
}
return out;
}
function assertRequired(cfg, key) {
if (!cfg[key]) throw new Error(`В конфиге отсутствует обязательный параметр: ${key}`);
}
function resolveConfigPath(argvPath) {
return argvPath
? path.resolve(argvPath)
: path.resolve(__dirname, "governance_token.config.env");
}
function loadKeypair(filePath) {
const arr = JSON.parse(fs.readFileSync(filePath, "utf8"));
return Keypair.fromSecretKey(Uint8Array.from(arr));
}
function saveKeypair(filePath, keypair) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(Array.from(keypair.secretKey)));
}
function parseCluster(cluster) {
if (cluster === "devnet" || cluster === "mainnet-beta" || cluster === "testnet") {
return clusterApiUrl(cluster);
}
return cluster;
}
function nowStamp() {
const d = new Date();
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}_${p(d.getHours())}-${p(
d.getMinutes()
)}-${p(d.getSeconds())}`;
}
async function askYes(prompt) {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise((resolve) => rl.question(prompt, resolve));
rl.close();
return answer.trim() === "yes";
}
function toPublicKey(v, fieldName) {
try {
return new PublicKey(v);
} catch (_) {
throw new Error(`Некорректный pubkey в ${fieldName}: ${v}`);
}
}
module.exports = {
parseEnvConfig,
assertRequired,
resolveConfigPath,
loadKeypair,
saveKeypair,
parseCluster,
nowStamp,
askYes,
toPublicKey,
};

View File

@ -0,0 +1,19 @@
# Конфиг governance token (Token-2022, non-transferable)
# devnet | mainnet-beta
GT_CLUSTER="devnet"
# Путь к keypair кошелька-оператора (issuer/admin)
GT_OPERATOR_KEYPAIR_PATH="$HOME/.config/solana/phantomWallet.json"
# Governance PDA (сюда передаем управляющие права после создания DAO)
GT_GOVERNANCE_PDA="REPLACE_WITH_GOVERNANCE_PDA"
# Если нужен vanity mint, этот файл можно заполнить автоматически 05-скриптом
GT_MINT_KEYPAIR_PATH=""
# Папка для результатов/логов
GT_RUNS_DIR="./scripts/governance_token/runs"
# Дефолт для vanity-подбора (05)
GT_VANITY_PREFIX="DAO"