ВНИМАНИЕ: это тестовая страница для внутренней разработки. Я разбираюсь и пишу смарт-контракт и 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
deleted file mode 100644
index b9d71fa..0000000
--- a/shine/programs/shine_payments_legacy/src/investments.rs
+++ /dev/null
@@ -1,326 +0,0 @@
-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
deleted file mode 100644
index 8aace15..0000000
--- a/shine/programs/shine_payments_legacy/src/lib.rs
+++ /dev/null
@@ -1,158 +0,0 @@
-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
deleted file mode 100644
index f78fa9c..0000000
--- a/shine/programs/shine_payments_legacy/src/nft.rs
+++ /dev/null
@@ -1,190 +0,0 @@
-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,
-}
diff --git a/shine/tests/shine.ts b/shine/tests/shine.ts
index 69b91f3..367b32c 100644
--- a/shine/tests/shine.ts
+++ b/shine/tests/shine.ts
@@ -1,16 +1,290 @@
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
+import {
+ Ed25519Program,
+ PublicKey,
+ SYSVAR_INSTRUCTIONS_PUBKEY,
+ SystemProgram,
+ Transaction,
+} from "@solana/web3.js";
+import { createHash } from "crypto";
+import { expect } from "chai";
import { Shine } from "../target/types/shine";
-describe("shine", () => {
- // Configure the client to use the local cluster.
- anchor.setProvider(anchor.AnchorProvider.env());
+const MAGIC = Buffer.from("SHiNE", "utf8");
+const FORMAT_MAJOR = 1;
+const FORMAT_MINOR = 0;
+const RESERVED = Buffer.from([0, 0, 0, 0, 0]);
+const ZERO_HASH = Buffer.alloc(32, 0);
+const KEY_STATUS_CREATED = 0;
+const LIMIT_STEP = 10_000n;
+const START_BONUS_LIMIT = 100_000n;
+const FEE_RECEIVER = new PublicKey("9vXFoN9ngfN1gpqQ3HT5n3y9Wp2r7HnSQckirgwVwWwb");
+
+type MutableFields = {
+ blockchainKey: PublicKey;
+ deviceKey: PublicKey;
+ chainNumber: number;
+ isServer: boolean;
+ serverKey: PublicKey;
+ serverAddress: string;
+ connectionServers: string[];
+ trustedCount: number;
+};
+
+type UnsignedRecord = {
+ createdAtMs: bigint;
+ updatedAtMs: bigint;
+ version: number;
+ prevHash: Buffer;
+ login: string;
+ rootKeyStatus: number;
+ rootKey: PublicKey;
+ blockchainKeyStatus: number;
+ blockchainKey: PublicKey;
+ deviceKeyStatus: number;
+ deviceKey: PublicKey;
+ chainNumber: number;
+ balance: bigint;
+ isServer: boolean;
+ serverKey: PublicKey;
+ serverAddress: string;
+ connectionServers: string[];
+ trustedCount: number;
+};
+
+function u16le(v: number): Buffer {
+ const b = Buffer.alloc(2);
+ b.writeUInt16LE(v, 0);
+ return b;
+}
+
+function u32le(v: number): Buffer {
+ const b = Buffer.alloc(4);
+ b.writeUInt32LE(v, 0);
+ return b;
+}
+
+function u64le(v: bigint): Buffer {
+ const b = Buffer.alloc(8);
+ b.writeBigUInt64LE(v, 0);
+ return b;
+}
+
+function serializeUnsignedRecord(r: UnsignedRecord): Buffer {
+ const loginBytes = Buffer.from(r.login, "utf8");
+ const serverAddressBytes = Buffer.from(r.serverAddress, "utf8");
+
+ const out: Buffer[] = [];
+ out.push(MAGIC);
+ out.push(Buffer.from([FORMAT_MAJOR]));
+ out.push(Buffer.from([FORMAT_MINOR]));
+ out.push(Buffer.alloc(2, 0)); // record_len placeholder
+
+ out.push(u64le(r.createdAtMs));
+ out.push(u64le(r.updatedAtMs));
+ out.push(u32le(r.version));
+ out.push(r.prevHash);
+
+ out.push(Buffer.from([loginBytes.length]));
+ out.push(loginBytes);
+
+ out.push(Buffer.from([r.rootKeyStatus]));
+ out.push(r.rootKey.toBuffer());
+ out.push(Buffer.from([r.blockchainKeyStatus]));
+ out.push(r.blockchainKey.toBuffer());
+ out.push(Buffer.from([r.deviceKeyStatus]));
+ out.push(r.deviceKey.toBuffer());
+
+ out.push(u16le(r.chainNumber));
+ out.push(u64le(r.balance));
+
+ out.push(Buffer.from([r.isServer ? 1 : 0]));
+ if (r.isServer) {
+ out.push(r.serverKey.toBuffer());
+ out.push(Buffer.from([serverAddressBytes.length]));
+ out.push(serverAddressBytes);
+ }
+
+ out.push(Buffer.from([r.connectionServers.length]));
+ for (const s of r.connectionServers) {
+ const sb = Buffer.from(s, "utf8");
+ out.push(Buffer.from([sb.length]));
+ out.push(sb);
+ }
+
+ out.push(Buffer.from([r.trustedCount]));
+ out.push(RESERVED);
+
+ const unsigned = Buffer.concat(out);
+ const recordLen = unsigned.length + 64;
+ unsigned.writeUInt16LE(recordLen, 7);
+ return unsigned;
+}
+
+function sha256(buf: Buffer): Buffer {
+ return createHash("sha256").update(buf).digest();
+}
+
+function extractSigFromEdIx(ixData: Buffer): Buffer {
+ const signatureOffset = ixData.readUInt16LE(2);
+ return ixData.subarray(signatureOffset, signatureOffset + 64);
+}
+
+describe("shine_users e2e", () => {
+ anchor.setProvider(anchor.AnchorProvider.env());
+ const provider = anchor.getProvider() as anchor.AnchorProvider;
const program = anchor.workspace.shine as Program;
- it("Is initialized!", async () => {
- // Add your test here.
- const tx = await program.methods.initialize().rpc();
- console.log("Your transaction signature", tx);
+ it("registers user and updates balance/server data", async () => {
+ const login = `u${Date.now().toString().slice(-10)}`;
+ const [userPda] = PublicKey.findProgramAddressSync(
+ [Buffer.from("login="), Buffer.from(login, "utf8")],
+ program.programId
+ );
+
+ const root = anchor.web3.Keypair.generate();
+ const blockchainKey = anchor.web3.Keypair.generate().publicKey;
+ const deviceKey = anchor.web3.Keypair.generate().publicKey;
+ const serverKey1 = anchor.web3.Keypair.generate().publicKey;
+ const serverKey2 = anchor.web3.Keypair.generate().publicKey;
+
+ const createdAtMs = BigInt(Date.now());
+ const additionalLimitCreate = 20_000n;
+ expect(additionalLimitCreate % LIMIT_STEP).eq(0n);
+
+ const createRecord: UnsignedRecord = {
+ createdAtMs,
+ updatedAtMs: createdAtMs,
+ version: 0,
+ prevHash: ZERO_HASH,
+ login,
+ rootKeyStatus: KEY_STATUS_CREATED,
+ rootKey: root.publicKey,
+ blockchainKeyStatus: KEY_STATUS_CREATED,
+ blockchainKey,
+ deviceKeyStatus: KEY_STATUS_CREATED,
+ deviceKey,
+ chainNumber: 1,
+ balance: START_BONUS_LIMIT + additionalLimitCreate,
+ isServer: true,
+ serverKey: serverKey1,
+ serverAddress: "https://srv-1.local",
+ connectionServers: ["srv_login_1"],
+ trustedCount: 0,
+ };
+
+ const createUnsigned = serializeUnsignedRecord(createRecord);
+ const createHash = sha256(createUnsigned);
+ const createEdIx = Ed25519Program.createInstructionWithPrivateKey({
+ privateKey: root.secretKey,
+ message: createHash,
+ });
+ const createSig = extractSigFromEdIx(Buffer.from(createEdIx.data));
+
+ const createIx = await program.methods
+ .createUserPda({
+ login,
+ rootKey: root.publicKey,
+ createdAtMs: new anchor.BN(createdAtMs.toString()),
+ additionalLimit: new anchor.BN(additionalLimitCreate.toString()),
+ fields: {
+ blockchainKey,
+ deviceKey,
+ chainNumber: 1,
+ isServer: true,
+ serverKey: serverKey1,
+ serverAddress: "https://srv-1.local",
+ connectionServers: ["srv_login_1"],
+ trustedCount: 0,
+ },
+ signature: createSig,
+ })
+ .accounts({
+ signer: provider.wallet.publicKey,
+ userPda,
+ systemProgram: SystemProgram.programId,
+ feeReceiver: FEE_RECEIVER,
+ instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
+ })
+ .instruction();
+
+ await provider.sendAndConfirm(new Transaction().add(createEdIx, createIx), []);
+
+ const createAcc = await provider.connection.getAccountInfo(userPda);
+ expect(createAcc).not.eq(null);
+ expect(createAcc!.owner.toBase58()).eq(program.programId.toBase58());
+
+ const additionalLimitUpdate = 30_000n;
+ expect(additionalLimitUpdate % LIMIT_STEP).eq(0n);
+
+ const updateRecord: UnsignedRecord = {
+ createdAtMs,
+ updatedAtMs: createdAtMs + 1_000n,
+ version: 1,
+ prevHash: sha256(createUnsigned),
+ login,
+ rootKeyStatus: KEY_STATUS_CREATED,
+ rootKey: root.publicKey,
+ blockchainKeyStatus: KEY_STATUS_CREATED,
+ blockchainKey: anchor.web3.Keypair.generate().publicKey,
+ deviceKeyStatus: KEY_STATUS_CREATED,
+ deviceKey: anchor.web3.Keypair.generate().publicKey,
+ chainNumber: 1,
+ balance: START_BONUS_LIMIT + additionalLimitCreate + additionalLimitUpdate,
+ isServer: true,
+ serverKey: serverKey2,
+ serverAddress: "https://srv-2.local",
+ connectionServers: ["srv_login_2", "srv_login_3"],
+ trustedCount: 0,
+ };
+
+ const updateUnsigned = serializeUnsignedRecord(updateRecord);
+ const updateHash = sha256(updateUnsigned);
+ const updateEdIx = Ed25519Program.createInstructionWithPrivateKey({
+ privateKey: root.secretKey,
+ message: updateHash,
+ });
+ const updateSig = extractSigFromEdIx(Buffer.from(updateEdIx.data));
+
+ const updateIx = await program.methods
+ .updateUserPda({
+ login,
+ rootKey: root.publicKey,
+ createdAtMs: new anchor.BN(createdAtMs.toString()),
+ updatedAtMs: new anchor.BN((createdAtMs + 1_000n).toString()),
+ version: 1,
+ prevHash: sha256(createUnsigned),
+ additionalLimit: new anchor.BN(additionalLimitUpdate.toString()),
+ fields: {
+ blockchainKey: updateRecord.blockchainKey,
+ deviceKey: updateRecord.deviceKey,
+ chainNumber: 1,
+ isServer: true,
+ serverKey: serverKey2,
+ serverAddress: "https://srv-2.local",
+ connectionServers: ["srv_login_2", "srv_login_3"],
+ trustedCount: 0,
+ },
+ signature: updateSig,
+ })
+ .accounts({
+ signer: provider.wallet.publicKey,
+ userPda,
+ systemProgram: SystemProgram.programId,
+ feeReceiver: FEE_RECEIVER,
+ instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
+ })
+ .instruction();
+
+ await provider.sendAndConfirm(new Transaction().add(updateEdIx, updateIx), []);
+
+ const updatedAcc = await provider.connection.getAccountInfo(userPda);
+ expect(updatedAcc).not.eq(null);
+ const data = updatedAcc!.data;
+ expect(data.subarray(0, 5).toString("utf8")).eq("SHiNE");
+ expect(data[5]).eq(FORMAT_MAJOR);
+ expect(data[6]).eq(FORMAT_MINOR);
});
});