shine-solana/shine/programs/common/src/utils.rs
2026-05-16 17:17:41 +03:00

351 lines
18 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<u8>,
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<u8>` с данными аккаунта.
/// Если аккаунта нет или его данные пусты — возвращается `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<u8> {
// ─────────────────────────────────────────────────────────────────────
// 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<u8>, чтобы дальше работать без 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(())
}