use anchor_lang::prelude::*; use anchor_lang::solana_program::{program::invoke_signed, system_instruction}; /// сдесь коды всех ошибок #[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, #[msg("Невалидная цифровая подпись записи")] InvalidSignature = 1013, #[msg("Невалидный формат записи")] InvalidRecordFormat = 1014, #[msg("Невалидная длина записи")] InvalidRecordLength = 1015, #[msg("Невалидные данные записи")] InvalidRecordData = 1016, #[msg("Невалидный хэш предыдущей версии")] InvalidPrevHash = 1017, #[msg("Попытка изменить неизменяемое поле")] ImmutableFieldChanged = 1018, #[msg("Попытка уменьшить лимит/баланс")] BalanceDecrease = 1019, #[msg("Невалидная версия записи")] InvalidVersion = 1020, #[msg("Размер записи превышает допустимый")] RecordTooLarge = 1021, #[msg("Переполнение при вычислении")] MathOverflow = 1022, #[msg("Неверный адрес получателя комиссии")] InvalidFeeReceiver = 1023, #[msg("Пополнение лимита должно быть кратно шагу")] InvalidLimitIncrement = 1024, #[msg("Невалидная magic-сигнатура записи")] InvalidRecordMagic = 1025, } ///---------------------------------------------------------------------------------------------------------- /// Базовые функции для работы с PDA ///---------------------------------------------------------------------------------------------------------- /// Создаёт PDA аккаунт (если его ещё нет), и записывает в него массив байт. /// /// Аргументы: /// - `pda_account`: аккаунт, куда записываем /// - `signer`: кто платит за создание (обычно пользователь) /// - `program_id`: адрес текущей программы /// - `seeds`: слайс сидов, по которым создавался PDA /// - `data`: байты для записи /// - `space`: желаемый размер аккаунта pub fn create_and_write_pda<'info>( pda_account: &AccountInfo<'info>, signer: &AccountInfo<'info>, system_program: &AccountInfo<'info>, program_id: &Pubkey, seeds: &[&[u8]], data: Vec, space: u64, ) -> Result<()> { // ─────────────────────────────────────────────── // 1. Проверяем, создан ли аккаунт (если нет — owner = default) if pda_account.owner == &Pubkey::default() { msg!("Создаём PDA с размером {} байт", space); let space = space; //+ 128; // Добавляется запас под метаданные // Вычисляем необходимую арендную плату let lamports = Rent::get()?.minimum_balance(space as usize); // Формируем инструкцию let create_instr = system_instruction::create_account( signer.key, pda_account.key, lamports, space, program_id, ); // Выполняем инструкцию с подписью от PDA invoke_signed( &create_instr, &[signer.clone(), pda_account.clone(), system_program.clone()], &[&seeds], )?; } // ─────────────────────────────────────────────── // 2. Пишем данные в аккаунт let mut account_data = pda_account.try_borrow_mut_data()?; let copy_len = std::cmp::min(account_data.len(), data.len()); account_data[..copy_len].copy_from_slice(&data[..copy_len]); // Если хочешь дополнить оставшееся нулями — раскомментируй: // for i in copy_len..account_data.len() { // account_data[i] = 0; // } msg!("Успешно записано {} байт в PDA", copy_len); Ok(()) } /// Создаёт PDA аккаунт (если его ещё нет). /// /// ⚠️ Если аккаунт уже существует, выбрасывается ошибка. /// Используется внутри инструкций смарт-контракта. /// /// Аргументы: /// - `pda_account`: аккаунт, который хотим создать (PDA) /// - `signer`: кто оплачивает создание аккаунта (обычно пользователь) /// - `system_program`: системная программа (`111...111`) /// - `program_id`: адрес текущей программы (используется для подписи PDA) /// - `seeds`: массив сидов, по которым вычислялся PDA /// - `space`: желаемый размер аккаунта в байтах (только данных, без метаданных) pub fn create_pda<'info>( pda_account: &AccountInfo<'info>, signer: &AccountInfo<'info>, system_program: &AccountInfo<'info>, program_id: &Pubkey, seeds: &[&[u8]], space: u64, ) -> Result<()> { // ─────────────────────────────────────────────── // 1. Проверяем, существует ли аккаунт if pda_account.owner != &Pubkey::default() { // Если владелец не равен Pubkey::default, значит аккаунт уже создан // Возвращаем ошибку с пояснением return Err(error!(ErrCode::PdaAlreadyExists)); } // ─────────────────────────────────────────────── // 2. Логируем, что будем создавать PDA msg!("Создаём PDA-аккаунт на {} байт", space); // Добавляем запас под метаданные Solana (примерно 128 байт) let full_space = space; // Получаем минимальный баланс для аренды (чтобы аккаунт не удалили) let lamports = Rent::get()?.minimum_balance(full_space as usize); // ─────────────────────────────────────────────── // 3. Создаём инструкцию system_program для создания аккаунта let create_instr = system_instruction::create_account( signer.key, // от имени кого pda_account.key, // для какого PDA lamports, // сколько лампортов перевести full_space, // сколько байт выделить program_id, // кто будет владельцем PDA ); // ─────────────────────────────────────────────── // 4. Выполняем инструкцию с подписью PDA (через сиды) invoke_signed( &create_instr, &[signer.clone(), pda_account.clone(), system_program.clone()], &[&seeds], // PDA сиды → для подписи )?; Ok(()) } /// Записывает массив байт в PDA аккаунт (в начало data-секции). /// /// ⚠️ Убедись, что PDA был передан как `#[account(mut)]` /// ⚠️ Эта функция ничего не создаёт, только пишет. /// /// Аргументы: /// - `pda_account`: аккаунт, в который пишем (должен быть mut) /// - `data`: бинарный массив, который нужно записать pub fn write_to_pda<'info>(pda_account: &AccountInfo<'info>, data: &[u8]) -> Result<()> { // ─────────────────────────────────────────────── // 1. Получаем доступ к данным PDA (на запись) let mut account_data = pda_account.try_borrow_mut_data()?; // ─────────────────────────────────────────────── // 2. Вычисляем сколько байт реально можно записать // (на случай, если data длиннее, чем выделено место) let copy_len = std::cmp::min(account_data.len(), data.len()); // ─────────────────────────────────────────────── // 3. Копируем данные в аккаунт (с самого начала) account_data[..copy_len].copy_from_slice(&data[..copy_len]); // Логируем, сколько байт записано msg!("Успешно записано {} байт в PDA", copy_len); Ok(()) } /// ------------------------------------------------------------------------ /// safe_read_pda ‒ «безопасное чтение PDA» /// ------------------------------------------------------------------------ /// /// * Принимает: ссылку на `AccountInfo<'info>` PDA-аккаунта. /// * Возвращает: `Vec` с данными аккаунта. /// Если аккаунта нет или его данные пусты — возвращается `Vec::new()` /// длиной 0 байт. /// /// Как работает ─────────────────────────────────────────────────────────── /// 1. Проверяем, что аккаунт **инициализирован**: у не-инициализированного /// owner = Pubkey::default(). Если owner нулевой — сразу отдаём пустой вектор. /// 2. Если длина буфера == 0 (Anchor helper `data_is_empty()`), тоже отдаём пустой. /// 3. Пытаемся безопасно (`try_borrow_data`) получить ссылку на данные. /// - Успех → копируем их в Vec и возвращаем. /// - Ошибка (например, конфликт borrow) → логируем и возвращаем пустой Vec. /// /// пример использования /// let raw_bytes = safe_read_pda(&ctx.accounts.readonly_pda); /// require!(!raw_bytes.is_empty(), ErrCode::EmptyPdaData); /// msg!("Размер считанных данных: {}", raw_bytes.len()); /// ------------------------------------------------------------------------ pub fn safe_read_pda<'info>(pda_account: &AccountInfo<'info>) -> Vec { // ───────────────────────────────────────────────────────────────────── // 1) Аккаунт Н*Е* СУЩЕСТВУЕТ или не инициализирован: // owner == Pubkey::default() (в Solana нулевой owner у пустого счёта) // ───────────────────────────────────────────────────────────────────── if pda_account.owner == &Pubkey::default() { msg!("safe_read_pda: аккаунт не инициализирован ‒ возвращаем пустой массив"); return Vec::new(); // [] } // ───────────────────────────────────────────────────────────────────── // 2) У аккаунта нет данных (длина 0) — тоже считаем «пустым» // ───────────────────────────────────────────────────────────────────── if pda_account.data_is_empty() { msg!("safe_read_pda: у аккаунта data_len == 0 ‒ возвращаем пустой массив"); return Vec::new(); } // ───────────────────────────────────────────────────────────────────── // 3) Пытаемся безопасно забрать буфер данных; ошибки перехватываем // ───────────────────────────────────────────────────────────────────── match pda_account.try_borrow_data() { Ok(data_ref) => { // to_vec() копирует bytes → Vec, чтобы дальше работать без borrow-лифа data_ref.to_vec() } Err(e) => { // Ошибка при borrow (например, уже есть активное мутабельное заимствование) msg!( "safe_read_pda: ошибка borrow_data ({:?}) ‒ возвращаем пустой массив", e ); Vec::new() } } } /// ------------------------------------------------------------------------ /// delete_pda_with_assign — закрыть PDA, вернуть ренту и освободить адрес /// ------------------------------------------------------------------------ /// /// Параметры: /// - `pda_account` : PDA-аккаунт (mut), который закрываем (owned вашей программой) /// - `recipient` : счёт, на который возвращаем лампорты (обычно пользователь) /// - `system_program`: системная программа (111...111) /// - `program_id` : Pubkey вашей программы (проверка владельца) /// - `seeds` : сиды PDA (в том же порядке, как при создании), чтобы PDA «подписал» assign /// /// Делает: /// 1) Проверяет, что PDA принадлежит вашей программе. /// 2) Обнуляет данные и сжимает их до 0 байт (realloc(0)). /// 3) Переводит все лампорты PDA на `recipient`. /// 4) Делает `assign` владельца на System Program (через `invoke_signed`). /// /// Результат: /// — В конце транзакции аккаунт с lamports=0 и data_len=0 будет удалён рантаймом, /// владелец = System Program (чисто/ожидаемо). /// — В следующей транзакции можно снова создать PDA с тем же сидом. /// ------------------------------------------------------------------------ pub fn delete_pda_return_rent<'info>( pda_account: &AccountInfo<'info>, recipient: &AccountInfo<'info>, program_id: &Pubkey, ) -> Result<()> { // 0) проверки require!( pda_account.owner != &Pubkey::default(), ErrCode::EmptyPdaData ); require!(pda_account.owner == program_id, ErrCode::InvalidPdaAddress); // 1) Переложить все лампорты с PDA на получателя (мы владелец, это разрешено) let amount = **pda_account.lamports.borrow(); if amount > 0 { **recipient.lamports.borrow_mut() = recipient .lamports() .checked_add(amount) .ok_or(ProgramError::InsufficientFunds)?; **pda_account.lamports.borrow_mut() = 0; } // 2) Нулим данные (если были) if !pda_account.data_is_empty() { let mut data = pda_account.try_borrow_mut_data()?; for b in data.iter_mut() { *b = 0; } } // 3) Сжать до 0 байт pda_account.realloc(0, false)?; // Никаких assign/transfer больше не делаем — это надёжнее. msg!("PDA закрыт: рента отправлена на {}", recipient.key); Ok(()) }