Закоммичены все текущие изменения и добавлено правило русских commit message
This commit is contained in:
parent
9fd2f8f495
commit
61c6a3208a
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
18
.idea/gradle.xml
generated
Normal file
18
.idea/gradle.xml
generated
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$/solana-shine-client-lib" />
|
||||
<option name="gradleJvm" value="21" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$/solana-shine-client-lib" />
|
||||
<option value="$PROJECT_DIR$/solana-shine-client-lib/solana-shine-lib" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/shine-solana.iml" filepath="$PROJECT_DIR$/.idea/shine-solana.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/shine-solana.iml
generated
Normal file
9
.idea/shine-solana.iml
generated
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
BIN
shine/.gradle/8.14.4/checksums/checksums.lock
Normal file
BIN
shine/.gradle/8.14.4/checksums/checksums.lock
Normal file
Binary file not shown.
BIN
shine/.gradle/8.14.4/executionHistory/executionHistory.lock
Normal file
BIN
shine/.gradle/8.14.4/executionHistory/executionHistory.lock
Normal file
Binary file not shown.
BIN
shine/.gradle/8.14.4/fileChanges/last-build.bin
Normal file
BIN
shine/.gradle/8.14.4/fileChanges/last-build.bin
Normal file
Binary file not shown.
BIN
shine/.gradle/8.14.4/fileHashes/fileHashes.lock
Normal file
BIN
shine/.gradle/8.14.4/fileHashes/fileHashes.lock
Normal file
Binary file not shown.
0
shine/.gradle/8.14.4/gc.properties
Normal file
0
shine/.gradle/8.14.4/gc.properties
Normal file
BIN
shine/.gradle/buildOutputCleanup/buildOutputCleanup.lock
Normal file
BIN
shine/.gradle/buildOutputCleanup/buildOutputCleanup.lock
Normal file
Binary file not shown.
2
shine/.gradle/buildOutputCleanup/cache.properties
Normal file
2
shine/.gradle/buildOutputCleanup/cache.properties
Normal file
@ -0,0 +1,2 @@
|
||||
#Mon May 11 16:21:48 MSK 2026
|
||||
gradle.version=8.14.4
|
||||
0
shine/.gradle/vcs-1/gc.properties
Normal file
0
shine/.gradle/vcs-1/gc.properties
Normal file
1
shine/.vendor/pyth-crosschain
Submodule
1
shine/.vendor/pyth-crosschain
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 3abc3959fa56192511bdc977615f500a55022e88
|
||||
@ -31,3 +31,18 @@
|
||||
- `GITEA_TOKEN`
|
||||
|
||||
Push выполнять через `http.extraHeader` (Authorization) без вывода токена в логи.
|
||||
|
||||
## Rule: Commit Messages
|
||||
|
||||
Текст commit message писать на русском языке.
|
||||
|
||||
## Rule: UI Deploy
|
||||
|
||||
Деплой UI Shine Payments выполнять через Gradle из папки `shine`:
|
||||
|
||||
1. `gradle deployUi`
|
||||
2. `gradle checkUiRemote`
|
||||
|
||||
Где смотреть детали (пути деплоя, путь Caddy, рабочие URL):
|
||||
|
||||
- комментарии в `build.gradle` (в корне `shine/`).
|
||||
|
||||
45
shine/build.gradle
Normal file
45
shine/build.gradle
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Gradle-задачи для утилитного деплоя UI Shine Payments.
|
||||
*
|
||||
* Куда деплоим файлы UI:
|
||||
* /home/player/sites/test-solana-tickets.shineup.me
|
||||
*
|
||||
* Где расположен Caddy-конфиг на сервере:
|
||||
* /home/player/SHiNE/caddy/Caddyfile
|
||||
*
|
||||
* По каким URL должен работать UI:
|
||||
* https://test-solana-tickets.shineup.me
|
||||
* https://sol.shiningpeople.ru
|
||||
*/
|
||||
|
||||
tasks.register("deployUi", Exec) {
|
||||
group = "deploy"
|
||||
description = "Деплой HTML UI Shine Payments на 45.136.124.227 в /home/player/sites/test-solana-tickets.shineup.me (URL: test-solana-tickets.shineup.me, sol.shiningpeople.ru)"
|
||||
|
||||
// Источник локальных UI-страниц:
|
||||
// shine/programs/shine_payments/web/
|
||||
def localUiDir = "${projectDir}/programs/shine_payments/web/"
|
||||
|
||||
// Целевая директория на сервере:
|
||||
// /home/player/sites/test-solana-tickets.shineup.me
|
||||
def remoteTarget = "player@45.136.124.227:/home/player/sites/test-solana-tickets.shineup.me/"
|
||||
|
||||
commandLine "rsync", "-av", "--delete", localUiDir, remoteTarget
|
||||
}
|
||||
|
||||
tasks.register("checkUiRemote", Exec) {
|
||||
group = "deploy"
|
||||
description = "Проверка на сервере: Caddy-конфиг и наличие новых Program ID в UI"
|
||||
|
||||
commandLine "ssh", "-o", "StrictHostKeyChecking=no", "player@45.136.124.227",
|
||||
"set -e; " +
|
||||
"echo 'Caddy file:'; " +
|
||||
"ls -la /home/player/SHiNE/caddy/Caddyfile; " +
|
||||
"echo; " +
|
||||
"echo 'Домены в Caddy:'; " +
|
||||
"grep -n 'test-solana-tickets.shineup.me\\|sol.shiningpeople.ru' /home/player/SHiNE/caddy/Caddyfile; " +
|
||||
"echo; " +
|
||||
"echo 'Program ID в загруженных html:'; " +
|
||||
"grep -R -n 'm48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR' /home/player/sites/test-solana-tickets.shineup.me/*.html"
|
||||
}
|
||||
|
||||
1
shine/keys/shine_payments-keypair.json
Normal file
1
shine/keys/shine_payments-keypair.json
Normal file
@ -0,0 +1 @@
|
||||
[112,191,53,183,50,74,71,149,251,216,158,157,67,198,143,219,67,188,22,43,49,6,18,31,92,254,78,219,18,245,187,50,50,133,54,215,248,187,162,135,70,192,63,182,33,87,44,169,230,248,155,7,216,43,188,126,253,71,54,123,202,174,140,97]
|
||||
1
shine/keys/shine_users-keypair.json
Normal file
1
shine/keys/shine_users-keypair.json
Normal file
@ -0,0 +1 @@
|
||||
[78,57,36,25,42,130,147,16,232,65,139,10,4,54,133,0,204,50,65,112,18,99,223,142,226,60,119,36,253,192,185,121,112,54,249,197,203,152,82,234,162,32,176,193,110,19,217,20,149,116,215,16,153,121,28,243,143,248,181,55,186,250,95,103]
|
||||
@ -1,14 +1,5 @@
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::solana_program::{
|
||||
program::invoke_signed,
|
||||
system_instruction,
|
||||
system_program
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
use anchor_lang::solana_program::{program::invoke_signed, system_instruction, system_program};
|
||||
|
||||
/// сдесь коды всех ошибок
|
||||
|
||||
@ -40,7 +31,6 @@ pub enum ErrCode {
|
||||
#[msg("PDA-аккаунт уже существует и не может быть создан повторно.")]
|
||||
PdaAlreadyExists = 1009,
|
||||
|
||||
|
||||
#[msg("Подписавший не совпадает с ожидаемым пользователем (это потому что пока временно можно регистрировать пользователя с другово аккаунта")]
|
||||
InvalidSigner = 1005,
|
||||
|
||||
@ -86,29 +76,8 @@ pub enum ErrCode {
|
||||
|
||||
#[msg("Невалидная magic-сигнатура записи")]
|
||||
InvalidRecordMagic = 1025,
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
///----------------------------------------------------------------------------------------------------------
|
||||
/// Базовые функции для работы с PDA
|
||||
///----------------------------------------------------------------------------------------------------------
|
||||
@ -137,7 +106,7 @@ pub fn create_and_write_pda<'info>(
|
||||
msg!("Создаём PDA с размером {} байт", space);
|
||||
|
||||
let space = space; //+ 128; // Добавляется запас под метаданные
|
||||
// Вычисляем необходимую арендную плату
|
||||
// Вычисляем необходимую арендную плату
|
||||
let lamports = Rent::get()?.minimum_balance(space as usize);
|
||||
|
||||
// Формируем инструкцию
|
||||
@ -152,11 +121,7 @@ pub fn create_and_write_pda<'info>(
|
||||
// Выполняем инструкцию с подписью от PDA
|
||||
invoke_signed(
|
||||
&create_instr,
|
||||
&[
|
||||
signer.clone(),
|
||||
pda_account.clone(),
|
||||
system_program.clone(),
|
||||
],
|
||||
&[signer.clone(), pda_account.clone(), system_program.clone()],
|
||||
&[&seeds],
|
||||
)?;
|
||||
}
|
||||
@ -177,9 +142,6 @@ pub fn create_and_write_pda<'info>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// Создаёт PDA аккаунт (если его ещё нет).
|
||||
///
|
||||
/// ⚠️ Если аккаунт уже существует, выбрасывается ошибка.
|
||||
@ -221,22 +183,18 @@ pub fn create_pda<'info>(
|
||||
// ───────────────────────────────────────────────
|
||||
// 3. Создаём инструкцию system_program для создания аккаунта
|
||||
let create_instr = system_instruction::create_account(
|
||||
signer.key, // от имени кого
|
||||
pda_account.key, // для какого PDA
|
||||
lamports, // сколько лампортов перевести
|
||||
full_space, // сколько байт выделить
|
||||
program_id, // кто будет владельцем PDA
|
||||
signer.key, // от имени кого
|
||||
pda_account.key, // для какого PDA
|
||||
lamports, // сколько лампортов перевести
|
||||
full_space, // сколько байт выделить
|
||||
program_id, // кто будет владельцем PDA
|
||||
);
|
||||
|
||||
// ───────────────────────────────────────────────
|
||||
// 4. Выполняем инструкцию с подписью PDA (через сиды)
|
||||
invoke_signed(
|
||||
&create_instr,
|
||||
&[
|
||||
signer.clone(),
|
||||
pda_account.clone(),
|
||||
system_program.clone(),
|
||||
],
|
||||
&[signer.clone(), pda_account.clone(), system_program.clone()],
|
||||
&[&seeds], // PDA сиды → для подписи
|
||||
)?;
|
||||
|
||||
@ -251,10 +209,7 @@ pub fn create_pda<'info>(
|
||||
/// Аргументы:
|
||||
/// - `pda_account`: аккаунт, в который пишем (должен быть mut)
|
||||
/// - `data`: бинарный массив, который нужно записать
|
||||
pub fn write_to_pda<'info>(
|
||||
pda_account: &AccountInfo<'info>,
|
||||
data: &[u8],
|
||||
) -> Result<()> {
|
||||
pub fn write_to_pda<'info>(pda_account: &AccountInfo<'info>, data: &[u8]) -> Result<()> {
|
||||
// ───────────────────────────────────────────────
|
||||
// 1. Получаем доступ к данным PDA (на запись)
|
||||
let mut account_data = pda_account.try_borrow_mut_data()?;
|
||||
@ -274,15 +229,6 @@ pub fn write_to_pda<'info>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/// ------------------------------------------------------------------------
|
||||
/// safe_read_pda ‒ «безопасное чтение PDA»
|
||||
/// ------------------------------------------------------------------------
|
||||
@ -333,17 +279,15 @@ pub fn safe_read_pda<'info>(pda_account: &AccountInfo<'info>) -> Vec<u8> {
|
||||
}
|
||||
Err(e) => {
|
||||
// Ошибка при borrow (например, уже есть активное мутабельное заимствование)
|
||||
msg!("safe_read_pda: ошибка borrow_data ({:?}) ‒ возвращаем пустой массив", e);
|
||||
msg!(
|
||||
"safe_read_pda: ошибка borrow_data ({:?}) ‒ возвращаем пустой массив",
|
||||
e
|
||||
);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/// ------------------------------------------------------------------------
|
||||
/// delete_pda_with_assign — закрыть PDA, вернуть ренту и освободить адрес
|
||||
/// ------------------------------------------------------------------------
|
||||
@ -373,7 +317,10 @@ pub fn delete_pda_return_rent<'info>(
|
||||
program_id: &Pubkey,
|
||||
) -> Result<()> {
|
||||
// 0) проверки
|
||||
require!(pda_account.owner != &Pubkey::default(), ErrCode::EmptyPdaData);
|
||||
require!(
|
||||
pda_account.owner != &Pubkey::default(),
|
||||
ErrCode::EmptyPdaData
|
||||
);
|
||||
require!(pda_account.owner == program_id, ErrCode::InvalidPdaAddress);
|
||||
|
||||
// 1) Переложить все лампорты с PDA на получателя (мы владелец, это разрешено)
|
||||
@ -389,7 +336,9 @@ pub fn delete_pda_return_rent<'info>(
|
||||
// 2) Нулим данные (если были)
|
||||
if !pda_account.data_is_empty() {
|
||||
let mut data = pda_account.try_borrow_mut_data()?;
|
||||
for b in data.iter_mut() { *b = 0; }
|
||||
for b in data.iter_mut() {
|
||||
*b = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Сжать до 0 байт
|
||||
@ -399,4 +348,3 @@ pub fn delete_pda_return_rent<'info>(
|
||||
msg!("PDA закрыт: рента отправлена на {}", recipient.key);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
19
shine/programs/shine_payments/oracle_check/README.md
Normal file
19
shine/programs/shine_payments/oracle_check/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Oracle Check
|
||||
|
||||
Мини-страница диагностики оракула Pyth для `SOL/USD`.
|
||||
|
||||
Файл:
|
||||
|
||||
- `index.html`
|
||||
|
||||
Что проверяет:
|
||||
|
||||
1. Чтение oracle account через RPC (`devnet`, `mainnet-beta`, `testnet`).
|
||||
2. Парсинг по текущим оффсетам из UI (`74/90/94`).
|
||||
3. Альтернативный парсинг (`73/89/93`) для проверки сдвига формата.
|
||||
4. Сравнение с Hermes API (эталонный источник цены по feed id).
|
||||
|
||||
Запуск:
|
||||
|
||||
Открыть `index.html` в браузере и нажать кнопку «Проверить все сети».
|
||||
|
||||
255
shine/programs/shine_payments/oracle_check/index.html
Normal file
255
shine/programs/shine_payments/oracle_check/index.html
Normal file
@ -0,0 +1,255 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Проверка оракула Pyth (Devnet/Mainnet/Testnet)</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #0f1218;
|
||||
--panel: #171b24;
|
||||
--text: #e8edf6;
|
||||
--muted: #97a3b8;
|
||||
--line: #2a3242;
|
||||
--ok: #55d48a;
|
||||
--warn: #ffbf5e;
|
||||
--err: #ff7d7d;
|
||||
--btn: #273247;
|
||||
--btn-hover: #32415c;
|
||||
--code: #1e2633;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: Arial, sans-serif; margin: 20px; background: var(--bg); color: var(--text); }
|
||||
.wrap { width: 100%; max-width: 1800px; }
|
||||
.panel { border: 1px solid var(--line); border-radius: 8px; padding: 14px; margin-bottom: 14px; background: var(--panel); }
|
||||
.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; }
|
||||
input { padding: 9px 10px; min-width: 340px; border: 1px solid var(--line); border-radius: 8px; background: #131824; color: var(--text); }
|
||||
button { padding: 9px 14px; border: 1px solid var(--line); border-radius: 8px; cursor: pointer; background: var(--btn); color: var(--text); }
|
||||
button:hover { background: var(--btn-hover); }
|
||||
.muted { color: var(--muted); }
|
||||
.ok { color: var(--ok); }
|
||||
.warn { color: var(--warn); }
|
||||
.err { color: var(--err); white-space: pre-wrap; }
|
||||
code { background: var(--code); padding: 2px 4px; border-radius: 4px; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
||||
th, td { border: 1px solid var(--line); padding: 6px; text-align: left; vertical-align: top; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>Диагностика оракула Pyth (SOL/USD)</h1>
|
||||
<div class="panel">
|
||||
<div class="muted">
|
||||
Страница нужна, чтобы проверить, что именно возвращает аккаунт оракула в разных сетях и где ломается парсинг.
|
||||
</div>
|
||||
<div class="muted">
|
||||
Проверяются три сети: <code>devnet</code>, <code>mainnet-beta</code>, <code>testnet</code>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>Настройки</h3>
|
||||
<div class="row">
|
||||
<label>Feed ID (hex с 0x):<br /><input id="feedId" value="0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d" /></label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Oracle account (devnet):<br /><input id="oracleDevnet" value="7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE" /></label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Oracle account (mainnet-beta):<br /><input id="oracleMainnet" value="7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE" /></label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Oracle account (testnet):<br /><input id="oracleTestnet" value="7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE" /></label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button id="runBtn">Проверить все сети</button>
|
||||
</div>
|
||||
<div class="muted">
|
||||
Если для сети аккаунт не существует, это тоже покажется в отчёте.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>Результаты</h3>
|
||||
<div id="out" class="muted">Нажмите «Проверить все сети».</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const NETWORKS = [
|
||||
{ name: "devnet", rpc: "https://api.devnet.solana.com", inputId: "oracleDevnet" },
|
||||
{ name: "mainnet-beta", rpc: "https://api.mainnet-beta.solana.com", inputId: "oracleMainnet" },
|
||||
{ name: "testnet", rpc: "https://api.testnet.solana.com", inputId: "oracleTestnet" },
|
||||
];
|
||||
|
||||
function readU64(data, offset) {
|
||||
let x = 0n;
|
||||
for (let i = 0; i < 8; i++) x |= BigInt(data[offset + i]) << (8n * BigInt(i));
|
||||
return x;
|
||||
}
|
||||
function readI64(data, offset) {
|
||||
let x = readU64(data, offset);
|
||||
if (x > 0x7fffffffffffffffn) x -= 0x10000000000000000n;
|
||||
return x;
|
||||
}
|
||||
function readU32(data, offset) {
|
||||
return data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24);
|
||||
}
|
||||
function readI32(data, offset) {
|
||||
let x = readU32(data, offset);
|
||||
if (x > 0x7fffffff) x -= 0x100000000;
|
||||
return x;
|
||||
}
|
||||
function toHex(bytes) {
|
||||
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
function trimZeros(v) {
|
||||
return v.replace(/(\.\d*?[1-9])0+$/u, "$1").replace(/\.0+$/u, "").replace(/\.$/u, "");
|
||||
}
|
||||
function priceToFloatStr(price, exponent) {
|
||||
const p = Number(price);
|
||||
if (!Number.isFinite(p)) return "NaN";
|
||||
return trimZeros((p * Math.pow(10, exponent)).toFixed(12));
|
||||
}
|
||||
function fmtErr(e) {
|
||||
const s = String(e?.message || e || "unknown error");
|
||||
return s.length > 350 ? s.slice(0, 350) + "..." : s;
|
||||
}
|
||||
|
||||
function parseWithOffsets(data, priceOffset, exponentOffset, publishOffset) {
|
||||
const price = readI64(data, priceOffset);
|
||||
const exponent = readI32(data, exponentOffset);
|
||||
const publishTime = readI64(data, publishOffset);
|
||||
return { price, exponent, publishTime, valueStr: priceToFloatStr(price, exponent) };
|
||||
}
|
||||
|
||||
function parseFeedIdFromLikelyPosition(data) {
|
||||
// Для текущего PriceUpdateV2 в аккаунте receiver:
|
||||
// 0..7 discriminator, 8..39 write_authority, 40..40 verification enum, 41..72 feed_id.
|
||||
if (data.length < 73) return null;
|
||||
const feedRaw = data.slice(41, 73);
|
||||
return "0x" + toHex(feedRaw);
|
||||
}
|
||||
|
||||
async function rpcGetAccountInfo(rpcUrl, pubkey) {
|
||||
const body = {
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "getAccountInfo",
|
||||
params: [pubkey, { encoding: "base64", commitment: "confirmed" }],
|
||||
};
|
||||
const res = await fetch(rpcUrl, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!res.ok) throw new Error(`RPC HTTP ${res.status}`);
|
||||
const json = await res.json();
|
||||
if (json.error) throw new Error(json.error.message || JSON.stringify(json.error));
|
||||
return json.result?.value || null;
|
||||
}
|
||||
|
||||
async function fetchHermesPrice(feedIdHex) {
|
||||
const url = `https://hermes.pyth.network/v2/updates/price/latest?ids[]=${encodeURIComponent(feedIdHex)}`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`Hermes HTTP ${res.status}`);
|
||||
const json = await res.json();
|
||||
const p = json?.parsed?.[0]?.price;
|
||||
if (!p) throw new Error("Hermes: price not found");
|
||||
return {
|
||||
price: BigInt(p.price),
|
||||
expo: Number(p.expo),
|
||||
publishTime: BigInt(p.publish_time),
|
||||
valueStr: priceToFloatStr(BigInt(p.price), Number(p.expo)),
|
||||
};
|
||||
}
|
||||
|
||||
async function runCheck() {
|
||||
const out = document.getElementById("out");
|
||||
out.textContent = "Проверка...";
|
||||
|
||||
const feedId = document.getElementById("feedId").value.trim().toLowerCase();
|
||||
const rows = [];
|
||||
|
||||
let hermes = null;
|
||||
let hermesErr = null;
|
||||
try {
|
||||
hermes = await fetchHermesPrice(feedId);
|
||||
} catch (e) {
|
||||
hermesErr = fmtErr(e);
|
||||
}
|
||||
|
||||
for (const n of NETWORKS) {
|
||||
const oracle = document.getElementById(n.inputId).value.trim();
|
||||
try {
|
||||
const ai = await rpcGetAccountInfo(n.rpc, oracle);
|
||||
if (!ai) {
|
||||
rows.push({
|
||||
network: n.name,
|
||||
status: "account not found",
|
||||
details: "Аккаунт оракула не найден в этой сети",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const data = Uint8Array.from(atob(ai.data[0]), c => c.charCodeAt(0));
|
||||
const feedFromData = parseFeedIdFromLikelyPosition(data);
|
||||
|
||||
const parsedCurrentUi = parseWithOffsets(data, 74, 90, 94); // как сейчас в UI проекта
|
||||
const parsedShiftMinus1 = parseWithOffsets(data, 73, 89, 93); // диагностический вариант
|
||||
|
||||
const currentUiValid = parsedCurrentUi.price > 0n;
|
||||
const shiftValid = parsedShiftMinus1.price > 0n;
|
||||
const feedMatch = feedFromData === feedId;
|
||||
|
||||
rows.push({
|
||||
network: n.name,
|
||||
status: "ok",
|
||||
details: `
|
||||
<div>RPC: <code>${n.rpc}</code></div>
|
||||
<div>Account owner: <code>${ai.owner}</code>, data len: <b>${data.length}</b></div>
|
||||
<div>Feed ID (из аккаунта): <code>${feedFromData || "n/a"}</code> ${feedMatch ? '<span class="ok">совпадает</span>' : '<span class="warn">НЕ совпадает</span>'}</div>
|
||||
<div>Парсер UI (offsets 74/90/94): price=<code>${parsedCurrentUi.price.toString()}</code>, exp=<code>${parsedCurrentUi.exponent}</code>, value=<b>${parsedCurrentUi.valueStr}</b> ${currentUiValid ? '<span class="ok">valid</span>' : '<span class="err">invalid</span>'}</div>
|
||||
<div>Альт. парсер (offsets 73/89/93): price=<code>${parsedShiftMinus1.price.toString()}</code>, exp=<code>${parsedShiftMinus1.exponent}</code>, value=<b>${parsedShiftMinus1.valueStr}</b> ${shiftValid ? '<span class="ok">valid</span>' : '<span class="err">invalid</span>'}</div>
|
||||
${hermes
|
||||
? `<div>Hermes (эталон): price=<code>${hermes.price.toString()}</code>, exp=<code>${hermes.expo}</code>, value=<b>${hermes.valueStr}</b></div>`
|
||||
: `<div class="warn">Hermes недоступен: ${hermesErr}</div>`
|
||||
}
|
||||
`,
|
||||
});
|
||||
} catch (e) {
|
||||
rows.push({
|
||||
network: n.name,
|
||||
status: "error",
|
||||
details: `<span class="err">${fmtErr(e)}</span>`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
out.innerHTML = `
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Сеть</th>
|
||||
<th>Статус</th>
|
||||
<th>Детали</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${rows.map((r) => `
|
||||
<tr>
|
||||
<td><b>${r.network}</b></td>
|
||||
<td>${r.status === "ok" ? '<span class="ok">ok</span>' : (r.status === "account not found" ? '<span class="warn">not found</span>' : '<span class="err">error</span>')}</td>
|
||||
<td>${r.details}</td>
|
||||
</tr>
|
||||
`).join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
document.getElementById("runBtn").addEventListener("click", runCheck);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -148,9 +148,9 @@
|
||||
return BigInt(Math.round(v * 1_000_000_000));
|
||||
}
|
||||
function parsePythPriceUpdateV2(data) {
|
||||
const price = readI64(data, 74);
|
||||
const exponent = readI32(data, 90);
|
||||
const publishTime = readI64(data, 94);
|
||||
const price = readI64(data, 73);
|
||||
const exponent = readI32(data, 89);
|
||||
const publishTime = readI64(data, 93);
|
||||
if (price <= 0n) throw new Error("Оракул вернул некорректную цену");
|
||||
let num = price * 100n;
|
||||
let den = 1n;
|
||||
|
||||
342
shine/programs/shine_payments/web/dao_revoke_vote.html
Normal file
342
shine/programs/shine_payments/web/dao_revoke_vote.html
Normal file
@ -0,0 +1,342 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DAO revoke vote — Shine Payments Devnet</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #0f1218;
|
||||
--panel: #171b24;
|
||||
--text: #e8edf6;
|
||||
--muted: #97a3b8;
|
||||
--line: #2a3242;
|
||||
--ok: #55d48a;
|
||||
--warn: #ffbf5e;
|
||||
--err: #ff7d7d;
|
||||
--btn: #273247;
|
||||
--btn-hover: #32415c;
|
||||
--code: #1e2633;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: Arial, sans-serif; margin: 20px; background: var(--bg); color: var(--text); }
|
||||
.wrap { width: 100%; max-width: 1200px; }
|
||||
.panel { border: 1px solid var(--line); border-radius: 8px; padding: 14px; margin-bottom: 14px; background: var(--panel); }
|
||||
.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; }
|
||||
label { display: inline-flex; flex-direction: column; gap: 6px; color: var(--muted); min-width: 280px; }
|
||||
input { padding: 9px 10px; border: 1px solid var(--line); border-radius: 8px; background: #131824; color: var(--text); }
|
||||
button { padding: 9px 14px; border: 1px solid var(--line); border-radius: 8px; cursor: pointer; background: var(--btn); color: var(--text); }
|
||||
button:hover { background: var(--btn-hover); }
|
||||
.muted { color: var(--muted); }
|
||||
.ok { color: var(--ok); }
|
||||
.warn { color: var(--warn); }
|
||||
.err { color: var(--err); white-space: pre-wrap; }
|
||||
code { background: var(--code); padding: 2px 4px; border-radius: 4px; }
|
||||
.back { color: var(--muted); text-decoration: none; font-size: 18px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div style="margin-bottom: 12px;"><a class="back" href="./index.html">← На главную</a></div>
|
||||
<h1>DAO: голосование на revoke/burn membership token (Devnet)</h1>
|
||||
<div class="muted">Governance program: <code id="govPid"></code></div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="row">
|
||||
<button id="connectBtn">Подключить Phantom</button>
|
||||
</div>
|
||||
<div id="walletInfo" class="muted">Кошелек: не подключен</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="row">
|
||||
<label>Realm
|
||||
<input id="realm" placeholder="Realm pubkey" />
|
||||
</label>
|
||||
<label>Governance
|
||||
<input id="governance" placeholder="Governance pubkey" />
|
||||
</label>
|
||||
<label>Community mint
|
||||
<input id="mint" placeholder="Mint pubkey" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Target owner
|
||||
<input id="targetOwner" placeholder="Кого лишаем governance token" />
|
||||
</label>
|
||||
<label>Amount
|
||||
<input id="amount" value="1" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button id="createVoteBtn">Create + SignOff + Vote</button>
|
||||
</div>
|
||||
<div id="proposalResult" class="muted"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="row">
|
||||
<label>Proposal
|
||||
<input id="proposal" placeholder="Proposal pubkey" />
|
||||
</label>
|
||||
<label>Proposal transaction
|
||||
<input id="proposalTx" placeholder="ProposalTransaction pubkey" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button id="executeBtn">Execute revoke</button>
|
||||
</div>
|
||||
<div class="muted">Если получите hold-up (`0x20d`) — дождитесь конца voting window/hold-up и повторите execute.</div>
|
||||
<div id="executeResult" class="muted"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import BN from "https://esm.sh/bn.js@5.2.1";
|
||||
import {
|
||||
Connection,
|
||||
PublicKey,
|
||||
Transaction,
|
||||
clusterApiUrl
|
||||
} from "https://esm.sh/@solana/web3.js@1.95.3";
|
||||
import {
|
||||
PROGRAM_VERSION_V3,
|
||||
Vote,
|
||||
YesNoVote,
|
||||
VoteType,
|
||||
InstructionData,
|
||||
AccountMetaData,
|
||||
withRevokeGoverningTokens,
|
||||
withCreateProposal,
|
||||
withInsertTransaction,
|
||||
withSignOffProposal,
|
||||
withCastVote,
|
||||
withExecuteTransaction,
|
||||
getTokenOwnerRecordAddress
|
||||
} from "https://esm.sh/@solana/spl-governance@0.3.28";
|
||||
|
||||
const GOVERNANCE_PROGRAM_ID = new PublicKey("GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw");
|
||||
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
|
||||
document.getElementById("govPid").textContent = GOVERNANCE_PROGRAM_ID.toBase58();
|
||||
|
||||
let wallet = null;
|
||||
let walletPubkey = null;
|
||||
|
||||
function out(id, html, cls = "muted") {
|
||||
const el = document.getElementById(id);
|
||||
el.className = cls;
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
function mustPubkey(id) {
|
||||
const raw = document.getElementById(id).value.trim();
|
||||
if (!raw) throw new Error(`Пустое поле: ${id}`);
|
||||
return new PublicKey(raw);
|
||||
}
|
||||
|
||||
function toGovernanceInstructionData(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),
|
||||
});
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
if (!window.solana || !window.solana.isPhantom) throw new Error("Phantom не найден");
|
||||
wallet = window.solana;
|
||||
const res = await wallet.connect();
|
||||
walletPubkey = new PublicKey(res.publicKey.toString());
|
||||
out("walletInfo", `Кошелек: <code>${walletPubkey.toBase58()}</code>`, "muted");
|
||||
}
|
||||
|
||||
async function sendIxs(ixs) {
|
||||
if (!walletPubkey) await connect();
|
||||
const tx = new Transaction().add(...ixs);
|
||||
tx.feePayer = walletPubkey;
|
||||
const bh = await connection.getLatestBlockhash("confirmed");
|
||||
tx.recentBlockhash = bh.blockhash;
|
||||
const signed = await wallet.signTransaction(tx);
|
||||
const sig = await connection.sendRawTransaction(signed.serialize(), { skipPreflight: false });
|
||||
await connection.confirmTransaction({ signature: sig, blockhash: bh.blockhash, lastValidBlockHeight: bh.lastValidBlockHeight }, "confirmed");
|
||||
return sig;
|
||||
}
|
||||
|
||||
async function createSignVote() {
|
||||
out("proposalResult", "Выполняю...", "muted");
|
||||
try {
|
||||
const realm = mustPubkey("realm");
|
||||
const governance = mustPubkey("governance");
|
||||
const mint = mustPubkey("mint");
|
||||
const targetOwner = mustPubkey("targetOwner");
|
||||
const amount = new BN(document.getElementById("amount").value.trim() || "1");
|
||||
if (amount.lten(0)) throw new Error("Amount должен быть > 0");
|
||||
|
||||
const proposerRecord = await getTokenOwnerRecordAddress(
|
||||
GOVERNANCE_PROGRAM_ID,
|
||||
realm,
|
||||
mint,
|
||||
walletPubkey
|
||||
);
|
||||
|
||||
const proposalName = `Revoke ${amount.toString()} from ${targetOwner.toBase58().slice(0, 8)}...`;
|
||||
const proposalDescription = "https://arweave.net/";
|
||||
|
||||
const ixCreateProposal = [];
|
||||
const proposalPk = await withCreateProposal(
|
||||
ixCreateProposal,
|
||||
GOVERNANCE_PROGRAM_ID,
|
||||
PROGRAM_VERSION_V3,
|
||||
realm,
|
||||
governance,
|
||||
proposerRecord,
|
||||
proposalName,
|
||||
proposalDescription,
|
||||
mint,
|
||||
walletPubkey,
|
||||
undefined,
|
||||
VoteType.SINGLE_CHOICE,
|
||||
["Approve"],
|
||||
true,
|
||||
walletPubkey
|
||||
);
|
||||
const txCreateProposal = await sendIxs(ixCreateProposal);
|
||||
|
||||
const ixRawRevoke = [];
|
||||
await withRevokeGoverningTokens(
|
||||
ixRawRevoke,
|
||||
GOVERNANCE_PROGRAM_ID,
|
||||
PROGRAM_VERSION_V3,
|
||||
realm,
|
||||
targetOwner,
|
||||
mint,
|
||||
governance,
|
||||
amount
|
||||
);
|
||||
const revokeInstructionData = toGovernanceInstructionData(ixRawRevoke[0]);
|
||||
|
||||
const ixInsert = [];
|
||||
const proposalTxPk = await withInsertTransaction(
|
||||
ixInsert,
|
||||
GOVERNANCE_PROGRAM_ID,
|
||||
PROGRAM_VERSION_V3,
|
||||
governance,
|
||||
proposalPk,
|
||||
proposerRecord,
|
||||
walletPubkey,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
[revokeInstructionData],
|
||||
walletPubkey
|
||||
);
|
||||
const txInsert = await sendIxs(ixInsert);
|
||||
|
||||
const ixSignOff = [];
|
||||
withSignOffProposal(
|
||||
ixSignOff,
|
||||
GOVERNANCE_PROGRAM_ID,
|
||||
PROGRAM_VERSION_V3,
|
||||
realm,
|
||||
governance,
|
||||
proposalPk,
|
||||
walletPubkey,
|
||||
undefined,
|
||||
proposerRecord
|
||||
);
|
||||
const txSignOff = await sendIxs(ixSignOff);
|
||||
|
||||
const ixVote = [];
|
||||
const vote = Vote.fromYesNoVote(YesNoVote.Yes);
|
||||
const voteRecordPk = await withCastVote(
|
||||
ixVote,
|
||||
GOVERNANCE_PROGRAM_ID,
|
||||
PROGRAM_VERSION_V3,
|
||||
realm,
|
||||
governance,
|
||||
proposalPk,
|
||||
proposerRecord,
|
||||
proposerRecord,
|
||||
walletPubkey,
|
||||
mint,
|
||||
vote,
|
||||
walletPubkey
|
||||
);
|
||||
const txVote = await sendIxs(ixVote);
|
||||
|
||||
document.getElementById("proposal").value = proposalPk.toBase58();
|
||||
document.getElementById("proposalTx").value = proposalTxPk.toBase58();
|
||||
|
||||
out(
|
||||
"proposalResult",
|
||||
`Proposal: <code>${proposalPk.toBase58()}</code><br/>` +
|
||||
`ProposalTx: <code>${proposalTxPk.toBase58()}</code><br/>` +
|
||||
`VoteRecord: <code>${voteRecordPk.toBase58()}</code><br/>` +
|
||||
`Tx create: <code>${txCreateProposal}</code><br/>` +
|
||||
`Tx insert: <code>${txInsert}</code><br/>` +
|
||||
`Tx signOff: <code>${txSignOff}</code><br/>` +
|
||||
`Tx vote: <code>${txVote}</code>`,
|
||||
"ok"
|
||||
);
|
||||
} catch (e) {
|
||||
out("proposalResult", String(e?.message || e), "err");
|
||||
}
|
||||
}
|
||||
|
||||
async function executeRevoke() {
|
||||
out("executeResult", "Выполняю execute...", "muted");
|
||||
try {
|
||||
const governance = mustPubkey("governance");
|
||||
const proposal = mustPubkey("proposal");
|
||||
const proposalTx = mustPubkey("proposalTx");
|
||||
const realm = mustPubkey("realm");
|
||||
const mint = mustPubkey("mint");
|
||||
const targetOwner = mustPubkey("targetOwner");
|
||||
const amount = new BN(document.getElementById("amount").value.trim() || "1");
|
||||
|
||||
const ixRawRevoke = [];
|
||||
await withRevokeGoverningTokens(
|
||||
ixRawRevoke,
|
||||
GOVERNANCE_PROGRAM_ID,
|
||||
PROGRAM_VERSION_V3,
|
||||
realm,
|
||||
targetOwner,
|
||||
mint,
|
||||
governance,
|
||||
amount
|
||||
);
|
||||
const revokeInstructionData = toGovernanceInstructionData(ixRawRevoke[0]);
|
||||
|
||||
const ixExecute = [];
|
||||
await withExecuteTransaction(
|
||||
ixExecute,
|
||||
GOVERNANCE_PROGRAM_ID,
|
||||
PROGRAM_VERSION_V3,
|
||||
governance,
|
||||
proposal,
|
||||
proposalTx,
|
||||
[revokeInstructionData]
|
||||
);
|
||||
const sig = await sendIxs(ixExecute);
|
||||
out("executeResult", `Execute success. Tx: <code>${sig}</code>`, "ok");
|
||||
} catch (e) {
|
||||
const msg = String(e?.message || e);
|
||||
out("executeResult", msg, "err");
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("connectBtn").addEventListener("click", async () => {
|
||||
try { await connect(); } catch (e) { out("walletInfo", String(e?.message || e), "err"); }
|
||||
});
|
||||
document.getElementById("createVoteBtn").addEventListener("click", createSignVote);
|
||||
document.getElementById("executeBtn").addEventListener("click", executeRevoke);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -61,6 +61,11 @@
|
||||
<div class="muted">Выдача лимитов менеджерам в USD для добавления билетов в очередь 1/2.</div>
|
||||
</a>
|
||||
|
||||
<a class="card" href="./dao_revoke_vote.html">
|
||||
<h3>DAO revoke governance token</h3>
|
||||
<div class="muted">UI для proposal/vote/execute на отзыв (burn/revoke) membership governance токенов.</div>
|
||||
</a>
|
||||
|
||||
<a class="card" href="./manager_tools.html">
|
||||
<h3>Инструменты менеджера</h3>
|
||||
<div class="muted">Показ лимитов менеджера и создание билетов в очередь 1/2 в USD.</div>
|
||||
|
||||
@ -142,9 +142,9 @@
|
||||
return s.includes("notenoughinflowforstep") || s.includes("0x177a");
|
||||
}
|
||||
function parsePythPriceUpdateV2(data) {
|
||||
const price = readI64(data, 74);
|
||||
const exponent = readI32(data, 90);
|
||||
const publishTime = readI64(data, 94);
|
||||
const price = readI64(data, 73);
|
||||
const exponent = readI32(data, 89);
|
||||
const publishTime = readI64(data, 93);
|
||||
if (price <= 0n) throw new Error("Оракул вернул некорректную цену");
|
||||
let num = price * 100n;
|
||||
let den = 1n;
|
||||
|
||||
@ -3,7 +3,7 @@ pub const USER_PDA_SEED_PREFIX: &str = "login=";
|
||||
// (в частности, сценарии ротации root key с дополнительной подписью старого ключа).
|
||||
pub const USER_PDA_SPACE: usize = 768;
|
||||
|
||||
pub const REGISTRATION_FEE_RECEIVER: &str = "6bFc5Gz5qF172GQhK5HpDbWs8F6qcSxdHn5XqAstf1fY";
|
||||
pub const REGISTRATION_FEE_RECEIVER: &str = "9vXFoN9ngfN1gpqQ3HT5n3y9Wp2r7HnSQckirgwVwWwb";
|
||||
pub const REGISTRATION_FEE_LAMPORTS: u64 = 10_000_000; // 0.01 SOL
|
||||
|
||||
pub const LIMIT_STEP: u64 = 10_000;
|
||||
|
||||
@ -5,8 +5,8 @@ use anchor_lang::solana_program::{
|
||||
hash::hashv,
|
||||
instruction::Instruction,
|
||||
program::invoke,
|
||||
sysvar::instructions::{load_current_index_checked, load_instruction_at_checked},
|
||||
system_instruction,
|
||||
sysvar::instructions::{load_current_index_checked, load_instruction_at_checked},
|
||||
};
|
||||
use common::utils::{create_pda, safe_read_pda, write_to_pda, ErrCode};
|
||||
use std::str::FromStr;
|
||||
@ -221,7 +221,11 @@ pub fn update_user_pda(ctx: Context<UpdateUserPda>, args: UpdateUserPdaArgs) ->
|
||||
old_record.created_at_ms == args.created_at_ms,
|
||||
ErrCode::ImmutableFieldChanged
|
||||
);
|
||||
require_keys_eq!(old_record.root_key, args.root_key, ErrCode::ImmutableFieldChanged);
|
||||
require_keys_eq!(
|
||||
old_record.root_key,
|
||||
args.root_key,
|
||||
ErrCode::ImmutableFieldChanged
|
||||
);
|
||||
require!(
|
||||
args.version == old_record.version.saturating_add(1),
|
||||
ErrCode::InvalidVersion
|
||||
@ -455,18 +459,18 @@ fn verify_record_signature(
|
||||
let provided_sig = vec_to_signature(signature)?;
|
||||
let msg_hash = hashv(&[unsigned]);
|
||||
|
||||
let current_ix_index =
|
||||
load_current_index_checked(instructions_sysvar).map_err(|_| error!(ErrCode::InvalidSignature))?;
|
||||
let current_ix_index = load_current_index_checked(instructions_sysvar)
|
||||
.map_err(|_| error!(ErrCode::InvalidSignature))?;
|
||||
require!(current_ix_index > 0, ErrCode::InvalidSignature);
|
||||
let ed_ix = load_instruction_at_checked(
|
||||
(current_ix_index - 1) as usize,
|
||||
instructions_sysvar,
|
||||
)
|
||||
.map_err(|_| error!(ErrCode::InvalidSignature))?;
|
||||
let ed_ix = load_instruction_at_checked((current_ix_index - 1) as usize, instructions_sysvar)
|
||||
.map_err(|_| error!(ErrCode::InvalidSignature))?;
|
||||
|
||||
let parsed = parse_ed25519_ix(&ed_ix)?;
|
||||
require_keys_eq!(parsed.pubkey, *root_key, ErrCode::InvalidSignature);
|
||||
require!(parsed.message == msg_hash.as_ref(), ErrCode::InvalidSignature);
|
||||
require!(
|
||||
parsed.message == msg_hash.as_ref(),
|
||||
ErrCode::InvalidSignature
|
||||
);
|
||||
require!(parsed.signature == provided_sig, ErrCode::InvalidSignature);
|
||||
|
||||
Ok(parsed.signature)
|
||||
@ -479,7 +483,11 @@ struct ParsedEd25519 {
|
||||
}
|
||||
|
||||
fn parse_ed25519_ix(ix: &Instruction) -> Result<ParsedEd25519> {
|
||||
require_keys_eq!(ix.program_id, ed25519_program::id(), ErrCode::InvalidSignature);
|
||||
require_keys_eq!(
|
||||
ix.program_id,
|
||||
ed25519_program::id(),
|
||||
ErrCode::InvalidSignature
|
||||
);
|
||||
|
||||
let data = &ix.data;
|
||||
require!(data.len() >= 16, ErrCode::InvalidSignature);
|
||||
@ -554,7 +562,10 @@ fn validate_login(login: &str) -> Result<()> {
|
||||
|
||||
fn validate_fields(fields: &UserMutableFields) -> Result<()> {
|
||||
if fields.is_server {
|
||||
require!(!fields.server_address.is_empty(), ErrCode::InvalidRecordData);
|
||||
require!(
|
||||
!fields.server_address.is_empty(),
|
||||
ErrCode::InvalidRecordData
|
||||
);
|
||||
require!(
|
||||
fields.server_address.as_bytes().len() <= u8::MAX as usize,
|
||||
ErrCode::InvalidRecordData
|
||||
@ -568,7 +579,10 @@ fn validate_fields(fields: &UserMutableFields) -> Result<()> {
|
||||
);
|
||||
for login in &fields.connection_servers {
|
||||
require!(!login.is_empty(), ErrCode::InvalidRecordData);
|
||||
require!(login.as_bytes().len() <= u8::MAX as usize, ErrCode::InvalidRecordData);
|
||||
require!(
|
||||
login.as_bytes().len() <= u8::MAX as usize,
|
||||
ErrCode::InvalidRecordData
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -590,7 +604,10 @@ fn transfer_lamports<'info>(
|
||||
return Ok(());
|
||||
}
|
||||
let ix = system_instruction::transfer(payer.key, recipient.key, lamports);
|
||||
invoke(&ix, &[payer.clone(), recipient.clone(), system_program.clone()])?;
|
||||
invoke(
|
||||
&ix,
|
||||
&[payer.clone(), recipient.clone(), system_program.clone()],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -629,7 +646,9 @@ fn vec_to_hash32(input: &[u8]) -> Result<[u8; 32]> {
|
||||
}
|
||||
|
||||
fn read_u8(data: &[u8], cursor: &mut usize) -> Result<u8> {
|
||||
let v = *data.get(*cursor).ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
let v = *data
|
||||
.get(*cursor)
|
||||
.ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
*cursor += 1;
|
||||
Ok(v)
|
||||
}
|
||||
@ -638,7 +657,9 @@ fn read_u16(data: &[u8], cursor: &mut usize) -> Result<u16> {
|
||||
let end = cursor
|
||||
.checked_add(2)
|
||||
.ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
let slice = data.get(*cursor..end).ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
let slice = data
|
||||
.get(*cursor..end)
|
||||
.ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
*cursor = end;
|
||||
Ok(u16::from_le_bytes([slice[0], slice[1]]))
|
||||
}
|
||||
@ -647,7 +668,9 @@ fn read_u32(data: &[u8], cursor: &mut usize) -> Result<u32> {
|
||||
let end = cursor
|
||||
.checked_add(4)
|
||||
.ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
let slice = data.get(*cursor..end).ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
let slice = data
|
||||
.get(*cursor..end)
|
||||
.ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
*cursor = end;
|
||||
Ok(u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]))
|
||||
}
|
||||
@ -656,7 +679,9 @@ fn read_u64(data: &[u8], cursor: &mut usize) -> Result<u64> {
|
||||
let end = cursor
|
||||
.checked_add(8)
|
||||
.ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
let slice = data.get(*cursor..end).ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
let slice = data
|
||||
.get(*cursor..end)
|
||||
.ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
*cursor = end;
|
||||
Ok(u64::from_le_bytes([
|
||||
slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], slice[6], slice[7],
|
||||
@ -667,7 +692,9 @@ fn read_fixed_32(data: &[u8], cursor: &mut usize) -> Result<[u8; 32]> {
|
||||
let end = cursor
|
||||
.checked_add(32)
|
||||
.ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
let slice = data.get(*cursor..end).ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
let slice = data
|
||||
.get(*cursor..end)
|
||||
.ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
*cursor = end;
|
||||
let mut out = [0u8; 32];
|
||||
out.copy_from_slice(slice);
|
||||
@ -678,7 +705,9 @@ fn read_fixed_64(data: &[u8], cursor: &mut usize) -> Result<[u8; 64]> {
|
||||
let end = cursor
|
||||
.checked_add(64)
|
||||
.ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
let slice = data.get(*cursor..end).ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
let slice = data
|
||||
.get(*cursor..end)
|
||||
.ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
*cursor = end;
|
||||
let mut out = [0u8; 64];
|
||||
out.copy_from_slice(slice);
|
||||
@ -690,7 +719,9 @@ fn read_len_prefixed_string(data: &[u8], cursor: &mut usize) -> Result<String> {
|
||||
let end = cursor
|
||||
.checked_add(len)
|
||||
.ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
let slice = data.get(*cursor..end).ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
let slice = data
|
||||
.get(*cursor..end)
|
||||
.ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
*cursor = end;
|
||||
let value = std::str::from_utf8(slice).map_err(|_| error!(ErrCode::InvalidRecordData))?;
|
||||
Ok(value.to_string())
|
||||
|
||||
60
shine/scripts/dao/README.md
Normal file
60
shine/scripts/dao/README.md
Normal file
@ -0,0 +1,60 @@
|
||||
# DAO scripts (актуальные)
|
||||
|
||||
## 1) Проверка конфигурации
|
||||
|
||||
```bash
|
||||
scripts/dao/create_realm_dao_full_test.sh scripts/dao/dao.config.env
|
||||
```
|
||||
|
||||
## 2) Реальное создание FULL DAO
|
||||
|
||||
```bash
|
||||
node scripts/dao/create_realm_dao_full_build_exec.js scripts/dao/dao.config.env
|
||||
```
|
||||
|
||||
Что делает:
|
||||
|
||||
1. Создает governance mint (SPL, decimals=0, supply из конфига).
|
||||
2. Добавляет on-chain metadata для mint (URI и картинка из Arweave).
|
||||
3. Создает Realm / Governance / Native Treasury.
|
||||
4. Депозитит governance токены в Realm.
|
||||
5. Пишет отчеты в `scripts/dao/runs/*.json` и `*.txt`.
|
||||
|
||||
## 3) Revoke/Burn membership токенов
|
||||
|
||||
### Вариант A (рекомендуется): через DAO голосование
|
||||
|
||||
```bash
|
||||
node scripts/dao/propose_vote_execute_revoke_full_exec.js \
|
||||
scripts/dao/dao.config.env \
|
||||
<REALM_PUBKEY> \
|
||||
<GOVERNANCE_PUBKEY> \
|
||||
<MINT_PUBKEY> \
|
||||
<TARGET_OWNER_PUBKEY> \
|
||||
[AMOUNT]
|
||||
```
|
||||
|
||||
Скрипт делает полный цикл:
|
||||
|
||||
1. `create proposal`
|
||||
2. `insert revoke instruction`
|
||||
3. `sign off`
|
||||
4. `cast vote`
|
||||
5. `execute`
|
||||
|
||||
### Вариант B (технический/админский): прямой revoke
|
||||
|
||||
```bash
|
||||
node scripts/dao/revoke_member_token_full_exec.js \
|
||||
scripts/dao/dao.config.env \
|
||||
<REALM_PUBKEY> \
|
||||
<MINT_PUBKEY> \
|
||||
<TARGET_OWNER_PUBKEY> \
|
||||
[AMOUNT]
|
||||
```
|
||||
|
||||
Важное:
|
||||
|
||||
1. Для `RevokeGoverningTokens` токен должен быть membership-типом (в full-скрипте это уже так).
|
||||
2. Для сценария “только DAO голосованием” используйте вариант A.
|
||||
3. Вариант B оставлен как технический инструмент.
|
||||
456
shine/scripts/dao/create_realm_dao_full_build_exec.js
Executable file
456
shine/scripts/dao/create_realm_dao_full_build_exec.js
Executable file
@ -0,0 +1,456 @@
|
||||
#!/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,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
Transaction,
|
||||
sendAndConfirmTransaction,
|
||||
clusterApiUrl,
|
||||
} = 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");
|
||||
|
||||
function parseEnvConfig(configPath) {
|
||||
const raw = fs.readFileSync(configPath, "utf8");
|
||||
const out = {};
|
||||
for (const line of raw.split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||
const eq = trimmed.indexOf("=");
|
||||
if (eq === -1) continue;
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
let val = trimmed.slice(eq + 1).trim();
|
||||
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
||||
val = val.slice(1, -1);
|
||||
}
|
||||
val = val.replace(/\$HOME/g, process.env.HOME || "");
|
||||
out[key] = val;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function assertRequired(cfg, key) {
|
||||
if (!cfg[key]) throw new Error(`В конфиге отсутствует обязательный параметр: ${key}`);
|
||||
}
|
||||
|
||||
function loadKeypair(filePath) {
|
||||
const arr = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
return Keypair.fromSecretKey(Uint8Array.from(arr));
|
||||
}
|
||||
|
||||
function lamportsToSol(lamports) {
|
||||
return Number(lamports) / 1_000_000_000;
|
||||
}
|
||||
|
||||
function nowStamp() {
|
||||
const d = new Date();
|
||||
const p = (n) => String(n).padStart(2, "0");
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}_${p(d.getHours())}-${p(
|
||||
d.getMinutes()
|
||||
)}-${p(d.getSeconds())}`;
|
||||
}
|
||||
|
||||
async function askYes() {
|
||||
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";
|
||||
}
|
||||
|
||||
function ensureArweaveUri(name, uri) {
|
||||
if (!uri) throw new Error(`${name} пустой`);
|
||||
if (!(uri.startsWith("https://arweave.net/") || uri.startsWith("ar://"))) {
|
||||
throw new Error(`${name} должен указывать на Arweave (https://arweave.net/... или ar://...)`);
|
||||
}
|
||||
}
|
||||
|
||||
async function attachTokenMetadataViaUmi(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(clusterApiUrl(cluster));
|
||||
const umiSigner = createSignerFromKeypair(umi, fromWeb3JsKeypair(issuer));
|
||||
const umiMintSigner = createSignerFromKeypair(umi, fromWeb3JsKeypair(mintKeypair));
|
||||
umi.use(signerIdentity(umiSigner));
|
||||
umi.use(mplTokenMetadata());
|
||||
|
||||
const builder = createV1(umi, {
|
||||
mint: umiMintSigner,
|
||||
authority: umiSigner,
|
||||
payer: umiSigner,
|
||||
updateAuthority: umiSigner,
|
||||
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 res = await builder.sendAndConfirm(umi);
|
||||
const sig = Buffer.from(res.signature).toString("base64");
|
||||
return sig;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const configPath = process.argv[2]
|
||||
? path.resolve(process.argv[2])
|
||||
: path.resolve(__dirname, "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(clusterApiUrl(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 (ограничение Realms)");
|
||||
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
|
||||
);
|
||||
const realmExists = (await connection.getAccountInfo(realmPda)) !== null;
|
||||
if (realmExists) throw new Error(`Realm уже существует: ${realmPda.toBase58()}`);
|
||||
|
||||
const startBalance = await connection.getBalance(issuer.publicKey, "confirmed");
|
||||
console.log("============================================================");
|
||||
console.log("СОЗДАНИЕ DAO (FULL)");
|
||||
console.log("------------------------------------------------------------");
|
||||
console.log("Сеть: ", cluster);
|
||||
console.log("Realm name: ", cfg.DAO_REALM_NAME);
|
||||
console.log("Realm PDA: ", realmPda.toBase58());
|
||||
console.log("Governance program: ", governanceProgramId.toBase58());
|
||||
console.log("Issuer: ", issuer.publicKey.toBase58());
|
||||
console.log("Баланс до старта: ", `${lamportsToSol(startBalance)} SOL`);
|
||||
console.log("Token name/symbol: ", `${cfg.DAO_GOV_NFT_NAME} / ${cfg.DAO_GOV_NFT_SYMBOL}`);
|
||||
console.log("Token supply: ", supply);
|
||||
console.log("Voting time sec: ", votingTimeSec);
|
||||
console.log("Threshold %: ", thresholdPct);
|
||||
console.log("Arweave metadata URI:", cfg.DAO_GOV_TOKEN_METADATA_URI);
|
||||
console.log("Arweave image URL: ", cfg.DAO_GOV_TOKEN_IMAGE_URL);
|
||||
console.log("============================================================");
|
||||
|
||||
const ok = await askYes();
|
||||
if (!ok) {
|
||||
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 attachTokenMetadataViaUmi(
|
||||
cfg,
|
||||
cluster,
|
||||
issuer,
|
||||
mintKeypair.publicKey,
|
||||
mintKeypair
|
||||
);
|
||||
|
||||
const programVersion = PROGRAM_VERSION_V3;
|
||||
const ixRealm = [];
|
||||
const communityTokenConfig = new GoverningTokenConfigAccountArgs({
|
||||
voterWeightAddin: undefined,
|
||||
maxVoterWeightAddin: undefined,
|
||||
tokenType: GoverningTokenType.Membership,
|
||||
});
|
||||
const realmPk = await withCreateRealm(
|
||||
ixRealm,
|
||||
governanceProgramId,
|
||||
programVersion,
|
||||
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,
|
||||
programVersion,
|
||||
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,
|
||||
programVersion,
|
||||
realmPk,
|
||||
realmPk,
|
||||
governanceConfig,
|
||||
tokenOwnerRecordPk,
|
||||
issuer.publicKey,
|
||||
issuer.publicKey
|
||||
);
|
||||
const treasuryPk = await withCreateNativeTreasury(ixGov, governanceProgramId, programVersion, governancePk, issuer.publicKey);
|
||||
const sigGov = await sendAndConfirmTransaction(connection, new Transaction().add(...ixGov), [issuer], {
|
||||
commitment: "confirmed",
|
||||
});
|
||||
|
||||
// Для DAO revoke governing tokens mint authority должен быть у governance PDA.
|
||||
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,
|
||||
programVersion,
|
||||
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,
|
||||
votingTimeSec,
|
||||
thresholdPercent: thresholdPct,
|
||||
tokenSupply: supply,
|
||||
startBalanceLamports: startBalance,
|
||||
endBalanceLamports: endBalance,
|
||||
spentLamports,
|
||||
startBalanceSol: lamportsToSol(startBalance),
|
||||
endBalanceSol: lamportsToSol(endBalance),
|
||||
spentSol: lamportsToSol(spentLamports),
|
||||
};
|
||||
const reportDir = path.resolve(__dirname, "runs");
|
||||
fs.mkdirSync(reportDir, { recursive: true });
|
||||
const reportBaseName = `${nowStamp()}_${cfg.DAO_REALM_NAME.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 80)}_full`;
|
||||
const reportJsonPath = path.join(reportDir, `${reportBaseName}.json`);
|
||||
const reportTxtPath = path.join(reportDir, `${reportBaseName}.txt`);
|
||||
fs.writeFileSync(reportJsonPath, JSON.stringify(report, null, 2));
|
||||
fs.writeFileSync(
|
||||
reportTxtPath,
|
||||
[
|
||||
`createdAt: ${report.createdAt}`,
|
||||
`cluster: ${report.cluster}`,
|
||||
`realmName: ${report.realmName}`,
|
||||
`governanceProgramId: ${report.governanceProgramId}`,
|
||||
`issuer: ${report.issuer}`,
|
||||
`communityMint: ${report.communityMint}`,
|
||||
`issuerAta: ${report.issuerAta}`,
|
||||
`realm: ${report.realm}`,
|
||||
`tokenOwnerRecord: ${report.tokenOwnerRecord}`,
|
||||
`governance: ${report.governance}`,
|
||||
`nativeTreasury: ${report.nativeTreasury}`,
|
||||
`metadataUri: ${report.metadataUri}`,
|
||||
`imageUrl: ${report.imageUrl}`,
|
||||
`txMint: ${report.txMint}`,
|
||||
`txMetadata: ${report.txMetadata}`,
|
||||
`txRealm: ${report.txRealm}`,
|
||||
`txDeposit: ${report.txDeposit}`,
|
||||
`txGovernanceTreasury: ${report.txGovernanceTreasury}`,
|
||||
`txSetMintAuthorityToGovernance: ${report.txSetMintAuthorityToGovernance}`,
|
||||
`txSetRealmAuthority: ${report.txSetRealmAuthority}`,
|
||||
`tokenSupply: ${report.tokenSupply}`,
|
||||
`votingTimeSec: ${report.votingTimeSec}`,
|
||||
`thresholdPercent: ${report.thresholdPercent}`,
|
||||
`startBalanceSol: ${report.startBalanceSol}`,
|
||||
`endBalanceSol: ${report.endBalanceSol}`,
|
||||
`spentSol: ${report.spentSol}`,
|
||||
`configPath: ${report.configPath}`,
|
||||
].join("\n") + "\n"
|
||||
);
|
||||
|
||||
console.log("============================================================");
|
||||
console.log("DAO FULL СОЗДАНО");
|
||||
console.log("------------------------------------------------------------");
|
||||
console.log("Community mint (SPL + metadata): ", mintKeypair.publicKey.toBase58());
|
||||
console.log("Realm: ", realmPk.toBase58());
|
||||
console.log("Governance: ", governancePk.toBase58());
|
||||
console.log("Native treasury PDA: ", treasuryPk.toBase58());
|
||||
console.log("Tx mint: ", sigMint);
|
||||
console.log("Tx metadata: ", sigMetadata);
|
||||
console.log("Tx realm: ", sigRealm);
|
||||
console.log("Tx deposit: ", sigDeposit);
|
||||
console.log("Tx governance+treasury: ", sigGov);
|
||||
console.log("Tx set mint authority -> governance: ", sigSetMintAuthority);
|
||||
console.log("Tx set realm authority -> governance: ", sigSetRealmAuthority);
|
||||
console.log("Баланс после: ", `${lamportsToSol(endBalance)} SOL`);
|
||||
console.log("Потрачено: ", `${lamportsToSol(spentLamports)} SOL`);
|
||||
console.log("Отчёт JSON: ", reportJsonPath);
|
||||
console.log("Отчёт TXT: ", reportTxtPath);
|
||||
console.log("============================================================");
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("Ошибка создания DAO FULL:", e?.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
106
shine/scripts/dao/create_realm_dao_full_test.sh
Executable file
106
shine/scripts/dao/create_realm_dao_full_test.sh
Executable file
@ -0,0 +1,106 @@
|
||||
#!/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_REALM_NAME}" == *"TEMPLATE"* || "${DAO_REALM_NAME}" == *"CHANGE_ME"* ]]; then
|
||||
echo "Ошибка: похоже, не заменили тестовое имя DAO_REALM_NAME"
|
||||
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
|
||||
)"
|
||||
|
||||
if [[ -z "$REALM_PDA" ]]; then
|
||||
echo "Ошибка: не удалось вычислить PDA realm."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REALM_EXISTS="no"
|
||||
if solana account "$REALM_PDA" --url "$DAO_CLUSTER" >/dev/null 2>&1; then
|
||||
REALM_EXISTS="yes"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
============================================================
|
||||
ПРЕДСТАРТОВАЯ ПРОВЕРКА DAO (Realms)
|
||||
------------------------------------------------------------
|
||||
Сеть: $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}
|
||||
NFT name: $DAO_GOV_NFT_NAME
|
||||
NFT symbol: $DAO_GOV_NFT_SYMBOL
|
||||
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
|
||||
echo "Проверка пройдена."
|
||||
echo "Этот скрипт делает только preflight-валидацию."
|
||||
echo "Для реального создания DAO запускайте исполняющий скрипт:"
|
||||
echo "node scripts/dao/create_realm_dao_full_build_exec.js scripts/dao/dao.config.env"
|
||||
37
shine/scripts/dao/dao.config.env
Normal file
37
shine/scripts/dao/dao.config.env
Normal file
@ -0,0 +1,37 @@
|
||||
# Конфиг создания DAO (Devnet/Mainnet)
|
||||
# ВАЖНО: перед каждым новым тестом меняйте DAO_REALM_NAME на новый.
|
||||
|
||||
# devnet | mainnet-beta
|
||||
DAO_CLUSTER="devnet"
|
||||
|
||||
# Человекочитаемое имя DAO (должно быть новым для каждого теста)
|
||||
DAO_REALM_NAME="Devnet DAO Full-1 2026-05-09"
|
||||
|
||||
# Название и символ governance NFT
|
||||
DAO_GOV_NFT_NAME="Shine Governance NFT"
|
||||
DAO_GOV_NFT_SYMBOL="SHINE-GOV"
|
||||
DAO_GOV_TOKEN_DESCRIPTION="Governance token for SHiNE DAO"
|
||||
DAO_GOV_TOKEN_IMAGE_URL="https://arweave.net/6cMl-qV-7vpgXSdOR5MVnMEPMHgLkRdfgdO43I9z5ek"
|
||||
DAO_GOV_TOKEN_METADATA_URI="https://arweave.net/6cMl-qV-7vpgXSdOR5MVnMEPMHgLkRdfgdO43I9z5ek"
|
||||
|
||||
|
||||
# Количество стартовых governance NFT
|
||||
DAO_GOV_NFT_SUPPLY="10"
|
||||
|
||||
# Время голосования в секундах (для теста 1 час = 3600)
|
||||
DAO_VOTING_TIME_SEC="3600"
|
||||
|
||||
# Минимальный порог "за" в процентах (больше половины = 51)
|
||||
DAO_APPROVAL_THRESHOLD_PERCENT="51"
|
||||
|
||||
# Кошелек эмиттера/инициализатора DAO
|
||||
DAO_ISSUER_KEYPAIR="$HOME/.config/solana/phantomWallet.json"
|
||||
|
||||
# Кто имеет право на force revoke/burn membership-токенов через RevokeGoverningTokens.
|
||||
# На тестовом этапе можно оставить тот же кошелек, что и issuer.
|
||||
# В production обычно передают управление через governance-процедуры DAO.
|
||||
DAO_REVOKE_AUTHORITY_KEYPAIR="$HOME/.config/solana/phantomWallet.json"
|
||||
|
||||
# Текущий governance program (Realms, SPL Governance)
|
||||
# Devnet/Mainnet обычно используют одинаковый ID программы.
|
||||
SPL_GOVERNANCE_PROGRAM_ID="GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw"
|
||||
108
shine/scripts/dao/execute_revoke_transaction_full_exec.js
Normal file
108
shine/scripts/dao/execute_revoke_transaction_full_exec.js
Normal file
@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const BN = require("bn.js");
|
||||
const { Connection, Keypair, PublicKey, Transaction, sendAndConfirmTransaction, clusterApiUrl } = require("@solana/web3.js");
|
||||
const {
|
||||
PROGRAM_VERSION_V3,
|
||||
InstructionData,
|
||||
AccountMetaData,
|
||||
withRevokeGoverningTokens,
|
||||
withExecuteTransaction,
|
||||
} = 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 === -1) continue;
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
let val = trimmed.slice(eq + 1).trim();
|
||||
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
||||
val = val.slice(1, -1);
|
||||
}
|
||||
val = val.replace(/\$HOME/g, process.env.HOME || "");
|
||||
out[key] = val;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function loadKeypair(filePath) {
|
||||
const arr = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
return Keypair.fromSecretKey(Uint8Array.from(arr));
|
||||
}
|
||||
|
||||
function toGovernanceInstructionData(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),
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const configPath = process.argv[2] ? path.resolve(process.argv[2]) : path.resolve(__dirname, "dao.config.env");
|
||||
const realm = new PublicKey(process.argv[3]);
|
||||
const governance = new PublicKey(process.argv[4]);
|
||||
const proposal = new PublicKey(process.argv[5]);
|
||||
const proposalTx = new PublicKey(process.argv[6]);
|
||||
const mint = new PublicKey(process.argv[7]);
|
||||
const targetOwner = new PublicKey(process.argv[8]);
|
||||
const amount = new BN(process.argv[9] || "1");
|
||||
if (!process.argv[8]) {
|
||||
throw new Error(
|
||||
"Использование: node scripts/dao/execute_revoke_transaction_full_exec.js <config.env> <realm> <governance> <proposal> <proposal_tx> <mint> <target_owner> [amount]"
|
||||
);
|
||||
}
|
||||
const cfg = parseEnvConfig(configPath);
|
||||
const cluster = cfg.DAO_CLUSTER || "devnet";
|
||||
const governanceProgramId = new PublicKey(cfg.SPL_GOVERNANCE_PROGRAM_ID);
|
||||
const signer = loadKeypair(path.resolve(cfg.DAO_ISSUER_KEYPAIR));
|
||||
const connection = new Connection(clusterApiUrl(cluster), "confirmed");
|
||||
|
||||
const ixRawRevoke = [];
|
||||
await withRevokeGoverningTokens(
|
||||
ixRawRevoke,
|
||||
governanceProgramId,
|
||||
PROGRAM_VERSION_V3,
|
||||
realm,
|
||||
targetOwner,
|
||||
mint,
|
||||
governance,
|
||||
amount
|
||||
);
|
||||
const revokeInstructionData = toGovernanceInstructionData(ixRawRevoke[0]);
|
||||
|
||||
const ixExecute = [];
|
||||
await withExecuteTransaction(
|
||||
ixExecute,
|
||||
governanceProgramId,
|
||||
PROGRAM_VERSION_V3,
|
||||
governance,
|
||||
proposal,
|
||||
proposalTx,
|
||||
[revokeInstructionData]
|
||||
);
|
||||
const sig = await sendAndConfirmTransaction(connection, new Transaction().add(...ixExecute), [signer], {
|
||||
commitment: "confirmed",
|
||||
});
|
||||
console.log("Execute success. Tx:", sig);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("Ошибка execute revoke:", e?.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
399
shine/scripts/dao/propose_vote_execute_revoke_full_exec.js
Executable file
399
shine/scripts/dao/propose_vote_execute_revoke_full_exec.js
Executable file
@ -0,0 +1,399 @@
|
||||
#!/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,
|
||||
PublicKey,
|
||||
Transaction,
|
||||
sendAndConfirmTransaction,
|
||||
clusterApiUrl,
|
||||
} = require("@solana/web3.js");
|
||||
const {
|
||||
PROGRAM_VERSION_V3,
|
||||
Vote,
|
||||
YesNoVote,
|
||||
VoteType,
|
||||
InstructionData,
|
||||
AccountMetaData,
|
||||
withRevokeGoverningTokens,
|
||||
withCreateProposal,
|
||||
withInsertTransaction,
|
||||
withSignOffProposal,
|
||||
withCastVote,
|
||||
withExecuteTransaction,
|
||||
withFinalizeVote,
|
||||
getTokenOwnerRecordAddress,
|
||||
getProposalTransactionAddress,
|
||||
} = 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 === -1) continue;
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
let val = trimmed.slice(eq + 1).trim();
|
||||
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
||||
val = val.slice(1, -1);
|
||||
}
|
||||
val = val.replace(/\$HOME/g, process.env.HOME || "");
|
||||
out[key] = val;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function loadKeypair(filePath) {
|
||||
const arr = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
return Keypair.fromSecretKey(Uint8Array.from(arr));
|
||||
}
|
||||
|
||||
function nowStamp() {
|
||||
const d = new Date();
|
||||
const p = (n) => String(n).padStart(2, "0");
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}_${p(d.getHours())}-${p(
|
||||
d.getMinutes()
|
||||
)}-${p(d.getSeconds())}`;
|
||||
}
|
||||
|
||||
async function askYes() {
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||
const answer = await new Promise((resolve) =>
|
||||
rl.question("Введите YES для proposal->vote->execute revoke: ", resolve)
|
||||
);
|
||||
rl.close();
|
||||
return answer.trim() === "YES";
|
||||
}
|
||||
|
||||
function toGovernanceInstructionData(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 classifyExecuteError(msg) {
|
||||
const s = String(msg || "").toLowerCase();
|
||||
if (s.includes("0x20d") || s.includes("hold up time")) {
|
||||
return "HOLD_UP_TIME";
|
||||
}
|
||||
if (s.includes("0x21d") || s.includes("invalid mint authority")) {
|
||||
return "INVALID_MINT_AUTHORITY";
|
||||
}
|
||||
return "OTHER";
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const configPath = process.argv[2]
|
||||
? path.resolve(process.argv[2])
|
||||
: path.resolve(__dirname, "dao.config.env");
|
||||
const realmStr = process.argv[3];
|
||||
const governanceStr = process.argv[4];
|
||||
const mintStr = process.argv[5];
|
||||
const targetOwnerStr = process.argv[6];
|
||||
const amountStr = process.argv[7] || "1";
|
||||
if (!realmStr || !governanceStr || !mintStr || !targetOwnerStr) {
|
||||
throw new Error(
|
||||
"Использование: node scripts/dao/propose_vote_execute_revoke_full_exec.js <config.env> <realm> <governance> <mint> <target_owner_pubkey> [amount]"
|
||||
);
|
||||
}
|
||||
|
||||
const cfg = parseEnvConfig(configPath);
|
||||
const cluster = cfg.DAO_CLUSTER || "devnet";
|
||||
const governanceProgramId = new PublicKey(cfg.SPL_GOVERNANCE_PROGRAM_ID);
|
||||
const proposerKpPath = cfg.DAO_ISSUER_KEYPAIR;
|
||||
if (!proposerKpPath) throw new Error("В конфиге нет DAO_ISSUER_KEYPAIR");
|
||||
const proposer = loadKeypair(path.resolve(proposerKpPath));
|
||||
|
||||
const realm = new PublicKey(realmStr);
|
||||
const governance = new PublicKey(governanceStr);
|
||||
const mint = new PublicKey(mintStr);
|
||||
const targetOwner = new PublicKey(targetOwnerStr);
|
||||
const amount = new BN(amountStr);
|
||||
if (amount.lten(0)) throw new Error("amount должен быть > 0");
|
||||
|
||||
const connection = new Connection(clusterApiUrl(cluster), "confirmed");
|
||||
const proposerRecord = await getTokenOwnerRecordAddress(
|
||||
governanceProgramId,
|
||||
realm,
|
||||
mint,
|
||||
proposer.publicKey
|
||||
);
|
||||
|
||||
console.log("============================================================");
|
||||
console.log("DAO REVOKE THROUGH VOTE");
|
||||
console.log("------------------------------------------------------------");
|
||||
console.log("Сеть: ", cluster);
|
||||
console.log("Governance program: ", governanceProgramId.toBase58());
|
||||
console.log("Realm: ", realm.toBase58());
|
||||
console.log("Governance: ", governance.toBase58());
|
||||
console.log("Mint: ", mint.toBase58());
|
||||
console.log("Target owner: ", targetOwner.toBase58());
|
||||
console.log("Amount: ", amount.toString());
|
||||
console.log("Proposer: ", proposer.publicKey.toBase58());
|
||||
console.log("Proposer record: ", proposerRecord.toBase58());
|
||||
console.log("============================================================");
|
||||
|
||||
const ok = await askYes();
|
||||
if (!ok) {
|
||||
console.log("Отменено пользователем.");
|
||||
return;
|
||||
}
|
||||
|
||||
const proposalName = `Revoke ${amount.toString()} from ${targetOwner
|
||||
.toBase58()
|
||||
.slice(0, 8)}...`;
|
||||
const proposalDescription = cfg.DAO_REVOKE_PROPOSAL_URI || cfg.DAO_GOV_TOKEN_METADATA_URI || "https://arweave.net/";
|
||||
|
||||
const ixCreateProposal = [];
|
||||
const proposalPk = await withCreateProposal(
|
||||
ixCreateProposal,
|
||||
governanceProgramId,
|
||||
PROGRAM_VERSION_V3,
|
||||
realm,
|
||||
governance,
|
||||
proposerRecord,
|
||||
proposalName,
|
||||
proposalDescription,
|
||||
mint,
|
||||
proposer.publicKey,
|
||||
undefined,
|
||||
VoteType.SINGLE_CHOICE,
|
||||
["Approve"],
|
||||
true,
|
||||
proposer.publicKey
|
||||
);
|
||||
const sigCreateProposal = await sendAndConfirmTransaction(
|
||||
connection,
|
||||
new Transaction().add(...ixCreateProposal),
|
||||
[proposer],
|
||||
{ commitment: "confirmed" }
|
||||
);
|
||||
|
||||
const ixRawRevoke = [];
|
||||
await withRevokeGoverningTokens(
|
||||
ixRawRevoke,
|
||||
governanceProgramId,
|
||||
PROGRAM_VERSION_V3,
|
||||
realm,
|
||||
targetOwner,
|
||||
mint,
|
||||
governance,
|
||||
amount
|
||||
);
|
||||
if (ixRawRevoke.length !== 1) throw new Error("Ожидалась одна инструкция revoke");
|
||||
const revokeInstructionData = toGovernanceInstructionData(ixRawRevoke[0]);
|
||||
|
||||
const ixInsert = [];
|
||||
const proposalTxPk = await withInsertTransaction(
|
||||
ixInsert,
|
||||
governanceProgramId,
|
||||
PROGRAM_VERSION_V3,
|
||||
governance,
|
||||
proposalPk,
|
||||
proposerRecord,
|
||||
proposer.publicKey,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
[revokeInstructionData],
|
||||
proposer.publicKey
|
||||
);
|
||||
const sigInsert = await sendAndConfirmTransaction(connection, new Transaction().add(...ixInsert), [proposer], {
|
||||
commitment: "confirmed",
|
||||
});
|
||||
|
||||
const ixSignOff = [];
|
||||
withSignOffProposal(
|
||||
ixSignOff,
|
||||
governanceProgramId,
|
||||
PROGRAM_VERSION_V3,
|
||||
realm,
|
||||
governance,
|
||||
proposalPk,
|
||||
proposer.publicKey,
|
||||
undefined,
|
||||
proposerRecord
|
||||
);
|
||||
const sigSignOff = await sendAndConfirmTransaction(connection, new Transaction().add(...ixSignOff), [proposer], {
|
||||
commitment: "confirmed",
|
||||
});
|
||||
|
||||
const ixVote = [];
|
||||
const vote = Vote.fromYesNoVote(YesNoVote.Yes);
|
||||
const voteRecordPk = await withCastVote(
|
||||
ixVote,
|
||||
governanceProgramId,
|
||||
PROGRAM_VERSION_V3,
|
||||
realm,
|
||||
governance,
|
||||
proposalPk,
|
||||
proposerRecord,
|
||||
proposerRecord,
|
||||
proposer.publicKey,
|
||||
mint,
|
||||
vote,
|
||||
proposer.publicKey
|
||||
);
|
||||
const sigVote = await sendAndConfirmTransaction(connection, new Transaction().add(...ixVote), [proposer], {
|
||||
commitment: "confirmed",
|
||||
});
|
||||
|
||||
const computedProposalTxPk = await getProposalTransactionAddress(
|
||||
governanceProgramId,
|
||||
PROGRAM_VERSION_V3,
|
||||
proposalPk,
|
||||
0,
|
||||
0
|
||||
);
|
||||
if (!computedProposalTxPk.equals(proposalTxPk)) {
|
||||
throw new Error("Несовпадение адреса proposal transaction");
|
||||
}
|
||||
|
||||
let sigFinalize = null;
|
||||
try {
|
||||
const ixFinalize = [];
|
||||
await withFinalizeVote(
|
||||
ixFinalize,
|
||||
governanceProgramId,
|
||||
PROGRAM_VERSION_V3,
|
||||
realm,
|
||||
governance,
|
||||
proposalPk,
|
||||
proposerRecord,
|
||||
mint
|
||||
);
|
||||
sigFinalize = await sendAndConfirmTransaction(connection, new Transaction().add(...ixFinalize), [proposer], {
|
||||
commitment: "confirmed",
|
||||
});
|
||||
} catch (_) {
|
||||
// Может быть уже tipped/succeeded без finalize.
|
||||
}
|
||||
|
||||
let sigExecute = null;
|
||||
let executeError = null;
|
||||
let executeErrorKind = null;
|
||||
try {
|
||||
const ixExecute = [];
|
||||
await withExecuteTransaction(
|
||||
ixExecute,
|
||||
governanceProgramId,
|
||||
PROGRAM_VERSION_V3,
|
||||
governance,
|
||||
proposalPk,
|
||||
proposalTxPk,
|
||||
[revokeInstructionData]
|
||||
);
|
||||
sigExecute = await sendAndConfirmTransaction(connection, new Transaction().add(...ixExecute), [proposer], {
|
||||
commitment: "confirmed",
|
||||
});
|
||||
} catch (e) {
|
||||
executeError = e?.message || String(e);
|
||||
executeErrorKind = classifyExecuteError(executeError);
|
||||
}
|
||||
|
||||
const report = {
|
||||
createdAt: new Date().toISOString(),
|
||||
cluster,
|
||||
configPath,
|
||||
governanceProgramId: governanceProgramId.toBase58(),
|
||||
realm: realm.toBase58(),
|
||||
governance: governance.toBase58(),
|
||||
mint: mint.toBase58(),
|
||||
targetOwner: targetOwner.toBase58(),
|
||||
amount: amount.toString(),
|
||||
proposer: proposer.publicKey.toBase58(),
|
||||
proposerRecord: proposerRecord.toBase58(),
|
||||
proposal: proposalPk.toBase58(),
|
||||
proposalTransaction: proposalTxPk.toBase58(),
|
||||
voteRecord: voteRecordPk.toBase58(),
|
||||
txCreateProposal: sigCreateProposal,
|
||||
txInsertTransaction: sigInsert,
|
||||
txSignOff: sigSignOff,
|
||||
txVote: sigVote,
|
||||
txFinalize: sigFinalize,
|
||||
txExecute: sigExecute,
|
||||
executeError,
|
||||
executeErrorKind,
|
||||
};
|
||||
|
||||
const reportDir = path.resolve(__dirname, "runs");
|
||||
fs.mkdirSync(reportDir, { recursive: true });
|
||||
const reportBaseName = `${nowStamp()}_revoke_${targetOwner.toBase58().slice(0, 10)}`;
|
||||
const reportJsonPath = path.join(reportDir, `${reportBaseName}.json`);
|
||||
const reportTxtPath = path.join(reportDir, `${reportBaseName}.txt`);
|
||||
fs.writeFileSync(reportJsonPath, JSON.stringify(report, null, 2));
|
||||
fs.writeFileSync(
|
||||
reportTxtPath,
|
||||
[
|
||||
`createdAt: ${report.createdAt}`,
|
||||
`cluster: ${report.cluster}`,
|
||||
`realm: ${report.realm}`,
|
||||
`governance: ${report.governance}`,
|
||||
`mint: ${report.mint}`,
|
||||
`targetOwner: ${report.targetOwner}`,
|
||||
`amount: ${report.amount}`,
|
||||
`proposer: ${report.proposer}`,
|
||||
`proposal: ${report.proposal}`,
|
||||
`proposalTransaction: ${report.proposalTransaction}`,
|
||||
`voteRecord: ${report.voteRecord}`,
|
||||
`txCreateProposal: ${report.txCreateProposal}`,
|
||||
`txInsertTransaction: ${report.txInsertTransaction}`,
|
||||
`txSignOff: ${report.txSignOff}`,
|
||||
`txVote: ${report.txVote}`,
|
||||
`txFinalize: ${report.txFinalize || "-"}`,
|
||||
`txExecute: ${report.txExecute || "-"}`,
|
||||
`executeError: ${report.executeError || "-"}`,
|
||||
`executeErrorKind: ${report.executeErrorKind || "-"}`,
|
||||
].join("\n") + "\n"
|
||||
);
|
||||
|
||||
console.log("============================================================");
|
||||
console.log("REVOKE ЧЕРЕЗ DAO ГОЛОСОВАНИЕ ВЫПОЛНЕН");
|
||||
console.log("------------------------------------------------------------");
|
||||
console.log("Proposal: ", proposalPk.toBase58());
|
||||
console.log("Proposal Tx: ", proposalTxPk.toBase58());
|
||||
console.log("Tx create proposal: ", sigCreateProposal);
|
||||
console.log("Tx insert revoke instruction: ", sigInsert);
|
||||
console.log("Tx sign off: ", sigSignOff);
|
||||
console.log("Tx cast vote: ", sigVote);
|
||||
if (sigFinalize) console.log("Tx finalize vote: ", sigFinalize);
|
||||
if (sigExecute) {
|
||||
console.log("Tx execute: ", sigExecute);
|
||||
} else {
|
||||
console.log("Execute сейчас не прошел (ожидание voting/hold-up):");
|
||||
console.log("Ошибка execute: ", executeError);
|
||||
if (executeErrorKind === "HOLD_UP_TIME") {
|
||||
console.log("Причина: ", "слишком рано для execute (hold-up / окно голосования еще не завершено)");
|
||||
} else if (executeErrorKind === "INVALID_MINT_AUTHORITY") {
|
||||
console.log("Причина: ", "community mint authority не передан на governance PDA при создании DAO");
|
||||
}
|
||||
console.log("Повтор execute через время этой командой:");
|
||||
console.log(
|
||||
`node scripts/dao/execute_revoke_transaction_full_exec.js ${configPath} ${realm.toBase58()} ${governance.toBase58()} ${proposalPk.toBase58()} ${proposalTxPk.toBase58()} ${mint.toBase58()} ${targetOwner.toBase58()} ${amount.toString()}`
|
||||
);
|
||||
}
|
||||
console.log("Отчёт JSON: ", reportJsonPath);
|
||||
console.log("Отчёт TXT: ", reportTxtPath);
|
||||
console.log("============================================================");
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("Ошибка proposal/vote/execute revoke:", e?.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
112
shine/scripts/dao/revoke_member_token_full_exec.js
Executable file
112
shine/scripts/dao/revoke_member_token_full_exec.js
Executable file
@ -0,0 +1,112 @@
|
||||
#!/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, PublicKey, Transaction, sendAndConfirmTransaction, clusterApiUrl } = require("@solana/web3.js");
|
||||
const { PROGRAM_VERSION_V3, withRevokeGoverningTokens } = 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 === -1) continue;
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
let val = trimmed.slice(eq + 1).trim();
|
||||
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
||||
val = val.slice(1, -1);
|
||||
}
|
||||
val = val.replace(/\$HOME/g, process.env.HOME || "");
|
||||
out[key] = val;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function loadKeypair(filePath) {
|
||||
const arr = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
return Keypair.fromSecretKey(Uint8Array.from(arr));
|
||||
}
|
||||
|
||||
async function askYes() {
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||
const answer = await new Promise((resolve) =>
|
||||
rl.question("Введите YES для отзыва (burn/revoke) governance токенов: ", resolve)
|
||||
);
|
||||
rl.close();
|
||||
return answer.trim() === "YES";
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const configPath = process.argv[2]
|
||||
? path.resolve(process.argv[2])
|
||||
: path.resolve(__dirname, "dao.config.env");
|
||||
const realmStr = process.argv[3];
|
||||
const mintStr = process.argv[4];
|
||||
const targetOwnerStr = process.argv[5];
|
||||
const amountStr = process.argv[6] || "1";
|
||||
if (!realmStr || !mintStr || !targetOwnerStr) {
|
||||
throw new Error(
|
||||
"Использование: node scripts/dao/revoke_member_token_full_exec.js <config.env> <realm> <mint> <target_owner_pubkey> [amount]"
|
||||
);
|
||||
}
|
||||
|
||||
const cfg = parseEnvConfig(configPath);
|
||||
const cluster = cfg.DAO_CLUSTER || "devnet";
|
||||
const governanceProgramId = new PublicKey(cfg.SPL_GOVERNANCE_PROGRAM_ID);
|
||||
const revokeKpPath = cfg.DAO_REVOKE_AUTHORITY_KEYPAIR || cfg.DAO_ISSUER_KEYPAIR;
|
||||
if (!revokeKpPath) throw new Error("В конфиге нет DAO_REVOKE_AUTHORITY_KEYPAIR и DAO_ISSUER_KEYPAIR");
|
||||
const revokeAuthority = loadKeypair(path.resolve(revokeKpPath));
|
||||
|
||||
const realm = new PublicKey(realmStr);
|
||||
const mint = new PublicKey(mintStr);
|
||||
const targetOwner = new PublicKey(targetOwnerStr);
|
||||
const amount = new BN(amountStr);
|
||||
if (amount.lten(0)) throw new Error("amount должен быть > 0");
|
||||
|
||||
console.log("============================================================");
|
||||
console.log("REVOKE/BURN GOVERNANCE TOKENS");
|
||||
console.log("------------------------------------------------------------");
|
||||
console.log("Сеть: ", cluster);
|
||||
console.log("Governance program: ", governanceProgramId.toBase58());
|
||||
console.log("Realm: ", realm.toBase58());
|
||||
console.log("Mint: ", mint.toBase58());
|
||||
console.log("Target owner: ", targetOwner.toBase58());
|
||||
console.log("Amount: ", amount.toString());
|
||||
console.log("Revoke authority: ", revokeAuthority.publicKey.toBase58());
|
||||
console.log("============================================================");
|
||||
|
||||
const ok = await askYes();
|
||||
if (!ok) {
|
||||
console.log("Отменено пользователем.");
|
||||
return;
|
||||
}
|
||||
|
||||
const connection = new Connection(clusterApiUrl(cluster), "confirmed");
|
||||
const ix = [];
|
||||
await withRevokeGoverningTokens(
|
||||
ix,
|
||||
governanceProgramId,
|
||||
PROGRAM_VERSION_V3,
|
||||
realm,
|
||||
targetOwner,
|
||||
mint,
|
||||
revokeAuthority.publicKey,
|
||||
amount
|
||||
);
|
||||
|
||||
const sig = await sendAndConfirmTransaction(connection, new Transaction().add(...ix), [revokeAuthority], {
|
||||
commitment: "confirmed",
|
||||
});
|
||||
console.log("Готово. Tx:", sig);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("Ошибка revoke:", e?.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
{
|
||||
"createdAt": "2026-05-08T23:21:53.557Z",
|
||||
"cluster": "devnet",
|
||||
"configPath": "/home/ai/work/SOLANA/shine-solana/shine/scripts/dao/dao.config.env",
|
||||
"realmName": "Devnet DAO Full-1 2026-05-09",
|
||||
"governanceProgramId": "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw",
|
||||
"issuer": "FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P",
|
||||
"communityMint": "AKKhTDHWbDZy39f3UKeiP2z1rFmbv5K4DvxVL7KTQy8b",
|
||||
"issuerAta": "CHfjpcyHotgczBErH7CCW9QMp8AeaGGyJxAtgV7ZyEwA",
|
||||
"realm": "4vPg3A7uPZ4gZkNGy6vbDwsqYLwPgeciYqRbfpfuRa7A",
|
||||
"tokenOwnerRecord": "97anU7DvmdpsQUY8ScmxswSPaGmMMw2FnNESiDQwzMjr",
|
||||
"governance": "3NS5DT83nxh88xcndCPNrziFMvZNAjCSRu5jF1SpPRRt",
|
||||
"nativeTreasury": "9vXFoN9ngfN1gpqQ3HT5n3y9Wp2r7HnSQckirgwVwWwb",
|
||||
"metadataUri": "https://arweave.net/6cMl-qV-7vpgXSdOR5MVnMEPMHgLkRdfgdO43I9z5ek",
|
||||
"imageUrl": "https://arweave.net/6cMl-qV-7vpgXSdOR5MVnMEPMHgLkRdfgdO43I9z5ek",
|
||||
"txMint": "47cWAoXbDHYpW74exnBRma9QBoa3NMVx7iEU9mZsn74NYy3AK4tPptM1oPbaW91Mw3nZBTgxQtzgKSKP2Gdr8Hyh",
|
||||
"txMetadata": "50d05LiBAXFmbt+jGZpw5tzvvNPAgwy/SiSmzb9l/v73GDFc2eN6+udw0fd9HdjYlP6t4oKYcsZB/kTRzqzMBg==",
|
||||
"txRealm": "4v22Zmsm5qkoDQaeNuzeziH7GjxiUDSDRVeXLDYwgrXF3Ltw5xSkZFrVwPAiTZRPXzrm5bhbtSHd2rA7xPq9v4kG",
|
||||
"txDeposit": "2gvWE3iA4yXhx3z1yshRRQgNEHcQgskmEsHSCo2X5WnvyXMzx8jtfyWZosvH7GWMmmtQegdaaTdtrAWeLXjiHtLw",
|
||||
"txGovernanceTreasury": "4mArdxprVHpFtLMwyxmz9E99J8jwrGSsYvFmjR1SCMeWPim3HMZZQai9zrwBBn461axx5QDWiN7c9YVP7hXFmBmi",
|
||||
"txSetRealmAuthority": "39qz1J4R63hxQQRrJ1EgdFG45PofBmtguP8V9Xx35FtPoo4APe5kPXhqyMKRK2o61iYHG5zvFob9bgUuzwvYPzaG",
|
||||
"votingTimeSec": 3600,
|
||||
"thresholdPercent": 51,
|
||||
"tokenSupply": 10,
|
||||
"startBalanceLamports": 19996489120,
|
||||
"endBalanceLamports": 19963662080,
|
||||
"spentLamports": 32827040,
|
||||
"startBalanceSol": 19.99648912,
|
||||
"endBalanceSol": 19.96366208,
|
||||
"spentSol": 0.03282704
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
createdAt: 2026-05-08T23:21:53.557Z
|
||||
cluster: devnet
|
||||
realmName: Devnet DAO Full-1 2026-05-09
|
||||
governanceProgramId: GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw
|
||||
issuer: FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P
|
||||
communityMint: AKKhTDHWbDZy39f3UKeiP2z1rFmbv5K4DvxVL7KTQy8b
|
||||
issuerAta: CHfjpcyHotgczBErH7CCW9QMp8AeaGGyJxAtgV7ZyEwA
|
||||
realm: 4vPg3A7uPZ4gZkNGy6vbDwsqYLwPgeciYqRbfpfuRa7A
|
||||
tokenOwnerRecord: 97anU7DvmdpsQUY8ScmxswSPaGmMMw2FnNESiDQwzMjr
|
||||
governance: 3NS5DT83nxh88xcndCPNrziFMvZNAjCSRu5jF1SpPRRt
|
||||
nativeTreasury: 9vXFoN9ngfN1gpqQ3HT5n3y9Wp2r7HnSQckirgwVwWwb
|
||||
metadataUri: https://arweave.net/6cMl-qV-7vpgXSdOR5MVnMEPMHgLkRdfgdO43I9z5ek
|
||||
imageUrl: https://arweave.net/6cMl-qV-7vpgXSdOR5MVnMEPMHgLkRdfgdO43I9z5ek
|
||||
txMint: 47cWAoXbDHYpW74exnBRma9QBoa3NMVx7iEU9mZsn74NYy3AK4tPptM1oPbaW91Mw3nZBTgxQtzgKSKP2Gdr8Hyh
|
||||
txMetadata: 50d05LiBAXFmbt+jGZpw5tzvvNPAgwy/SiSmzb9l/v73GDFc2eN6+udw0fd9HdjYlP6t4oKYcsZB/kTRzqzMBg==
|
||||
txRealm: 4v22Zmsm5qkoDQaeNuzeziH7GjxiUDSDRVeXLDYwgrXF3Ltw5xSkZFrVwPAiTZRPXzrm5bhbtSHd2rA7xPq9v4kG
|
||||
txDeposit: 2gvWE3iA4yXhx3z1yshRRQgNEHcQgskmEsHSCo2X5WnvyXMzx8jtfyWZosvH7GWMmmtQegdaaTdtrAWeLXjiHtLw
|
||||
txGovernanceTreasury: 4mArdxprVHpFtLMwyxmz9E99J8jwrGSsYvFmjR1SCMeWPim3HMZZQai9zrwBBn461axx5QDWiN7c9YVP7hXFmBmi
|
||||
txSetRealmAuthority: 39qz1J4R63hxQQRrJ1EgdFG45PofBmtguP8V9Xx35FtPoo4APe5kPXhqyMKRK2o61iYHG5zvFob9bgUuzwvYPzaG
|
||||
tokenSupply: 10
|
||||
votingTimeSec: 3600
|
||||
thresholdPercent: 51
|
||||
startBalanceSol: 19.99648912
|
||||
endBalanceSol: 19.96366208
|
||||
spentSol: 0.03282704
|
||||
configPath: /home/ai/work/SOLANA/shine-solana/shine/scripts/dao/dao.config.env
|
||||
@ -0,0 +1,23 @@
|
||||
{
|
||||
"createdAt": "2026-05-08T23:24:43.905Z",
|
||||
"cluster": "devnet",
|
||||
"configPath": "/home/ai/work/SOLANA/shine-solana/shine/scripts/dao/dao.config.env",
|
||||
"governanceProgramId": "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw",
|
||||
"realm": "4vPg3A7uPZ4gZkNGy6vbDwsqYLwPgeciYqRbfpfuRa7A",
|
||||
"governance": "3NS5DT83nxh88xcndCPNrziFMvZNAjCSRu5jF1SpPRRt",
|
||||
"mint": "AKKhTDHWbDZy39f3UKeiP2z1rFmbv5K4DvxVL7KTQy8b",
|
||||
"targetOwner": "FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P",
|
||||
"amount": "1",
|
||||
"proposer": "FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P",
|
||||
"proposerRecord": "97anU7DvmdpsQUY8ScmxswSPaGmMMw2FnNESiDQwzMjr",
|
||||
"proposal": "8A9cthr1PG6t2xdUyC928KZVpTzZPW7YoJsYPYjw8o2E",
|
||||
"proposalTransaction": "krWkKxVaQDd3C6pdcJmH7Wu1tgk6RtS2AcoJba4VZGq",
|
||||
"voteRecord": "EyWuYWt5jhCoySPWvFcm7DBaCVM5v4Y9N8Kcd3LCf24D",
|
||||
"txCreateProposal": "kt4vtQbwcXY4KF4SKCHkzv86qxYXaA5Wapjug4VtB6xi6WmwxURzn9az41gb9sYKrhZ1U9qdgePUr4jLt7iTCkZ",
|
||||
"txInsertTransaction": "47KrAnsj7whE7GyeQZzKoc1ciUfy6r3YFZFsF7huorzewx1SSxCXCF2QaJCRXdBw87u53CLrS7JM1D4BDXUZWbsA",
|
||||
"txSignOff": "5X69zNCHrQAcSR5guneKh7NG5qfMvaYtvH9vz4pvwY4B2zQs3NhnMk47X5xF25EV6Cf2fANkpjkNdkqSmVZU9U2E",
|
||||
"txVote": "5Dwnp7568BaRQJ67FHJC46KT9Ym4RSNyZWSSSM9KyFx5U1fSwsejBkvEstQQ4wuoBtcpNgsTcwjdMPUMrw5N3Haf",
|
||||
"txFinalize": null,
|
||||
"txExecute": null,
|
||||
"executeError": "Simulation failed. \nMessage: Transaction simulation failed: Error processing Instruction 0: custom program error: 0x20d. \nLogs: \n[\n \"Program GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw invoke [1]\",\n \"Program log: VERSION:\\\"3.1.2\\\"\",\n \"Program log: GOVERNANCE-INSTRUCTION: ExecuteTransaction\",\n \"Program log: GOVERNANCE-ERROR: Can't execute transaction within its hold up time\",\n \"Program GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw consumed 10045 of 200000 compute units\",\n \"Program GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw failed: custom program error: 0x20d\"\n]. \nCatch the `SendTransactionError` and call `getLogs()` on it for full details."
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
createdAt: 2026-05-08T23:24:43.905Z
|
||||
cluster: devnet
|
||||
realm: 4vPg3A7uPZ4gZkNGy6vbDwsqYLwPgeciYqRbfpfuRa7A
|
||||
governance: 3NS5DT83nxh88xcndCPNrziFMvZNAjCSRu5jF1SpPRRt
|
||||
mint: AKKhTDHWbDZy39f3UKeiP2z1rFmbv5K4DvxVL7KTQy8b
|
||||
targetOwner: FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P
|
||||
amount: 1
|
||||
proposer: FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P
|
||||
proposal: 8A9cthr1PG6t2xdUyC928KZVpTzZPW7YoJsYPYjw8o2E
|
||||
proposalTransaction: krWkKxVaQDd3C6pdcJmH7Wu1tgk6RtS2AcoJba4VZGq
|
||||
voteRecord: EyWuYWt5jhCoySPWvFcm7DBaCVM5v4Y9N8Kcd3LCf24D
|
||||
txCreateProposal: kt4vtQbwcXY4KF4SKCHkzv86qxYXaA5Wapjug4VtB6xi6WmwxURzn9az41gb9sYKrhZ1U9qdgePUr4jLt7iTCkZ
|
||||
txInsertTransaction: 47KrAnsj7whE7GyeQZzKoc1ciUfy6r3YFZFsF7huorzewx1SSxCXCF2QaJCRXdBw87u53CLrS7JM1D4BDXUZWbsA
|
||||
txSignOff: 5X69zNCHrQAcSR5guneKh7NG5qfMvaYtvH9vz4pvwY4B2zQs3NhnMk47X5xF25EV6Cf2fANkpjkNdkqSmVZU9U2E
|
||||
txVote: 5Dwnp7568BaRQJ67FHJC46KT9Ym4RSNyZWSSSM9KyFx5U1fSwsejBkvEstQQ4wuoBtcpNgsTcwjdMPUMrw5N3Haf
|
||||
txFinalize: -
|
||||
txExecute: -
|
||||
executeError: Simulation failed.
|
||||
Message: Transaction simulation failed: Error processing Instruction 0: custom program error: 0x20d.
|
||||
Logs:
|
||||
[
|
||||
"Program GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw invoke [1]",
|
||||
"Program log: VERSION:\"3.1.2\"",
|
||||
"Program log: GOVERNANCE-INSTRUCTION: ExecuteTransaction",
|
||||
"Program log: GOVERNANCE-ERROR: Can't execute transaction within its hold up time",
|
||||
"Program GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw consumed 10045 of 200000 compute units",
|
||||
"Program GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw failed: custom program error: 0x20d"
|
||||
].
|
||||
Catch the `SendTransactionError` and call `getLogs()` on it for full details.
|
||||
7
shine/scripts/dao_legacy/README.md
Normal file
7
shine/scripts/dao_legacy/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# LEGACY
|
||||
|
||||
Эта папка содержит старые варианты скриптов создания DAO.
|
||||
|
||||
Их оставили только для истории и сравнения.
|
||||
|
||||
Актуальные скрипты находятся в `scripts/dao`.
|
||||
379
shine/scripts/dao_legacy/create_realm_dao_build_exec.js
Executable file
379
shine/scripts/dao_legacy/create_realm_dao_build_exec.js
Executable file
@ -0,0 +1,379 @@
|
||||
#!/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,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
Transaction,
|
||||
sendAndConfirmTransaction,
|
||||
clusterApiUrl,
|
||||
} = require("@solana/web3.js");
|
||||
const {
|
||||
TOKEN_PROGRAM_ID,
|
||||
getMintLen,
|
||||
createInitializeMintInstruction,
|
||||
getAssociatedTokenAddressSync,
|
||||
createAssociatedTokenAccountIdempotentInstruction,
|
||||
createMintToInstruction,
|
||||
} = require("@solana/spl-token");
|
||||
const {
|
||||
MintMaxVoteWeightSource,
|
||||
VoteThreshold,
|
||||
VoteThresholdType,
|
||||
VoteTipping,
|
||||
GovernanceConfig,
|
||||
withCreateRealm,
|
||||
withDepositGoverningTokens,
|
||||
withCreateGovernance,
|
||||
withCreateNativeTreasury,
|
||||
PROGRAM_VERSION_V3,
|
||||
} = 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 === -1) continue;
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
let val = trimmed.slice(eq + 1).trim();
|
||||
if (
|
||||
(val.startsWith('"') && val.endsWith('"')) ||
|
||||
(val.startsWith("'") && val.endsWith("'"))
|
||||
) {
|
||||
val = val.slice(1, -1);
|
||||
}
|
||||
val = val.replace(/\$HOME/g, process.env.HOME || "");
|
||||
out[key] = val;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function assertRequired(cfg, key) {
|
||||
if (!cfg[key]) {
|
||||
throw new Error(`В конфиге отсутствует обязательный параметр: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
function loadKeypair(filePath) {
|
||||
const arr = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
return Keypair.fromSecretKey(Uint8Array.from(arr));
|
||||
}
|
||||
|
||||
function lamportsToSol(lamports) {
|
||||
return Number(lamports) / 1_000_000_000;
|
||||
}
|
||||
|
||||
function nowStamp() {
|
||||
const d = new Date();
|
||||
const p = (n) => String(n).padStart(2, "0");
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}_${p(d.getHours())}-${p(
|
||||
d.getMinutes()
|
||||
)}-${p(d.getSeconds())}`;
|
||||
}
|
||||
|
||||
async function askYes() {
|
||||
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 main() {
|
||||
const configPath = process.argv[2]
|
||||
? path.resolve(process.argv[2])
|
||||
: path.resolve(__dirname, "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",
|
||||
].forEach((k) => assertRequired(cfg, k));
|
||||
|
||||
const cluster = cfg.DAO_CLUSTER;
|
||||
const connection = new Connection(clusterApiUrl(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 <= 0) {
|
||||
throw new Error("DAO_VOTING_TIME_SEC должен быть целым > 0");
|
||||
}
|
||||
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
|
||||
);
|
||||
const realmExists = (await connection.getAccountInfo(realmPda)) !== null;
|
||||
if (realmExists) {
|
||||
throw new Error(
|
||||
`Realm уже существует для имени '${cfg.DAO_REALM_NAME}': ${realmPda.toBase58()}`
|
||||
);
|
||||
}
|
||||
|
||||
const startBalance = await connection.getBalance(issuer.publicKey, "confirmed");
|
||||
console.log("============================================================");
|
||||
console.log("СОЗДАНИЕ DAO (EXEC)");
|
||||
console.log("------------------------------------------------------------");
|
||||
console.log("Сеть: ", cluster);
|
||||
console.log("Realm name: ", cfg.DAO_REALM_NAME);
|
||||
console.log("Realm PDA: ", realmPda.toBase58());
|
||||
console.log("Governance program: ", governanceProgramId.toBase58());
|
||||
console.log("Issuer: ", issuer.publicKey.toBase58());
|
||||
console.log("Баланс до старта: ", `${lamportsToSol(startBalance)} SOL`);
|
||||
console.log("NFT symbol/name: ", `${cfg.DAO_GOV_NFT_SYMBOL} / ${cfg.DAO_GOV_NFT_NAME}`);
|
||||
console.log("NFT supply: ", supply);
|
||||
console.log("Voting time sec: ", votingTimeSec);
|
||||
console.log("Threshold %: ", thresholdPct);
|
||||
console.log("Конфиг: ", configPath);
|
||||
console.log("============================================================");
|
||||
|
||||
const ok = await askYes();
|
||||
if (!ok) {
|
||||
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 programVersion = PROGRAM_VERSION_V3;
|
||||
const ixRealm = [];
|
||||
const realmPk = await withCreateRealm(
|
||||
ixRealm,
|
||||
governanceProgramId,
|
||||
programVersion,
|
||||
cfg.DAO_REALM_NAME,
|
||||
issuer.publicKey,
|
||||
mintKeypair.publicKey,
|
||||
issuer.publicKey,
|
||||
undefined,
|
||||
MintMaxVoteWeightSource.FULL_SUPPLY_FRACTION,
|
||||
new BN(1)
|
||||
);
|
||||
const txRealm = new Transaction().add(...ixRealm);
|
||||
const sigRealm = await sendAndConfirmTransaction(connection, txRealm, [issuer], {
|
||||
commitment: "confirmed",
|
||||
});
|
||||
|
||||
const ixDeposit = [];
|
||||
const tokenOwnerRecordPk = await withDepositGoverningTokens(
|
||||
ixDeposit,
|
||||
governanceProgramId,
|
||||
programVersion,
|
||||
realmPk,
|
||||
issuerAta,
|
||||
mintKeypair.publicKey,
|
||||
issuer.publicKey,
|
||||
issuer.publicKey,
|
||||
issuer.publicKey,
|
||||
new BN(supply),
|
||||
true
|
||||
);
|
||||
const txDeposit = new Transaction().add(...ixDeposit);
|
||||
const sigDeposit = await sendAndConfirmTransaction(connection, txDeposit, [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.Strict,
|
||||
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,
|
||||
programVersion,
|
||||
realmPk,
|
||||
realmPk,
|
||||
governanceConfig,
|
||||
tokenOwnerRecordPk,
|
||||
issuer.publicKey,
|
||||
issuer.publicKey
|
||||
);
|
||||
const treasuryPk = await withCreateNativeTreasury(
|
||||
ixGov,
|
||||
governanceProgramId,
|
||||
programVersion,
|
||||
governancePk,
|
||||
issuer.publicKey
|
||||
);
|
||||
const txGov = new Transaction().add(...ixGov);
|
||||
const sigGov = await sendAndConfirmTransaction(connection, txGov, [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(),
|
||||
txMint: sigMint,
|
||||
txRealm: sigRealm,
|
||||
txDeposit: sigDeposit,
|
||||
txGovernanceTreasury: sigGov,
|
||||
votingTimeSec,
|
||||
thresholdPercent: thresholdPct,
|
||||
nftSupply: supply,
|
||||
startBalanceLamports: startBalance,
|
||||
endBalanceLamports: endBalance,
|
||||
spentLamports,
|
||||
startBalanceSol: lamportsToSol(startBalance),
|
||||
endBalanceSol: lamportsToSol(endBalance),
|
||||
spentSol: lamportsToSol(spentLamports),
|
||||
};
|
||||
const reportDir = path.resolve(__dirname, "runs");
|
||||
fs.mkdirSync(reportDir, { recursive: true });
|
||||
const reportBaseName = `${nowStamp()}_${cfg.DAO_REALM_NAME
|
||||
.replace(/[^a-zA-Z0-9._-]+/g, "_")
|
||||
.slice(0, 80)}`;
|
||||
const reportJsonPath = path.join(reportDir, `${reportBaseName}.json`);
|
||||
const reportTxtPath = path.join(reportDir, `${reportBaseName}.txt`);
|
||||
fs.writeFileSync(reportJsonPath, JSON.stringify(report, null, 2));
|
||||
fs.writeFileSync(
|
||||
reportTxtPath,
|
||||
[
|
||||
`createdAt: ${report.createdAt}`,
|
||||
`cluster: ${report.cluster}`,
|
||||
`realmName: ${report.realmName}`,
|
||||
`governanceProgramId: ${report.governanceProgramId}`,
|
||||
`issuer: ${report.issuer}`,
|
||||
`communityMint: ${report.communityMint}`,
|
||||
`issuerAta: ${report.issuerAta}`,
|
||||
`realm: ${report.realm}`,
|
||||
`tokenOwnerRecord: ${report.tokenOwnerRecord}`,
|
||||
`governance: ${report.governance}`,
|
||||
`nativeTreasury: ${report.nativeTreasury}`,
|
||||
`txMint: ${report.txMint}`,
|
||||
`txRealm: ${report.txRealm}`,
|
||||
`txDeposit: ${report.txDeposit}`,
|
||||
`txGovernanceTreasury: ${report.txGovernanceTreasury}`,
|
||||
`nftSupply: ${report.nftSupply}`,
|
||||
`votingTimeSec: ${report.votingTimeSec}`,
|
||||
`thresholdPercent: ${report.thresholdPercent}`,
|
||||
`startBalanceSol: ${report.startBalanceSol}`,
|
||||
`endBalanceSol: ${report.endBalanceSol}`,
|
||||
`spentSol: ${report.spentSol}`,
|
||||
`configPath: ${report.configPath}`,
|
||||
].join("\n") + "\n"
|
||||
);
|
||||
|
||||
console.log("============================================================");
|
||||
console.log("DAO СОЗДАНО");
|
||||
console.log("------------------------------------------------------------");
|
||||
console.log("Community mint (SPL Token, transferable): ", mintKeypair.publicKey.toBase58());
|
||||
console.log("Issuer ATA: ", issuerAta.toBase58());
|
||||
console.log("Realm: ", realmPk.toBase58());
|
||||
console.log("TokenOwnerRecord: ", tokenOwnerRecordPk.toBase58());
|
||||
console.log("Governance: ", governancePk.toBase58());
|
||||
console.log("Native treasury PDA: ", treasuryPk.toBase58());
|
||||
console.log("Tx mint: ", sigMint);
|
||||
console.log("Tx realm: ", sigRealm);
|
||||
console.log("Tx deposit: ", sigDeposit);
|
||||
console.log("Tx governance+treasury: ", sigGov);
|
||||
console.log("Баланс после: ", `${lamportsToSol(endBalance)} SOL`);
|
||||
console.log("Потрачено: ", `${lamportsToSol(spentLamports)} SOL`);
|
||||
console.log("Отчёт JSON: ", reportJsonPath);
|
||||
console.log("Отчёт TXT: ", reportTxtPath);
|
||||
console.log("============================================================");
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("Ошибка создания DAO:", e?.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
106
shine/scripts/dao_legacy/create_realm_dao_test.sh
Executable file
106
shine/scripts/dao_legacy/create_realm_dao_test.sh
Executable file
@ -0,0 +1,106 @@
|
||||
#!/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_REALM_NAME}" == *"TEMPLATE"* || "${DAO_REALM_NAME}" == *"CHANGE_ME"* ]]; then
|
||||
echo "Ошибка: похоже, не заменили тестовое имя DAO_REALM_NAME"
|
||||
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
|
||||
)"
|
||||
|
||||
if [[ -z "$REALM_PDA" ]]; then
|
||||
echo "Ошибка: не удалось вычислить PDA realm."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REALM_EXISTS="no"
|
||||
if solana account "$REALM_PDA" --url "$DAO_CLUSTER" >/dev/null 2>&1; then
|
||||
REALM_EXISTS="yes"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
============================================================
|
||||
ПРЕДСТАРТОВАЯ ПРОВЕРКА DAO (Realms)
|
||||
------------------------------------------------------------
|
||||
Сеть: $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}
|
||||
NFT name: $DAO_GOV_NFT_NAME
|
||||
NFT symbol: $DAO_GOV_NFT_SYMBOL
|
||||
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
|
||||
echo "Проверка пройдена."
|
||||
echo "Этот скрипт делает только preflight-валидацию."
|
||||
echo "Для реального создания DAO запускайте исполняющий скрипт:"
|
||||
echo "node scripts/dao/create_realm_dao_without_burn_build_exec.js scripts/dao/dao.config.env"
|
||||
379
shine/scripts/dao_legacy/create_realm_dao_without_burn_build_exec.js
Executable file
379
shine/scripts/dao_legacy/create_realm_dao_without_burn_build_exec.js
Executable file
@ -0,0 +1,379 @@
|
||||
#!/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,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
Transaction,
|
||||
sendAndConfirmTransaction,
|
||||
clusterApiUrl,
|
||||
} = require("@solana/web3.js");
|
||||
const {
|
||||
TOKEN_PROGRAM_ID,
|
||||
getMintLen,
|
||||
createInitializeMintInstruction,
|
||||
getAssociatedTokenAddressSync,
|
||||
createAssociatedTokenAccountIdempotentInstruction,
|
||||
createMintToInstruction,
|
||||
} = require("@solana/spl-token");
|
||||
const {
|
||||
MintMaxVoteWeightSource,
|
||||
VoteThreshold,
|
||||
VoteThresholdType,
|
||||
VoteTipping,
|
||||
GovernanceConfig,
|
||||
withCreateRealm,
|
||||
withDepositGoverningTokens,
|
||||
withCreateGovernance,
|
||||
withCreateNativeTreasury,
|
||||
PROGRAM_VERSION_V3,
|
||||
} = 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 === -1) continue;
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
let val = trimmed.slice(eq + 1).trim();
|
||||
if (
|
||||
(val.startsWith('"') && val.endsWith('"')) ||
|
||||
(val.startsWith("'") && val.endsWith("'"))
|
||||
) {
|
||||
val = val.slice(1, -1);
|
||||
}
|
||||
val = val.replace(/\$HOME/g, process.env.HOME || "");
|
||||
out[key] = val;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function assertRequired(cfg, key) {
|
||||
if (!cfg[key]) {
|
||||
throw new Error(`В конфиге отсутствует обязательный параметр: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
function loadKeypair(filePath) {
|
||||
const arr = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
return Keypair.fromSecretKey(Uint8Array.from(arr));
|
||||
}
|
||||
|
||||
function lamportsToSol(lamports) {
|
||||
return Number(lamports) / 1_000_000_000;
|
||||
}
|
||||
|
||||
function nowStamp() {
|
||||
const d = new Date();
|
||||
const p = (n) => String(n).padStart(2, "0");
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}_${p(d.getHours())}-${p(
|
||||
d.getMinutes()
|
||||
)}-${p(d.getSeconds())}`;
|
||||
}
|
||||
|
||||
async function askYes() {
|
||||
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 main() {
|
||||
const configPath = process.argv[2]
|
||||
? path.resolve(process.argv[2])
|
||||
: path.resolve(__dirname, "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",
|
||||
].forEach((k) => assertRequired(cfg, k));
|
||||
|
||||
const cluster = cfg.DAO_CLUSTER;
|
||||
const connection = new Connection(clusterApiUrl(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 <= 0) {
|
||||
throw new Error("DAO_VOTING_TIME_SEC должен быть целым > 0");
|
||||
}
|
||||
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
|
||||
);
|
||||
const realmExists = (await connection.getAccountInfo(realmPda)) !== null;
|
||||
if (realmExists) {
|
||||
throw new Error(
|
||||
`Realm уже существует для имени '${cfg.DAO_REALM_NAME}': ${realmPda.toBase58()}`
|
||||
);
|
||||
}
|
||||
|
||||
const startBalance = await connection.getBalance(issuer.publicKey, "confirmed");
|
||||
console.log("============================================================");
|
||||
console.log("СОЗДАНИЕ DAO (EXEC)");
|
||||
console.log("------------------------------------------------------------");
|
||||
console.log("Сеть: ", cluster);
|
||||
console.log("Realm name: ", cfg.DAO_REALM_NAME);
|
||||
console.log("Realm PDA: ", realmPda.toBase58());
|
||||
console.log("Governance program: ", governanceProgramId.toBase58());
|
||||
console.log("Issuer: ", issuer.publicKey.toBase58());
|
||||
console.log("Баланс до старта: ", `${lamportsToSol(startBalance)} SOL`);
|
||||
console.log("NFT symbol/name: ", `${cfg.DAO_GOV_NFT_SYMBOL} / ${cfg.DAO_GOV_NFT_NAME}`);
|
||||
console.log("NFT supply: ", supply);
|
||||
console.log("Voting time sec: ", votingTimeSec);
|
||||
console.log("Threshold %: ", thresholdPct);
|
||||
console.log("Конфиг: ", configPath);
|
||||
console.log("============================================================");
|
||||
|
||||
const ok = await askYes();
|
||||
if (!ok) {
|
||||
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 programVersion = PROGRAM_VERSION_V3;
|
||||
const ixRealm = [];
|
||||
const realmPk = await withCreateRealm(
|
||||
ixRealm,
|
||||
governanceProgramId,
|
||||
programVersion,
|
||||
cfg.DAO_REALM_NAME,
|
||||
issuer.publicKey,
|
||||
mintKeypair.publicKey,
|
||||
issuer.publicKey,
|
||||
undefined,
|
||||
MintMaxVoteWeightSource.FULL_SUPPLY_FRACTION,
|
||||
new BN(1)
|
||||
);
|
||||
const txRealm = new Transaction().add(...ixRealm);
|
||||
const sigRealm = await sendAndConfirmTransaction(connection, txRealm, [issuer], {
|
||||
commitment: "confirmed",
|
||||
});
|
||||
|
||||
const ixDeposit = [];
|
||||
const tokenOwnerRecordPk = await withDepositGoverningTokens(
|
||||
ixDeposit,
|
||||
governanceProgramId,
|
||||
programVersion,
|
||||
realmPk,
|
||||
issuerAta,
|
||||
mintKeypair.publicKey,
|
||||
issuer.publicKey,
|
||||
issuer.publicKey,
|
||||
issuer.publicKey,
|
||||
new BN(supply),
|
||||
true
|
||||
);
|
||||
const txDeposit = new Transaction().add(...ixDeposit);
|
||||
const sigDeposit = await sendAndConfirmTransaction(connection, txDeposit, [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.Strict,
|
||||
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,
|
||||
programVersion,
|
||||
realmPk,
|
||||
realmPk,
|
||||
governanceConfig,
|
||||
tokenOwnerRecordPk,
|
||||
issuer.publicKey,
|
||||
issuer.publicKey
|
||||
);
|
||||
const treasuryPk = await withCreateNativeTreasury(
|
||||
ixGov,
|
||||
governanceProgramId,
|
||||
programVersion,
|
||||
governancePk,
|
||||
issuer.publicKey
|
||||
);
|
||||
const txGov = new Transaction().add(...ixGov);
|
||||
const sigGov = await sendAndConfirmTransaction(connection, txGov, [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(),
|
||||
txMint: sigMint,
|
||||
txRealm: sigRealm,
|
||||
txDeposit: sigDeposit,
|
||||
txGovernanceTreasury: sigGov,
|
||||
votingTimeSec,
|
||||
thresholdPercent: thresholdPct,
|
||||
nftSupply: supply,
|
||||
startBalanceLamports: startBalance,
|
||||
endBalanceLamports: endBalance,
|
||||
spentLamports,
|
||||
startBalanceSol: lamportsToSol(startBalance),
|
||||
endBalanceSol: lamportsToSol(endBalance),
|
||||
spentSol: lamportsToSol(spentLamports),
|
||||
};
|
||||
const reportDir = path.resolve(__dirname, "runs");
|
||||
fs.mkdirSync(reportDir, { recursive: true });
|
||||
const reportBaseName = `${nowStamp()}_${cfg.DAO_REALM_NAME
|
||||
.replace(/[^a-zA-Z0-9._-]+/g, "_")
|
||||
.slice(0, 80)}`;
|
||||
const reportJsonPath = path.join(reportDir, `${reportBaseName}.json`);
|
||||
const reportTxtPath = path.join(reportDir, `${reportBaseName}.txt`);
|
||||
fs.writeFileSync(reportJsonPath, JSON.stringify(report, null, 2));
|
||||
fs.writeFileSync(
|
||||
reportTxtPath,
|
||||
[
|
||||
`createdAt: ${report.createdAt}`,
|
||||
`cluster: ${report.cluster}`,
|
||||
`realmName: ${report.realmName}`,
|
||||
`governanceProgramId: ${report.governanceProgramId}`,
|
||||
`issuer: ${report.issuer}`,
|
||||
`communityMint: ${report.communityMint}`,
|
||||
`issuerAta: ${report.issuerAta}`,
|
||||
`realm: ${report.realm}`,
|
||||
`tokenOwnerRecord: ${report.tokenOwnerRecord}`,
|
||||
`governance: ${report.governance}`,
|
||||
`nativeTreasury: ${report.nativeTreasury}`,
|
||||
`txMint: ${report.txMint}`,
|
||||
`txRealm: ${report.txRealm}`,
|
||||
`txDeposit: ${report.txDeposit}`,
|
||||
`txGovernanceTreasury: ${report.txGovernanceTreasury}`,
|
||||
`nftSupply: ${report.nftSupply}`,
|
||||
`votingTimeSec: ${report.votingTimeSec}`,
|
||||
`thresholdPercent: ${report.thresholdPercent}`,
|
||||
`startBalanceSol: ${report.startBalanceSol}`,
|
||||
`endBalanceSol: ${report.endBalanceSol}`,
|
||||
`spentSol: ${report.spentSol}`,
|
||||
`configPath: ${report.configPath}`,
|
||||
].join("\n") + "\n"
|
||||
);
|
||||
|
||||
console.log("============================================================");
|
||||
console.log("DAO СОЗДАНО");
|
||||
console.log("------------------------------------------------------------");
|
||||
console.log("Community mint (SPL Token, transferable): ", mintKeypair.publicKey.toBase58());
|
||||
console.log("Issuer ATA: ", issuerAta.toBase58());
|
||||
console.log("Realm: ", realmPk.toBase58());
|
||||
console.log("TokenOwnerRecord: ", tokenOwnerRecordPk.toBase58());
|
||||
console.log("Governance: ", governancePk.toBase58());
|
||||
console.log("Native treasury PDA: ", treasuryPk.toBase58());
|
||||
console.log("Tx mint: ", sigMint);
|
||||
console.log("Tx realm: ", sigRealm);
|
||||
console.log("Tx deposit: ", sigDeposit);
|
||||
console.log("Tx governance+treasury: ", sigGov);
|
||||
console.log("Баланс после: ", `${lamportsToSol(endBalance)} SOL`);
|
||||
console.log("Потрачено: ", `${lamportsToSol(spentLamports)} SOL`);
|
||||
console.log("Отчёт JSON: ", reportJsonPath);
|
||||
console.log("Отчёт TXT: ", reportTxtPath);
|
||||
console.log("============================================================");
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("Ошибка создания DAO:", e?.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
@ -1,3 +1,11 @@
|
||||
⚠️ УСТАРЕВШЕЕ (LEGACY)
|
||||
|
||||
Папка `scripts/devnet` относится к старой логике (legacy) и для текущей версии `shine_payments` больше не используется.
|
||||
|
||||
Актуальные скрипты по DAO и новой схеме находятся в `scripts/dao`.
|
||||
|
||||
---
|
||||
|
||||
Devnet E2E тест: NFT-модуль + add_bonus
|
||||
|
||||
Ветка содержит скрипты для проверки (NFT + add_bonus) в devnet.
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
{
|
||||
"createdAt": "2026-05-15T12:20:22.373Z",
|
||||
"prefix": "DAo",
|
||||
"count": 1,
|
||||
"ignoreCase": false,
|
||||
"runsDir": "/home/ai/work/SOLANA/shine-solana/shine/scripts/governance_token/runs",
|
||||
"avgExpectedTriesPerMatch": 195112,
|
||||
"attemptsObserved": 0,
|
||||
"foundHintsInOutput": 1,
|
||||
"command": "solana-keygen grind --starts-with DAo:1",
|
||||
"outputLog": [
|
||||
"Searching with 16 threads for:",
|
||||
"1 pubkey that starts with 'DAo' and ends with ''",
|
||||
"Wrote keypair to DAou7SeaykoMooghA5SURLYhkkU8NEhV5Y2T6fsXD7rn.json"
|
||||
]
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
[115,67,66,30,87,145,246,163,61,151,201,124,183,214,51,151,22,218,111,91,138,240,184,170,169,117,123,68,99,10,100,161,180,207,127,168,102,124,209,83,46,144,109,253,200,122,20,82,223,74,69,105,53,218,226,231,88,238,93,98,54,161,167,31]
|
||||
@ -0,0 +1,16 @@
|
||||
{
|
||||
"createdAt": "2026-05-15T12:20:09.856Z",
|
||||
"prefix": "DAo",
|
||||
"count": 1,
|
||||
"ignoreCase": false,
|
||||
"runsDir": "/home/ai/work/SOLANA/shine-solana/shine/scripts/governance_token/scripts/governance_token/runs",
|
||||
"avgExpectedTriesPerMatch": 195112,
|
||||
"attemptsObserved": 0,
|
||||
"foundHintsInOutput": 1,
|
||||
"command": "solana-keygen grind --starts-with DAo:1",
|
||||
"outputLog": [
|
||||
"Searching with 16 threads for:",
|
||||
"1 pubkey that starts with 'DAo' and ends with ''",
|
||||
"Wrote keypair to DAoxa7YRxa8Se5KFQmkBbxBJ3Fo8FeY8N9td6NGz38zu.json"
|
||||
]
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
[127,127,232,76,177,128,31,8,169,68,116,117,202,252,44,203,63,250,250,210,30,186,201,66,110,211,193,137,111,186,165,157,180,207,144,236,191,129,155,243,98,167,69,154,219,76,79,141,228,101,60,132,255,6,252,105,99,29,143,220,127,120,236,26]
|
||||
2
shine/settings.gradle
Normal file
2
shine/settings.gradle
Normal file
@ -0,0 +1,2 @@
|
||||
rootProject.name = "shine-tools"
|
||||
|
||||
876
shine/yarn.lock
876
shine/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -1,35 +1,58 @@
|
||||
# Кошельки Devnet (тестовые)
|
||||
# Кошельки Devnet (тестовые, приватные ключи в Base58)
|
||||
|
||||
> Только для тестового этапа. Не использовать в production.
|
||||
> Только для тестов. Не использовать в production.
|
||||
>
|
||||
> Балансы ниже актуальны на 2026-05-08 (Devnet), могут меняться после транзакций.
|
||||
|
||||
## DAO (текущий)
|
||||
## DAO (текущий для Shine Payments)
|
||||
|
||||
- public: `6bFc5Gz5qF172GQhK5HpDbWs8F6qcSxdHn5XqAstf1fY`
|
||||
- private (base58): `3VYfYZZ3ugmgwisiQQAfcimX9T65AE9BmwmYVixAUj4jyneccSE9rzbC3g5twvH7ECZ8xgp7emJo3pR4yQqCwjGn`
|
||||
- роль: основной DAO-кошелёк в `shine_payments` (получает DAO-часть выплат, выдаёт лимиты менеджерам)
|
||||
|
||||
## Manager для coef/limit (текущий)
|
||||
## Manager / deploy wallet (текущий системный)
|
||||
|
||||
- public: `4yzHKs2zFXpyqqCETe8KpAs4xhEo4QhJ2ybyTgRZphZv`
|
||||
- private (json array):
|
||||
|
||||
```json
|
||||
[90,184,152,226,36,29,0,20,192,35,239,186,138,46,197,219,65,216,46,150,150,113,95,216,66,95,68,79,178,239,166,133,59,44,90,189,77,253,249,234,240,215,66,104,14,194,200,186,203,176,232,154,245,165,226,23,127,115,246,181,134,24,148,45]
|
||||
```
|
||||
- private (base58): `2pCe8GWpXL7hYf3tFQZg4azfPw3hm1UwvybRbJ6j5rKBvTjBme2DuWESKM5e5jd2JLw7b4D5sWno4YbxRdot4Gap`
|
||||
- роль:
|
||||
- upgrade authority и кошелёк деплоя программ;
|
||||
- `MANAGER_WALLET` в настройках (имеет право менять `coef/limit`).
|
||||
|
||||
## Тестовый кошелёк key1
|
||||
|
||||
- public: `HMww7YSVfwVm4i8sugqj7wyH26dqzHykzv3wzWwzEvPA`
|
||||
- private (base58): `5pbFo9Zq1VsNheHwbEp6AZKa6R62CZHoGkJFZnugpMEtCmkQFjuUP7TgA5hSPqv4NABGmPP62qVnDPHmRqEAwvJc`
|
||||
- роль: дополнительный тестовый пользователь (покупка билетов, получение выплат)
|
||||
|
||||
## Тестовый кошелёк key2
|
||||
|
||||
- public: `E3ZDHbWv1qiFvDTmaRc9wjFCgbQw6UmKJLJYbaTNvjAh`
|
||||
- private (base58): `5qm1GJGXB1fFJ3YsU5Y3XXgTiQfaimqBWk79oEveFASH9D2of3jqUoT7dumBvS449fW5j5Sw8MgAMH2QBMmFPdry`
|
||||
- роль: дополнительный тестовый пользователь (второй участник сценариев)
|
||||
|
||||
## Тестовый кошелёк phantomWallet
|
||||
## Тестовый кошелёк phantomWallet (твой основной рабочий)
|
||||
|
||||
- private (json array):
|
||||
- public: `FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P`
|
||||
- private (base58): `5RpEoxRKSr2norQP3vEnq9XokQGh9EbGN8q8xUUVAdm1M5mTD1vMuyJPYJfViMWFf6c8qT5mj2bt64gLE2zm6VG3`
|
||||
- роль:
|
||||
- основной кошелёк для ручного тестирования через UI;
|
||||
- можно использовать как менеджер очередей (после выдачи лимитов DAO через `grant_manager_limits`).
|
||||
|
||||
```json
|
||||
[221,119,143,125,90,136,155,115,191,198,210,85,228,111,251,118,168,138,27,60,249,62,247,24,121,228,139,112,218,69,55,143,215,21,229,69,219,1,74,36,10,239,63,163,48,240,58,208,237,251,209,37,17,202,215,77,13,165,178,18,141,21,193,64]
|
||||
```
|
||||
## testDaoCreator (Phantom-совместимый)
|
||||
|
||||
- public: `A9AP6TMUuxbXwR8H2xN8hA7SXddnnxRH1vhP2qwEuG2r`
|
||||
- private (base58): `5JR37dCQUB4jtjSzoLp3pkGMkCoTpNLxZLXJDrGZnYCDcBgX9cmqmHVbQz2VbYgGZnG2StNSQ5cMgJf1PSa8gvpv`
|
||||
- роль: резервный тестовый кошелёк (можно использовать как отдельный DAO/менеджер в экспериментах)
|
||||
|
||||
## Program keypair (для деплоя)
|
||||
|
||||
- `shine_payments`
|
||||
- program id: `4QDCcaURt7phJGcvDS4VQQtrqDmUSMXvkWnMnMKCiUyE`
|
||||
- private key (base58): `3Fk2zYKTfoLMoU1tqMypGBsxj9zFF7PdXNEtodamv9aqtm8KWJNqsmT5DE9Z8pyDLCSqwdLM59LxbfdYp99b4xGg`
|
||||
- роль: ключ адреса программы (не пользовательский кошелёк)
|
||||
- баланс Devnet (program account): `0.00114144 SOL`
|
||||
- `shine_users`
|
||||
- program id: `8Z3HQizFRhyVu5cNBwWNBXZHTpu89VMkn7Wuk1oCtkeJ`
|
||||
- private key (base58): `2Zi4zNSFv69s3PPWr2vCtySiKSxiLA37TSSvHHVTygEQrehHScPoDkjKXYjVXDAHXi1Kg6LiZvYAq1ftV6aPSdrN`
|
||||
- роль: ключ адреса программы (не пользовательский кошелёк)
|
||||
- баланс Devnet (program account): `0.00114144 SOL`
|
||||
|
||||
Loading…
Reference in New Issue
Block a user