diff --git a/doc/instructions/coturn-install.md b/Dev_Docs/instructions/coturn-install.md similarity index 100% rename from doc/instructions/coturn-install.md rename to Dev_Docs/instructions/coturn-install.md diff --git a/doc/instructions/turn-connect-to-shine-server.md b/Dev_Docs/instructions/turn-connect-to-shine-server.md similarity index 100% rename from doc/instructions/turn-connect-to-shine-server.md rename to Dev_Docs/instructions/turn-connect-to-shine-server.md diff --git a/Dev_Docs/libs/Agetn.ms b/Dev_Docs/libs/Agetn.ms new file mode 100644 index 0000000..172c0e0 --- /dev/null +++ b/Dev_Docs/libs/Agetn.ms @@ -0,0 +1 @@ +Данная документация местами устарела и не соответствует реальному коду \ No newline at end of file diff --git a/DOC/libs/shine-server-bd/DOC.md b/Dev_Docs/libs/shine-server-bd/DOC.md similarity index 100% rename from DOC/libs/shine-server-bd/DOC.md rename to Dev_Docs/libs/shine-server-bd/DOC.md diff --git a/DOC/libs/shine-server-blockchain/Doc.md b/Dev_Docs/libs/shine-server-blockchain/Doc.md similarity index 100% rename from DOC/libs/shine-server-blockchain/Doc.md rename to Dev_Docs/libs/shine-server-blockchain/Doc.md diff --git a/DOC/libs/shine-server-blockchain/Общая структура блока.md b/Dev_Docs/libs/shine-server-blockchain/Общая структура блока.md similarity index 100% rename from DOC/libs/shine-server-blockchain/Общая структура блока.md rename to Dev_Docs/libs/shine-server-blockchain/Общая структура блока.md diff --git a/DOC/libs/shine-server-blockchain/Формат существующих Body.md b/Dev_Docs/libs/shine-server-blockchain/Формат существующих Body.md similarity index 100% rename from DOC/libs/shine-server-blockchain/Формат существующих Body.md rename to Dev_Docs/libs/shine-server-blockchain/Формат существующих Body.md diff --git a/DOC/libs/shine-server-config/doc.md b/Dev_Docs/libs/shine-server-config/doc.md similarity index 100% rename from DOC/libs/shine-server-config/doc.md rename to Dev_Docs/libs/shine-server-config/doc.md diff --git a/DOC/libs/shine-server-crypto/DOC.md b/Dev_Docs/libs/shine-server-crypto/DOC.md similarity index 100% rename from DOC/libs/shine-server-crypto/DOC.md rename to Dev_Docs/libs/shine-server-crypto/DOC.md diff --git a/DOC/libs/shine-server-geo/DOC.md b/Dev_Docs/libs/shine-server-geo/DOC.md similarity index 100% rename from DOC/libs/shine-server-geo/DOC.md rename to Dev_Docs/libs/shine-server-geo/DOC.md diff --git a/DOC/libs/shine-server-log/DOC.md b/Dev_Docs/libs/shine-server-log/DOC.md similarity index 100% rename from DOC/libs/shine-server-log/DOC.md rename to Dev_Docs/libs/shine-server-log/DOC.md diff --git a/DOC/libs/shine-server-protocol/doc.md b/Dev_Docs/libs/shine-server-protocol/doc.md similarity index 100% rename from DOC/libs/shine-server-protocol/doc.md rename to Dev_Docs/libs/shine-server-protocol/doc.md diff --git a/Players/malvviiina/shine-staging-screenshots/channels-list.png b/Players/malvviiina/shine-staging-screenshots/channels-list.png new file mode 100644 index 0000000..3f33dcf Binary files /dev/null and b/Players/malvviiina/shine-staging-screenshots/channels-list.png differ diff --git a/Players/malvviiina/shine-staging-screenshots/chat-view_u1.png b/Players/malvviiina/shine-staging-screenshots/chat-view_u1.png new file mode 100644 index 0000000..698a188 Binary files /dev/null and b/Players/malvviiina/shine-staging-screenshots/chat-view_u1.png differ diff --git a/Players/malvviiina/shine-staging-screenshots/contact-search-view.png b/Players/malvviiina/shine-staging-screenshots/contact-search-view.png new file mode 100644 index 0000000..afea30b Binary files /dev/null and b/Players/malvviiina/shine-staging-screenshots/contact-search-view.png differ diff --git a/Players/malvviiina/shine-staging-screenshots/login.png b/Players/malvviiina/shine-staging-screenshots/login.png new file mode 100644 index 0000000..411333f Binary files /dev/null and b/Players/malvviiina/shine-staging-screenshots/login.png differ diff --git a/Players/malvviiina/shine-staging-screenshots/messages-list.png b/Players/malvviiina/shine-staging-screenshots/messages-list.png new file mode 100644 index 0000000..91499d9 Binary files /dev/null and b/Players/malvviiina/shine-staging-screenshots/messages-list.png differ diff --git a/Players/malvviiina/shine-staging-screenshots/network-view.png b/Players/malvviiina/shine-staging-screenshots/network-view.png new file mode 100644 index 0000000..b0a1ff7 Binary files /dev/null and b/Players/malvviiina/shine-staging-screenshots/network-view.png differ diff --git a/Players/malvviiina/shine-staging-screenshots/notifications-view.png b/Players/malvviiina/shine-staging-screenshots/notifications-view.png new file mode 100644 index 0000000..91499d9 Binary files /dev/null and b/Players/malvviiina/shine-staging-screenshots/notifications-view.png differ diff --git a/Players/malvviiina/shine-staging-screenshots/register.png b/Players/malvviiina/shine-staging-screenshots/register.png new file mode 100644 index 0000000..5dcdb8c Binary files /dev/null and b/Players/malvviiina/shine-staging-screenshots/register.png differ diff --git a/Players/malvviiina/shine-staging-screenshots/settings-view.png b/Players/malvviiina/shine-staging-screenshots/settings-view.png new file mode 100644 index 0000000..91499d9 Binary files /dev/null and b/Players/malvviiina/shine-staging-screenshots/settings-view.png differ diff --git a/Players/malvviiina/shine-staging-screenshots/start.png b/Players/malvviiina/shine-staging-screenshots/start.png new file mode 100644 index 0000000..ff94443 Binary files /dev/null and b/Players/malvviiina/shine-staging-screenshots/start.png differ diff --git a/Players/malvviiina/shine-staging-screenshots/wallet-view.png b/Players/malvviiina/shine-staging-screenshots/wallet-view.png new file mode 100644 index 0000000..91499d9 Binary files /dev/null and b/Players/malvviiina/shine-staging-screenshots/wallet-view.png differ diff --git a/VERSION.properties b/VERSION.properties index e6d1706..cf0adbd 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.289 -server.version=1.2.269 +client.version=1.2.290 +server.version=1.2.270 diff --git a/shine-solana/shine/create-SHiNE-DAO/README.md b/shine-solana/shine/create-SHiNE-DAO/README.md new file mode 100644 index 0000000..ebfd5d9 --- /dev/null +++ b/shine-solana/shine/create-SHiNE-DAO/README.md @@ -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 \ + \ + +``` + +### 8. Выполнить mint proposal + +```bash +node create-SHiNE-DAO/scripts/06_execute_mint_nft.js \ + ./create-SHiNE-DAO/nft-flow.config.env \ + \ + \ + \ + +``` + +### 9. Поднять proposal на burn NFT + +```bash +node create-SHiNE-DAO/scripts/07_propose_vote_burn_nft.js \ + ./create-SHiNE-DAO/nft-flow.config.env \ + \ + +``` + +### 10. Выполнить burn proposal + +```bash +node create-SHiNE-DAO/scripts/08_execute_burn_nft.js \ + ./create-SHiNE-DAO/nft-flow.config.env \ + \ + \ + \ + +``` + +## Что важно про картинку 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`. diff --git a/shine-solana/shine/create-SHiNE-DAO/dao.config.env.example b/shine-solana/shine/create-SHiNE-DAO/dao.config.env.example new file mode 100644 index 0000000..22d03ab --- /dev/null +++ b/shine-solana/shine/create-SHiNE-DAO/dao.config.env.example @@ -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" diff --git a/shine-solana/shine/create-SHiNE-DAO/docs/FUTURE_FAST_BURN_EXECUTOR.md b/shine-solana/shine/create-SHiNE-DAO/docs/FUTURE_FAST_BURN_EXECUTOR.md new file mode 100644 index 0000000..3a17bcb --- /dev/null +++ b/shine-solana/shine/create-SHiNE-DAO/docs/FUTURE_FAST_BURN_EXECUTOR.md @@ -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 пока не реализована. + +Здесь зафиксирована только правильная целевая архитектура, чтобы потом делать ее как отдельный следующий этап без повторного обсуждения логики. diff --git a/shine-solana/shine/create-SHiNE-DAO/nft-flow.config.env.example b/shine-solana/shine/create-SHiNE-DAO/nft-flow.config.env.example new file mode 100644 index 0000000..984e625 --- /dev/null +++ b/shine-solana/shine/create-SHiNE-DAO/nft-flow.config.env.example @@ -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" diff --git a/shine-solana/shine/create-SHiNE-DAO/scripts/01_check_dao_config.sh b/shine-solana/shine/create-SHiNE-DAO/scripts/01_check_dao_config.sh new file mode 100755 index 0000000..0dbc020 --- /dev/null +++ b/shine-solana/shine/create-SHiNE-DAO/scripts/01_check_dao_config.sh @@ -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 < + 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); +}); diff --git a/shine-solana/shine/create-SHiNE-DAO/scripts/03_prepare_second_voter.js b/shine-solana/shine/create-SHiNE-DAO/scripts/03_prepare_second_voter.js new file mode 100644 index 0000000..6559ae8 --- /dev/null +++ b/shine-solana/shine/create-SHiNE-DAO/scripts/03_prepare_second_voter.js @@ -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); +}); diff --git a/shine-solana/shine/create-SHiNE-DAO/scripts/04_create_empty_nft_template_with_metadata.js b/shine-solana/shine/create-SHiNE-DAO/scripts/04_create_empty_nft_template_with_metadata.js new file mode 100644 index 0000000..297f8d4 --- /dev/null +++ b/shine-solana/shine/create-SHiNE-DAO/scripts/04_create_empty_nft_template_with_metadata.js @@ -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); +}); diff --git a/shine-solana/shine/create-SHiNE-DAO/scripts/05_propose_vote_mint_nft.js b/shine-solana/shine/create-SHiNE-DAO/scripts/05_propose_vote_mint_nft.js new file mode 100644 index 0000000..3be7be1 --- /dev/null +++ b/shine-solana/shine/create-SHiNE-DAO/scripts/05_propose_vote_mint_nft.js @@ -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 "); + + 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); +}); diff --git a/shine-solana/shine/create-SHiNE-DAO/scripts/06_execute_mint_nft.js b/shine-solana/shine/create-SHiNE-DAO/scripts/06_execute_mint_nft.js new file mode 100644 index 0000000..d656064 --- /dev/null +++ b/shine-solana/shine/create-SHiNE-DAO/scripts/06_execute_mint_nft.js @@ -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 "); + + 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); +}); diff --git a/shine-solana/shine/create-SHiNE-DAO/scripts/07_propose_vote_burn_nft.js b/shine-solana/shine/create-SHiNE-DAO/scripts/07_propose_vote_burn_nft.js new file mode 100644 index 0000000..6a9428f --- /dev/null +++ b/shine-solana/shine/create-SHiNE-DAO/scripts/07_propose_vote_burn_nft.js @@ -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 "); + + 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); +}); diff --git a/shine-solana/shine/create-SHiNE-DAO/scripts/08_execute_burn_nft.js b/shine-solana/shine/create-SHiNE-DAO/scripts/08_execute_burn_nft.js new file mode 100644 index 0000000..d7deb09 --- /dev/null +++ b/shine-solana/shine/create-SHiNE-DAO/scripts/08_execute_burn_nft.js @@ -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 "); + + 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); +}); diff --git a/shine-solana/shine/create-SHiNE-DAO/scripts/js_common.js b/shine-solana/shine/create-SHiNE-DAO/scripts/js_common.js new file mode 100644 index 0000000..ddc39fb --- /dev/null +++ b/shine-solana/shine/create-SHiNE-DAO/scripts/js_common.js @@ -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, +}; diff --git a/shine-solana/shine/programs/shine_login_guard/src/dictionaries/premium/nicknames_people.txt b/shine-solana/shine/programs/shine_login_guard/src/dictionaries/premium/nicknames_people.txt index eb5e607..0113e9d 100644 --- a/shine-solana/shine/programs/shine_login_guard/src/dictionaries/premium/nicknames_people.txt +++ b/shine-solana/shine/programs/shine_login_guard/src/dictionaries/premium/nicknames_people.txt @@ -25,6 +25,8 @@ delta diesel drake duke +dryg +drug eagle echo falcon @@ -46,12 +48,14 @@ marshal master matrix maverick +mira ninja nomad onyx phantom pilot pirate +pixel predator prince pro @@ -298,6 +302,7 @@ evoker falconer fencer fierce +fire firehawk firestarter fist @@ -414,8 +419,11 @@ warpath wavebreaker westwind whitewolf -wildfire +wild +wise windstorm wolfpack wrangler zenith +zero +zorro \ No newline at end of file diff --git a/shine-solana/shine/programs/shine_login_guard/src/dictionaries/premium/people_names.txt b/shine-solana/shine/programs/shine_login_guard/src/dictionaries/premium/people_names.txt index d2ab65d..5ed6f1f 100644 --- a/shine-solana/shine/programs/shine_login_guard/src/dictionaries/premium/people_names.txt +++ b/shine-solana/shine/programs/shine_login_guard/src/dictionaries/premium/people_names.txt @@ -102,6 +102,8 @@ martin max maxim maxwell +milana +malvina michael mike nathan diff --git a/test/avatars/SOURCES.md b/test/avatars/SOURCES.md deleted file mode 100644 index cdaba52..0000000 --- a/test/avatars/SOURCES.md +++ /dev/null @@ -1,5 +0,0 @@ -# Источники тестовых аватаров - -- `avatar_001.png` ... `avatar_050.png`: портреты с `randomuser.me` -- `avatar_051.png` ... `avatar_100.png`: мультяшные аватары с `dicebear.com` -- Все файлы приведены к размеру `512x512`. diff --git a/test/avatars/avatar_001.png b/test/avatars/avatar_001.png deleted file mode 100644 index e5327c9..0000000 Binary files a/test/avatars/avatar_001.png and /dev/null differ diff --git a/test/avatars/avatar_002.png b/test/avatars/avatar_002.png deleted file mode 100644 index 9de22c6..0000000 Binary files a/test/avatars/avatar_002.png and /dev/null differ diff --git a/test/avatars/avatar_003.png b/test/avatars/avatar_003.png deleted file mode 100644 index 94afe1a..0000000 Binary files a/test/avatars/avatar_003.png and /dev/null differ diff --git a/test/avatars/avatar_004.png b/test/avatars/avatar_004.png deleted file mode 100644 index 82a34dd..0000000 Binary files a/test/avatars/avatar_004.png and /dev/null differ diff --git a/test/avatars/avatar_005.png b/test/avatars/avatar_005.png deleted file mode 100644 index 2019c3c..0000000 Binary files a/test/avatars/avatar_005.png and /dev/null differ diff --git a/test/avatars/avatar_006.png b/test/avatars/avatar_006.png deleted file mode 100644 index 3f756ad..0000000 Binary files a/test/avatars/avatar_006.png and /dev/null differ diff --git a/test/avatars/avatar_007.png b/test/avatars/avatar_007.png deleted file mode 100644 index 6808342..0000000 Binary files a/test/avatars/avatar_007.png and /dev/null differ diff --git a/test/avatars/avatar_008.png b/test/avatars/avatar_008.png deleted file mode 100644 index c3d7487..0000000 Binary files a/test/avatars/avatar_008.png and /dev/null differ diff --git a/test/avatars/avatar_009.png b/test/avatars/avatar_009.png deleted file mode 100644 index 3608b44..0000000 Binary files a/test/avatars/avatar_009.png and /dev/null differ diff --git a/test/avatars/avatar_010.png b/test/avatars/avatar_010.png deleted file mode 100644 index 66f1a43..0000000 Binary files a/test/avatars/avatar_010.png and /dev/null differ diff --git a/test/avatars/avatar_011.png b/test/avatars/avatar_011.png deleted file mode 100644 index bf9d985..0000000 Binary files a/test/avatars/avatar_011.png and /dev/null differ diff --git a/test/avatars/avatar_012.png b/test/avatars/avatar_012.png deleted file mode 100644 index c4c1155..0000000 Binary files a/test/avatars/avatar_012.png and /dev/null differ diff --git a/test/avatars/avatar_013.png b/test/avatars/avatar_013.png deleted file mode 100644 index 7d41512..0000000 Binary files a/test/avatars/avatar_013.png and /dev/null differ diff --git a/test/avatars/avatar_014.png b/test/avatars/avatar_014.png deleted file mode 100644 index 0345d58..0000000 Binary files a/test/avatars/avatar_014.png and /dev/null differ diff --git a/test/avatars/avatar_015.png b/test/avatars/avatar_015.png deleted file mode 100644 index b94ad43..0000000 Binary files a/test/avatars/avatar_015.png and /dev/null differ diff --git a/test/avatars/avatar_016.png b/test/avatars/avatar_016.png deleted file mode 100644 index ae165cb..0000000 Binary files a/test/avatars/avatar_016.png and /dev/null differ diff --git a/test/avatars/avatar_017.png b/test/avatars/avatar_017.png deleted file mode 100644 index b8568f5..0000000 Binary files a/test/avatars/avatar_017.png and /dev/null differ diff --git a/test/avatars/avatar_018.png b/test/avatars/avatar_018.png deleted file mode 100644 index 9cefdf0..0000000 Binary files a/test/avatars/avatar_018.png and /dev/null differ diff --git a/test/avatars/avatar_019.png b/test/avatars/avatar_019.png deleted file mode 100644 index 9e8e3d8..0000000 Binary files a/test/avatars/avatar_019.png and /dev/null differ diff --git a/test/avatars/avatar_020.png b/test/avatars/avatar_020.png deleted file mode 100644 index af11a01..0000000 Binary files a/test/avatars/avatar_020.png and /dev/null differ diff --git a/test/avatars/avatar_021.png b/test/avatars/avatar_021.png deleted file mode 100644 index c11748a..0000000 Binary files a/test/avatars/avatar_021.png and /dev/null differ diff --git a/test/avatars/avatar_022.png b/test/avatars/avatar_022.png deleted file mode 100644 index 04bd058..0000000 Binary files a/test/avatars/avatar_022.png and /dev/null differ diff --git a/test/avatars/avatar_023.png b/test/avatars/avatar_023.png deleted file mode 100644 index bd367d0..0000000 Binary files a/test/avatars/avatar_023.png and /dev/null differ diff --git a/test/avatars/avatar_024.png b/test/avatars/avatar_024.png deleted file mode 100644 index 6618a27..0000000 Binary files a/test/avatars/avatar_024.png and /dev/null differ diff --git a/test/avatars/avatar_025.png b/test/avatars/avatar_025.png deleted file mode 100644 index 8c6bd82..0000000 Binary files a/test/avatars/avatar_025.png and /dev/null differ diff --git a/test/avatars/avatar_026.png b/test/avatars/avatar_026.png deleted file mode 100644 index 8d7bd6b..0000000 Binary files a/test/avatars/avatar_026.png and /dev/null differ diff --git a/test/avatars/avatar_027.png b/test/avatars/avatar_027.png deleted file mode 100644 index 6fd2666..0000000 Binary files a/test/avatars/avatar_027.png and /dev/null differ diff --git a/test/avatars/avatar_028.png b/test/avatars/avatar_028.png deleted file mode 100644 index 6abdf64..0000000 Binary files a/test/avatars/avatar_028.png and /dev/null differ diff --git a/test/avatars/avatar_029.png b/test/avatars/avatar_029.png deleted file mode 100644 index 4df4201..0000000 Binary files a/test/avatars/avatar_029.png and /dev/null differ diff --git a/test/avatars/avatar_030.png b/test/avatars/avatar_030.png deleted file mode 100644 index e780453..0000000 Binary files a/test/avatars/avatar_030.png and /dev/null differ diff --git a/test/avatars/avatar_031.png b/test/avatars/avatar_031.png deleted file mode 100644 index ef4769e..0000000 Binary files a/test/avatars/avatar_031.png and /dev/null differ diff --git a/test/avatars/avatar_032.png b/test/avatars/avatar_032.png deleted file mode 100644 index 3f04b41..0000000 Binary files a/test/avatars/avatar_032.png and /dev/null differ diff --git a/test/avatars/avatar_033.png b/test/avatars/avatar_033.png deleted file mode 100644 index 76a939b..0000000 Binary files a/test/avatars/avatar_033.png and /dev/null differ diff --git a/test/avatars/avatar_034.png b/test/avatars/avatar_034.png deleted file mode 100644 index f5151ec..0000000 Binary files a/test/avatars/avatar_034.png and /dev/null differ diff --git a/test/avatars/avatar_035.png b/test/avatars/avatar_035.png deleted file mode 100644 index c89eac7..0000000 Binary files a/test/avatars/avatar_035.png and /dev/null differ diff --git a/test/avatars/avatar_036.png b/test/avatars/avatar_036.png deleted file mode 100644 index 88e9f3c..0000000 Binary files a/test/avatars/avatar_036.png and /dev/null differ diff --git a/test/avatars/avatar_037.png b/test/avatars/avatar_037.png deleted file mode 100644 index 36950ef..0000000 Binary files a/test/avatars/avatar_037.png and /dev/null differ diff --git a/test/avatars/avatar_038.png b/test/avatars/avatar_038.png deleted file mode 100644 index d903551..0000000 Binary files a/test/avatars/avatar_038.png and /dev/null differ diff --git a/test/avatars/avatar_039.png b/test/avatars/avatar_039.png deleted file mode 100644 index 3f22f80..0000000 Binary files a/test/avatars/avatar_039.png and /dev/null differ diff --git a/test/avatars/avatar_040.png b/test/avatars/avatar_040.png deleted file mode 100644 index 4919adb..0000000 Binary files a/test/avatars/avatar_040.png and /dev/null differ diff --git a/test/avatars/avatar_041.png b/test/avatars/avatar_041.png deleted file mode 100644 index 1679866..0000000 Binary files a/test/avatars/avatar_041.png and /dev/null differ diff --git a/test/avatars/avatar_042.png b/test/avatars/avatar_042.png deleted file mode 100644 index a77e4ee..0000000 Binary files a/test/avatars/avatar_042.png and /dev/null differ diff --git a/test/avatars/avatar_043.png b/test/avatars/avatar_043.png deleted file mode 100644 index 7688d3a..0000000 Binary files a/test/avatars/avatar_043.png and /dev/null differ diff --git a/test/avatars/avatar_044.png b/test/avatars/avatar_044.png deleted file mode 100644 index 498392c..0000000 Binary files a/test/avatars/avatar_044.png and /dev/null differ diff --git a/test/avatars/avatar_045.png b/test/avatars/avatar_045.png deleted file mode 100644 index f88ac89..0000000 Binary files a/test/avatars/avatar_045.png and /dev/null differ diff --git a/test/avatars/avatar_046.png b/test/avatars/avatar_046.png deleted file mode 100644 index 837d6eb..0000000 Binary files a/test/avatars/avatar_046.png and /dev/null differ diff --git a/test/avatars/avatar_047.png b/test/avatars/avatar_047.png deleted file mode 100644 index c21c2aa..0000000 Binary files a/test/avatars/avatar_047.png and /dev/null differ diff --git a/test/avatars/avatar_048.png b/test/avatars/avatar_048.png deleted file mode 100644 index 64abc73..0000000 Binary files a/test/avatars/avatar_048.png and /dev/null differ diff --git a/test/avatars/avatar_049.png b/test/avatars/avatar_049.png deleted file mode 100644 index f5a8f43..0000000 Binary files a/test/avatars/avatar_049.png and /dev/null differ