diff --git a/shine/doc/SHINE_PAYMENTS_V2.md b/shine/doc/SHINE_PAYMENTS_V2.md index d9e15e5..5c3b646 100644 --- a/shine/doc/SHINE_PAYMENTS_V2.md +++ b/shine/doc/SHINE_PAYMENTS_V2.md @@ -92,7 +92,13 @@ - если обе очереди пусты/выплачены: - переводит весь доступный остаток inflow-вольта в DAO (без reward). -9. Экономика покупки +9. `change_ticket_recipient` + - текущий `recipient_wallet` тикета может сменить получателя на другой кошелек; + - тикет должен быть невыплаченным; + - смена запрещена, если этот тикет является следующим к выплате в текущем шаге + (с учетом приоритета очереди 1 над очередью 2). + +10. Экономика покупки - сумма покупки идет в DAO; - тикет получает выплату `purchase_usd_cents * coef_ppm / 1_000_000`; - проверка лимита выполняется по `q1_sum_total_usd_cents` (исторически накопленная сумма, без вычета уже выплаченного). diff --git a/shine/programs/DEPLOY_CONFIG_CHECKLIST.md b/shine/programs/DEPLOY_CONFIG_CHECKLIST.md new file mode 100644 index 0000000..867c104 --- /dev/null +++ b/shine/programs/DEPLOY_CONFIG_CHECKLIST.md @@ -0,0 +1,81 @@ +# DEPLOY CONFIG CHECKLIST (Shine Programs) + +Документ для подготовки к реальному деплою (mainnet/prod): какие адреса и где заменить. + +## 1) Program IDs + +1. `shine/programs/shine_payments/src/lib.rs` + - `declare_id!("...")` для `shine_payments`. +2. `shine/programs/shine_users/src/lib.rs` + - `declare_id!("...")` для `shine_users`. +3. `shine/Anchor.toml` + - обновить `programs.devnet` / `programs.localnet` (и при необходимости добавить/обновить секцию под mainnet workflow). + +## 2) Shine Payments on-chain settings + +Файл: `shine/programs/shine_payments/src/settings.rs` + +Обязательные адреса: +1. `DAO_WALLET` +2. `MANAGER_WALLET` +3. `PYTH_SOL_USD_ACCOUNT` +4. `PYTH_SOL_USD_FEED_ID` (идентификатор feed для SOL/USD) + +Параметры экономики (по необходимости): +1. `START_COEF_PPM` +2. `START_LIMIT_USD_CENTS` +3. `START_CALL_REWARD_LAMPORTS` +4. `MAX_CALL_REWARD_LAMPORTS` +5. `ORACLE_MAX_AGE_SECS` + +## 3) Shine Users on-chain settings + +Файл: `shine/programs/shine_users/src/settings.rs` + +Обязательные параметры: +1. `REGISTRATION_FEE_RECEIVER` (куда идет комиссия регистрации) +2. `REGISTRATION_FEE_LAMPORTS` +3. при необходимости скорректировать лимитные/бонусные константы: + - `LIMIT_STEP` + - `LAMPORTS_PER_LIMIT_STEP` + - `START_BONUS_LIMIT` + +## 4) Web UI constants (hardcoded values) + +Проверить и заменить Program ID / Oracle account в HTML: +1. `shine/programs/shine_payments/web/buy_ticket.html` +2. `shine/programs/shine_payments/web/track_ticket.html` +3. `shine/programs/shine_payments/web/admin_tools.html` +4. `shine/programs/shine_payments/web/dao_tools.html` +5. `shine/programs/shine_payments/web/manager_tools.html` + +Проверить RPC endpoint для нужной сети в соответствующих страницах. + +## 5) Скрипты и окружение + +Проверить конфиги и env-файлы, где участвуют адреса: +1. `shine/scripts/**/config.env` +2. `shine/scripts/**/dao.config.env` +3. `shine/scripts/**/governance_token.config.env` + +## 6) Проверка перед деплоем + +1. `cargo check -p shine_payments` +2. `cargo check -p shine_users` +3. сверить, что `declare_id` совпадает с ключами деплоя программ. +4. убедиться, что `PYTH_SOL_USD_ACCOUNT` читается в выбранной сети. +5. прогнать smoke-тесты UI (buy / track / admin / dao / manager). + +## 7) Проверка после деплоя + +1. Выполнить `init` для `shine_payments`. +2. Проверить существование PDA: + - `config_pda` + - `coef_limit_pda` + - `queues_pda` + - `inflow_vault_pda` +3. Проверить покупку тикета и шаг выплаты на малой сумме. +4. Проверить `change_ticket_recipient`: + - разрешено для не-next тикета; + - запрещено для next тикета. + diff --git a/shine/programs/shine_payments/src/lib.rs b/shine/programs/shine_payments/src/lib.rs index 18f12f1..874b6cb 100644 --- a/shine/programs/shine_payments/src/lib.rs +++ b/shine/programs/shine_payments/src/lib.rs @@ -500,6 +500,61 @@ pub mod shine_payments { Ok(()) } + + pub fn change_ticket_recipient( + ctx: Context, + args: ChangeTicketRecipientArgs, + ) -> Result<()> { + let queues = read_state::(&ctx.accounts.queues_pda)?; + let mut ticket = read_state::(&ctx.accounts.ticket_pda)?; + + require!(!ticket.is_paid, PaymentsError::TicketAlreadyPaid); + require_keys_eq!( + ctx.accounts.signer.key(), + ticket.recipient_wallet, + PaymentsError::UnauthorizedTicketOwner + ); + + let (expected_ticket_pda, _) = find_ticket_pda(ctx.program_id, ticket.queue_id, ticket.index); + require_keys_eq!( + expected_ticket_pda, + ctx.accounts.ticket_pda.key(), + ErrCode::InvalidPdaAddress + ); + + let q1_pending = queues + .q1_tickets_total + .checked_sub(queues.q1_tickets_paid) + .ok_or(error!(ErrCode::MathOverflow))?; + let q2_pending = queues + .q2_tickets_total + .checked_sub(queues.q2_tickets_paid) + .ok_or(error!(ErrCode::MathOverflow))?; + + if q1_pending > 0 || q2_pending > 0 { + let target_queue = if q1_pending > 0 { 1 } else { 2 }; + let next_index = if target_queue == 1 { + queues + .q1_tickets_paid + .checked_add(1) + .ok_or(error!(ErrCode::MathOverflow))? + } else { + queues + .q2_tickets_paid + .checked_add(1) + .ok_or(error!(ErrCode::MathOverflow))? + }; + + require!( + !(ticket.queue_id == target_queue && ticket.index == next_index), + PaymentsError::CannotChangeRecipientForNextPayoutTicket + ); + } + + ticket.recipient_wallet = args.new_recipient_wallet; + write_state(&ctx.accounts.ticket_pda, &ticket)?; + Ok(()) + } } #[derive(Accounts)] @@ -619,6 +674,19 @@ pub struct StepPayout<'info> { pub sol_usd_price_update: Account<'info, PriceUpdateV2>, } +#[derive(Accounts)] +pub struct ChangeTicketRecipient<'info> { + /// CHECK: подписант-владелец текущего recipient тикета. + #[account(mut, signer)] + pub signer: AccountInfo<'info>, + /// CHECK: PDA очередей, читается вручную. + #[account(mut)] + pub queues_pda: AccountInfo<'info>, + /// CHECK: PDA тикета, читается и валидируется вручную. + #[account(mut)] + pub ticket_pda: AccountInfo<'info>, +} + #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct UpdateCoefLimitArgs { pub coef_ppm: u64, @@ -660,6 +728,11 @@ pub struct ManagerAddTicketArgs { pub payout_usd_cents: u64, } +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct ChangeTicketRecipientArgs { + pub new_recipient_wallet: Pubkey, +} + #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct ConfigState { pub version: u8, @@ -758,6 +831,10 @@ pub enum PaymentsError { InvalidManagerWallet, #[msg("Лимит менеджера по выбранной очереди превышен")] ManagerLimitExceeded, + #[msg("Только текущий получатель тикета может изменить получателя")] + UnauthorizedTicketOwner, + #[msg("Нельзя менять получателя у следующего тикета на выплату")] + CannotChangeRecipientForNextPayoutTicket, #[msg("Оракул передан неверный")] InvalidOracleAccount, #[msg("Некорректный feed id оракула")] diff --git a/shine/programs/shine_payments/web/track_ticket.html b/shine/programs/shine_payments/web/track_ticket.html index f1c5390..0475d1d 100644 --- a/shine/programs/shine_payments/web/track_ticket.html +++ b/shine/programs/shine_payments/web/track_ticket.html @@ -35,6 +35,7 @@ .err { color: var(--err); white-space: pre-wrap; } .paid { color: var(--ok); font-weight: 700; } .waiting { color: var(--muted); } + .xfer { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line); } code { background: var(--code); padding: 2px 4px; border-radius: 4px; } @@ -266,6 +267,12 @@ if (q2Pending > 0n) return 2; return 0; } + function nextPayoutTicket(queues) { + const queue = nextStepQueue(queues); + if (queue === 0) return null; + const index = queue === 1 ? (queues.q1Paid + 1n) : (queues.q2Paid + 1n); + return { queue, index }; + } async function refreshPayoutInfo() { const el = document.getElementById("payoutInfo"); @@ -320,6 +327,17 @@ } function renderTicketCard(core, pda, t) { + const next = nextPayoutTicket(core.queues); + const isNext = !!next && next.queue === t.queueId && next.index === t.index; + const isOwner = walletPubkey && walletPubkey.toBase58() === t.recipient.toBase58(); + const canTransfer = !t.isPaid && isOwner && !isNext; + const whyBlocked = t.isPaid + ? "Тикет уже выплачен" + : !isOwner + ? "Передача доступна только текущему получателю тикета" + : isNext + ? "Это следующий тикет на выплату, передача заблокирована" + : ""; return `
Тикет #${t.index.toString()} (очередь ${t.queueId}) (${t.isPaid ? "выплачен" : "ожидание"})
@@ -327,10 +345,55 @@
Получатель: ${t.recipient.toBase58()}
Сумма выплаты: ${centsToUsdStr(t.payoutUsdCents)} USD (~${usdCentsToSolStr(t.payoutUsdCents, core.pyth)} SOL)
Изначально очередь перед тикетом: ${centsToUsdStr(t.debtBeforeUsdCents)} USD
+
+
Передача билета
+
+ + +
+
${canTransfer ? "Доступно для текущего владельца тикета." : whyBlocked}
+
`; } + async function changeTicketRecipient(queueId, index, ticketPdaBase58) { + const resultEl = document.getElementById(`transferResult_${queueId}_${index}`); + const inputEl = document.getElementById(`newRecipient_${queueId}_${index}`); + resultEl.className = "muted"; + resultEl.textContent = ""; + try { + if (!walletPubkey) await connectWallet(); + const newRecipientRaw = (inputEl.value || "").trim(); + if (!newRecipientRaw) throw new Error("Введите адрес нового получателя"); + const newRecipient = new solanaWeb3.PublicKey(newRecipientRaw); + + const core = cachedCore || await loadCoreState(); + const disc = await ixDiscriminator("change_ticket_recipient"); + const data = concat(disc, newRecipient.toBytes()); + const keys = [ + { pubkey: walletPubkey, isSigner: true, isWritable: true }, + { pubkey: core.pdas.queuesPda, isSigner: false, isWritable: true }, + { pubkey: new solanaWeb3.PublicKey(ticketPdaBase58), isSigner: false, isWritable: true }, + ]; + const ix = new solanaWeb3.TransactionInstruction({ programId: PROGRAM_ID, keys, data }); + const sig = await sendInstruction(ix); + resultEl.className = "ok"; + resultEl.innerHTML = `Передача выполнена. Tx: ${sig}`; + await refreshAll(); + await findTickets(); + } catch (e) { + resultEl.className = "err"; + resultEl.textContent = String(e.message || e); + } + } + async function findTickets() { const out = document.getElementById("ticketResult"); out.textContent = ""; @@ -430,6 +493,14 @@ document.getElementById("refreshBtn").addEventListener("click", refreshAll); document.getElementById("findBtn").addEventListener("click", findTickets); document.getElementById("stepBtn").addEventListener("click", stepPayout); + document.getElementById("ticketResult").addEventListener("click", (e) => { + const btn = e.target.closest(".transferBtn"); + if (!btn) return; + const queueId = Number(btn.dataset.queue); + const index = btn.dataset.index; + const pda = btn.dataset.pda; + changeTicketRecipient(queueId, index, pda); + }); refreshAll(); diff --git a/shine/programs/shine_payments_legacy/Cargo.toml b/shine/programs/shine_payments_legacy/Cargo.toml deleted file mode 100644 index add95e4..0000000 --- a/shine/programs/shine_payments_legacy/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "shine_payments" -version = "0.1.0" -description = "Payments and investments smart contract" -edition = "2021" - -[lib] -crate-type = ["cdylib", "lib"] -name = "shine_payments" -test = false -doctest = false -bench = false - -[dependencies] -anchor-lang = "0.31.1" -common = { path = "../common" } - -# ==== добавлено для NFT-функционала ==== -anchor-spl = { version = "0.31.1", features = ["associated_token", "token"] } -mpl-token-metadata = "5.1.1" -spl-token = { version = "4.0.0", features = ["no-entrypoint"] } -# ====================================== - -[features] -default = [] -no-entrypoint = [] -no-idl = [] -no-log-ix-name = [] -anchor-debug = [] -custom-heap = [] -custom-panic = [] -cpi = [] -idl-build = ["anchor-lang/idl-build"] diff --git a/shine/programs/shine_payments_legacy/LEGACY_NOTICE.md b/shine/programs/shine_payments_legacy/LEGACY_NOTICE.md deleted file mode 100644 index e9b68fb..0000000 --- a/shine/programs/shine_payments_legacy/LEGACY_NOTICE.md +++ /dev/null @@ -1,6 +0,0 @@ -# Важно - -Эта папка содержит устаревшую версию `shine_payments`. - -- Не использовать для новых доработок. -- Актуальная реализация находится в `programs/shine_payments`. diff --git a/shine/programs/shine_payments_legacy/dApp/copyToServer.sh b/shine/programs/shine_payments_legacy/dApp/copyToServer.sh deleted file mode 100755 index 55f4dfd..0000000 --- a/shine/programs/shine_payments_legacy/dApp/copyToServer.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash - -# Скрипт для копирования dApp на тестовый сервер - -# Настройки -LOCAL_FILE="init.html" -REMOTE_USER="aidar" -REMOTE_HOST="shineup.me" -REMOTE_PATH="/home/aidar/Docker_server/site/dApp" - -# Проверка, что файл существует -if [ ! -f "$LOCAL_FILE" ]; then - echo "Ошибка: файл $LOCAL_FILE не найден." - exit 1 -fi - -# Копирование файла -scp "$LOCAL_FILE" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}" - -# Проверка результата -if [ $? -eq 0 ]; then - echo "Файл успешно загружен на сервер." -else - echo "Ошибка при загрузке файла на сервер." - exit 1 -fi - -#echo -#echo "Нажмите Enter, чтобы закрыть..." -#read diff --git a/shine/programs/shine_payments_legacy/dApp/init.html b/shine/programs/shine_payments_legacy/dApp/init.html deleted file mode 100644 index 3e2b8ac..0000000 --- a/shine/programs/shine_payments_legacy/dApp/init.html +++ /dev/null @@ -1,404 +0,0 @@ - - - - - - Shine Payments — Phantom demo (devnet, deep logs) - - - - - - - -

Shine Payments — Phantom wallet (devnet)

- -
- - - - - -
- -
- В Phantom выбери сеть Devnet. Логи ниже и в консоли (F12 → Console). -
- -

-
-
-
-
-
-
diff --git a/shine/programs/shine_payments_legacy/src/investments.rs b/shine/programs/shine_payments_legacy/src/investments.rs
deleted file mode 100644
index b9d71fa..0000000
--- a/shine/programs/shine_payments_legacy/src/investments.rs
+++ /dev/null
@@ -1,326 +0,0 @@
-use anchor_lang::prelude::*;
-
-use anchor_lang::solana_program::{program::invoke_signed, system_instruction};
-use common::utils::*; // тянем общие PDA-хелперы из programs/common
-
-// === добавлено: используем наш NFT-модуль ===
-use crate::nft::{CreateNftParams, create_nft_with_freeze};
-// ============================================
-
-/// Утилита чтения структуры из PDA: читает байты и десериализует.
-/// Возвращает ошибку, если данных нет/пустые/неверный формат.
-fn read_state_from_pda(pda: &AccountInfo) -> Result {
-    let raw = safe_read_pda(pda);                         // ← берём Vec (или пустой)
-    require!(!raw.is_empty(), ErrCode::EmptyPdaData);     // ← пусто — ошибка
-    let st = deserialize_invest_state(&raw)?;             // ← десериализуем по формату
-    require!(st.format == INVEST_STATE_FORMAT_V1, ErrCode::UnsupportedFormat); // ← проверяем версию
-    Ok(st)
-}
-
-/// Утилита записи структуры в PDA: сериализует и пишет.
-/// Важно: сам аккаунт уже должен существовать и быть #[account(mut)].
-fn write_state_to_pda(pda: &AccountInfo, s: &InvestState) -> Result<()> {
-    let raw = serialize_invest_state_v1(s); // ← 24 байта
-    write_to_pda(pda, &raw)                 // ← записываем в начало data
-}
-
-/// ==============================================
-/// Контексты инструкций (минимально необходимые)
-/// ==============================================
-
-/// init: создаём PDA и кладём в него PayStateV1 {format=1, coef=10, ...0}
-#[derive(Accounts)]
-pub struct Init<'info> {
-    /// Плательщик аренды за PDA; подписант транзакции.
-    #[account(mut)]
-    pub payer: Signer<'info>,
-
-    /// Наш PDA (с произвольным типом, чтобы работать через AccountInfo).
-    /// Проверку адреса делаем в handler (по seed + bump), чтобы избежать подмены.
-    /// CHECK: проверяется вручную по адресу
-    #[account(mut)]
-    pub state_pda: UncheckedAccount<'info>,
-
-    /// Системная программа.
-    pub system_program: Program<'info, System>,
-}
-
-/// Общие аккаунты для invest/add_bonus/claim:
-/// Везде просто читаем/пишем одно и то же состояние из того же PDA.
-#[derive(Accounts)]
-pub struct UseState<'info> {
-    /// Любой платящий/подписант (в реальном коде — свои проверки).
-    pub signer: Signer<'info>,
-
-    /// Тот же PDA с состоянием (должен уже существовать).
-    /// CHECK: проверяется вручную по адресу
-    #[account(mut)]
-    pub state_pda: UncheckedAccount<'info>,
-
-    /// Системная программа (на всякий случай; может не понадобиться).
-    pub system_program: Program<'info, System>,
-}
-
-/// ==============================================
-/// Программа
-/// ==============================================
-
-use super::*;
-use anchor_lang::prelude::*;
-
-
-/// ------------------------------------------
-/// init: создаёт PDA и записывает в него дефолтное состояние.
-/// format = 1, coef = 10, остальные поля = 0.
-/// ------------------------------------------
-pub fn init(ctx: Context) -> Result<()> {
-    let program_id = ctx.program_id; // ← адрес этой программы
-
-    // 1. Проверка что вызывает именно разрешённый ключ
-    /* todo   пока все могут вызыватьно                                         !! но в итоге будет добавленна проверка что бы только дао могло вызвать эту функцию один раз
-    require_keys_eq!(
-        ctx.accounts.payer.key(),
-        ALLOWED_INIT_CALLER,
-        ErrCode::InvalidSigner
-    );
-*/
-
-    // 2. Проверка что PDA ещё не создан
-    if ctx.accounts.state_pda.data_len() > 0 && ctx.accounts.state_pda.owner != &System::id() {
-        return Err(error!(ErrCode::PdaAlreadyExists));
-    }
-    
-    // 2. Ещё раз Проверка что PDA ещё не создан  
-    if ctx.accounts.state_pda.owner != &System::id()
-        || ctx.accounts.state_pda.lamports() > 0
-    {
-        // Если аккаунт уже создан и не пустой
-        return Err(error!(ErrCode::PdaAlreadyExists));
-    }
-    
-    let pda_key_expected = Pubkey::find_program_address(&[crate::PDA_SEED_PREFIX], program_id).0; // ← вычисляем PDA
-    require_keys_eq!(
-        pda_key_expected,
-        ctx.accounts.state_pda.key(),
-        ErrCode::InvalidPdaAddress
-    ); // ← убеждаемся, что нам подали именно правильный PDA
-
-    // Конструируем дефолтную структуру состояния.
-    let state = InvestState {
-        format: INVEST_STATE_FORMAT_V1,  // ← 1
-        coef: crate::DEFAULT_COEF,       // ← 10
-        q1_tokens: 0,                    // ← нули
-        sum1_bonus: 0,
-        q1_paid_tokens: 0,
-        sum1_paid_bonus: 0,
-    };
-
-    // Сериализуем в 24 байта.
-    let data = serialize_invest_state_v1(&state);
-
-    // Для подписи PDA нужен bump; здесь получим (ключ, bump).
-    let (_pda_key, bump) = Pubkey::find_program_address(&[crate::PDA_SEED_PREFIX], program_id);
-
-    // Сиды для invoke_signed: [seed, bump]
-    let seeds: [&[u8]; 2] = [crate::PDA_SEED_PREFIX, &[bump]];
-
-    // Создаём и сразу записываем, арендный минимум оплачивает payer.
-    create_and_write_pda(
-        &ctx.accounts.state_pda.to_account_info(),   // куда пишем
-        &ctx.accounts.payer.to_account_info(),       // кто платит
-        &ctx.accounts.system_program.to_account_info(),
-        program_id,
-        &seeds,
-        data,
-        crate::PAY_STATE_SPACE,                      // резерв с запасом
-    )?;
-
-    Ok(())
-}
-
-/// ------------------------------------------
-/// invest: «внос инвестиций».
-/// По заданию: в начале читаем состояние, в конце сохраняем.
-/// (Здесь логика модификации не задана — оставляем как заглушку.)
-/// ------------------------------------------
-pub fn invest(ctx: Context, _amount: u64) -> Result<()> {
-    // 1) читаем
-    let mut st = read_state_from_pda(&ctx.accounts.state_pda.to_account_info())?; // ← PayStateV1
-
-    // --- тут можно модифицировать st по твоей бизнес-логике ---
-    // Например, ничего не меняем сейчас (заглушка).
-    let _ = &mut st; // чтоб компилятор не ругался, если пока не используем
-
-    // 2) сохраняем
-    write_state_to_pda(&ctx.accounts.state_pda.to_account_info(), &st)?;
-    Ok(())
-}
-
-/// ------------------------------------------
-/// add_bonus: «начисление бонусов» (обычно вызывать от DAO).
-/// По заданию: читаем в начале, создаём/добавляем NFT в очередь, сохраняем в конце.
-/// Для операций с NFT используем расширенный контекст AddBonusCtx (см. lib.rs).
-/// ------------------------------------------
-pub fn add_bonus(ctx: Context, investor: Pubkey, amount: u64) -> Result<()> {
-    // 1) читаем состояние
-    let mut st = read_state_from_pda(&ctx.accounts.state_pda.to_account_info())?;
-
-    // 2) создаём/добавляем NFT через модуль nft (создание metadata, mint 1, freeze, master edition, verify)
-    let next_index = st.q1_tokens as u64 + 1;
-    let params = CreateNftParams {
-        name: format!("Bonus #{}", next_index),
-        symbol: "BN".to_string(),
-        uri: "https://example.com/nft.json".to_string(), // заглушка для devnet-теста
-        index: next_index,
-        recipient: investor,
-    };
-
-    // ВАЖНО: mint_pda должен быть создан ТЕСТОМ заранее с decimals=0, mint_authority=signer, freeze_authority=signer.
-    create_nft_with_freeze(&ctx, params)?;
-
-    // 3) обновляем агрегаты очереди (минимально: увеличим счётчик и сумму бонусов)
-    st.q1_tokens = st.q1_tokens.saturating_add(1);
-    let add = u32::try_from(core::cmp::min(amount, u64::from(u32::MAX))).unwrap_or(u32::MAX);
-    st.sum1_bonus = st.sum1_bonus.saturating_add(add);
-
-    // 4) сохраняем
-    write_state_to_pda(&ctx.accounts.state_pda.to_account_info(), &st)?;
-    Ok(())
-}
-
-/// ------------------------------------------
-/// claim: «выплата».
-/// По заданию: читаем в начале, сохраняем в конце.
-/// ------------------------------------------
-pub fn claim(ctx: Context) -> Result<()> {
-    // 1) читаем
-    let mut st = read_state_from_pda(&ctx.accounts.state_pda.to_account_info())?;
-
-    // --- тут твоя логика списаний/выплат ---
-    let _ = &mut st; // заглушка
-
-    // 2) сохраняем
-    write_state_to_pda(&ctx.accounts.state_pda.to_account_info(), &st)?;
-    Ok(())
-}
-
-
-
-
-
-//todo
-
-
-
-
-
-/// ==============================================
-/// Коды ошибок (берём из твоего блока; можно расширять)
-/// ==============================================
-
-#[error_code]
-pub enum ErrCode {
-    /// Система уже инициализирована и не может быть инициализирована повторно!
-    #[msg("Система уже инициализирована и не может быть инициализирована повторно!")]
-    SystemAlreadyInitialized = 1000,
-
-    #[msg("PDA не содержит данных или не инициализирован")]
-    EmptyPdaData = 1002,
-
-    #[msg("Пользователь уже зарегистрирован")]
-    UserAlreadyExists = 1003,
-
-    #[msg("Некорректный логин")]
-    InvalidLogin = 1004,
-
-    #[msg("Не совпадает PDA адрес")]
-    InvalidPdaAddress = 1006,
-
-    #[msg("Формат данных не поддерживается")]
-    UnsupportedFormat = 1011,
-
-    #[msg("Ошибка при десериализации")]
-    DeserializationError = 1012,
-
-    /// PDA уже существует, создание невозможно
-    #[msg("PDA-аккаунт уже существует и не может быть создан повторно.")]
-    PdaAlreadyExists = 1009,
-
-    #[msg("Подписавший не совпадает с ожидаемым пользователем (временное ограничение)")]
-    InvalidSigner = 1005,
-
-    /// Не получилось создать пользователя
-    #[msg("Не получилось создать пользователя, система уже перегружена, попробуйте позже!")]
-    NoSuitableIdPda = 1010,
-}
-
-
-
-
-
-use anchor_lang::prelude::*;
-
-/// ================================
-/// КОНСТАНТЫ ФОРМАТА / ДЛИНЫ ДАННЫХ
-/// ================================
-
-/// Версия формата хранения состояния.
-/// Мы жёстко фиксируем «1», чтобы код мог отличать будущие версии.
-pub const INVEST_STATE_FORMAT_V1: u32 = 1;
-
-/// Сырые данные состояния V1 занимают ровно 6 * 4 = 24 байта.
-pub const INVEST_STATE_RAW_LEN_V1: usize = 24; // байт
-
-/// ================================
-/// ОПИСАНИЕ СТРУКТУРЫ СОСТОЯНИЯ (V1)
-/// ================================
-#[derive(Clone, Copy, Debug, Default)]
-pub struct InvestState {
-    pub format: u32,
-    pub coef: u32,
-    pub q1_tokens: u32,
-    pub sum1_bonus: u32,
-    pub q1_paid_tokens: u32,
-    pub sum1_paid_bonus: u32,
-}
-
-/// ========================================
-/// СЕРИАЛИЗАЦИЯ (структура -> массив байт)
-/// ========================================
-pub fn serialize_invest_state_v1(s: &InvestState) -> Vec {
-    let mut out = Vec::with_capacity(INVEST_STATE_RAW_LEN_V1);
-    out.extend_from_slice(&INVEST_STATE_FORMAT_V1.to_le_bytes()); // [0..4)
-    out.extend_from_slice(&s.coef.to_le_bytes());            // [4..8)
-    out.extend_from_slice(&s.q1_tokens.to_le_bytes());       // [8..12)
-    out.extend_from_slice(&s.sum1_bonus.to_le_bytes());      // [12..16)
-    out.extend_from_slice(&s.q1_paid_tokens.to_le_bytes());  // [16..20)
-    out.extend_from_slice(&s.sum1_paid_bonus.to_le_bytes()); // [20..24)
-    debug_assert_eq!(out.len(), INVEST_STATE_RAW_LEN_V1);
-    out
-}
-
-/// ===========================================
-/// ДЕСЕРИАЛИЗАЦИЯ (массив байт -> структура)
-/// ===========================================
-pub fn deserialize_invest_state(data: &[u8]) -> Result {
-    if data.len() < INVEST_STATE_RAW_LEN_V1 {
-        return Err(error!(ErrCode::DeserializationError));
-    }
-    fn read_u32_le(slice: &[u8], start: usize) -> u32 {
-        let bytes: [u8; 4] = slice[start..start + 4]
-            .try_into()
-            .expect("slice has enough length due to pre-check");
-        u32::from_le_bytes(bytes)
-    }
-    let format = read_u32_le(data, 0);
-    if format != INVEST_STATE_FORMAT_V1 {
-        return Err(error!(ErrCode::UnsupportedFormat));
-    }
-    let coef            = read_u32_le(data, 4);
-    let q1_tokens       = read_u32_le(data, 8);
-    let sum1_bonus      = read_u32_le(data, 12);
-    let q1_paid_tokens  = read_u32_le(data, 16);
-    let sum1_paid_bonus = read_u32_le(data, 20);
-
-    Ok(InvestState { format, coef, q1_tokens, sum1_bonus, q1_paid_tokens, sum1_paid_bonus })
-}
diff --git a/shine/programs/shine_payments_legacy/src/lib.rs b/shine/programs/shine_payments_legacy/src/lib.rs
deleted file mode 100644
index 8aace15..0000000
--- a/shine/programs/shine_payments_legacy/src/lib.rs
+++ /dev/null
@@ -1,158 +0,0 @@
-use anchor_lang::prelude::*;
-
-declare_id!("qpgnAKhsXgPPaqQWfXhpme7UnG8GyStssuoSjF6Fzy3");
-
-
-/// Подключаем модуль с полной реализацией.
-pub mod investments;
-use investments::*; // импортируем всё в корень
-
-// === модуль NFT ===
-pub mod nft;
-
-// ==============================================
-// Константы формата / сидов / размеров
-// ==============================================
-
-/// Префикс (seed) для PDA, где храним глобальное состояние выплат.
-/// Важно: сид — это просто набор байт; здесь он фиксированный.
-pub const PDA_SEED_PREFIX: &[u8] = b"shine_investments_state";
-
-/// Значение коэффициента «по умолчанию» при инициализации.
-pub const DEFAULT_COEF: u32 = 10; // ← «коэффициент» = 10 при init
-
-/// Ровно столько байт резервируем под PDA-данные.
-/// (Можно добавить запас на будущее, но по заданию — только 28.)
-pub const PAY_STATE_SPACE: u64 = 50; // просто сделал с запасом
-
-// ==============================================
-// Программа
-// ==============================================
-
-#[program]
-pub mod shine_payments {
-    use super::*;
-    // Явно подтягиваем типы и функции, чтобы не было путаницы после предыдущих ошибок парсера
-    use crate::investments::{Init, UseState};
-    use crate::investments::{
-        add_bonus as inv_add_bonus, claim as inv_claim, init as inv_init, invest as inv_invest,
-        ErrCode,
-    };
-
-    /// init — создаёт PDA и кладёт дефолтное состояние.
-    pub fn init(ctx: Context) -> Result<()> {
-        inv_init(ctx)
-    }
-
-    /// invest — в начале читает состояние, в конце сохраняет (логика внутри модуля).
-    pub fn invest(ctx: Context, amount: u64) -> Result<()> {
-        inv_invest(ctx, amount)
-    }
-
-    /// add_bonus — начисление бонусов (обычно от DAO).
-    /// Для NFT используем расширенный контекст AddBonusCtx (с аккаунтами коллекции и т.п.).
-    pub fn add_bonus(ctx: Context, investor: Pubkey, amount: u64) -> Result<()> {
-        inv_add_bonus(ctx, investor, amount)
-    }
-
-    /// claim — выплата.
-    pub fn claim(ctx: Context) -> Result<()> {
-        inv_claim(ctx)
-    }
-
-    /// ВРЕМЕННАЯ ФУНКЦИЯ только для тестов (в итоговой версии её не будет):
-    /// deleteInit — удалить PDA из init и вернуть ренту подписанту.
-    pub fn delete_init(ctx: Context) -> Result<()> {
-        let program_id = ctx.program_id;
-
-        // PDA по тем же сид/бамп, что и в init
-        let (expected_pda, _bump) = Pubkey::find_program_address(&[PDA_SEED_PREFIX], program_id);
-        require_keys_eq!(
-            expected_pda,
-            ctx.accounts.state_pda.key(),
-            ErrCode::InvalidPdaAddress
-        );
-
-        // Рента уйдёт на счёт подписанта (signer)
-        common::utils::delete_pda_return_rent(
-            &ctx.accounts.state_pda.to_account_info(),
-            &ctx.accounts.signer.to_account_info(),
-            program_id,
-        )
-    }
-}
-
-// ==============================================
-// Контексты вне #[program]
-// ==============================================
-
-/// Контекст для deleteInit (временный для тестов)
-#[derive(Accounts)]
-pub struct DeleteInit<'info> {
-    /// Подписант транзакции — ПОЛУЧАТЕЛЬ ренты
-    #[account(mut)]
-    pub signer: Signer<'info>,
-
-    /// Тот самый PDA из init
-    /// CHECK: адрес валидируем в хендлере по сид-у
-    #[account(mut)]
-    pub state_pda: UncheckedAccount<'info>,
-
-    /// Системная программа
-    pub system_program: Program<'info, System>,
-}
-
-/// Контекст для add_bonus: полный набор аккаунтов для операций с NFT и коллекцией.
-/// (Комменты по стилю проекта оставлены.)
-#[derive(Accounts)]
-pub struct AddBonusCtx<'info> {
-    /// Любой платящий/подписант (в реальном коде — свои проверки).
-    #[account(mut)]
-    pub signer: Signer<'info>,
-
-    /// Тот же PDA с состоянием (должен уже существовать).
-    /// CHECK: проверяется вручную по адресу
-    #[account(mut)]
-    pub state_pda: UncheckedAccount<'info>,
-
-    // --- аккаунты минтимого NFT ---
-    /// Mint создаваемого NFT (должен быть создан заранее: decimals=0, mint_authority=signer, freeze_authority=signer)
-    /// CHECK
-    #[account(mut)]
-    pub mint_pda: UncheckedAccount<'info>,
-
-    /// ATA получателя (может быть предсоздан тестом)
-    /// CHECK
-    #[account(mut)]
-    pub recipient_ata: UncheckedAccount<'info>,
-    /// Владелец ATA (инвестор)
-    /// CHECK
-    pub recipient_owner: UncheckedAccount<'info>,
-
-    // --- аккаунты коллекции (уже созданной заранее) ---
-    /// CHECK
-    pub collection_mint: UncheckedAccount<'info>,
-    /// CHECK
-    #[account(mut)]
-    pub collection_metadata_pda: UncheckedAccount<'info>,
-    /// CHECK
-    #[account(mut)]
-    pub collection_master_edition_pda: UncheckedAccount<'info>,
-    /// Апдейтер коллекции (update authority)
-    pub collection_update_authority: Signer<'info>,
-
-    // --- metadata + master edition для создаваемого NFT ---
-    /// CHECK
-    #[account(mut)]
-    pub metadata_pda: UncheckedAccount<'info>,
-    /// CHECK
-    #[account(mut)]
-    pub master_edition_pda: UncheckedAccount<'info>,
-
-    // --- программы ---
-    /// CHECK: проверяется по ID внутри nft.rs
-    pub token_metadata_program: UncheckedAccount<'info>,
-    pub token_program: Program<'info, anchor_spl::token::Token>,
-    pub associated_token_program: Program<'info, anchor_spl::associated_token::AssociatedToken>,
-    pub system_program: Program<'info, System>,
-}
diff --git a/shine/programs/shine_payments_legacy/src/nft.rs b/shine/programs/shine_payments_legacy/src/nft.rs
deleted file mode 100644
index f78fa9c..0000000
--- a/shine/programs/shine_payments_legacy/src/nft.rs
+++ /dev/null
@@ -1,190 +0,0 @@
-use anchor_lang::prelude::*;
-use anchor_lang::solana_program::{program::invoke, program::invoke_signed};
-use anchor_spl::{associated_token::AssociatedToken, token::Token};
-
-use mpl_token_metadata::{
-    ID as TM_ID,
-    instructions::{
-        CreateMasterEditionV3Builder,
-        CreateMetadataAccountV3Builder,
-        SetAndVerifySizedCollectionItemBuilder,
-    },
-    types::{Collection, Creator, DataV2, Uses, UseMethod},
-};
-
-use spl_token::instruction as spl_ix;
-
-/// Параметры для минта NFT
-#[derive(Clone)]
-pub struct CreateNftParams {
-    pub name: String,
-    pub symbol: String,
-    pub uri: String,
-    pub index: u64,
-    pub recipient: Pubkey,
-}
-
-/// Создание metadata, чеканка 1 токена, freeze ATA, создание master edition, verify в коллекции.
-pub fn create_nft_with_freeze(
-    ctx: &Context,
-    params: CreateNftParams,
-) -> Result<()> {
-    let a = &ctx.accounts;
-
-    // Проверяем что это именно программа Metaplex Token Metadata
-    require_keys_eq!(a.token_metadata_program.key(), TM_ID, CustomError::InvalidMetadataProgram);
-
-    // 1) Создание Metadata для нового NFT
-    let creators = Some(vec![Creator {
-        address: a.collection_update_authority.key(),
-        verified: true,
-        share: 100,
-    }]);
-
-    let data = DataV2 {
-        name: truncate(¶ms.name, 32),
-        symbol: truncate(¶ms.symbol, 10),
-        uri: truncate(¶ms.uri, 256),
-        seller_fee_basis_points: 0,
-        creators,
-        collection: Some(Collection {
-            verified: false, // отметим как часть коллекции позже через verify
-            key: a.collection_mint.key(),
-        }),
-        uses: Some(Uses {
-            use_method: UseMethod::Burn,
-            remaining: 1,
-            total: 1,
-        }),
-    };
-
-    // В mpl-token-metadata v5 update_authority(pubkey, is_signer: bool)
-    let ix_md = CreateMetadataAccountV3Builder::new()
-        .metadata(a.metadata_pda.key())
-        .mint(a.mint_pda.key())
-        .mint_authority(a.signer.key())
-        .payer(a.signer.key())
-        .update_authority(a.collection_update_authority.key(), true)
-        .system_program(a.system_program.key())
-        .data(data)
-        .is_mutable(true)
-        .instruction();
-
-    invoke_signed(
-        &ix_md,
-        &[
-            a.metadata_pda.to_account_info(),
-            a.mint_pda.to_account_info(),
-            a.signer.to_account_info(),
-            a.collection_update_authority.to_account_info(),
-            a.system_program.to_account_info(),
-            a.token_metadata_program.to_account_info(),
-        ],
-        &[],
-    )?;
-
-    // 2) Чеканим 1 токен на ATA получателя
-    let ix_mint_to = spl_ix::mint_to(
-        &a.token_program.key(),
-        &a.mint_pda.key(),
-        &a.recipient_ata.key(),
-        &a.signer.key(),
-        &[],
-        1,
-    )?;
-    invoke(
-        &ix_mint_to,
-        &[
-            a.mint_pda.to_account_info(),
-            a.recipient_ata.to_account_info(),
-            a.signer.to_account_info(),
-            a.token_program.to_account_info(),
-        ],
-    )?;
-
-    // 3) Замораживаем ATA получателя (freeze authority = signer)
-    let ix_freeze = spl_ix::freeze_account(
-        &a.token_program.key(),
-        &a.recipient_ata.key(),
-        &a.mint_pda.key(),
-        &a.signer.key(),
-        &[],
-    )?;
-    invoke(
-        &ix_freeze,
-        &[
-            a.recipient_ata.to_account_info(),
-            a.mint_pda.to_account_info(),
-            a.signer.to_account_info(),
-            a.token_program.to_account_info(),
-        ],
-    )?;
-
-    // 4) Создаём Master Edition
-    let ix_me = CreateMasterEditionV3Builder::new()
-        .edition(a.master_edition_pda.key())
-        .mint(a.mint_pda.key())
-        .update_authority(a.collection_update_authority.key())
-        .mint_authority(a.signer.key())
-        .payer(a.signer.key())
-        .metadata(a.metadata_pda.key())
-        .token_program(a.token_program.key())
-        .system_program(a.system_program.key())
-        .max_supply(0)
-        .instruction();
-
-    invoke_signed(
-        &ix_me,
-        &[
-            a.master_edition_pda.to_account_info(),
-            a.mint_pda.to_account_info(),
-            a.collection_update_authority.to_account_info(),
-            a.signer.to_account_info(),
-            a.metadata_pda.to_account_info(),
-            a.token_program.to_account_info(),
-            a.system_program.to_account_info(),
-            a.token_metadata_program.to_account_info(),
-        ],
-        &[],
-    )?;
-
-    // 5) Verify как часть коллекции
-    // Метод называется collection_master_edition_account(...)
-    let ix_verify = SetAndVerifySizedCollectionItemBuilder::new()
-        .metadata(a.metadata_pda.key())
-        .collection_authority(a.collection_update_authority.key())
-        .payer(a.signer.key())
-        .update_authority(a.collection_update_authority.key())
-        .collection_mint(a.collection_mint.key())
-        .collection(a.collection_metadata_pda.key())
-        .collection_master_edition_account(a.collection_master_edition_pda.key())
-        .instruction();
-
-    invoke_signed(
-        &ix_verify,
-        &[
-            a.metadata_pda.to_account_info(),
-            a.collection_update_authority.to_account_info(),
-            a.signer.to_account_info(),
-            a.collection_update_authority.to_account_info(),
-            a.collection_mint.to_account_info(),
-            a.collection_metadata_pda.to_account_info(),
-            a.collection_master_edition_pda.to_account_info(),
-            a.token_metadata_program.to_account_info(),
-        ],
-        &[],
-    )?;
-
-    msg!("NFT создан, заморожен, мастер-издание создано и верифицировано в коллекции (index={})", params.index);
-    Ok(())
-}
-
-fn truncate(s: &str, max: usize) -> String {
-    if s.len() <= max { s.to_string() } else { s.chars().take(max).collect() }
-}
-
-#[error_code]
-pub enum CustomError {
-    #[msg("Invalid Token Metadata program account")]
-    InvalidMetadataProgram,
-}