From 890e10de9fb89f71f5d51e469b01727b75d4adc8 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Sat, 16 May 2026 17:17:41 +0300 Subject: [PATCH] Commit remaining workspace changes --- shine/programs/common/src/utils.rs | 2 +- .../programs/shine_payments_legacy/Cargo.toml | 33 -- .../shine_payments_legacy/LEGACY_NOTICE.md | 6 - .../dApp/copyToServer.sh | 30 -- .../shine_payments_legacy/dApp/init.html | 404 ------------------ .../shine_payments_legacy/src/investments.rs | 326 -------------- .../programs/shine_payments_legacy/src/lib.rs | 158 ------- .../programs/shine_payments_legacy/src/nft.rs | 190 -------- shine/tests/shine.ts | 288 ++++++++++++- 9 files changed, 282 insertions(+), 1155 deletions(-) delete mode 100644 shine/programs/shine_payments_legacy/Cargo.toml delete mode 100644 shine/programs/shine_payments_legacy/LEGACY_NOTICE.md delete mode 100755 shine/programs/shine_payments_legacy/dApp/copyToServer.sh delete mode 100644 shine/programs/shine_payments_legacy/dApp/init.html delete mode 100644 shine/programs/shine_payments_legacy/src/investments.rs delete mode 100644 shine/programs/shine_payments_legacy/src/lib.rs delete mode 100644 shine/programs/shine_payments_legacy/src/nft.rs diff --git a/shine/programs/common/src/utils.rs b/shine/programs/common/src/utils.rs index ae8ce47..8ead0a8 100644 --- a/shine/programs/common/src/utils.rs +++ b/shine/programs/common/src/utils.rs @@ -1,5 +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}; /// сдесь коды всех ошибок 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,
-}
diff --git a/shine/tests/shine.ts b/shine/tests/shine.ts
index 69b91f3..367b32c 100644
--- a/shine/tests/shine.ts
+++ b/shine/tests/shine.ts
@@ -1,16 +1,290 @@
 import * as anchor from "@coral-xyz/anchor";
 import { Program } from "@coral-xyz/anchor";
+import {
+  Ed25519Program,
+  PublicKey,
+  SYSVAR_INSTRUCTIONS_PUBKEY,
+  SystemProgram,
+  Transaction,
+} from "@solana/web3.js";
+import { createHash } from "crypto";
+import { expect } from "chai";
 import { Shine } from "../target/types/shine";
 
-describe("shine", () => {
-  // Configure the client to use the local cluster.
-  anchor.setProvider(anchor.AnchorProvider.env());
+const MAGIC = Buffer.from("SHiNE", "utf8");
+const FORMAT_MAJOR = 1;
+const FORMAT_MINOR = 0;
+const RESERVED = Buffer.from([0, 0, 0, 0, 0]);
+const ZERO_HASH = Buffer.alloc(32, 0);
+const KEY_STATUS_CREATED = 0;
 
+const LIMIT_STEP = 10_000n;
+const START_BONUS_LIMIT = 100_000n;
+const FEE_RECEIVER = new PublicKey("9vXFoN9ngfN1gpqQ3HT5n3y9Wp2r7HnSQckirgwVwWwb");
+
+type MutableFields = {
+  blockchainKey: PublicKey;
+  deviceKey: PublicKey;
+  chainNumber: number;
+  isServer: boolean;
+  serverKey: PublicKey;
+  serverAddress: string;
+  connectionServers: string[];
+  trustedCount: number;
+};
+
+type UnsignedRecord = {
+  createdAtMs: bigint;
+  updatedAtMs: bigint;
+  version: number;
+  prevHash: Buffer;
+  login: string;
+  rootKeyStatus: number;
+  rootKey: PublicKey;
+  blockchainKeyStatus: number;
+  blockchainKey: PublicKey;
+  deviceKeyStatus: number;
+  deviceKey: PublicKey;
+  chainNumber: number;
+  balance: bigint;
+  isServer: boolean;
+  serverKey: PublicKey;
+  serverAddress: string;
+  connectionServers: string[];
+  trustedCount: number;
+};
+
+function u16le(v: number): Buffer {
+  const b = Buffer.alloc(2);
+  b.writeUInt16LE(v, 0);
+  return b;
+}
+
+function u32le(v: number): Buffer {
+  const b = Buffer.alloc(4);
+  b.writeUInt32LE(v, 0);
+  return b;
+}
+
+function u64le(v: bigint): Buffer {
+  const b = Buffer.alloc(8);
+  b.writeBigUInt64LE(v, 0);
+  return b;
+}
+
+function serializeUnsignedRecord(r: UnsignedRecord): Buffer {
+  const loginBytes = Buffer.from(r.login, "utf8");
+  const serverAddressBytes = Buffer.from(r.serverAddress, "utf8");
+
+  const out: Buffer[] = [];
+  out.push(MAGIC);
+  out.push(Buffer.from([FORMAT_MAJOR]));
+  out.push(Buffer.from([FORMAT_MINOR]));
+  out.push(Buffer.alloc(2, 0)); // record_len placeholder
+
+  out.push(u64le(r.createdAtMs));
+  out.push(u64le(r.updatedAtMs));
+  out.push(u32le(r.version));
+  out.push(r.prevHash);
+
+  out.push(Buffer.from([loginBytes.length]));
+  out.push(loginBytes);
+
+  out.push(Buffer.from([r.rootKeyStatus]));
+  out.push(r.rootKey.toBuffer());
+  out.push(Buffer.from([r.blockchainKeyStatus]));
+  out.push(r.blockchainKey.toBuffer());
+  out.push(Buffer.from([r.deviceKeyStatus]));
+  out.push(r.deviceKey.toBuffer());
+
+  out.push(u16le(r.chainNumber));
+  out.push(u64le(r.balance));
+
+  out.push(Buffer.from([r.isServer ? 1 : 0]));
+  if (r.isServer) {
+    out.push(r.serverKey.toBuffer());
+    out.push(Buffer.from([serverAddressBytes.length]));
+    out.push(serverAddressBytes);
+  }
+
+  out.push(Buffer.from([r.connectionServers.length]));
+  for (const s of r.connectionServers) {
+    const sb = Buffer.from(s, "utf8");
+    out.push(Buffer.from([sb.length]));
+    out.push(sb);
+  }
+
+  out.push(Buffer.from([r.trustedCount]));
+  out.push(RESERVED);
+
+  const unsigned = Buffer.concat(out);
+  const recordLen = unsigned.length + 64;
+  unsigned.writeUInt16LE(recordLen, 7);
+  return unsigned;
+}
+
+function sha256(buf: Buffer): Buffer {
+  return createHash("sha256").update(buf).digest();
+}
+
+function extractSigFromEdIx(ixData: Buffer): Buffer {
+  const signatureOffset = ixData.readUInt16LE(2);
+  return ixData.subarray(signatureOffset, signatureOffset + 64);
+}
+
+describe("shine_users e2e", () => {
+  anchor.setProvider(anchor.AnchorProvider.env());
+  const provider = anchor.getProvider() as anchor.AnchorProvider;
   const program = anchor.workspace.shine as Program;
 
-  it("Is initialized!", async () => {
-    // Add your test here.
-    const tx = await program.methods.initialize().rpc();
-    console.log("Your transaction signature", tx);
+  it("registers user and updates balance/server data", async () => {
+    const login = `u${Date.now().toString().slice(-10)}`;
+    const [userPda] = PublicKey.findProgramAddressSync(
+      [Buffer.from("login="), Buffer.from(login, "utf8")],
+      program.programId
+    );
+
+    const root = anchor.web3.Keypair.generate();
+    const blockchainKey = anchor.web3.Keypair.generate().publicKey;
+    const deviceKey = anchor.web3.Keypair.generate().publicKey;
+    const serverKey1 = anchor.web3.Keypair.generate().publicKey;
+    const serverKey2 = anchor.web3.Keypair.generate().publicKey;
+
+    const createdAtMs = BigInt(Date.now());
+    const additionalLimitCreate = 20_000n;
+    expect(additionalLimitCreate % LIMIT_STEP).eq(0n);
+
+    const createRecord: UnsignedRecord = {
+      createdAtMs,
+      updatedAtMs: createdAtMs,
+      version: 0,
+      prevHash: ZERO_HASH,
+      login,
+      rootKeyStatus: KEY_STATUS_CREATED,
+      rootKey: root.publicKey,
+      blockchainKeyStatus: KEY_STATUS_CREATED,
+      blockchainKey,
+      deviceKeyStatus: KEY_STATUS_CREATED,
+      deviceKey,
+      chainNumber: 1,
+      balance: START_BONUS_LIMIT + additionalLimitCreate,
+      isServer: true,
+      serverKey: serverKey1,
+      serverAddress: "https://srv-1.local",
+      connectionServers: ["srv_login_1"],
+      trustedCount: 0,
+    };
+
+    const createUnsigned = serializeUnsignedRecord(createRecord);
+    const createHash = sha256(createUnsigned);
+    const createEdIx = Ed25519Program.createInstructionWithPrivateKey({
+      privateKey: root.secretKey,
+      message: createHash,
+    });
+    const createSig = extractSigFromEdIx(Buffer.from(createEdIx.data));
+
+    const createIx = await program.methods
+      .createUserPda({
+        login,
+        rootKey: root.publicKey,
+        createdAtMs: new anchor.BN(createdAtMs.toString()),
+        additionalLimit: new anchor.BN(additionalLimitCreate.toString()),
+        fields: {
+          blockchainKey,
+          deviceKey,
+          chainNumber: 1,
+          isServer: true,
+          serverKey: serverKey1,
+          serverAddress: "https://srv-1.local",
+          connectionServers: ["srv_login_1"],
+          trustedCount: 0,
+        },
+        signature: createSig,
+      })
+      .accounts({
+        signer: provider.wallet.publicKey,
+        userPda,
+        systemProgram: SystemProgram.programId,
+        feeReceiver: FEE_RECEIVER,
+        instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
+      })
+      .instruction();
+
+    await provider.sendAndConfirm(new Transaction().add(createEdIx, createIx), []);
+
+    const createAcc = await provider.connection.getAccountInfo(userPda);
+    expect(createAcc).not.eq(null);
+    expect(createAcc!.owner.toBase58()).eq(program.programId.toBase58());
+
+    const additionalLimitUpdate = 30_000n;
+    expect(additionalLimitUpdate % LIMIT_STEP).eq(0n);
+
+    const updateRecord: UnsignedRecord = {
+      createdAtMs,
+      updatedAtMs: createdAtMs + 1_000n,
+      version: 1,
+      prevHash: sha256(createUnsigned),
+      login,
+      rootKeyStatus: KEY_STATUS_CREATED,
+      rootKey: root.publicKey,
+      blockchainKeyStatus: KEY_STATUS_CREATED,
+      blockchainKey: anchor.web3.Keypair.generate().publicKey,
+      deviceKeyStatus: KEY_STATUS_CREATED,
+      deviceKey: anchor.web3.Keypair.generate().publicKey,
+      chainNumber: 1,
+      balance: START_BONUS_LIMIT + additionalLimitCreate + additionalLimitUpdate,
+      isServer: true,
+      serverKey: serverKey2,
+      serverAddress: "https://srv-2.local",
+      connectionServers: ["srv_login_2", "srv_login_3"],
+      trustedCount: 0,
+    };
+
+    const updateUnsigned = serializeUnsignedRecord(updateRecord);
+    const updateHash = sha256(updateUnsigned);
+    const updateEdIx = Ed25519Program.createInstructionWithPrivateKey({
+      privateKey: root.secretKey,
+      message: updateHash,
+    });
+    const updateSig = extractSigFromEdIx(Buffer.from(updateEdIx.data));
+
+    const updateIx = await program.methods
+      .updateUserPda({
+        login,
+        rootKey: root.publicKey,
+        createdAtMs: new anchor.BN(createdAtMs.toString()),
+        updatedAtMs: new anchor.BN((createdAtMs + 1_000n).toString()),
+        version: 1,
+        prevHash: sha256(createUnsigned),
+        additionalLimit: new anchor.BN(additionalLimitUpdate.toString()),
+        fields: {
+          blockchainKey: updateRecord.blockchainKey,
+          deviceKey: updateRecord.deviceKey,
+          chainNumber: 1,
+          isServer: true,
+          serverKey: serverKey2,
+          serverAddress: "https://srv-2.local",
+          connectionServers: ["srv_login_2", "srv_login_3"],
+          trustedCount: 0,
+        },
+        signature: updateSig,
+      })
+      .accounts({
+        signer: provider.wallet.publicKey,
+        userPda,
+        systemProgram: SystemProgram.programId,
+        feeReceiver: FEE_RECEIVER,
+        instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
+      })
+      .instruction();
+
+    await provider.sendAndConfirm(new Transaction().add(updateEdIx, updateIx), []);
+
+    const updatedAcc = await provider.connection.getAccountInfo(userPda);
+    expect(updatedAcc).not.eq(null);
+    const data = updatedAcc!.data;
+    expect(data.subarray(0, 5).toString("utf8")).eq("SHiNE");
+    expect(data[5]).eq(FORMAT_MAJOR);
+    expect(data[6]).eq(FORMAT_MINOR);
   });
 });