327 lines
14 KiB
Rust
327 lines
14 KiB
Rust
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<InvestState> {
|
||
let raw = safe_read_pda(pda); // ← берём Vec<u8> (или пустой)
|
||
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<Init>) -> 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<UseState>, _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<crate::AddBonusCtx>, 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<UseState>) -> 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<u8> {
|
||
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<InvestState> {
|
||
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 })
|
||
}
|