diff --git a/shine/programs/shine_payments_legacy/Cargo.toml b/shine/programs/shine_payments_legacy/Cargo.toml new file mode 100644 index 0000000..add95e4 --- /dev/null +++ b/shine/programs/shine_payments_legacy/Cargo.toml @@ -0,0 +1,33 @@ +[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 new file mode 100644 index 0000000..e9b68fb --- /dev/null +++ b/shine/programs/shine_payments_legacy/LEGACY_NOTICE.md @@ -0,0 +1,6 @@ +# Важно + +Эта папка содержит устаревшую версию `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 new file mode 100755 index 0000000..55f4dfd --- /dev/null +++ b/shine/programs/shine_payments_legacy/dApp/copyToServer.sh @@ -0,0 +1,30 @@ +#!/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 new file mode 100644 index 0000000..3e2b8ac --- /dev/null +++ b/shine/programs/shine_payments_legacy/dApp/init.html @@ -0,0 +1,404 @@ + + + + + + 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
new file mode 100644
index 0000000..b9d71fa
--- /dev/null
+++ b/shine/programs/shine_payments_legacy/src/investments.rs
@@ -0,0 +1,326 @@
+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
new file mode 100644
index 0000000..8aace15
--- /dev/null
+++ b/shine/programs/shine_payments_legacy/src/lib.rs
@@ -0,0 +1,158 @@
+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
new file mode 100644
index 0000000..f78fa9c
--- /dev/null
+++ b/shine/programs/shine_payments_legacy/src/nft.rs
@@ -0,0 +1,190 @@
+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,
+}