use crate::settings; use anchor_lang::prelude::*; use anchor_lang::solana_program::{ ed25519_program, hash::hashv, instruction::Instruction, program::invoke, system_instruction, sysvar::instructions::{load_current_index_checked, load_instruction_at_checked}, }; use common::utils::{create_pda, safe_read_pda, write_to_pda, ErrCode}; use std::str::FromStr; const MAGIC: &[u8; 5] = b"SHiNE"; const FORMAT_MAJOR: u8 = 1; const FORMAT_MINOR: u8 = 0; const KEY_STATUS_CREATED: u8 = 0; const RESERVED_BYTES: [u8; 5] = [0, 0, 0, 0, 0]; const ZERO_HASH: [u8; 32] = [0; 32]; #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct UserMutableFields { pub blockchain_key: Pubkey, pub device_key: Pubkey, pub chain_number: u16, pub is_server: bool, pub server_key: Pubkey, pub server_address: String, pub connection_servers: Vec, pub trusted_count: u8, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct CreateUserPdaArgs { pub login: String, pub root_key: Pubkey, pub created_at_ms: u64, pub additional_limit: u64, pub fields: UserMutableFields, pub signature: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct UpdateUserPdaArgs { pub login: String, pub root_key: Pubkey, pub created_at_ms: u64, pub updated_at_ms: u64, pub version: u32, pub prev_hash: Vec, pub additional_limit: u64, pub fields: UserMutableFields, pub signature: Vec, } pub struct UserRecord { pub created_at_ms: u64, pub updated_at_ms: u64, pub version: u32, pub prev_hash: [u8; 32], pub login: String, pub root_key_status: u8, pub root_key: Pubkey, pub blockchain_key_status: u8, pub blockchain_key: Pubkey, pub device_key_status: u8, pub device_key: Pubkey, pub chain_number: u16, pub balance: u64, pub is_server: bool, pub server_key: Pubkey, pub server_address: String, pub connection_servers: Vec, pub trusted_count: u8, pub signature: [u8; 64], } #[derive(AnchorSerialize, AnchorDeserialize, 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(Accounts)] pub struct CreateUserPda<'info> { /// CHECK: подписант транзакции, валидируется Anchor как signer и mut. #[account(mut, signer)] pub signer: AccountInfo<'info>, /// CHECK: PDA пользователя, адрес проверяется вручную через seed в обработчике. #[account(mut)] pub user_pda: AccountInfo<'info>, pub system_program: Program<'info, System>, /// CHECK: inflow-вольт shine_payments, адрес проверяется вручную как PDA. #[account(mut)] pub inflow_vault: AccountInfo<'info>, /// CHECK: sysvar инструкций, нужен для проверки встроенной Ed25519Program инструкции. pub instructions: AccountInfo<'info>, /// CHECK: PDA с экономическими настройками пользователей, адрес проверяется вручную. pub users_economy_config_pda: AccountInfo<'info>, } #[derive(Accounts)] pub struct UpdateUserPda<'info> { /// CHECK: подписант транзакции, валидируется Anchor как signer и mut. #[account(mut, signer)] pub signer: AccountInfo<'info>, /// CHECK: PDA пользователя, адрес проверяется вручную через seed в обработчике. #[account(mut)] pub user_pda: AccountInfo<'info>, pub system_program: Program<'info, System>, /// CHECK: inflow-вольт shine_payments, адрес проверяется вручную как PDA. #[account(mut)] pub inflow_vault: AccountInfo<'info>, /// CHECK: sysvar инструкций, нужен для проверки встроенной Ed25519Program инструкции. pub instructions: AccountInfo<'info>, /// CHECK: PDA с экономическими настройками пользователей, адрес проверяется вручную. pub users_economy_config_pda: AccountInfo<'info>, } #[derive(Accounts)] pub struct InitUsersEconomyConfig<'info> { /// CHECK: подписант и плательщик, валидируется Anchor как signer и mut. #[account(mut, signer)] pub signer: AccountInfo<'info>, /// CHECK: PDA с экономическими настройками пользователей, адрес проверяется вручную. #[account(mut)] pub users_economy_config_pda: AccountInfo<'info>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct UpdateUsersEconomyConfig<'info> { /// CHECK: подписант (должен быть DAO authority из settings). #[account(mut, signer)] pub signer: AccountInfo<'info>, /// CHECK: PDA с экономическими настройками пользователей, адрес проверяется вручную. #[account(mut)] pub users_economy_config_pda: AccountInfo<'info>, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct UpdateUsersEconomyConfigArgs { pub registration_fee_lamports: u64, pub lamports_per_limit_step: u64, pub start_bonus_limit: u64, } pub fn init_users_economy_config(ctx: Context) -> Result<()> { let (expected_pda, bump) = find_users_economy_config_pda(ctx.program_id); require_keys_eq!( expected_pda, ctx.accounts.users_economy_config_pda.key(), ErrCode::InvalidPdaAddress ); require!( ctx.accounts.users_economy_config_pda.owner == &Pubkey::default(), ErrCode::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 bytes = state .try_to_vec() .map_err(|_| error!(ErrCode::DeserializationError))?; let seeds: &[&[u8]] = &[settings::USERS_ECONOMY_CONFIG_SEED, &[bump]]; create_pda( &ctx.accounts.users_economy_config_pda, &ctx.accounts.signer, &ctx.accounts.system_program.to_account_info(), ctx.program_id, seeds, settings::USERS_ECONOMY_CONFIG_SPACE as u64, )?; write_to_pda(&ctx.accounts.users_economy_config_pda, &bytes)?; Ok(()) } pub fn update_users_economy_config( ctx: Context, args: UpdateUsersEconomyConfigArgs, ) -> Result<()> { let dao_authority = Pubkey::from_str(settings::DAO_AUTHORITY).map_err(|_| error!(ErrCode::InvalidSigner))?; require_keys_eq!(dao_authority, ctx.accounts.signer.key(), ErrCode::InvalidSigner); let (expected_pda, _) = find_users_economy_config_pda(ctx.program_id); require_keys_eq!( expected_pda, ctx.accounts.users_economy_config_pda.key(), ErrCode::InvalidPdaAddress ); require!( ctx.accounts.users_economy_config_pda.owner == ctx.program_id, ErrCode::InvalidPdaAddress ); require!(args.lamports_per_limit_step > 0, ErrCode::InvalidRecordData); let mut state = read_users_economy_config(&ctx.accounts.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; let bytes = state .try_to_vec() .map_err(|_| error!(ErrCode::DeserializationError))?; write_to_pda(&ctx.accounts.users_economy_config_pda, &bytes)?; Ok(()) } pub fn create_user_pda(ctx: Context, args: CreateUserPdaArgs) -> Result<()> { validate_login(&args.login)?; validate_fields(&args.fields)?; validate_inflow_vault(&ctx.accounts.inflow_vault)?; require!( args.additional_limit % settings::LIMIT_STEP == 0, ErrCode::InvalidLimitIncrement ); let economy = read_users_economy_config(&ctx.accounts.users_economy_config_pda)?; let (expected_pda, bump) = find_user_pda(ctx.program_id, &args.login); require_keys_eq!( expected_pda, ctx.accounts.user_pda.key(), ErrCode::InvalidPdaAddress ); require!( ctx.accounts.user_pda.owner == &Pubkey::default(), ErrCode::UserAlreadyExists ); let start_balance = economy .start_bonus_limit .checked_add(args.additional_limit) .ok_or(error!(ErrCode::MathOverflow))?; let mut record = UserRecord { created_at_ms: args.created_at_ms, updated_at_ms: args.created_at_ms, version: 0, prev_hash: ZERO_HASH, login: args.login.clone(), root_key_status: KEY_STATUS_CREATED, root_key: args.root_key, blockchain_key_status: KEY_STATUS_CREATED, blockchain_key: args.fields.blockchain_key, device_key_status: KEY_STATUS_CREATED, device_key: args.fields.device_key, chain_number: args.fields.chain_number, balance: start_balance, is_server: args.fields.is_server, server_key: args.fields.server_key, server_address: args.fields.server_address.clone(), connection_servers: args.fields.connection_servers.clone(), trusted_count: args.fields.trusted_count, signature: [0; 64], }; let unsigned = serialize_unsigned_record(&record)?; record.signature = verify_record_signature( &ctx.accounts.instructions, &record.root_key, &args.signature, &unsigned, )?; let serialized = serialize_full_record(&record)?; require!( serialized.len() <= settings::USER_PDA_SPACE, ErrCode::RecordTooLarge ); let padded = pad_to_fixed_size(serialized, settings::USER_PDA_SPACE)?; let pda_seeds: &[&[u8]] = &[ settings::USER_PDA_SEED_PREFIX.as_bytes(), args.login.as_bytes(), &[bump], ]; create_pda( &ctx.accounts.user_pda, &ctx.accounts.signer, &ctx.accounts.system_program.to_account_info(), ctx.program_id, pda_seeds, settings::USER_PDA_SPACE as u64, )?; write_to_pda(&ctx.accounts.user_pda, &padded)?; let total_fee = economy .registration_fee_lamports .checked_add(limit_fee_lamports(args.additional_limit, economy.lamports_per_limit_step)?) .ok_or(error!(ErrCode::MathOverflow))?; transfer_lamports( &ctx.accounts.signer, &ctx.accounts.inflow_vault, &ctx.accounts.system_program.to_account_info(), total_fee, )?; Ok(()) } pub fn update_user_pda(ctx: Context, args: UpdateUserPdaArgs) -> Result<()> { validate_login(&args.login)?; validate_fields(&args.fields)?; validate_inflow_vault(&ctx.accounts.inflow_vault)?; require!( args.additional_limit % settings::LIMIT_STEP == 0, ErrCode::InvalidLimitIncrement ); let economy = read_users_economy_config(&ctx.accounts.users_economy_config_pda)?; let (expected_pda, _) = find_user_pda(ctx.program_id, &args.login); require_keys_eq!( expected_pda, ctx.accounts.user_pda.key(), ErrCode::InvalidPdaAddress ); require!( ctx.accounts.user_pda.owner == ctx.program_id, ErrCode::InvalidPdaAddress ); let raw = safe_read_pda(&ctx.accounts.user_pda); require!(!raw.is_empty(), ErrCode::EmptyPdaData); let old_record = deserialize_record_from_pda(&raw)?; require!( old_record.login == args.login, ErrCode::ImmutableFieldChanged ); require!( old_record.created_at_ms == args.created_at_ms, ErrCode::ImmutableFieldChanged ); require_keys_eq!( old_record.root_key, args.root_key, ErrCode::ImmutableFieldChanged ); require!( old_record.root_key_status == KEY_STATUS_CREATED && old_record.blockchain_key_status == KEY_STATUS_CREATED && old_record.device_key_status == KEY_STATUS_CREATED, ErrCode::InvalidRecordData ); require!( args.version == old_record.version.saturating_add(1), ErrCode::InvalidVersion ); let expected_prev_hash = hash_unsigned_record(&old_record)?; let provided_prev_hash = vec_to_hash32(&args.prev_hash)?; require!( expected_prev_hash == provided_prev_hash, ErrCode::InvalidPrevHash ); let new_balance = old_record .balance .checked_add(args.additional_limit) .ok_or(error!(ErrCode::MathOverflow))?; require!(new_balance >= old_record.balance, ErrCode::BalanceDecrease); let mut new_record = UserRecord { created_at_ms: old_record.created_at_ms, updated_at_ms: args.updated_at_ms, version: args.version, prev_hash: provided_prev_hash, login: old_record.login.clone(), root_key_status: old_record.root_key_status, root_key: old_record.root_key, blockchain_key_status: old_record.blockchain_key_status, blockchain_key: args.fields.blockchain_key, device_key_status: old_record.device_key_status, device_key: args.fields.device_key, chain_number: args.fields.chain_number, balance: new_balance, is_server: args.fields.is_server, server_key: args.fields.server_key, server_address: args.fields.server_address.clone(), connection_servers: args.fields.connection_servers.clone(), trusted_count: args.fields.trusted_count, signature: [0; 64], }; let unsigned = serialize_unsigned_record(&new_record)?; new_record.signature = verify_record_signature( &ctx.accounts.instructions, &new_record.root_key, &args.signature, &unsigned, )?; let serialized = serialize_full_record(&new_record)?; require!( serialized.len() <= settings::USER_PDA_SPACE, ErrCode::RecordTooLarge ); let padded = pad_to_fixed_size(serialized, settings::USER_PDA_SPACE)?; write_to_pda(&ctx.accounts.user_pda, &padded)?; let topup_fee = limit_fee_lamports(args.additional_limit, economy.lamports_per_limit_step)?; if topup_fee > 0 { transfer_lamports( &ctx.accounts.signer, &ctx.accounts.inflow_vault, &ctx.accounts.system_program.to_account_info(), topup_fee, )?; } Ok(()) } fn serialize_unsigned_record(record: &UserRecord) -> Result> { let login_bytes = record.login.as_bytes(); require!(login_bytes.len() <= u8::MAX as usize, ErrCode::InvalidLogin); let server_address_bytes = record.server_address.as_bytes(); require!( server_address_bytes.len() <= u8::MAX as usize, ErrCode::InvalidRecordData ); require!( record.connection_servers.len() <= u8::MAX as usize, ErrCode::InvalidRecordData ); 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.version.to_le_bytes()); out.extend_from_slice(&record.prev_hash); out.push(login_bytes.len() as u8); out.extend_from_slice(login_bytes); out.push(record.root_key_status); out.extend_from_slice(record.root_key.as_ref()); out.push(record.blockchain_key_status); out.extend_from_slice(record.blockchain_key.as_ref()); out.push(record.device_key_status); out.extend_from_slice(record.device_key.as_ref()); out.extend_from_slice(&record.chain_number.to_le_bytes()); out.extend_from_slice(&record.balance.to_le_bytes()); out.push(if record.is_server { 1 } else { 0 }); if record.is_server { out.extend_from_slice(record.server_key.as_ref()); out.push(server_address_bytes.len() as u8); out.extend_from_slice(server_address_bytes); } out.push(record.connection_servers.len() as u8); for login in &record.connection_servers { let bytes = login.as_bytes(); require!(bytes.len() <= u8::MAX as usize, ErrCode::InvalidRecordData); out.push(bytes.len() as u8); out.extend_from_slice(bytes); } out.push(record.trusted_count); out.extend_from_slice(&RESERVED_BYTES); let record_len = out .len() .checked_add(64) .ok_or(error!(ErrCode::MathOverflow))?; require!(record_len <= u16::MAX as usize, ErrCode::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(record: &UserRecord) -> Result> { let mut out = serialize_unsigned_record(record)?; out.extend_from_slice(&record.signature); Ok(out) } fn deserialize_record_from_pda(raw: &[u8]) -> Result { require!(raw.len() >= 9, ErrCode::InvalidRecordData); require!(&raw[0..5] == MAGIC, ErrCode::InvalidRecordMagic); require!( raw[5] == FORMAT_MAJOR && raw[6] == FORMAT_MINOR, ErrCode::InvalidRecordFormat ); let record_len = u16::from_le_bytes([raw[7], raw[8]]) as usize; require!(record_len >= 9 + 64, ErrCode::InvalidRecordLength); require!(record_len <= raw.len(), ErrCode::InvalidRecordLength); let useful = &raw[..record_len]; let mut cursor = 9usize; let created_at_ms = read_u64(useful, &mut cursor)?; let updated_at_ms = read_u64(useful, &mut cursor)?; let version = read_u32(useful, &mut cursor)?; let prev_hash = read_fixed_32(useful, &mut cursor)?; let login = read_len_prefixed_string(useful, &mut cursor)?; let root_key_status = read_u8(useful, &mut cursor)?; let root_key = Pubkey::new_from_array(read_fixed_32(useful, &mut cursor)?); let blockchain_key_status = read_u8(useful, &mut cursor)?; let blockchain_key = Pubkey::new_from_array(read_fixed_32(useful, &mut cursor)?); let device_key_status = read_u8(useful, &mut cursor)?; let device_key = Pubkey::new_from_array(read_fixed_32(useful, &mut cursor)?); let chain_number = read_u16(useful, &mut cursor)?; let balance = read_u64(useful, &mut cursor)?; let is_server = read_u8(useful, &mut cursor)? == 1; let (server_key, server_address) = if is_server { ( Pubkey::new_from_array(read_fixed_32(useful, &mut cursor)?), read_len_prefixed_string(useful, &mut cursor)?, ) } else { (Pubkey::default(), String::new()) }; let connections_count = read_u8(useful, &mut cursor)? as usize; let mut connection_servers = Vec::with_capacity(connections_count); for _ in 0..connections_count { connection_servers.push(read_len_prefixed_string(useful, &mut cursor)?); } let trusted_count = read_u8(useful, &mut cursor)?; require!( useful.get(cursor..cursor + 5) == Some(&RESERVED_BYTES), ErrCode::InvalidRecordData ); cursor += 5; let signature = read_fixed_64(useful, &mut cursor)?; require!(cursor == useful.len(), ErrCode::InvalidRecordLength); Ok(UserRecord { created_at_ms, updated_at_ms, version, prev_hash, login, root_key_status, root_key, blockchain_key_status, blockchain_key, device_key_status, device_key, chain_number, balance, is_server, server_key, server_address, connection_servers, trusted_count, signature, }) } fn hash_unsigned_record(record: &UserRecord) -> Result<[u8; 32]> { let unsigned = serialize_unsigned_record(record)?; let digest = hashv(&[&unsigned]); let mut out = [0u8; 32]; out.copy_from_slice(digest.as_ref()); Ok(out) } fn verify_record_signature( instructions_sysvar: &AccountInfo, root_key: &Pubkey, signature: &[u8], unsigned: &[u8], ) -> Result<[u8; 64]> { require_keys_eq!( *instructions_sysvar.key, anchor_lang::solana_program::sysvar::instructions::id(), ErrCode::InvalidSignature ); let provided_sig = vec_to_signature(signature)?; let msg_hash = hashv(&[unsigned]); let current_ix_index = load_current_index_checked(instructions_sysvar) .map_err(|_| error!(ErrCode::InvalidSignature))?; require!(current_ix_index > 0, ErrCode::InvalidSignature); let ed_ix = load_instruction_at_checked((current_ix_index - 1) as usize, instructions_sysvar) .map_err(|_| error!(ErrCode::InvalidSignature))?; let parsed = parse_ed25519_ix(&ed_ix)?; require_keys_eq!(parsed.pubkey, *root_key, ErrCode::InvalidSignature); require!( parsed.message == msg_hash.as_ref(), ErrCode::InvalidSignature ); require!(parsed.signature == provided_sig, ErrCode::InvalidSignature); Ok(parsed.signature) } struct ParsedEd25519 { pub pubkey: Pubkey, pub signature: [u8; 64], pub message: Vec, } fn parse_ed25519_ix(ix: &Instruction) -> Result { require_keys_eq!( ix.program_id, ed25519_program::id(), ErrCode::InvalidSignature ); let data = &ix.data; require!(data.len() >= 16, ErrCode::InvalidSignature); require!(data[0] == 1, ErrCode::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, ErrCode::InvalidSignature); require!(pubkey_ix_index == u16::MAX, ErrCode::InvalidSignature); require!(message_ix_index == u16::MAX, ErrCode::InvalidSignature); let signature_end = signature_offset .checked_add(64) .ok_or(error!(ErrCode::InvalidSignature))?; let pubkey_end = pubkey_offset .checked_add(32) .ok_or(error!(ErrCode::InvalidSignature))?; let message_end = message_offset .checked_add(message_size) .ok_or(error!(ErrCode::InvalidSignature))?; let signature_slice = data .get(signature_offset..signature_end) .ok_or(error!(ErrCode::InvalidSignature))?; let pubkey_slice = data .get(pubkey_offset..pubkey_end) .ok_or(error!(ErrCode::InvalidSignature))?; let message = data .get(message_offset..message_end) .ok_or(error!(ErrCode::InvalidSignature))? .to_vec(); 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(|_| error!(ErrCode::InvalidSignature))?, ); Ok(ParsedEd25519 { pubkey, signature, message, }) } fn le_u16(data: &[u8], offset: usize) -> Result { let end = offset .checked_add(2) .ok_or(error!(ErrCode::InvalidSignature))?; let s = data .get(offset..end) .ok_or(error!(ErrCode::InvalidSignature))?; Ok(u16::from_le_bytes([s[0], s[1]])) } fn validate_login(login: &str) -> Result<()> { require!(!login.is_empty(), ErrCode::InvalidLogin); require!(login.len() <= 25, ErrCode::InvalidLogin); for ch in login.chars() { if !(ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_') { return Err(error!(ErrCode::InvalidLogin)); } } Ok(()) } fn validate_fields(fields: &UserMutableFields) -> Result<()> { if fields.is_server { require!( !fields.server_address.is_empty(), ErrCode::InvalidRecordData ); require!( fields.server_address.as_bytes().len() <= u8::MAX as usize, ErrCode::InvalidRecordData ); } else { require!(fields.server_address.is_empty(), ErrCode::InvalidRecordData); } require!( fields.connection_servers.len() <= u8::MAX as usize, ErrCode::InvalidRecordData ); for login in &fields.connection_servers { require!(!login.is_empty(), ErrCode::InvalidRecordData); require!( login.as_bytes().len() <= u8::MAX as usize, ErrCode::InvalidRecordData ); } Ok(()) } fn validate_inflow_vault(inflow_vault: &AccountInfo) -> Result<()> { let payments_program_id = Pubkey::from_str(settings::SHINE_PAYMENTS_PROGRAM_ID) .map_err(|_| error!(ErrCode::InvalidFeeReceiver))?; let (expected, _) = Pubkey::find_program_address( &[settings::SHINE_PAYMENTS_INFLOW_VAULT_SEED], &payments_program_id, ); require_keys_eq!(expected, *inflow_vault.key, ErrCode::InvalidFeeReceiver); Ok(()) } fn transfer_lamports<'info>( payer: &AccountInfo<'info>, recipient: &AccountInfo<'info>, system_program: &AccountInfo<'info>, lamports: u64, ) -> Result<()> { if lamports == 0 { return Ok(()); } let ix = system_instruction::transfer(payer.key, recipient.key, lamports); invoke( &ix, &[payer.clone(), recipient.clone(), system_program.clone()], )?; Ok(()) } fn limit_fee_lamports(limit_delta: u64, lamports_per_limit_step: u64) -> Result { let units = limit_delta / settings::LIMIT_STEP; units .checked_mul(lamports_per_limit_step) .ok_or(error!(ErrCode::MathOverflow)) } 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 read_users_economy_config(pda: &AccountInfo) -> Result { let raw = safe_read_pda(pda); require!(!raw.is_empty(), ErrCode::EmptyPdaData); let mut slice: &[u8] = &raw; UsersEconomyConfigState::deserialize(&mut slice) .map_err(|_| error!(ErrCode::DeserializationError)) } fn pad_to_fixed_size(mut bytes: Vec, target_size: usize) -> Result> { require!(bytes.len() <= target_size, ErrCode::RecordTooLarge); bytes.resize(target_size, 0); Ok(bytes) } fn vec_to_signature(input: &[u8]) -> Result<[u8; 64]> { require!(input.len() == 64, ErrCode::InvalidSignature); let mut out = [0u8; 64]; out.copy_from_slice(input); Ok(out) } fn vec_to_hash32(input: &[u8]) -> Result<[u8; 32]> { require!(input.len() == 32, ErrCode::InvalidPrevHash); let mut out = [0u8; 32]; out.copy_from_slice(input); Ok(out) } fn read_u8(data: &[u8], cursor: &mut usize) -> Result { let v = *data .get(*cursor) .ok_or(error!(ErrCode::InvalidRecordData))?; *cursor += 1; Ok(v) } fn read_u16(data: &[u8], cursor: &mut usize) -> Result { let end = cursor .checked_add(2) .ok_or(error!(ErrCode::InvalidRecordData))?; let slice = data .get(*cursor..end) .ok_or(error!(ErrCode::InvalidRecordData))?; *cursor = end; Ok(u16::from_le_bytes([slice[0], slice[1]])) } fn read_u32(data: &[u8], cursor: &mut usize) -> Result { let end = cursor .checked_add(4) .ok_or(error!(ErrCode::InvalidRecordData))?; let slice = data .get(*cursor..end) .ok_or(error!(ErrCode::InvalidRecordData))?; *cursor = end; Ok(u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]])) } fn read_u64(data: &[u8], cursor: &mut usize) -> Result { let end = cursor .checked_add(8) .ok_or(error!(ErrCode::InvalidRecordData))?; let slice = data .get(*cursor..end) .ok_or(error!(ErrCode::InvalidRecordData))?; *cursor = end; Ok(u64::from_le_bytes([ slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], slice[6], slice[7], ])) } fn read_fixed_32(data: &[u8], cursor: &mut usize) -> Result<[u8; 32]> { let end = cursor .checked_add(32) .ok_or(error!(ErrCode::InvalidRecordData))?; let slice = data .get(*cursor..end) .ok_or(error!(ErrCode::InvalidRecordData))?; *cursor = end; let mut out = [0u8; 32]; out.copy_from_slice(slice); Ok(out) } fn read_fixed_64(data: &[u8], cursor: &mut usize) -> Result<[u8; 64]> { let end = cursor .checked_add(64) .ok_or(error!(ErrCode::InvalidRecordData))?; let slice = data .get(*cursor..end) .ok_or(error!(ErrCode::InvalidRecordData))?; *cursor = end; let mut out = [0u8; 64]; out.copy_from_slice(slice); Ok(out) } fn read_len_prefixed_string(data: &[u8], cursor: &mut usize) -> Result { let len = read_u8(data, cursor)? as usize; let end = cursor .checked_add(len) .ok_or(error!(ErrCode::InvalidRecordData))?; let slice = data .get(*cursor..end) .ok_or(error!(ErrCode::InvalidRecordData))?; *cursor = end; let value = std::str::from_utf8(slice).map_err(|_| error!(ErrCode::InvalidRecordData))?; Ok(value.to_string()) }