SHA256
1256 lines
61 KiB
Rust
1256 lines
61 KiB
Rust
use solana_program::{
|
|
account_info::{next_account_info, AccountInfo},
|
|
clock::Clock,
|
|
entrypoint,
|
|
entrypoint::ProgramResult,
|
|
hash::hashv,
|
|
instruction::Instruction,
|
|
program::{get_return_data, invoke, invoke_signed},
|
|
program_error::ProgramError,
|
|
program_memory::sol_memcmp,
|
|
pubkey::Pubkey,
|
|
rent::Rent,
|
|
system_instruction,
|
|
system_program,
|
|
sysvar::{instructions::{load_current_index_checked, load_instruction_at_checked}, Sysvar},
|
|
};
|
|
use std::{convert::TryFrom, str::FromStr};
|
|
|
|
pub mod settings;
|
|
|
|
solana_program::declare_id!("SHiNEPr1APdAgNBteUyBXcNovaHctpSjUu8oH2ZJdN6");
|
|
entrypoint!(process_instruction);
|
|
|
|
const MAGIC: &[u8; 5] = b"SHiNE";
|
|
const FORMAT_MAJOR: u8 = 1;
|
|
const FORMAT_MINOR: u8 = 2;
|
|
const LEGACY_FORMAT_MINOR: u8 = 0;
|
|
const MAX_AUTO_REALLOC_INCREASE: usize = 10_000;
|
|
const ZERO_HASH: [u8; 32] = [0; 32];
|
|
const BLOCK_TYPE_ROOT_KEY: u8 = 1;
|
|
const BLOCK_TYPE_CLIENT_KEY: u8 = 2;
|
|
const BLOCK_TYPE_BLOCKCHAIN_REGISTRY: u8 = 3;
|
|
const BLOCK_TYPE_SERVER_PROFILE: u8 = 30;
|
|
const BLOCK_TYPE_ACCESS_SERVERS: u8 = 40;
|
|
const BLOCK_VERSION_0: u8 = 0;
|
|
|
|
const AUTH_MODE_BLOCKCHAIN: u8 = 0;
|
|
const AUTH_MODE_ROOT: u8 = 1;
|
|
const DAY_MS: u64 = 86_400_000;
|
|
const FORK_COOLDOWN_MS: u64 = 3 * DAY_MS;
|
|
const MAX_CLIENT_CLOCK_SKEW_MS: u64 = 5 * 60 * 1000;
|
|
|
|
const IX_INIT_USERS_ECONOMY_CONFIG: u8 = 1;
|
|
const IX_UPDATE_USERS_ECONOMY_CONFIG: u8 = 2;
|
|
const IX_CREATE_USER_PDA: u8 = 3;
|
|
const IX_UPDATE_USER_PDA: u8 = 4;
|
|
const IX_UPSERT_PROMO_SELLER: u8 = 5;
|
|
const IX_CLOSE_LEGACY_USER_PDA: u8 = 6;
|
|
const LOGIN_GUARD_IX_CLASSIFY_LOGIN: u8 = 1;
|
|
|
|
#[repr(u32)]
|
|
#[derive(Clone, Copy, Debug)]
|
|
enum ShineUsersError {
|
|
InvalidInstruction = 1,
|
|
InvalidSigner = 2,
|
|
InvalidPdaAddress = 3,
|
|
UserAlreadyExists = 4,
|
|
EmptyPdaData = 5,
|
|
InvalidRecordData = 6,
|
|
InvalidRecordMagic = 7,
|
|
InvalidRecordFormat = 8,
|
|
InvalidRecordLength = 9,
|
|
InvalidLogin = 10,
|
|
InvalidLimitIncrement = 11,
|
|
InvalidFeeReceiver = 12,
|
|
InvalidLoginGuardResponse = 13,
|
|
PremiumLogin = 14,
|
|
TrademarkLoginRequiresReview = 15,
|
|
InvalidVersion = 16,
|
|
InvalidPrevHash = 17,
|
|
ImmutableFieldChanged = 18,
|
|
BalanceDecrease = 19,
|
|
InvalidSignature = 20,
|
|
RecordTooLarge = 21,
|
|
MathOverflow = 22,
|
|
SystemAlreadyInitialized = 23,
|
|
MissingRequiredSignature = 24,
|
|
InvalidSystemProgram = 25,
|
|
InvalidAccountOwner = 26,
|
|
InvalidAccountData = 27,
|
|
InvalidPromoCode = 28,
|
|
PromoSellerNotFound = 29,
|
|
PromoSalesExhausted = 30,
|
|
PromoLoginTooShort = 31,
|
|
InvalidPromoSellerState = 32,
|
|
PromoSignatureMismatch = 33,
|
|
ForkCooldown = 34,
|
|
DuplicateBlockchainKey = 35,
|
|
PaidLimitTooLarge = 36,
|
|
CannotCloseCurrentFormat = 37,
|
|
RootChangeRequiresRoot = 38,
|
|
AmbiguousAuthorityRotation = 39,
|
|
}
|
|
|
|
impl From<ShineUsersError> for ProgramError {
|
|
fn from(value: ShineUsersError) -> Self {
|
|
ProgramError::Custom(value as u32)
|
|
}
|
|
}
|
|
|
|
macro_rules! require {
|
|
($cond:expr, $err:expr) => {
|
|
if !($cond) {
|
|
return Err(ProgramError::from($err));
|
|
}
|
|
};
|
|
}
|
|
|
|
macro_rules! require_keys_eq {
|
|
($left:expr, $right:expr, $err:expr) => {
|
|
if $left != $right {
|
|
return Err(ProgramError::from($err));
|
|
}
|
|
};
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct ForkRecordV12 {
|
|
pub blockchain_key: Pubkey,
|
|
pub created_at_ms: u64,
|
|
pub paid_limit_bytes: u32,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct ServerAddressRecordV12 {
|
|
pub address_format_type: u8,
|
|
pub address_format_version: u8,
|
|
pub address: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct UserRecordV12 {
|
|
pub created_at_ms: u64,
|
|
pub updated_at_ms: u64,
|
|
pub record_number: u32,
|
|
pub prev_record_hash: [u8; 32],
|
|
pub login: String,
|
|
pub root_key: Pubkey,
|
|
pub client_key: Pubkey,
|
|
pub forks: Vec<ForkRecordV12>,
|
|
pub server_addresses: Vec<ServerAddressRecordV12>,
|
|
pub access_servers: Vec<String>,
|
|
pub signature: [u8; 64],
|
|
}
|
|
|
|
impl UserRecordV12 {
|
|
fn active_fork(&self) -> Result<&ForkRecordV12, ProgramError> {
|
|
self.forks.last().ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct CreateUserPdaArgs {
|
|
pub login: String,
|
|
pub root_key: Pubkey,
|
|
pub created_at_ms: u64,
|
|
pub additional_limit: u64,
|
|
pub client_key: Pubkey,
|
|
pub blockchain_key: Pubkey,
|
|
pub server_addresses: Vec<ServerAddressRecordV12>,
|
|
pub access_servers: Vec<String>,
|
|
pub signature: [u8; 64],
|
|
pub promo_seller_login: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct UpdateUserPdaArgs {
|
|
pub login: String,
|
|
pub root_key: Pubkey,
|
|
pub updated_at_ms: u64,
|
|
pub additional_limit: u64,
|
|
pub client_key: Pubkey,
|
|
pub auth_mode: u8,
|
|
pub new_blockchain_key: Option<Pubkey>,
|
|
pub server_addresses: Vec<ServerAddressRecordV12>,
|
|
pub access_servers: Vec<String>,
|
|
pub signature: [u8; 64],
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct CloseLegacyUserPdaArgs {
|
|
pub login: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct UpdateUsersEconomyConfigArgs {
|
|
pub registration_fee_lamports: u64,
|
|
pub lamports_per_limit_step: u64,
|
|
pub start_bonus_limit: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct UsersEconomyConfigState {
|
|
pub version: u8,
|
|
pub registration_fee_lamports: u64,
|
|
pub lamports_per_limit_step: u64,
|
|
pub start_bonus_limit: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct UpsertPromoSellerArgs {
|
|
pub seller_login: String,
|
|
pub remaining_sales: u64,
|
|
pub min_login_length: u8,
|
|
pub signer_pubkey: Pubkey,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct PromoSellerState {
|
|
pub version: u8,
|
|
pub remaining_sales: u64,
|
|
pub min_login_length: u8,
|
|
pub signer_pubkey: Pubkey,
|
|
}
|
|
|
|
struct Reader<'a> {
|
|
data: &'a [u8],
|
|
cursor: usize,
|
|
}
|
|
|
|
impl<'a> Reader<'a> {
|
|
fn new(data: &'a [u8]) -> Self { Self { data, cursor: 0 } }
|
|
fn read_u8(&mut self) -> Result<u8, ProgramError> {
|
|
let v = *self.data.get(self.cursor).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
self.cursor += 1;
|
|
Ok(v)
|
|
}
|
|
fn read_u16(&mut self) -> Result<u16, ProgramError> {
|
|
let end = self.cursor.checked_add(2).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
self.cursor = end;
|
|
Ok(u16::from_le_bytes([s[0], s[1]]))
|
|
}
|
|
fn read_u32(&mut self) -> Result<u32, ProgramError> {
|
|
let end = self.cursor.checked_add(4).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
self.cursor = end;
|
|
Ok(u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
|
|
}
|
|
fn read_u64(&mut self) -> Result<u64, ProgramError> {
|
|
let end = self.cursor.checked_add(8).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
self.cursor = end;
|
|
Ok(u64::from_le_bytes([s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]]))
|
|
}
|
|
fn read_fixed_32(&mut self) -> Result<[u8; 32], ProgramError> {
|
|
let end = self.cursor.checked_add(32).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
self.cursor = end;
|
|
<[u8; 32]>::try_from(s).map_err(|_| ProgramError::from(ShineUsersError::InvalidInstruction))
|
|
}
|
|
fn read_fixed_64(&mut self) -> Result<[u8; 64], ProgramError> {
|
|
let end = self.cursor.checked_add(64).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
self.cursor = end;
|
|
<[u8; 64]>::try_from(s).map_err(|_| ProgramError::from(ShineUsersError::InvalidInstruction))
|
|
}
|
|
fn read_pubkey(&mut self) -> Result<Pubkey, ProgramError> {
|
|
Ok(Pubkey::new_from_array(self.read_fixed_32()?))
|
|
}
|
|
fn read_string_u8(&mut self) -> Result<String, ProgramError> {
|
|
let len = self.read_u8()? as usize;
|
|
let end = self.cursor.checked_add(len).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
self.cursor = end;
|
|
std::str::from_utf8(s).map(|v| v.to_string()).map_err(|_| ProgramError::from(ShineUsersError::InvalidInstruction))
|
|
}
|
|
fn remaining(&self) -> usize {
|
|
self.data.len().saturating_sub(self.cursor)
|
|
}
|
|
fn finish(self) -> Result<(), ProgramError> {
|
|
require!(self.cursor == self.data.len(), ShineUsersError::InvalidInstruction);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn process_instruction<'a>(program_id: &Pubkey, accounts: &'a [AccountInfo<'a>], instruction_data: &[u8]) -> ProgramResult {
|
|
let mut r = Reader::new(instruction_data);
|
|
let tag = r.read_u8()?;
|
|
match tag {
|
|
IX_INIT_USERS_ECONOMY_CONFIG => {
|
|
r.finish()?;
|
|
process_init_users_economy_config(program_id, accounts)
|
|
}
|
|
IX_UPDATE_USERS_ECONOMY_CONFIG => {
|
|
let args = UpdateUsersEconomyConfigArgs {
|
|
registration_fee_lamports: r.read_u64()?,
|
|
lamports_per_limit_step: r.read_u64()?,
|
|
start_bonus_limit: r.read_u64()?,
|
|
};
|
|
r.finish()?;
|
|
process_update_users_economy_config(program_id, accounts, args)
|
|
}
|
|
IX_CREATE_USER_PDA => dispatch_create_user_pda(program_id, accounts, r),
|
|
IX_UPDATE_USER_PDA => dispatch_update_user_pda(program_id, accounts, r),
|
|
IX_UPSERT_PROMO_SELLER => dispatch_upsert_promo_seller(program_id, accounts, r),
|
|
IX_CLOSE_LEGACY_USER_PDA => dispatch_close_legacy_user_pda(program_id, accounts, r),
|
|
_ => Err(ProgramError::from(ShineUsersError::InvalidInstruction)),
|
|
}
|
|
}
|
|
|
|
fn dispatch_create_user_pda<'a>(program_id: &Pubkey, accounts: &'a [AccountInfo<'a>], mut r: Reader<'_>) -> ProgramResult {
|
|
let args = Box::new(parse_create_args(&mut r)?);
|
|
r.finish()?;
|
|
process_create_user_pda(program_id, accounts, args)
|
|
}
|
|
|
|
fn dispatch_update_user_pda(program_id: &Pubkey, accounts: &[AccountInfo], mut r: Reader<'_>) -> ProgramResult {
|
|
let args = Box::new(parse_update_args(&mut r)?);
|
|
r.finish()?;
|
|
process_update_user_pda(program_id, accounts, args)
|
|
}
|
|
|
|
fn dispatch_upsert_promo_seller(program_id: &Pubkey, accounts: &[AccountInfo], mut r: Reader<'_>) -> ProgramResult {
|
|
let args = UpsertPromoSellerArgs {
|
|
seller_login: r.read_string_u8()?,
|
|
remaining_sales: r.read_u64()?,
|
|
min_login_length: r.read_u8()?,
|
|
signer_pubkey: r.read_pubkey()?,
|
|
};
|
|
r.finish()?;
|
|
process_upsert_promo_seller(program_id, accounts, args)
|
|
}
|
|
|
|
fn parse_server_addresses(r: &mut Reader<'_>) -> Result<Vec<ServerAddressRecordV12>, ProgramError> {
|
|
let count = r.read_u8()? as usize;
|
|
let mut addresses = Vec::with_capacity(count);
|
|
for _ in 0..count {
|
|
addresses.push(ServerAddressRecordV12 {
|
|
address_format_type: r.read_u8()?,
|
|
address_format_version: r.read_u8()?,
|
|
address: r.read_string_u8()?,
|
|
});
|
|
}
|
|
Ok(addresses)
|
|
}
|
|
|
|
fn parse_access_servers(r: &mut Reader<'_>) -> Result<Vec<String>, ProgramError> {
|
|
let count = r.read_u8()? as usize;
|
|
let mut servers = Vec::with_capacity(count);
|
|
for _ in 0..count {
|
|
servers.push(r.read_string_u8()?);
|
|
}
|
|
Ok(servers)
|
|
}
|
|
|
|
fn parse_create_args(r: &mut Reader<'_>) -> Result<CreateUserPdaArgs, ProgramError> {
|
|
Ok(CreateUserPdaArgs {
|
|
login: r.read_string_u8()?,
|
|
root_key: r.read_pubkey()?,
|
|
created_at_ms: r.read_u64()?,
|
|
additional_limit: r.read_u64()?,
|
|
client_key: r.read_pubkey()?,
|
|
blockchain_key: r.read_pubkey()?,
|
|
server_addresses: parse_server_addresses(r)?,
|
|
access_servers: parse_access_servers(r)?,
|
|
signature: r.read_fixed_64()?,
|
|
promo_seller_login: if r.remaining() > 0 { Some(r.read_string_u8()?) } else { None },
|
|
})
|
|
}
|
|
|
|
fn parse_update_args(r: &mut Reader<'_>) -> Result<UpdateUserPdaArgs, ProgramError> {
|
|
let login = r.read_string_u8()?;
|
|
let root_key = r.read_pubkey()?;
|
|
let updated_at_ms = r.read_u64()?;
|
|
let additional_limit = r.read_u64()?;
|
|
let client_key = r.read_pubkey()?;
|
|
let auth_mode = r.read_u8()?;
|
|
let new_blockchain_key = match r.read_u8()? {
|
|
0 => None,
|
|
1 => Some(r.read_pubkey()?),
|
|
_ => return Err(ProgramError::from(ShineUsersError::InvalidInstruction)),
|
|
};
|
|
let server_addresses = parse_server_addresses(r)?;
|
|
let access_servers = parse_access_servers(r)?;
|
|
let signature = r.read_fixed_64()?;
|
|
Ok(UpdateUserPdaArgs {
|
|
login,
|
|
root_key,
|
|
updated_at_ms,
|
|
additional_limit,
|
|
client_key,
|
|
auth_mode,
|
|
new_blockchain_key,
|
|
server_addresses,
|
|
access_servers,
|
|
signature,
|
|
})
|
|
}
|
|
|
|
fn dispatch_close_legacy_user_pda(program_id: &Pubkey, accounts: &[AccountInfo], mut r: Reader<'_>) -> ProgramResult {
|
|
let args = CloseLegacyUserPdaArgs { login: r.read_string_u8()? };
|
|
r.finish()?;
|
|
process_close_legacy_user_pda(program_id, accounts, args)
|
|
}
|
|
|
|
fn process_init_users_economy_config(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
|
|
let mut it = accounts.iter();
|
|
let signer = next_account_info(&mut it)?;
|
|
let users_economy_config_pda = next_account_info(&mut it)?;
|
|
let system_program_ai = next_account_info(&mut it)?;
|
|
require!(it.next().is_none(), ShineUsersError::InvalidInstruction);
|
|
|
|
require!(signer.is_signer, ShineUsersError::InvalidSigner);
|
|
require_keys_eq!(*system_program_ai.key, system_program::id(), ShineUsersError::InvalidSystemProgram);
|
|
|
|
let (expected_pda, bump) = find_users_economy_config_pda(program_id);
|
|
require_keys_eq!(expected_pda, *users_economy_config_pda.key, ShineUsersError::InvalidPdaAddress);
|
|
require!(users_economy_config_pda.owner == &system_program::id(), ShineUsersError::SystemAlreadyInitialized);
|
|
require!(users_economy_config_pda.data_is_empty(), ShineUsersError::SystemAlreadyInitialized);
|
|
|
|
let state = UsersEconomyConfigState {
|
|
version: 1,
|
|
registration_fee_lamports: settings::START_REGISTRATION_FEE_LAMPORTS,
|
|
lamports_per_limit_step: settings::START_LAMPORTS_PER_LIMIT_STEP,
|
|
start_bonus_limit: settings::START_BONUS_LIMIT,
|
|
};
|
|
let data = serialize_users_economy_config(&state);
|
|
create_pda_account(
|
|
signer,
|
|
users_economy_config_pda,
|
|
system_program_ai,
|
|
program_id,
|
|
&[settings::USERS_ECONOMY_CONFIG_SEED, &[bump]],
|
|
data.len(),
|
|
)?;
|
|
write_pda_exact(users_economy_config_pda, &data)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn process_update_users_economy_config(program_id: &Pubkey, accounts: &[AccountInfo], args: UpdateUsersEconomyConfigArgs) -> ProgramResult {
|
|
let mut it = accounts.iter();
|
|
let signer = next_account_info(&mut it)?;
|
|
let users_economy_config_pda = next_account_info(&mut it)?;
|
|
require!(it.next().is_none(), ShineUsersError::InvalidInstruction);
|
|
|
|
require!(signer.is_signer, ShineUsersError::InvalidSigner);
|
|
let dao_authority = Pubkey::from_str(settings::DAO_AUTHORITY).map_err(|_| ProgramError::from(ShineUsersError::InvalidSigner))?;
|
|
require_keys_eq!(dao_authority, *signer.key, ShineUsersError::InvalidSigner);
|
|
let (expected_pda, _) = find_users_economy_config_pda(program_id);
|
|
require_keys_eq!(expected_pda, *users_economy_config_pda.key, ShineUsersError::InvalidPdaAddress);
|
|
require!(users_economy_config_pda.owner == program_id, ShineUsersError::InvalidPdaAddress);
|
|
require!(args.lamports_per_limit_step > 0, ShineUsersError::InvalidRecordData);
|
|
|
|
let mut state = read_users_economy_config(users_economy_config_pda)?;
|
|
state.registration_fee_lamports = args.registration_fee_lamports;
|
|
state.lamports_per_limit_step = args.lamports_per_limit_step;
|
|
state.start_bonus_limit = args.start_bonus_limit;
|
|
write_pda_exact(users_economy_config_pda, &serialize_users_economy_config(&state))?;
|
|
Ok(())
|
|
}
|
|
|
|
fn process_upsert_promo_seller(program_id: &Pubkey, accounts: &[AccountInfo], args: UpsertPromoSellerArgs) -> ProgramResult {
|
|
let mut it = accounts.iter();
|
|
let signer = next_account_info(&mut it)?;
|
|
let promo_seller_pda = next_account_info(&mut it)?;
|
|
let system_program_ai = next_account_info(&mut it)?;
|
|
require!(it.next().is_none(), ShineUsersError::InvalidInstruction);
|
|
|
|
require!(signer.is_signer, ShineUsersError::InvalidSigner);
|
|
require_keys_eq!(*system_program_ai.key, system_program::id(), ShineUsersError::InvalidSystemProgram);
|
|
validate_login(&args.seller_login)?;
|
|
require!((1..=20).contains(&args.min_login_length), ShineUsersError::InvalidPromoSellerState);
|
|
|
|
let dao_authority = Pubkey::from_str(settings::DAO_AUTHORITY).map_err(|_| ProgramError::from(ShineUsersError::InvalidSigner))?;
|
|
require_keys_eq!(dao_authority, *signer.key, ShineUsersError::InvalidSigner);
|
|
|
|
let seller_seed = login_seed_normalized(&args.seller_login);
|
|
let (expected_pda, bump) = find_promo_seller_pda(program_id, &seller_seed);
|
|
require_keys_eq!(expected_pda, *promo_seller_pda.key, ShineUsersError::InvalidPdaAddress);
|
|
|
|
if promo_seller_pda.owner == &system_program::id() {
|
|
create_pda_account(
|
|
signer,
|
|
promo_seller_pda,
|
|
system_program_ai,
|
|
program_id,
|
|
&[settings::PROMO_SELLER_PDA_SEED_PREFIX.as_bytes(), seller_seed.as_bytes(), &[bump]],
|
|
settings::PROMO_SELLER_PDA_SPACE,
|
|
)?;
|
|
} else {
|
|
require!(promo_seller_pda.owner == program_id, ShineUsersError::InvalidPdaAddress);
|
|
ensure_pda_size_and_rent(promo_seller_pda, signer, system_program_ai, settings::PROMO_SELLER_PDA_SPACE)?;
|
|
}
|
|
|
|
let state = PromoSellerState {
|
|
version: 1,
|
|
remaining_sales: args.remaining_sales,
|
|
min_login_length: args.min_login_length,
|
|
signer_pubkey: args.signer_pubkey,
|
|
};
|
|
write_pda_exact(promo_seller_pda, &serialize_promo_seller_state(&state))?;
|
|
Ok(())
|
|
}
|
|
|
|
fn process_create_user_pda<'a>(program_id: &Pubkey, accounts: &'a [AccountInfo<'a>], args: Box<CreateUserPdaArgs>) -> ProgramResult {
|
|
let mut it = accounts.iter();
|
|
let signer = next_account_info(&mut it)?;
|
|
let user_pda = next_account_info(&mut it)?;
|
|
let system_program_ai = next_account_info(&mut it)?;
|
|
let inflow_vault = next_account_info(&mut it)?;
|
|
let instructions_sysvar = next_account_info(&mut it)?;
|
|
let users_economy_config_pda = next_account_info(&mut it)?;
|
|
let login_guard_program = next_account_info(&mut it)?;
|
|
let promo_seller_pda = it.next();
|
|
require!(it.next().is_none(), ShineUsersError::InvalidInstruction);
|
|
|
|
require!(signer.is_signer, ShineUsersError::InvalidSigner);
|
|
require_keys_eq!(*system_program_ai.key, system_program::id(), ShineUsersError::InvalidSystemProgram);
|
|
|
|
validate_login(&args.login)?;
|
|
validate_server_addresses(&args.server_addresses)?;
|
|
validate_access_servers(&args.access_servers)?;
|
|
validate_inflow_vault(inflow_vault)?;
|
|
require!(args.additional_limit % settings::LIMIT_STEP == 0, ShineUsersError::InvalidLimitIncrement);
|
|
require_keys_eq!(*login_guard_program.key, Pubkey::from_str(settings::SHINE_LOGIN_GUARD_PROGRAM_ID).map_err(|_| ProgramError::from(ShineUsersError::InvalidLoginGuardResponse))?, ShineUsersError::InvalidLoginGuardResponse);
|
|
validate_users_economy_config_pda(program_id, users_economy_config_pda)?;
|
|
|
|
let promo_context = if let Some(seller_login) = args.promo_seller_login.as_deref().filter(|value| !value.trim().is_empty()) {
|
|
let seller_pda = promo_seller_pda.ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
Some(verify_promo_registration(program_id, instructions_sysvar, seller_pda, &args.login, seller_login)?)
|
|
} else {
|
|
require!(promo_seller_pda.is_none(), ShineUsersError::InvalidInstruction);
|
|
None
|
|
};
|
|
if promo_context.is_none() {
|
|
classify_login_or_fail(login_guard_program, &args.login)?;
|
|
}
|
|
|
|
let economy = read_users_economy_config(users_economy_config_pda)?;
|
|
let login_seed = login_seed_normalized(&args.login);
|
|
let (expected_pda, bump) = find_user_pda(program_id, &login_seed);
|
|
require_keys_eq!(expected_pda, *user_pda.key, ShineUsersError::InvalidPdaAddress);
|
|
require!(user_pda.owner == &system_program::id(), ShineUsersError::UserAlreadyExists);
|
|
require!(user_pda.data_is_empty(), ShineUsersError::UserAlreadyExists);
|
|
|
|
let start_balance_u64 = economy.start_bonus_limit.checked_add(args.additional_limit).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?;
|
|
let start_balance = u32::try_from(start_balance_u64).map_err(|_| ProgramError::from(ShineUsersError::PaidLimitTooLarge))?;
|
|
let now_ms = current_time_ms()?;
|
|
validate_client_time_ms(args.created_at_ms, now_ms)?;
|
|
let fork_created_at_ms = args.created_at_ms;
|
|
|
|
let mut record = UserRecordV12 {
|
|
created_at_ms: args.created_at_ms,
|
|
updated_at_ms: args.created_at_ms,
|
|
record_number: 0,
|
|
prev_record_hash: ZERO_HASH,
|
|
login: args.login,
|
|
root_key: args.root_key,
|
|
client_key: args.client_key,
|
|
forks: vec![ForkRecordV12 {
|
|
blockchain_key: args.blockchain_key,
|
|
created_at_ms: fork_created_at_ms,
|
|
paid_limit_bytes: start_balance,
|
|
}],
|
|
server_addresses: args.server_addresses,
|
|
access_servers: args.access_servers,
|
|
signature: [0; 64],
|
|
};
|
|
|
|
let unsigned = serialize_unsigned_record_v12(&record)?;
|
|
let unsigned_hash = hashv(&[&unsigned]);
|
|
// На create root доказывает владение recovery-ключом, а сама новая запись
|
|
// подписывается активным blockchain-key (новым authority состояния).
|
|
verify_ed25519_instruction_pubkey_message(instructions_sysvar, -2, &record.root_key, unsigned_hash.as_ref())?;
|
|
record.signature = verify_record_signature_hash_at(instructions_sysvar, -1, &record.active_fork()?.blockchain_key, &args.signature, unsigned_hash.as_ref())?;
|
|
|
|
let serialized = pad_to_fixed_size(serialize_full_record_v12(&record)?, settings::USER_PDA_SPACE)?;
|
|
create_pda_account(
|
|
signer,
|
|
user_pda,
|
|
system_program_ai,
|
|
program_id,
|
|
&[settings::USER_PDA_SEED_PREFIX.as_bytes(), login_seed.as_bytes(), &[bump]],
|
|
settings::USER_PDA_SPACE,
|
|
)?;
|
|
write_pda_exact(user_pda, &serialized)?;
|
|
|
|
if let Some(promo) = promo_context {
|
|
write_pda_exact(promo.promo_seller_pda, &serialize_promo_seller_state(&promo.updated_state))?;
|
|
}
|
|
|
|
let total_fee = economy.registration_fee_lamports.checked_add(limit_fee_lamports(args.additional_limit, economy.lamports_per_limit_step)?).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?;
|
|
transfer_lamports(signer, inflow_vault, system_program_ai, total_fee)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn process_update_user_pda(program_id: &Pubkey, accounts: &[AccountInfo], args: Box<UpdateUserPdaArgs>) -> ProgramResult {
|
|
let mut it = accounts.iter();
|
|
let signer = next_account_info(&mut it)?;
|
|
let user_pda = next_account_info(&mut it)?;
|
|
let system_program_ai = next_account_info(&mut it)?;
|
|
let inflow_vault = next_account_info(&mut it)?;
|
|
let instructions_sysvar = next_account_info(&mut it)?;
|
|
let users_economy_config_pda = next_account_info(&mut it)?;
|
|
require!(it.next().is_none(), ShineUsersError::InvalidInstruction);
|
|
|
|
require!(signer.is_signer, ShineUsersError::InvalidSigner);
|
|
require_keys_eq!(*system_program_ai.key, system_program::id(), ShineUsersError::InvalidSystemProgram);
|
|
validate_login(&args.login)?;
|
|
validate_server_addresses(&args.server_addresses)?;
|
|
validate_access_servers(&args.access_servers)?;
|
|
validate_inflow_vault(inflow_vault)?;
|
|
require!(args.additional_limit % settings::LIMIT_STEP == 0, ShineUsersError::InvalidLimitIncrement);
|
|
validate_users_economy_config_pda(program_id, users_economy_config_pda)?;
|
|
let economy = read_users_economy_config(users_economy_config_pda)?;
|
|
|
|
let normalized_login = login_seed_normalized(&args.login);
|
|
require_keys_eq!(find_user_pda(program_id, &normalized_login).0, *user_pda.key, ShineUsersError::InvalidPdaAddress);
|
|
require!(user_pda.owner == program_id, ShineUsersError::InvalidPdaAddress);
|
|
|
|
let raw = read_pda_all(user_pda)?;
|
|
require!(record_format_minor(&raw)? == FORMAT_MINOR, ShineUsersError::InvalidRecordFormat);
|
|
let old_record = deserialize_record_v12_from_pda(&raw)?;
|
|
require!(old_record.login == args.login, ShineUsersError::ImmutableFieldChanged);
|
|
require!(args.updated_at_ms >= old_record.updated_at_ms, ShineUsersError::InvalidRecordData);
|
|
let now_ms = current_time_ms()?;
|
|
validate_client_time_ms(args.updated_at_ms, now_ms)?;
|
|
require!(args.auth_mode == AUTH_MODE_BLOCKCHAIN || args.auth_mode == AUTH_MODE_ROOT, ShineUsersError::InvalidInstruction);
|
|
|
|
let root_changed = args.root_key != old_record.root_key;
|
|
if args.auth_mode == AUTH_MODE_BLOCKCHAIN {
|
|
require!(!root_changed, ShineUsersError::RootChangeRequiresRoot);
|
|
}
|
|
if root_changed && args.new_blockchain_key.is_some() {
|
|
return Err(ProgramError::from(ShineUsersError::AmbiguousAuthorityRotation));
|
|
}
|
|
|
|
let old_active_key = old_record.active_fork()?.blockchain_key;
|
|
let old_authority = if args.auth_mode == AUTH_MODE_ROOT { old_record.root_key } else { old_active_key };
|
|
let mut forks = old_record.forks.clone();
|
|
|
|
let additional_limit_u32 = u32::try_from(args.additional_limit).map_err(|_| ProgramError::from(ShineUsersError::PaidLimitTooLarge))?;
|
|
let mut appended_key = None;
|
|
if let Some(new_key) = args.new_blockchain_key {
|
|
for fork in &forks {
|
|
require!(fork.blockchain_key != new_key, ShineUsersError::DuplicateBlockchainKey);
|
|
}
|
|
if args.auth_mode == AUTH_MODE_BLOCKCHAIN {
|
|
enforce_fork_cooldown(old_record.active_fork()?.created_at_ms, args.updated_at_ms)?;
|
|
}
|
|
|
|
let current_limit = old_record.active_fork()?.paid_limit_bytes;
|
|
let new_limit = current_limit.checked_add(additional_limit_u32).ok_or(ProgramError::from(ShineUsersError::PaidLimitTooLarge))?;
|
|
forks.push(ForkRecordV12 { blockchain_key: new_key, created_at_ms: args.updated_at_ms, paid_limit_bytes: new_limit });
|
|
appended_key = Some(new_key);
|
|
} else if additional_limit_u32 > 0 {
|
|
let active = forks.last_mut().ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?;
|
|
active.paid_limit_bytes = active.paid_limit_bytes.checked_add(additional_limit_u32).ok_or(ProgramError::from(ShineUsersError::PaidLimitTooLarge))?;
|
|
}
|
|
|
|
let prev_hash = hash_unsigned_record_v12(&old_record)?;
|
|
let mut new_record = UserRecordV12 {
|
|
created_at_ms: old_record.created_at_ms,
|
|
updated_at_ms: args.updated_at_ms,
|
|
record_number: old_record.record_number.checked_add(1).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?,
|
|
prev_record_hash: prev_hash,
|
|
login: old_record.login.clone(),
|
|
root_key: args.root_key,
|
|
client_key: args.client_key,
|
|
forks,
|
|
server_addresses: args.server_addresses.clone(),
|
|
access_servers: args.access_servers.clone(),
|
|
signature: [0; 64],
|
|
};
|
|
|
|
let record_signer = if root_changed {
|
|
new_record.root_key
|
|
} else if let Some(new_key) = appended_key {
|
|
new_key
|
|
} else if args.auth_mode == AUTH_MODE_ROOT {
|
|
new_record.root_key
|
|
} else {
|
|
new_record.active_fork()?.blockchain_key
|
|
};
|
|
|
|
let unsigned = serialize_unsigned_record_v12(&new_record)?;
|
|
let unsigned_hash = hashv(&[&unsigned]);
|
|
// Первая подпись разрешает переход старым authority. Вторая подпись хранится
|
|
// в PDA и принадлежит authority уже нового состояния.
|
|
verify_ed25519_instruction_pubkey_message(instructions_sysvar, -2, &old_authority, unsigned_hash.as_ref())?;
|
|
new_record.signature = verify_record_signature_hash_at(instructions_sysvar, -1, &record_signer, &args.signature, unsigned_hash.as_ref())?;
|
|
|
|
let serialized = serialize_full_record_v12(&new_record)?;
|
|
ensure_pda_size_and_rent(user_pda, signer, system_program_ai, serialized.len())?;
|
|
write_pda_exact(user_pda, &serialized)?;
|
|
|
|
let topup_fee = limit_fee_lamports(args.additional_limit, economy.lamports_per_limit_step)?;
|
|
if topup_fee > 0 {
|
|
transfer_lamports(signer, inflow_vault, system_program_ai, topup_fee)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn process_close_legacy_user_pda(program_id: &Pubkey, accounts: &[AccountInfo], args: CloseLegacyUserPdaArgs) -> ProgramResult {
|
|
let mut it = accounts.iter();
|
|
let caller = next_account_info(&mut it)?;
|
|
let user_pda = next_account_info(&mut it)?;
|
|
require!(it.next().is_none(), ShineUsersError::InvalidInstruction);
|
|
require!(caller.is_signer, ShineUsersError::InvalidSigner);
|
|
require!(caller.is_writable, ShineUsersError::InvalidAccountData);
|
|
require!(user_pda.is_writable, ShineUsersError::InvalidAccountData);
|
|
|
|
validate_login(&args.login)?;
|
|
let normalized_login = login_seed_normalized(&args.login);
|
|
require_keys_eq!(find_user_pda(program_id, &normalized_login).0, *user_pda.key, ShineUsersError::InvalidPdaAddress);
|
|
require!(user_pda.owner == program_id, ShineUsersError::InvalidPdaAddress);
|
|
|
|
let raw = read_pda_all(user_pda)?;
|
|
require!(record_format_minor(&raw)? == LEGACY_FORMAT_MINOR, ShineUsersError::CannotCloseCurrentFormat);
|
|
|
|
let lamports = user_pda.lamports();
|
|
{
|
|
let mut caller_lamports = caller.try_borrow_mut_lamports().map_err(|_| ProgramError::from(ShineUsersError::InvalidAccountData))?;
|
|
**caller_lamports = (**caller_lamports).checked_add(lamports).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?;
|
|
}
|
|
{
|
|
let mut pda_lamports = user_pda.try_borrow_mut_lamports().map_err(|_| ProgramError::from(ShineUsersError::InvalidAccountData))?;
|
|
**pda_lamports = 0;
|
|
}
|
|
user_pda.realloc(0, false)?;
|
|
Ok(())
|
|
}
|
|
|
|
struct PromoRegistrationContext<'a> {
|
|
promo_seller_pda: &'a AccountInfo<'a>,
|
|
updated_state: PromoSellerState,
|
|
}
|
|
|
|
fn verify_promo_registration<'a>(
|
|
program_id: &Pubkey,
|
|
instructions_sysvar: &AccountInfo<'a>,
|
|
promo_seller_pda: &'a AccountInfo<'a>,
|
|
target_login: &str,
|
|
seller_login: &str,
|
|
) -> Result<PromoRegistrationContext<'a>, ProgramError> {
|
|
validate_login(seller_login)?;
|
|
let normalized_seller_login = login_seed_normalized(&seller_login);
|
|
let expected_pda = find_promo_seller_pda(program_id, &normalized_seller_login).0;
|
|
require_keys_eq!(expected_pda, *promo_seller_pda.key, ShineUsersError::InvalidPdaAddress);
|
|
require!(promo_seller_pda.owner == program_id, ShineUsersError::PromoSellerNotFound);
|
|
|
|
let state = read_promo_seller_state(promo_seller_pda)?;
|
|
require!(state.remaining_sales > 0, ShineUsersError::PromoSalesExhausted);
|
|
require!(target_login.len() >= state.min_login_length as usize, ShineUsersError::PromoLoginTooShort);
|
|
|
|
let message = build_promo_sign_message(target_login);
|
|
verify_ed25519_instruction_pubkey_message(instructions_sysvar, -3, &state.signer_pubkey, message.as_bytes())
|
|
.map_err(|_| ProgramError::from(ShineUsersError::PromoSignatureMismatch))?;
|
|
|
|
let updated_state = PromoSellerState {
|
|
version: state.version,
|
|
remaining_sales: state.remaining_sales.saturating_sub(1),
|
|
min_login_length: state.min_login_length,
|
|
signer_pubkey: state.signer_pubkey,
|
|
};
|
|
Ok(PromoRegistrationContext {
|
|
promo_seller_pda,
|
|
updated_state,
|
|
})
|
|
}
|
|
|
|
fn classify_login_or_fail(login_guard_program: &AccountInfo, login: &str) -> ProgramResult {
|
|
let login_guard_program_id = Pubkey::from_str(settings::SHINE_LOGIN_GUARD_PROGRAM_ID)
|
|
.map_err(|_| ProgramError::from(ShineUsersError::InvalidLoginGuardResponse))?;
|
|
require_keys_eq!(*login_guard_program.key, login_guard_program_id, ShineUsersError::InvalidLoginGuardResponse);
|
|
let mut data = Vec::with_capacity(1 + 4 + login.len());
|
|
data.push(LOGIN_GUARD_IX_CLASSIFY_LOGIN);
|
|
data.extend_from_slice(&(login.len() as u32).to_le_bytes());
|
|
data.extend_from_slice(login.as_bytes());
|
|
let ix = Instruction { program_id: login_guard_program_id, accounts: vec![], data };
|
|
invoke(&ix, &[login_guard_program.clone()])?;
|
|
let (program_id, raw) = get_return_data().ok_or(ProgramError::from(ShineUsersError::InvalidLoginGuardResponse))?;
|
|
require_keys_eq!(program_id, login_guard_program_id, ShineUsersError::InvalidLoginGuardResponse);
|
|
require!(raw.len() == 4, ShineUsersError::InvalidLoginGuardResponse);
|
|
let class = u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]);
|
|
match class {
|
|
0 => Ok(()),
|
|
1 => Err(ProgramError::from(ShineUsersError::PremiumLogin)),
|
|
2 => Err(ProgramError::from(ShineUsersError::TrademarkLoginRequiresReview)),
|
|
_ => Err(ProgramError::from(ShineUsersError::InvalidLoginGuardResponse)),
|
|
}
|
|
}
|
|
|
|
fn serialize_users_economy_config(state: &UsersEconomyConfigState) -> Vec<u8> {
|
|
let mut out = Vec::with_capacity(1 + 8 + 8 + 8);
|
|
out.push(state.version);
|
|
out.extend_from_slice(&state.registration_fee_lamports.to_le_bytes());
|
|
out.extend_from_slice(&state.lamports_per_limit_step.to_le_bytes());
|
|
out.extend_from_slice(&state.start_bonus_limit.to_le_bytes());
|
|
out
|
|
}
|
|
|
|
fn serialize_promo_seller_state(state: &PromoSellerState) -> Vec<u8> {
|
|
let mut out = Vec::with_capacity(1 + 8 + 1 + 32);
|
|
out.push(state.version);
|
|
out.extend_from_slice(&state.remaining_sales.to_le_bytes());
|
|
out.push(state.min_login_length);
|
|
out.extend_from_slice(state.signer_pubkey.as_ref());
|
|
out
|
|
}
|
|
|
|
fn validate_users_economy_config_pda(program_id: &Pubkey, pda: &AccountInfo) -> ProgramResult {
|
|
let (expected_pda, _) = find_users_economy_config_pda(program_id);
|
|
require_keys_eq!(expected_pda, *pda.key, ShineUsersError::InvalidPdaAddress);
|
|
require!(pda.owner == program_id, ShineUsersError::InvalidPdaAddress);
|
|
Ok(())
|
|
}
|
|
|
|
fn read_users_economy_config(pda: &AccountInfo) -> Result<UsersEconomyConfigState, ProgramError> {
|
|
let raw = read_pda_all(pda)?;
|
|
require!(!raw.is_empty(), ShineUsersError::EmptyPdaData);
|
|
require!(raw.len() >= 25, ShineUsersError::InvalidAccountData);
|
|
Ok(UsersEconomyConfigState {
|
|
version: raw[0],
|
|
registration_fee_lamports: u64::from_le_bytes(raw[1..9].try_into().unwrap()),
|
|
lamports_per_limit_step: u64::from_le_bytes(raw[9..17].try_into().unwrap()),
|
|
start_bonus_limit: u64::from_le_bytes(raw[17..25].try_into().unwrap()),
|
|
})
|
|
}
|
|
|
|
fn read_promo_seller_state(pda: &AccountInfo) -> Result<PromoSellerState, ProgramError> {
|
|
let raw = read_pda_all(pda)?;
|
|
require!(!raw.is_empty(), ShineUsersError::PromoSellerNotFound);
|
|
require!(raw.len() >= 42, ShineUsersError::InvalidPromoSellerState);
|
|
let signer_pubkey = Pubkey::new_from_array(raw[10..42].try_into().map_err(|_| ProgramError::from(ShineUsersError::InvalidPromoSellerState))?);
|
|
Ok(PromoSellerState {
|
|
version: raw[0],
|
|
remaining_sales: u64::from_le_bytes(raw[1..9].try_into().unwrap()),
|
|
min_login_length: raw[9],
|
|
signer_pubkey,
|
|
})
|
|
}
|
|
|
|
fn record_format_minor(raw: &[u8]) -> Result<u8, ProgramError> {
|
|
require!(raw.len() >= 9, ShineUsersError::InvalidRecordData);
|
|
require!(sol_memcmp(&raw[0..5], MAGIC, 5) == 0, ShineUsersError::InvalidRecordMagic);
|
|
require!(raw[5] == FORMAT_MAJOR, ShineUsersError::InvalidRecordFormat);
|
|
Ok(raw[6])
|
|
}
|
|
|
|
fn deserialize_record_v12_from_pda(raw: &[u8]) -> Result<UserRecordV12, ProgramError> {
|
|
require!(raw.len() >= 9, ShineUsersError::InvalidRecordData);
|
|
require!(sol_memcmp(&raw[0..5], MAGIC, 5) == 0, ShineUsersError::InvalidRecordMagic);
|
|
require!(raw[5] == FORMAT_MAJOR && raw[6] == FORMAT_MINOR, ShineUsersError::InvalidRecordFormat);
|
|
let record_len = u16::from_le_bytes([raw[7], raw[8]]) as usize;
|
|
require!(record_len >= 9 + 64, ShineUsersError::InvalidRecordLength);
|
|
require!(record_len <= raw.len(), ShineUsersError::InvalidRecordLength);
|
|
let useful = &raw[..record_len];
|
|
let mut cursor = 9usize;
|
|
|
|
let created_at_ms = read_u64_from(useful, &mut cursor)?;
|
|
let updated_at_ms = read_u64_from(useful, &mut cursor)?;
|
|
let record_number = read_u32_from(useful, &mut cursor)?;
|
|
let prev_record_hash = read_fixed_32_from(useful, &mut cursor)?;
|
|
let login = read_len_prefixed_string_from(useful, &mut cursor)?;
|
|
let blocks_count = read_u8_from(useful, &mut cursor)? as usize;
|
|
|
|
let mut root_key = None;
|
|
let mut client_key = None;
|
|
let mut forks: Option<Vec<ForkRecordV12>> = None;
|
|
let mut server_addresses = Vec::new();
|
|
let mut access_servers = Vec::new();
|
|
|
|
for _ in 0..blocks_count {
|
|
let block_type = read_u8_from(useful, &mut cursor)?;
|
|
let block_version = read_u8_from(useful, &mut cursor)?;
|
|
match block_type {
|
|
BLOCK_TYPE_ROOT_KEY => {
|
|
require!(block_version == BLOCK_VERSION_0 && root_key.is_none(), ShineUsersError::InvalidRecordFormat);
|
|
root_key = Some(Pubkey::new_from_array(read_fixed_32_from(useful, &mut cursor)?));
|
|
}
|
|
BLOCK_TYPE_CLIENT_KEY => {
|
|
require!(block_version == BLOCK_VERSION_0 && client_key.is_none(), ShineUsersError::InvalidRecordFormat);
|
|
client_key = Some(Pubkey::new_from_array(read_fixed_32_from(useful, &mut cursor)?));
|
|
}
|
|
_ => {
|
|
let payload_len = read_u16_from(useful, &mut cursor)? as usize;
|
|
let payload_end = cursor.checked_add(payload_len).ok_or(ProgramError::from(ShineUsersError::InvalidRecordLength))?;
|
|
require!(payload_end <= useful.len().saturating_sub(64), ShineUsersError::InvalidRecordLength);
|
|
if block_version == BLOCK_VERSION_0 {
|
|
match block_type {
|
|
BLOCK_TYPE_BLOCKCHAIN_REGISTRY => {
|
|
require!(forks.is_none(), ShineUsersError::InvalidRecordData);
|
|
let mut p = cursor;
|
|
let count = read_u16_from(useful, &mut p)? as usize;
|
|
require!(count > 0, ShineUsersError::InvalidRecordData);
|
|
let mut list = Vec::with_capacity(count);
|
|
for _ in 0..count {
|
|
list.push(ForkRecordV12 {
|
|
blockchain_key: Pubkey::new_from_array(read_fixed_32_from(useful, &mut p)?),
|
|
created_at_ms: read_u64_from(useful, &mut p)?,
|
|
paid_limit_bytes: read_u32_from(useful, &mut p)?,
|
|
});
|
|
}
|
|
for pair in list.windows(2) {
|
|
require!(pair[0].created_at_ms <= pair[1].created_at_ms, ShineUsersError::InvalidRecordData);
|
|
}
|
|
require!(p == payload_end, ShineUsersError::InvalidRecordLength);
|
|
forks = Some(list);
|
|
}
|
|
BLOCK_TYPE_SERVER_PROFILE => {
|
|
require!(server_addresses.is_empty(), ShineUsersError::InvalidRecordData);
|
|
let mut p = cursor;
|
|
let count = read_u8_from(useful, &mut p)? as usize;
|
|
for _ in 0..count {
|
|
server_addresses.push(ServerAddressRecordV12 {
|
|
address_format_type: read_u8_from(useful, &mut p)?,
|
|
address_format_version: read_u8_from(useful, &mut p)?,
|
|
address: read_len_prefixed_string_from(useful, &mut p)?,
|
|
});
|
|
}
|
|
require!(p == payload_end, ShineUsersError::InvalidRecordLength);
|
|
}
|
|
BLOCK_TYPE_ACCESS_SERVERS => {
|
|
require!(access_servers.is_empty(), ShineUsersError::InvalidRecordData);
|
|
let mut p = cursor;
|
|
let count = read_u8_from(useful, &mut p)? as usize;
|
|
for _ in 0..count { access_servers.push(read_len_prefixed_string_from(useful, &mut p)?); }
|
|
require!(p == payload_end, ShineUsersError::InvalidRecordLength);
|
|
}
|
|
_ => {
|
|
// Неизвестный variable block 1.2 можно безопасно пропустить по payload_len.
|
|
}
|
|
}
|
|
}
|
|
cursor = payload_end;
|
|
}
|
|
}
|
|
}
|
|
|
|
let signature = read_fixed_64_from(useful, &mut cursor)?;
|
|
require!(cursor == useful.len(), ShineUsersError::InvalidRecordLength);
|
|
let record = UserRecordV12 {
|
|
created_at_ms,
|
|
updated_at_ms,
|
|
record_number,
|
|
prev_record_hash,
|
|
login,
|
|
root_key: root_key.ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?,
|
|
client_key: client_key.ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?,
|
|
forks: forks.ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?,
|
|
server_addresses,
|
|
access_servers,
|
|
signature,
|
|
};
|
|
validate_server_addresses(&record.server_addresses)?;
|
|
validate_access_servers(&record.access_servers)?;
|
|
Ok(record)
|
|
}
|
|
|
|
fn serialize_unsigned_record_v12(record: &UserRecordV12) -> Result<Vec<u8>, ProgramError> {
|
|
let login_bytes = record.login.as_bytes();
|
|
require!(login_bytes.len() <= u8::MAX as usize, ShineUsersError::InvalidLogin);
|
|
require!(!record.forks.is_empty() && record.forks.len() <= u16::MAX as usize, ShineUsersError::InvalidRecordData);
|
|
validate_server_addresses(&record.server_addresses)?;
|
|
validate_access_servers(&record.access_servers)?;
|
|
|
|
let mut out = Vec::new();
|
|
out.extend_from_slice(MAGIC);
|
|
out.push(FORMAT_MAJOR);
|
|
out.push(FORMAT_MINOR);
|
|
out.extend_from_slice(&0u16.to_le_bytes());
|
|
out.extend_from_slice(&record.created_at_ms.to_le_bytes());
|
|
out.extend_from_slice(&record.updated_at_ms.to_le_bytes());
|
|
out.extend_from_slice(&record.record_number.to_le_bytes());
|
|
out.extend_from_slice(&record.prev_record_hash);
|
|
out.push(login_bytes.len() as u8);
|
|
out.extend_from_slice(login_bytes);
|
|
|
|
let mut blocks_count = 3usize;
|
|
if !record.server_addresses.is_empty() { blocks_count += 1; }
|
|
if !record.access_servers.is_empty() { blocks_count += 1; }
|
|
require!(blocks_count <= u8::MAX as usize, ShineUsersError::RecordTooLarge);
|
|
out.push(blocks_count as u8);
|
|
|
|
// Fixed-size core blocks intentionally omit payload_len.
|
|
out.push(BLOCK_TYPE_ROOT_KEY);
|
|
out.push(BLOCK_VERSION_0);
|
|
out.extend_from_slice(record.root_key.as_ref());
|
|
out.push(BLOCK_TYPE_CLIENT_KEY);
|
|
out.push(BLOCK_VERSION_0);
|
|
out.extend_from_slice(record.client_key.as_ref());
|
|
|
|
write_variable_block(&mut out, BLOCK_TYPE_BLOCKCHAIN_REGISTRY, BLOCK_VERSION_0, |payload| {
|
|
payload.extend_from_slice(&(record.forks.len() as u16).to_le_bytes());
|
|
for fork in &record.forks {
|
|
payload.extend_from_slice(fork.blockchain_key.as_ref());
|
|
payload.extend_from_slice(&fork.created_at_ms.to_le_bytes());
|
|
payload.extend_from_slice(&fork.paid_limit_bytes.to_le_bytes());
|
|
}
|
|
Ok(())
|
|
})?;
|
|
|
|
if !record.server_addresses.is_empty() {
|
|
write_variable_block(&mut out, BLOCK_TYPE_SERVER_PROFILE, BLOCK_VERSION_0, |payload| {
|
|
require!(record.server_addresses.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData);
|
|
payload.push(record.server_addresses.len() as u8);
|
|
for address in &record.server_addresses {
|
|
payload.push(address.address_format_type);
|
|
payload.push(address.address_format_version);
|
|
write_len_prefixed_string(payload, &address.address)?;
|
|
}
|
|
Ok(())
|
|
})?;
|
|
}
|
|
|
|
if !record.access_servers.is_empty() {
|
|
write_variable_block(&mut out, BLOCK_TYPE_ACCESS_SERVERS, BLOCK_VERSION_0, |payload| {
|
|
require!(record.access_servers.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData);
|
|
payload.push(record.access_servers.len() as u8);
|
|
for login in &record.access_servers { write_len_prefixed_string(payload, login)?; }
|
|
Ok(())
|
|
})?;
|
|
}
|
|
|
|
let record_len = out.len().checked_add(64).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?;
|
|
require!(record_len <= u16::MAX as usize, ShineUsersError::RecordTooLarge);
|
|
let len_bytes = (record_len as u16).to_le_bytes();
|
|
out[7] = len_bytes[0];
|
|
out[8] = len_bytes[1];
|
|
Ok(out)
|
|
}
|
|
|
|
fn serialize_full_record_v12(record: &UserRecordV12) -> Result<Vec<u8>, ProgramError> {
|
|
let mut out = serialize_unsigned_record_v12(record)?;
|
|
out.extend_from_slice(&record.signature);
|
|
Ok(out)
|
|
}
|
|
|
|
fn hash_unsigned_record_v12(record: &UserRecordV12) -> Result<[u8; 32], ProgramError> {
|
|
let unsigned = serialize_unsigned_record_v12(record)?;
|
|
let digest = hashv(&[&unsigned]);
|
|
let mut out = [0u8; 32];
|
|
out.copy_from_slice(digest.as_ref());
|
|
Ok(out)
|
|
}
|
|
|
|
fn write_variable_block<F>(out: &mut Vec<u8>, block_type: u8, block_version: u8, writer: F) -> Result<(), ProgramError>
|
|
where
|
|
F: FnOnce(&mut Vec<u8>) -> Result<(), ProgramError>,
|
|
{
|
|
out.push(block_type);
|
|
out.push(block_version);
|
|
let len_pos = out.len();
|
|
out.extend_from_slice(&0u16.to_le_bytes());
|
|
let payload_start = out.len();
|
|
writer(out)?;
|
|
let payload_len = out.len().checked_sub(payload_start).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?;
|
|
require!(payload_len <= u16::MAX as usize, ShineUsersError::RecordTooLarge);
|
|
let len = (payload_len as u16).to_le_bytes();
|
|
out[len_pos] = len[0];
|
|
out[len_pos + 1] = len[1];
|
|
Ok(())
|
|
}
|
|
|
|
fn write_len_prefixed_string(out: &mut Vec<u8>, value: &str) -> Result<(), ProgramError> {
|
|
let bytes = value.as_bytes();
|
|
require!(bytes.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData);
|
|
out.push(bytes.len() as u8);
|
|
out.extend_from_slice(bytes);
|
|
Ok(())
|
|
}
|
|
|
|
fn verify_record_signature_hash_at(instructions_sysvar: &AccountInfo, index_relative_to_current: i64, key: &Pubkey, signature: &[u8; 64], message_hash: &[u8]) -> Result<[u8; 64], ProgramError> {
|
|
verify_ed25519_signature_instruction(instructions_sysvar, index_relative_to_current, key, signature, message_hash)?;
|
|
Ok(*signature)
|
|
}
|
|
|
|
struct ParsedEd25519Data {
|
|
pubkey: Pubkey,
|
|
signature: [u8; 64],
|
|
message: Vec<u8>,
|
|
}
|
|
|
|
fn verify_ed25519_signature_instruction(instructions_sysvar: &AccountInfo, index_relative_to_current: i64, expected_pubkey: &Pubkey, expected_signature: &[u8; 64], expected_message: &[u8]) -> ProgramResult {
|
|
let parsed = load_parsed_ed25519_instruction(instructions_sysvar, index_relative_to_current)?;
|
|
require!(parsed.pubkey == *expected_pubkey, ShineUsersError::InvalidSignature);
|
|
require!(parsed.signature == *expected_signature, ShineUsersError::InvalidSignature);
|
|
require!(parsed.message == expected_message, ShineUsersError::InvalidSignature);
|
|
Ok(())
|
|
}
|
|
|
|
fn verify_ed25519_instruction_pubkey_message(instructions_sysvar: &AccountInfo, index_relative_to_current: i64, expected_pubkey: &Pubkey, expected_message: &[u8]) -> ProgramResult {
|
|
let parsed = load_parsed_ed25519_instruction(instructions_sysvar, index_relative_to_current)?;
|
|
require!(parsed.pubkey == *expected_pubkey, ShineUsersError::InvalidSignature);
|
|
require!(parsed.message == expected_message, ShineUsersError::InvalidSignature);
|
|
Ok(())
|
|
}
|
|
|
|
fn load_parsed_ed25519_instruction(instructions_sysvar: &AccountInfo, index_relative_to_current: i64) -> Result<ParsedEd25519Data, ProgramError> {
|
|
require_keys_eq!(*instructions_sysvar.key, solana_program::sysvar::instructions::id(), ShineUsersError::InvalidSignature);
|
|
let current_index = load_current_index_checked(instructions_sysvar).map_err(|_| ProgramError::from(ShineUsersError::InvalidSignature))? as i64;
|
|
let target_index = current_index.checked_add(index_relative_to_current).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?;
|
|
require!(target_index >= 0, ShineUsersError::InvalidSignature);
|
|
let ed_ix = load_instruction_at_checked(target_index as usize, instructions_sysvar).map_err(|_| ProgramError::from(ShineUsersError::InvalidSignature))?;
|
|
require_keys_eq!(ed_ix.program_id, solana_program::ed25519_program::id(), ShineUsersError::InvalidSignature);
|
|
parse_ed25519_ix(ed_ix.data.as_slice())
|
|
}
|
|
|
|
fn parse_ed25519_ix(data: &[u8]) -> Result<ParsedEd25519Data, ProgramError> {
|
|
require!(data.len() >= 16, ShineUsersError::InvalidSignature);
|
|
require!(data[0] == 1, ShineUsersError::InvalidSignature);
|
|
let signature_offset = le_u16(data, 2)? as usize;
|
|
let signature_ix_index = le_u16(data, 4)?;
|
|
let pubkey_offset = le_u16(data, 6)? as usize;
|
|
let pubkey_ix_index = le_u16(data, 8)?;
|
|
let message_offset = le_u16(data, 10)? as usize;
|
|
let message_size = le_u16(data, 12)? as usize;
|
|
let message_ix_index = le_u16(data, 14)?;
|
|
require!(signature_ix_index == u16::MAX, ShineUsersError::InvalidSignature);
|
|
require!(pubkey_ix_index == u16::MAX, ShineUsersError::InvalidSignature);
|
|
require!(message_ix_index == u16::MAX, ShineUsersError::InvalidSignature);
|
|
|
|
let signature_end = signature_offset.checked_add(64).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?;
|
|
let pubkey_end = pubkey_offset.checked_add(32).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?;
|
|
let message_end = message_offset.checked_add(message_size).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?;
|
|
let signature_slice = data.get(signature_offset..signature_end).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?;
|
|
let pubkey_slice = data.get(pubkey_offset..pubkey_end).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?;
|
|
let message = data.get(message_offset..message_end).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?;
|
|
let mut signature = [0u8; 64]; signature.copy_from_slice(signature_slice);
|
|
let pubkey = Pubkey::new_from_array(<[u8; 32]>::try_from(pubkey_slice).map_err(|_| ProgramError::from(ShineUsersError::InvalidSignature))?);
|
|
Ok(ParsedEd25519Data { pubkey, signature, message: message.to_vec() })
|
|
}
|
|
|
|
fn le_u16(data: &[u8], offset: usize) -> Result<u16, ProgramError> {
|
|
let end = offset.checked_add(2).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?;
|
|
let s = data.get(offset..end).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?;
|
|
Ok(u16::from_le_bytes([s[0], s[1]]))
|
|
}
|
|
|
|
fn build_promo_sign_message(login: &str) -> String {
|
|
let mut message = String::with_capacity(settings::PROMO_SIGN_PREFIX.len() + login.len());
|
|
message.push_str(settings::PROMO_SIGN_PREFIX);
|
|
message.push_str(login);
|
|
message
|
|
}
|
|
|
|
fn validate_login(login: &str) -> ProgramResult {
|
|
require!(!login.is_empty(), ShineUsersError::InvalidLogin);
|
|
require!(login.len() <= 20, ShineUsersError::InvalidLogin);
|
|
for ch in login.chars() { if !(ch.is_ascii_alphabetic() || ch.is_ascii_digit() || ch == '_') { return Err(ProgramError::from(ShineUsersError::InvalidLogin)); } }
|
|
Ok(())
|
|
}
|
|
fn login_seed_normalized(login: &str) -> String { login.to_ascii_lowercase() }
|
|
|
|
fn validate_server_addresses(addresses: &[ServerAddressRecordV12]) -> ProgramResult {
|
|
require!(addresses.len() <= 1, ShineUsersError::InvalidRecordData);
|
|
for address in addresses {
|
|
let bytes = address.address.as_bytes();
|
|
require!(!bytes.is_empty() && bytes.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_access_servers(servers: &[String]) -> ProgramResult {
|
|
require!(servers.len() <= 1, ShineUsersError::InvalidRecordData);
|
|
for login in servers {
|
|
validate_login(login)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn current_time_ms() -> Result<u64, ProgramError> {
|
|
let seconds = Clock::get()?.unix_timestamp;
|
|
require!(seconds >= 0, ShineUsersError::InvalidRecordData);
|
|
(seconds as u64).checked_mul(1000).ok_or(ProgramError::from(ShineUsersError::MathOverflow))
|
|
}
|
|
|
|
fn validate_client_time_ms(value_ms: u64, now_ms: u64) -> ProgramResult {
|
|
let delta = if value_ms >= now_ms { value_ms - now_ms } else { now_ms - value_ms };
|
|
require!(delta <= MAX_CLIENT_CLOCK_SKEW_MS, ShineUsersError::InvalidRecordData);
|
|
Ok(())
|
|
}
|
|
|
|
fn enforce_fork_cooldown(last_fork_ms: u64, now_ms: u64) -> ProgramResult {
|
|
require!(now_ms >= last_fork_ms, ShineUsersError::InvalidRecordData);
|
|
require!(now_ms.saturating_sub(last_fork_ms) >= FORK_COOLDOWN_MS, ShineUsersError::ForkCooldown);
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_inflow_vault(inflow_vault: &AccountInfo) -> ProgramResult {
|
|
let payments_program_id = Pubkey::from_str(settings::SHINE_PAYMENTS_PROGRAM_ID).map_err(|_| ProgramError::from(ShineUsersError::InvalidFeeReceiver))?;
|
|
let (expected, _) = Pubkey::find_program_address(&[settings::SHINE_PAYMENTS_INFLOW_VAULT_SEED], &payments_program_id);
|
|
require_keys_eq!(expected, *inflow_vault.key, ShineUsersError::InvalidFeeReceiver);
|
|
Ok(())
|
|
}
|
|
|
|
fn transfer_lamports<'a>(payer: &AccountInfo<'a>, recipient: &AccountInfo<'a>, system_program_ai: &AccountInfo<'a>, lamports: u64) -> ProgramResult {
|
|
if lamports == 0 { return Ok(()); }
|
|
let ix = system_instruction::transfer(payer.key, recipient.key, lamports);
|
|
invoke(&ix, &[payer.clone(), recipient.clone(), system_program_ai.clone()])
|
|
}
|
|
|
|
// Создание PDA, устойчивое к «минированию» детерминированного адреса.
|
|
// Адрес будущей записи логина выводится из самого логина, поэтому злоумышленник
|
|
// может заранее вычислить адрес и перевести на него немного лампортов обычным
|
|
// system-переводом. Тогда обычный system_instruction::create_account упал бы
|
|
// («account already in use») и заблокировал бы регистрацию этого логина навсегда.
|
|
// Чтобы это исключить, при уже существующих на адресе лампортах создаём аккаунт
|
|
// «поверх предзаполненного»: доводим ренту переводом, затем allocate + assign
|
|
// под подписью PDA. Подсев чужих лампортов больше ничего не ломает.
|
|
fn create_pda_account<'a>(payer: &AccountInfo<'a>, pda: &AccountInfo<'a>, system_program_ai: &AccountInfo<'a>, owner: &Pubkey, seeds: &[&[u8]], space: usize) -> ProgramResult {
|
|
let rent = Rent::get()?;
|
|
let required_lamports = rent.minimum_balance(space);
|
|
let current_lamports = pda.lamports();
|
|
|
|
if current_lamports == 0 {
|
|
// Быстрый путь: адрес пуст — обычное создание аккаунта одной инструкцией.
|
|
let ix = system_instruction::create_account(payer.key, pda.key, required_lamports, space as u64, owner);
|
|
return invoke_signed(&ix, &[payer.clone(), pda.clone(), system_program_ai.clone()], &[seeds]);
|
|
}
|
|
|
|
// На адресе уже лежат лампорты (вероятно, «подсев» атакующим). Доводим баланс
|
|
// до рент-экземпта, выделяем место и назначаем владельцем нашу программу.
|
|
let top_up = required_lamports.saturating_sub(current_lamports);
|
|
transfer_lamports(payer, pda, system_program_ai, top_up)?;
|
|
|
|
let allocate_ix = system_instruction::allocate(pda.key, space as u64);
|
|
invoke_signed(&allocate_ix, &[pda.clone(), system_program_ai.clone()], &[seeds])?;
|
|
|
|
let assign_ix = system_instruction::assign(pda.key, owner);
|
|
invoke_signed(&assign_ix, &[pda.clone(), system_program_ai.clone()], &[seeds])
|
|
}
|
|
|
|
fn ensure_pda_size_and_rent<'a>(pda: &AccountInfo<'a>, payer: &AccountInfo<'a>, system_program_ai: &AccountInfo<'a>, required_len: usize) -> ProgramResult {
|
|
let current_len = pda.data_len();
|
|
if required_len <= current_len { return Ok(()); }
|
|
let increase = required_len.checked_sub(current_len).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?;
|
|
require!(increase <= MAX_AUTO_REALLOC_INCREASE, ShineUsersError::RecordTooLarge);
|
|
let rent = Rent::get()?;
|
|
let required_lamports = rent.minimum_balance(required_len);
|
|
let current_lamports = pda.lamports();
|
|
let top_up = required_lamports.saturating_sub(current_lamports);
|
|
if top_up > 0 { transfer_lamports(payer, pda, system_program_ai, top_up)?; }
|
|
pda.realloc(required_len, false)
|
|
}
|
|
|
|
fn find_user_pda(program_id: &Pubkey, login: &str) -> (Pubkey, u8) { Pubkey::find_program_address(&[settings::USER_PDA_SEED_PREFIX.as_bytes(), login.as_bytes()], program_id) }
|
|
fn find_users_economy_config_pda(program_id: &Pubkey) -> (Pubkey, u8) { Pubkey::find_program_address(&[settings::USERS_ECONOMY_CONFIG_SEED], program_id) }
|
|
fn find_promo_seller_pda(program_id: &Pubkey, seller_login: &str) -> (Pubkey, u8) { Pubkey::find_program_address(&[settings::PROMO_SELLER_PDA_SEED_PREFIX.as_bytes(), seller_login.as_bytes()], program_id) }
|
|
fn limit_fee_lamports(limit_delta: u64, lamports_per_limit_step: u64) -> Result<u64, ProgramError> { (limit_delta / settings::LIMIT_STEP).checked_mul(lamports_per_limit_step).ok_or(ProgramError::from(ShineUsersError::MathOverflow)) }
|
|
fn pad_to_fixed_size(mut bytes: Vec<u8>, target_size: usize) -> Result<Vec<u8>, ProgramError> { require!(bytes.len() <= target_size, ShineUsersError::RecordTooLarge); bytes.resize(target_size, 0); Ok(bytes) }
|
|
|
|
fn read_pda_all(pda: &AccountInfo) -> Result<Vec<u8>, ProgramError> { Ok(pda.try_borrow_data().map_err(|_| ProgramError::from(ShineUsersError::InvalidAccountData))?.to_vec()) }
|
|
fn write_pda_exact(pda: &AccountInfo, data: &[u8]) -> ProgramResult { let mut dst = pda.try_borrow_mut_data().map_err(|_| ProgramError::from(ShineUsersError::InvalidAccountData))?; require!(data.len() <= dst.len(), ShineUsersError::RecordTooLarge); dst[..data.len()].copy_from_slice(data); for b in &mut dst[data.len()..] { *b = 0; } Ok(()) }
|
|
fn write_pda_prefix(pda: &AccountInfo, data: &[u8]) -> ProgramResult { let mut dst = pda.try_borrow_mut_data().map_err(|_| ProgramError::from(ShineUsersError::InvalidAccountData))?; require!(data.len() <= dst.len(), ShineUsersError::RecordTooLarge); dst[..data.len()].copy_from_slice(data); Ok(()) }
|
|
|
|
fn read_u8_from(data: &[u8], cursor: &mut usize) -> Result<u8, ProgramError> { let v = *data.get(*cursor).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor += 1; Ok(v) }
|
|
fn read_u16_from(data: &[u8], cursor: &mut usize) -> Result<u16, ProgramError> { let end = cursor.checked_add(2).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; Ok(u16::from_le_bytes([s[0], s[1]])) }
|
|
fn read_u32_from(data: &[u8], cursor: &mut usize) -> Result<u32, ProgramError> { let end = cursor.checked_add(4).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; Ok(u32::from_le_bytes([s[0], s[1], s[2], s[3]])) }
|
|
fn read_u64_from(data: &[u8], cursor: &mut usize) -> Result<u64, ProgramError> { let end = cursor.checked_add(8).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; Ok(u64::from_le_bytes([s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]])) }
|
|
fn read_fixed_32_from(data: &[u8], cursor: &mut usize) -> Result<[u8; 32], ProgramError> { let end = cursor.checked_add(32).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; <[u8; 32]>::try_from(s).map_err(|_| ProgramError::from(ShineUsersError::InvalidRecordData)) }
|
|
fn read_fixed_64_from(data: &[u8], cursor: &mut usize) -> Result<[u8; 64], ProgramError> { let end = cursor.checked_add(64).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; <[u8; 64]>::try_from(s).map_err(|_| ProgramError::from(ShineUsersError::InvalidRecordData)) }
|
|
fn read_len_prefixed_string_from(data: &[u8], cursor: &mut usize) -> Result<String, ProgramError> { let len = read_u8_from(data, cursor)? as usize; let end = cursor.checked_add(len).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; std::str::from_utf8(s).map(|v| v.to_string()).map_err(|_| ProgramError::from(ShineUsersError::InvalidRecordData)) }
|