use solana_pubkey::Pubkey;
use crate::ix::constants::{
PHOENIX_GLOBAL_CONFIGURATION, PHOENIX_LOG_AUTHORITY, PHOENIX_PROGRAM_ID, SYSTEM_PROGRAM_ID,
register_trader_discriminant,
};
use crate::ix::error::PhoenixIxError;
use crate::ix::types::{AccountMeta, Instruction};
#[derive(Debug, Clone)]
pub struct RegisterTraderParams {
payer: Pubkey,
trader: Pubkey,
trader_account: Pubkey,
max_positions: u64,
trader_pda_index: u8,
subaccount_index: u8,
}
impl RegisterTraderParams {
pub fn builder() -> RegisterTraderParamsBuilder {
RegisterTraderParamsBuilder::new()
}
pub fn payer(&self) -> Pubkey {
self.payer
}
pub fn trader(&self) -> Pubkey {
self.trader
}
pub fn trader_account(&self) -> Pubkey {
self.trader_account
}
pub fn max_positions(&self) -> u64 {
self.max_positions
}
pub fn trader_pda_index(&self) -> u8 {
self.trader_pda_index
}
pub fn subaccount_index(&self) -> u8 {
self.subaccount_index
}
}
#[derive(Default)]
pub struct RegisterTraderParamsBuilder {
payer: Option<Pubkey>,
trader: Option<Pubkey>,
trader_account: Option<Pubkey>,
max_positions: Option<u64>,
trader_pda_index: Option<u8>,
subaccount_index: Option<u8>,
}
impl RegisterTraderParamsBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn payer(mut self, payer: Pubkey) -> Self {
self.payer = Some(payer);
self
}
pub fn trader(mut self, trader: Pubkey) -> Self {
self.trader = Some(trader);
self
}
pub fn trader_account(mut self, trader_account: Pubkey) -> Self {
self.trader_account = Some(trader_account);
self
}
pub fn max_positions(mut self, max_positions: u64) -> Self {
self.max_positions = Some(max_positions);
self
}
pub fn trader_pda_index(mut self, trader_pda_index: u8) -> Self {
self.trader_pda_index = Some(trader_pda_index);
self
}
pub fn subaccount_index(mut self, subaccount_index: u8) -> Self {
self.subaccount_index = Some(subaccount_index);
self
}
pub fn build(self) -> Result<RegisterTraderParams, PhoenixIxError> {
Ok(RegisterTraderParams {
payer: self.payer.ok_or(PhoenixIxError::MissingField("payer"))?,
trader: self.trader.ok_or(PhoenixIxError::MissingField("trader"))?,
trader_account: self
.trader_account
.ok_or(PhoenixIxError::MissingField("trader_account"))?,
max_positions: self
.max_positions
.ok_or(PhoenixIxError::MissingField("max_positions"))?,
trader_pda_index: self
.trader_pda_index
.ok_or(PhoenixIxError::MissingField("trader_pda_index"))?,
subaccount_index: self
.subaccount_index
.ok_or(PhoenixIxError::MissingField("subaccount_index"))?,
})
}
}
pub fn create_register_trader_ix(
params: RegisterTraderParams,
) -> Result<Instruction, PhoenixIxError> {
validate(¶ms)?;
let data = encode_register_trader(¶ms);
let accounts = build_accounts(¶ms);
Ok(Instruction {
program_id: PHOENIX_PROGRAM_ID,
accounts,
data,
})
}
fn validate(params: &RegisterTraderParams) -> Result<(), PhoenixIxError> {
if params.max_positions() == 0 || params.max_positions() > 128 {
return Err(PhoenixIxError::MissingField(
"max_positions must be between 1 and 128",
));
}
if params.subaccount_index() > 100 {
return Err(PhoenixIxError::InvalidSubaccountIndex);
}
Ok(())
}
fn encode_register_trader(params: &RegisterTraderParams) -> Vec<u8> {
let mut data = Vec::with_capacity(18);
data.extend_from_slice(®ister_trader_discriminant());
data.extend_from_slice(¶ms.max_positions().to_le_bytes());
data.push(params.trader_pda_index());
data.push(params.subaccount_index());
data
}
fn build_accounts(params: &RegisterTraderParams) -> Vec<AccountMeta> {
vec![
AccountMeta::readonly(PHOENIX_PROGRAM_ID),
AccountMeta::readonly(PHOENIX_LOG_AUTHORITY),
AccountMeta::readonly(PHOENIX_GLOBAL_CONFIGURATION),
AccountMeta::writable_signer(params.payer()),
AccountMeta::readonly(params.trader()),
AccountMeta::writable(params.trader_account()),
AccountMeta::readonly(SYSTEM_PROGRAM_ID),
]
}
#[cfg(test)]
mod tests {
use super::*;
fn build_cross_margin_params() -> RegisterTraderParams {
RegisterTraderParams::builder()
.payer(Pubkey::new_unique())
.trader(Pubkey::new_unique())
.trader_account(Pubkey::new_unique())
.max_positions(128)
.trader_pda_index(0)
.subaccount_index(0)
.build()
.unwrap()
}
fn build_isolated_margin_params() -> RegisterTraderParams {
RegisterTraderParams::builder()
.payer(Pubkey::new_unique())
.trader(Pubkey::new_unique())
.trader_account(Pubkey::new_unique())
.max_positions(1)
.trader_pda_index(0)
.subaccount_index(1)
.build()
.unwrap()
}
#[test]
fn test_create_register_trader_ix_cross_margin() {
let params = build_cross_margin_params();
let ix = create_register_trader_ix(params).unwrap();
assert_eq!(ix.program_id, PHOENIX_PROGRAM_ID);
assert_eq!(ix.accounts.len(), 7);
assert_eq!(&ix.data[..8], ®ister_trader_discriminant());
}
#[test]
fn test_create_register_trader_ix_isolated_margin() {
let params = build_isolated_margin_params();
let ix = create_register_trader_ix(params).unwrap();
assert_eq!(ix.program_id, PHOENIX_PROGRAM_ID);
assert_eq!(ix.accounts.len(), 7);
assert_eq!(&ix.data[..8], ®ister_trader_discriminant());
}
#[test]
fn test_register_trader_data_encoding() {
let params = RegisterTraderParams::builder()
.payer(Pubkey::new_unique())
.trader(Pubkey::new_unique())
.trader_account(Pubkey::new_unique())
.max_positions(128)
.trader_pda_index(0)
.subaccount_index(5)
.build()
.unwrap();
let ix = create_register_trader_ix(params).unwrap();
assert_eq!(ix.data.len(), 18);
assert_eq!(&ix.data[8..16], &128u64.to_le_bytes());
assert_eq!(ix.data[16], 0);
assert_eq!(ix.data[17], 5);
}
#[test]
fn test_register_trader_account_order() {
let payer = Pubkey::new_unique();
let trader = Pubkey::new_unique();
let trader_account = Pubkey::new_unique();
let params = RegisterTraderParams::builder()
.payer(payer)
.trader(trader)
.trader_account(trader_account)
.max_positions(128)
.trader_pda_index(0)
.subaccount_index(0)
.build()
.unwrap();
let ix = create_register_trader_ix(params).unwrap();
assert_eq!(ix.accounts[0].pubkey, PHOENIX_PROGRAM_ID);
assert!(!ix.accounts[0].is_signer);
assert!(!ix.accounts[0].is_writable);
assert_eq!(ix.accounts[1].pubkey, PHOENIX_LOG_AUTHORITY);
assert!(!ix.accounts[1].is_signer);
assert!(!ix.accounts[1].is_writable);
assert_eq!(ix.accounts[2].pubkey, PHOENIX_GLOBAL_CONFIGURATION);
assert!(!ix.accounts[2].is_signer);
assert!(!ix.accounts[2].is_writable);
assert_eq!(ix.accounts[3].pubkey, payer);
assert!(ix.accounts[3].is_signer);
assert!(ix.accounts[3].is_writable);
assert_eq!(ix.accounts[4].pubkey, trader);
assert!(!ix.accounts[4].is_signer);
assert!(!ix.accounts[4].is_writable);
assert_eq!(ix.accounts[5].pubkey, trader_account);
assert!(!ix.accounts[5].is_signer);
assert!(ix.accounts[5].is_writable);
assert_eq!(ix.accounts[6].pubkey, SYSTEM_PROGRAM_ID);
assert!(!ix.accounts[6].is_signer);
assert!(!ix.accounts[6].is_writable);
}
#[test]
fn test_builder_missing_required_field() {
let result = RegisterTraderParams::builder()
.payer(Pubkey::new_unique())
.build();
assert!(matches!(result, Err(PhoenixIxError::MissingField(_))));
}
#[test]
fn test_invalid_subaccount_index() {
let params = RegisterTraderParams::builder()
.payer(Pubkey::new_unique())
.trader(Pubkey::new_unique())
.trader_account(Pubkey::new_unique())
.max_positions(1)
.trader_pda_index(0)
.subaccount_index(101)
.build()
.unwrap();
let result = create_register_trader_ix(params);
assert!(matches!(
result,
Err(PhoenixIxError::InvalidSubaccountIndex)
));
}
#[test]
fn test_invalid_max_positions_zero() {
let params = RegisterTraderParams::builder()
.payer(Pubkey::new_unique())
.trader(Pubkey::new_unique())
.trader_account(Pubkey::new_unique())
.max_positions(0)
.trader_pda_index(0)
.subaccount_index(0)
.build()
.unwrap();
let result = create_register_trader_ix(params);
assert!(result.is_err());
}
#[test]
fn test_invalid_max_positions_too_large() {
let params = RegisterTraderParams::builder()
.payer(Pubkey::new_unique())
.trader(Pubkey::new_unique())
.trader_account(Pubkey::new_unique())
.max_positions(129)
.trader_pda_index(0)
.subaccount_index(0)
.build()
.unwrap();
let result = create_register_trader_ix(params);
assert!(result.is_err());
}
}