Переписать shine_users и shine_login_guard на чистый Rust

This commit is contained in:
AidarKC
2026-06-04 23:05:45 +04:00
parent 60049442f1
commit 832eea5889
17 changed files with 1355 additions and 1621 deletions
@@ -12,15 +12,9 @@ doctest = false
bench = false
[dependencies]
anchor-lang = "0.31.1"
solana-program = "2.1.21"
[features]
default = []
no-entrypoint = []
no-idl = []
no-log-ix-name = []
anchor-debug = []
custom-heap = []
custom-panic = []
cpi = []
idl-build = ["anchor-lang/idl-build"]
@@ -1,39 +1,83 @@
use anchor_lang::prelude::*;
use anchor_lang::solana_program::program::set_return_data;
declare_id!("3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo");
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
program::set_return_data,
program_error::ProgramError,
pubkey::Pubkey,
};
mod wordlist {
include!(concat!(env!("OUT_DIR"), "/generated_dictionary.rs"));
}
const CLASS_FREE: u32 = 0;
const CLASS_PREMIUM: u32 = 1;
const CLASS_TRADEMARK: u32 = 2;
const MAX_WORDS_PER_LOGIN: usize = 3;
pub const INSTRUCTION_CLASSIFY_LOGIN: u8 = 1;
pub const CLASS_FREE: u32 = 0;
pub const CLASS_PREMIUM: u32 = 1;
pub const CLASS_TRADEMARK: u32 = 2;
pub const MAX_WORDS_PER_LOGIN: usize = 3;
pub const MAX_LOGIN_LEN: usize = 20;
#[program]
pub mod shine_login_guard {
use super::*;
solana_program::declare_id!("3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo");
pub fn classify_login(_ctx: Context<ClassifyLogin>, login: String) -> Result<()> {
let class = classify(&login);
set_return_data(&class.to_le_bytes());
Ok(())
entrypoint!(process_instruction);
pub fn process_instruction(
_program_id: &Pubkey,
_accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let class = match parse_instruction(instruction_data)? {
Instruction::ClassifyLogin { login } => classify(&login),
};
set_return_data(&class.to_le_bytes());
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Instruction {
ClassifyLogin { login: String },
}
fn parse_instruction(data: &[u8]) -> Result<Instruction, ProgramError> {
let (&tag, rest) = data
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
match tag {
INSTRUCTION_CLASSIFY_LOGIN => {
let login = read_string_u32(rest)?;
Ok(Instruction::ClassifyLogin { login })
}
_ => Err(ProgramError::InvalidInstructionData),
}
}
#[derive(Accounts)]
pub struct ClassifyLogin<'info> {
pub signer: Signer<'info>,
fn read_string_u32(data: &[u8]) -> Result<String, ProgramError> {
if data.len() < 4 {
return Err(ProgramError::InvalidInstructionData);
}
let len = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
let body = data
.get(4..4 + len)
.ok_or(ProgramError::InvalidInstructionData)?;
std::str::from_utf8(body)
.map(|s| s.to_string())
.map_err(|_| ProgramError::InvalidInstructionData)
}
fn classify(login: &str) -> u32 {
pub fn build_classify_login_instruction_data(login: &str) -> Vec<u8> {
let bytes = login.as_bytes();
let mut out = Vec::with_capacity(1 + 4 + bytes.len());
out.push(INSTRUCTION_CLASSIFY_LOGIN);
out.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
out.extend_from_slice(bytes);
out
}
pub fn classify(login: &str) -> u32 {
let Some(normalized) = normalize_login(login) else {
return CLASS_PREMIUM;
};
// Сначала пытаемся классифицировать по словарям (в т.ч. trademark/company),
// и только если не нашли совпадений — применяем правило короткого логина.
if let Some(v) = classify_split(&normalized) {
return v;
}
@@ -44,7 +88,7 @@ fn classify(login: &str) -> u32 {
}
fn normalize_login(login: &str) -> Option<String> {
if login.is_empty() || login.len() > 20 {
if login.is_empty() || login.len() > MAX_LOGIN_LEN {
return None;
}
let mut out = String::with_capacity(login.len());
@@ -57,7 +101,7 @@ fn normalize_login(login: &str) -> Option<String> {
}
out.push(ch.to_ascii_lowercase());
}
if out.is_empty() || out.len() > 20 {
if out.is_empty() || out.len() > MAX_LOGIN_LEN {
return None;
}
Some(out)
@@ -74,7 +118,7 @@ fn classify_split(login: &str) -> Option<u32> {
if depth >= MAX_WORDS_PER_LOGIN {
return None;
}
let max_piece = rest.len().min(20);
let max_piece = rest.len().min(MAX_LOGIN_LEN);
let mut premium_found = false;
for i in 1..=max_piece {
let candidate = &rest[..i];
@@ -87,7 +131,7 @@ fn classify_split(login: &str) -> Option<u32> {
Some(CLASS_TRADEMARK) => return Some(CLASS_TRADEMARK),
Some(CLASS_PREMIUM) => premium_found = true,
_ => {}
};
}
}
if premium_found {
Some(CLASS_PREMIUM)
@@ -105,3 +149,35 @@ fn is_premium_word(word: &str) -> bool {
fn is_trademark_word(word: &str) -> bool {
wordlist::TRADEMARK_WORDS.binary_search(&word).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_removes_underscores_and_lowercases() {
assert_eq!(normalize_login("Ab_C1"), Some("abc1".to_string()));
}
#[test]
fn normalize_rejects_invalid_chars() {
assert_eq!(normalize_login("ab-c"), None);
}
#[test]
fn short_unknown_login_is_premium() {
assert_eq!(classify("abc123"), CLASS_PREMIUM);
}
#[test]
fn long_unknown_login_is_free() {
assert_eq!(classify("abcdefgh9"), CLASS_FREE);
}
#[test]
fn instruction_roundtrip() {
let data = build_classify_login_instruction_data("Alice_1");
let parsed = parse_instruction(&data).unwrap();
assert_eq!(parsed, Instruction::ClassifyLogin { login: "Alice_1".to_string() });
}
}
@@ -12,18 +12,10 @@ doctest = false
bench = false
[dependencies]
anchor-lang = "0.31.1"
common = { path = "../common" }
shine_login_guard = { path = "../shine_login_guard", features = ["cpi", "no-entrypoint"] }
solana-program = "2.1.21"
[features]
default = []
no-entrypoint = []
no-idl = []
no-log-ix-name = []
anchor-debug = []
custom-heap = []
custom-panic = []
cpi = []
idl-build = ["anchor-lang/idl-build"]
File diff suppressed because it is too large Load Diff
@@ -1,30 +1,27 @@
use common::deploy_config;
/// `USER_PDA_SEED_PREFIX` — префикс seed для пользовательского PDA (`login=<...>`).
pub const USER_PDA_SEED_PREFIX: &str = "login=";
/// `USERS_ECONOMY_CONFIG_SEED` — seed PDA с экономическими параметрами программы `shine_users`.
pub const USERS_ECONOMY_CONFIG_SEED: &[u8] = b"shine_users_economy_config";
/// `USER_PDA_SPACE` — стартовый размер PDA пользователя, дальше запись может расширяться через realloc.
/// Префикс seed для пользовательского PDA новой чисто-rust реализации `shine_users`.
pub const USER_PDA_SEED_PREFIX: &str = "user_login=";
/// Seed PDA с экономическими параметрами программы `shine_users`.
pub const USERS_ECONOMY_CONFIG_SEED: &[u8] = b"shine_users_economy_config_v2";
/// Стартовый размер PDA пользователя, дальше запись может расширяться через realloc.
pub const USER_PDA_SPACE: usize = 768;
/// `USERS_ECONOMY_CONFIG_SPACE` — размер PDA с экономическими параметрами `shine_users`.
pub const USERS_ECONOMY_CONFIG_SPACE: usize = 8 + 96;
/// Размер PDA с экономическими параметрами `shine_users`.
pub const USERS_ECONOMY_CONFIG_SPACE: usize = 32;
/// `DAO_AUTHORITY` — адрес DAO-авторити, который имеет право обновлять economy-конфиг.
pub const DAO_AUTHORITY: &str = deploy_config::DAO_AUTHORITY;
/// Адрес DAO authority, который имеет право обновлять economy-конфиг.
pub const DAO_AUTHORITY: &str = "FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P";
/// `SHINE_PAYMENTS_PROGRAM_ID` — адрес программы `shine_payments`, от которой вычисляется PDA inflow-вольта.
pub const SHINE_PAYMENTS_PROGRAM_ID: &str = deploy_config::SHINE_PAYMENTS_PROGRAM_ID;
/// `SHINE_PAYMENTS_INFLOW_VAULT_SEED` — seed inflow-вольта в программе `shine_payments` (должен совпадать с payments settings).
/// Адрес программы `shine_payments`, от которой вычисляется PDA inflow-вольта.
pub const SHINE_PAYMENTS_PROGRAM_ID: &str = "m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR";
/// Seed inflow-вольта в программе `shine_payments`.
pub const SHINE_PAYMENTS_INFLOW_VAULT_SEED: &[u8] = b"shine_payments_inflow_vault";
/// `SHINE_LOGIN_GUARD_PROGRAM_ID` — адрес отдельной программы проверки премиальности логина.
pub const SHINE_LOGIN_GUARD_PROGRAM_ID: &str = deploy_config::SHINE_LOGIN_GUARD_PROGRAM_ID;
/// `START_REGISTRATION_FEE_LAMPORTS` — стартовая комиссия регистрации (0.01 SOL) для initial economy-конфига.
/// Адрес отдельной программы проверки премиальности логина.
pub const SHINE_LOGIN_GUARD_PROGRAM_ID: &str = "3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo";
/// Стартовая комиссия регистрации (0.01 SOL) для initial economy-конфига.
pub const START_REGISTRATION_FEE_LAMPORTS: u64 = 10_000_000;
/// `LIMIT_STEP` — шаг пополнения лимита; `additional_limit` должен быть кратен этому значению.
/// Шаг пополнения лимита.
pub const LIMIT_STEP: u64 = 10_000;
/// `START_LAMPORTS_PER_LIMIT_STEP` — стартовая цена одного шага лимита (0.0001 SOL за 10_000 лимита).
/// Стартовая цена одного шага лимита.
pub const START_LAMPORTS_PER_LIMIT_STEP: u64 = 100_000;
/// `START_BONUS_LIMIT` — стартовый бонус лимита, выдаваемый пользователю при создании записи.
/// Стартовый бонус лимита.
pub const START_BONUS_LIMIT: u64 = 100_000;
File diff suppressed because it is too large Load Diff