Обновить документацию и словари
@@ -0,0 +1 @@
|
|||||||
|
Данная документация местами устарела и не соответствует реальному коду
|
||||||
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 28 KiB |
@@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.289
|
client.version=1.2.290
|
||||||
server.version=1.2.269
|
server.version=1.2.270
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
# create-SHiNE-DAO
|
||||||
|
|
||||||
|
Чистый актуальный комплект для создания SHiNE DAO с governance NFT и для работы с NFT, которые:
|
||||||
|
|
||||||
|
1. можно выпускать через голосование DAO;
|
||||||
|
2. можно сжигать через голосование DAO;
|
||||||
|
3. в будущем можно будет сжигать через отдельную быструю программу-исполнитель только для одного конкретного `mint`.
|
||||||
|
|
||||||
|
Этот набор сделан как production-style папка:
|
||||||
|
|
||||||
|
- без старых `runs/`;
|
||||||
|
- без старых `keypairs/`;
|
||||||
|
- без legacy-скриптов;
|
||||||
|
- без смешивания нескольких исторических подходов.
|
||||||
|
|
||||||
|
## Что внутри
|
||||||
|
|
||||||
|
- `dao.config.env.example` - шаблон конфига для создания самого 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.
|
||||||
|
- `docs/FUTURE_FAST_BURN_EXECUTOR.md` - описание будущей программы для быстрого burn одного конкретного `mint`.
|
||||||
|
|
||||||
|
## Подтвержденная база, на которой собран комплект
|
||||||
|
|
||||||
|
Комплект собран из тех сценариев, которые уже были подтверждены на devnet:
|
||||||
|
|
||||||
|
- DAO с `Realm / Governance / Native Treasury`;
|
||||||
|
- mint NFT через DAO;
|
||||||
|
- burn NFT через DAO.
|
||||||
|
|
||||||
|
## Важное ограничение текущей схемы burn через DAO
|
||||||
|
|
||||||
|
Текущий стандартный burn через proposal работает так:
|
||||||
|
|
||||||
|
1. в proposal вшивается конкретная burn-инструкция;
|
||||||
|
2. в ней зашит конкретный token account, из которого надо сжигать NFT;
|
||||||
|
3. если владелец успеет переложить NFT в другой token account до `execute`, этот `execute` может не пройти.
|
||||||
|
|
||||||
|
То есть:
|
||||||
|
|
||||||
|
- burn определяется по `mint`;
|
||||||
|
- но исполняется из конкретного текущего token account;
|
||||||
|
- если токен двигают между proposal и execute, нужна более быстрая схема.
|
||||||
|
|
||||||
|
Именно для этого отдельно описан future-подход с одноразовым fast-burn executor.
|
||||||
|
|
||||||
|
## Что нужно заранее
|
||||||
|
|
||||||
|
В корне Solana-модуля:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd shine-solana/shine
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Нужны зависимости:
|
||||||
|
|
||||||
|
- `@solana/web3.js`
|
||||||
|
- `@solana/spl-token`
|
||||||
|
- `@solana/spl-governance`
|
||||||
|
- `@metaplex-foundation/mpl-token-metadata`
|
||||||
|
- `@metaplex-foundation/umi`
|
||||||
|
|
||||||
|
Также должны быть установлены CLI:
|
||||||
|
|
||||||
|
- `solana`
|
||||||
|
- `solana-keygen`
|
||||||
|
- `node`
|
||||||
|
|
||||||
|
## Быстрый сценарий: создать DAO и запустить NFT-flow
|
||||||
|
|
||||||
|
### 1. Создать конфиг DAO
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp create-SHiNE-DAO/dao.config.env.example create-SHiNE-DAO/dao.config.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Заполнить:
|
||||||
|
|
||||||
|
- имя DAO;
|
||||||
|
- путь к keypair создателя;
|
||||||
|
- имя и символ governance NFT;
|
||||||
|
- `DAO_GOV_NFT_SUPPLY` (обычно `10`);
|
||||||
|
- `DAO_GOV_TOKEN_METADATA_URI` и `DAO_GOV_TOKEN_IMAGE_URL`.
|
||||||
|
|
||||||
|
### 2. Проверить конфиг DAO
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./create-SHiNE-DAO/scripts/01_check_dao_config.sh ./create-SHiNE-DAO/dao.config.env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Создать DAO
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node create-SHiNE-DAO/scripts/02_create_dao_with_governance_nfts.js ./create-SHiNE-DAO/dao.config.env
|
||||||
|
```
|
||||||
|
|
||||||
|
После этого из отчета взять:
|
||||||
|
|
||||||
|
- `realm`
|
||||||
|
- `governance`
|
||||||
|
- `communityMint`
|
||||||
|
|
||||||
|
### 4. Создать конфиг NFT-flow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp create-SHiNE-DAO/nft-flow.config.env.example create-SHiNE-DAO/nft-flow.config.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Подставить:
|
||||||
|
|
||||||
|
- `REALM`
|
||||||
|
- `GOVERNANCE`
|
||||||
|
- `GOVERNING_MINT`
|
||||||
|
- `MAIN_KEYPAIR`
|
||||||
|
- `VOTER2_KEYPAIR`
|
||||||
|
- metadata URI и картинку для целевого NFT.
|
||||||
|
|
||||||
|
### 5. Подготовить второго голосующего
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node create-SHiNE-DAO/scripts/03_prepare_second_voter.js ./create-SHiNE-DAO/nft-flow.config.env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Создать NFT mint-шаблон с metadata
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node create-SHiNE-DAO/scripts/04_create_empty_nft_template_with_metadata.js ./create-SHiNE-DAO/nft-flow.config.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт вернет `mint` будущего NFT.
|
||||||
|
|
||||||
|
### 7. Поднять proposal на mint NFT
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node create-SHiNE-DAO/scripts/05_propose_vote_mint_nft.js \
|
||||||
|
./create-SHiNE-DAO/nft-flow.config.env \
|
||||||
|
<TARGET_WALLET> \
|
||||||
|
<NFT_MINT>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Выполнить mint proposal
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node create-SHiNE-DAO/scripts/06_execute_mint_nft.js \
|
||||||
|
./create-SHiNE-DAO/nft-flow.config.env \
|
||||||
|
<PROPOSAL> \
|
||||||
|
<PROPOSAL_TX> \
|
||||||
|
<TARGET_WALLET> \
|
||||||
|
<NFT_MINT>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Поднять proposal на burn NFT
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node create-SHiNE-DAO/scripts/07_propose_vote_burn_nft.js \
|
||||||
|
./create-SHiNE-DAO/nft-flow.config.env \
|
||||||
|
<CURRENT_OWNER_WALLET> \
|
||||||
|
<NFT_MINT>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10. Выполнить burn proposal
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node create-SHiNE-DAO/scripts/08_execute_burn_nft.js \
|
||||||
|
./create-SHiNE-DAO/nft-flow.config.env \
|
||||||
|
<PROPOSAL> \
|
||||||
|
<PROPOSAL_TX> \
|
||||||
|
<CURRENT_OWNER_WALLET> \
|
||||||
|
<NFT_MINT>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Что важно про картинку NFT
|
||||||
|
|
||||||
|
У конкретного NFT картинка идет не отдельным on-chain полем, а через metadata JSON по `uri`.
|
||||||
|
|
||||||
|
То есть нужно:
|
||||||
|
|
||||||
|
1. загрузить JSON metadata на Arweave/IPFS/HTTPS;
|
||||||
|
2. в JSON указать `image`;
|
||||||
|
3. в конфиге передать `NFT_METADATA_URI`;
|
||||||
|
4. `NFT_IMAGE_URL` хранить для явной проверки и документации.
|
||||||
|
|
||||||
|
## Что важно про стартовые governance NFT
|
||||||
|
|
||||||
|
На старте governance NFT mint'ятся создателю DAO, а затем депонируются в Realm.
|
||||||
|
|
||||||
|
Практически это означает:
|
||||||
|
|
||||||
|
- первоначальный voting power принадлежит создателю DAO;
|
||||||
|
- затем governance NFT можно дополнительно распределять между другими участниками.
|
||||||
|
|
||||||
|
## Future
|
||||||
|
|
||||||
|
Если нужен сценарий "хитрый владелец постоянно перекладывает NFT", смотреть:
|
||||||
|
|
||||||
|
- `docs/FUTURE_FAST_BURN_EXECUTOR.md`
|
||||||
|
|
||||||
|
Там описана целевая отдельная программа, которой DAO может выдать одноразовое право сжечь только один конкретный `mint`.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# create-SHiNE-DAO / конфиг создания DAO
|
||||||
|
|
||||||
|
# devnet | mainnet-beta | custom rpc url
|
||||||
|
DAO_CLUSTER="devnet"
|
||||||
|
|
||||||
|
# Для каждого нового запуска должен быть новый realm name.
|
||||||
|
DAO_REALM_NAME="SHiNE NFT DAO 2026-06-30"
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
# Стартовое число governance NFT
|
||||||
|
DAO_GOV_NFT_SUPPLY="10"
|
||||||
|
|
||||||
|
# Минимум для тестов Realms - 3600
|
||||||
|
DAO_VOTING_TIME_SEC="3600"
|
||||||
|
|
||||||
|
# 51..100
|
||||||
|
DAO_APPROVAL_THRESHOLD_PERCENT="51"
|
||||||
|
|
||||||
|
# Создатель DAO
|
||||||
|
DAO_ISSUER_KEYPAIR="$HOME/.config/solana/phantomWallet.json"
|
||||||
|
|
||||||
|
# Governance program (Realms)
|
||||||
|
SPL_GOVERNANCE_PROGRAM_ID="GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw"
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Future Fast Burn Executor
|
||||||
|
|
||||||
|
## Зачем это нужно
|
||||||
|
|
||||||
|
Обычный DAO burn proposal уязвим к сценарию:
|
||||||
|
|
||||||
|
1. proposal уже создан;
|
||||||
|
2. в него уже зашит конкретный token account;
|
||||||
|
3. владелец NFT быстро перекладывает токен на другой кошелек;
|
||||||
|
4. `execute` DAO burn больше не совпадает с актуальным местом хранения.
|
||||||
|
|
||||||
|
Для такого случая нужен отдельный быстрый исполнитель.
|
||||||
|
|
||||||
|
## Идея
|
||||||
|
|
||||||
|
DAO не сжигает токен напрямую через обычную burn-инструкцию, а делает одноразовое разрешение для отдельной программы.
|
||||||
|
|
||||||
|
Эта программа:
|
||||||
|
|
||||||
|
1. получает право только на один конкретный `mint`;
|
||||||
|
2. получает ограничение по числу исполнений `max_uses = 1`;
|
||||||
|
3. может иметь `expires_at`;
|
||||||
|
4. может принимать исполнение только от разрешенного исполнителя;
|
||||||
|
5. после успешного burn закрывает право навсегда.
|
||||||
|
|
||||||
|
## Что должно быть зашито в разрешении
|
||||||
|
|
||||||
|
- `target_mint`
|
||||||
|
- `governance_realm`
|
||||||
|
- `dao_governance`
|
||||||
|
- `executor_pubkey`
|
||||||
|
- `max_uses = 1`
|
||||||
|
- `expires_at`
|
||||||
|
- `status = active | used | expired | revoked`
|
||||||
|
|
||||||
|
## Как должен работать burn
|
||||||
|
|
||||||
|
1. DAO голосованием создает запись-разрешение.
|
||||||
|
2. В запись попадает один конкретный `mint`.
|
||||||
|
3. Быстрый исполнитель вызывает программу.
|
||||||
|
4. Программа находит текущее место хранения NFT.
|
||||||
|
5. Программа проверяет, что найден именно нужный `mint`.
|
||||||
|
6. Программа делает burn.
|
||||||
|
7. Программа помечает разрешение как `used`.
|
||||||
|
|
||||||
|
## Главная цель
|
||||||
|
|
||||||
|
Исполнитель не должен иметь глобальное право сжигать любые токены.
|
||||||
|
|
||||||
|
Он должен иметь только одноразовое право:
|
||||||
|
|
||||||
|
- либо на один конкретный `mint`;
|
||||||
|
- либо на маленький заранее зафиксированный список `mint`;
|
||||||
|
- но не на всю коллекцию и не на весь governance mint.
|
||||||
|
|
||||||
|
## Что это дает
|
||||||
|
|
||||||
|
- DAO принимает политическое решение голосованием;
|
||||||
|
- исполнение становится быстрым;
|
||||||
|
- даже если владелец перекладывает NFT, программа сможет сжечь актуальный текущий token account;
|
||||||
|
- риск "исполнитель спалит всех" резко снижается.
|
||||||
|
|
||||||
|
## Текущее состояние
|
||||||
|
|
||||||
|
В этом комплекте программа fast-burn executor пока не реализована.
|
||||||
|
|
||||||
|
Здесь зафиксирована только правильная целевая архитектура, чтобы потом делать ее как отдельный следующий этап без повторного обсуждения логики.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# create-SHiNE-DAO / конфиг NFT flow через уже созданное DAO
|
||||||
|
|
||||||
|
# devnet | mainnet-beta | custom rpc url
|
||||||
|
CLUSTER="devnet"
|
||||||
|
|
||||||
|
SPL_GOVERNANCE_PROGRAM_ID="GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw"
|
||||||
|
|
||||||
|
# Подставить из отчета создания DAO
|
||||||
|
REALM="REPLACE_REALM"
|
||||||
|
GOVERNANCE="REPLACE_GOVERNANCE"
|
||||||
|
GOVERNING_MINT="REPLACE_GOVERNING_MINT"
|
||||||
|
|
||||||
|
# Основной голосующий и второй голосующий
|
||||||
|
MAIN_KEYPAIR="./local/main.json"
|
||||||
|
VOTER2_KEYPAIR="./local/voter2.json"
|
||||||
|
|
||||||
|
# Metadata целевого NFT, который будет mint/burn через DAO
|
||||||
|
NFT_NAME="SHiNE Membership NFT"
|
||||||
|
NFT_SYMBOL="SHINE-NFT"
|
||||||
|
NFT_METADATA_URI="https://arweave.net/CHANGE_ME_NFT_METADATA_JSON"
|
||||||
|
NFT_IMAGE_URL="https://arweave.net/CHANGE_ME_NFT_IMAGE"
|
||||||
|
|
||||||
|
# Куда складывать отчеты локально
|
||||||
|
RUNS_DIR="./local/runs"
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -25,6 +25,8 @@ delta
|
|||||||
diesel
|
diesel
|
||||||
drake
|
drake
|
||||||
duke
|
duke
|
||||||
|
dryg
|
||||||
|
drug
|
||||||
eagle
|
eagle
|
||||||
echo
|
echo
|
||||||
falcon
|
falcon
|
||||||
@@ -46,12 +48,14 @@ marshal
|
|||||||
master
|
master
|
||||||
matrix
|
matrix
|
||||||
maverick
|
maverick
|
||||||
|
mira
|
||||||
ninja
|
ninja
|
||||||
nomad
|
nomad
|
||||||
onyx
|
onyx
|
||||||
phantom
|
phantom
|
||||||
pilot
|
pilot
|
||||||
pirate
|
pirate
|
||||||
|
pixel
|
||||||
predator
|
predator
|
||||||
prince
|
prince
|
||||||
pro
|
pro
|
||||||
@@ -298,6 +302,7 @@ evoker
|
|||||||
falconer
|
falconer
|
||||||
fencer
|
fencer
|
||||||
fierce
|
fierce
|
||||||
|
fire
|
||||||
firehawk
|
firehawk
|
||||||
firestarter
|
firestarter
|
||||||
fist
|
fist
|
||||||
@@ -414,8 +419,11 @@ warpath
|
|||||||
wavebreaker
|
wavebreaker
|
||||||
westwind
|
westwind
|
||||||
whitewolf
|
whitewolf
|
||||||
wildfire
|
wild
|
||||||
|
wise
|
||||||
windstorm
|
windstorm
|
||||||
wolfpack
|
wolfpack
|
||||||
wrangler
|
wrangler
|
||||||
zenith
|
zenith
|
||||||
|
zero
|
||||||
|
zorro
|
||||||
@@ -102,6 +102,8 @@ martin
|
|||||||
max
|
max
|
||||||
maxim
|
maxim
|
||||||
maxwell
|
maxwell
|
||||||
|
milana
|
||||||
|
malvina
|
||||||
michael
|
michael
|
||||||
mike
|
mike
|
||||||
nathan
|
nathan
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# Источники тестовых аватаров
|
|
||||||
|
|
||||||
- `avatar_001.png` ... `avatar_050.png`: портреты с `randomuser.me`
|
|
||||||
- `avatar_051.png` ... `avatar_100.png`: мультяшные аватары с `dicebear.com`
|
|
||||||
- Все файлы приведены к размеру `512x512`.
|
|
||||||
|
Before Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 253 KiB |
|
Before Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 284 KiB |
|
Before Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 257 KiB |
|
Before Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 239 KiB |
|
Before Width: | Height: | Size: 205 KiB |
|
Before Width: | Height: | Size: 265 KiB |
|
Before Width: | Height: | Size: 231 KiB |
|
Before Width: | Height: | Size: 252 KiB |
|
Before Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 261 KiB |
|
Before Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 286 KiB |
|
Before Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 231 KiB |
|
Before Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 234 KiB |
|
Before Width: | Height: | Size: 286 KiB |
|
Before Width: | Height: | Size: 219 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 253 KiB |
|
Before Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 272 KiB |
|
Before Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 267 KiB |
|
Before Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 257 KiB |
|
Before Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 303 KiB |