ВНИМАНИЕ: это тестовая страница для внутренней разработки. Я разбираюсь и пишу смарт-контракт и dApp для будущего токена и тестирую его работу в Devnet и подключение кошелька Phantom. Страница не является рабочим продуктом и предназначена только для внутренних тестов.
+
NOTICE: this is a development test page. I’m building a smart contract and dApp for a future token and testing it on Devnet and Phantom wallet connection. This page is not a live product and is intended for internal testing only.
+
+
+
+
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,
+}