use solana_pubkey::Pubkey;
use crate::ix::constants::{EMBER_PROGRAM_ID, SPL_TOKEN_PROGRAM_ID, ember_withdraw_discriminant};
use crate::ix::error::PhoenixIxError;
use crate::ix::types::{AccountMeta, Instruction};
use crate::ix::{get_ember_state_address, get_ember_vault_address};
#[derive(Debug, Clone)]
pub struct EmberWithdrawParams {
trader: Pubkey,
usdc_mint: Pubkey,
canonical_mint: Pubkey,
trader_usdc_account: Pubkey,
trader_phoenix_account: Pubkey,
amount: Option<u64>,
}
impl EmberWithdrawParams {
pub fn builder() -> EmberWithdrawParamsBuilder {
EmberWithdrawParamsBuilder::new()
}
pub fn trader(&self) -> Pubkey {
self.trader
}
pub fn usdc_mint(&self) -> Pubkey {
self.usdc_mint
}
pub fn canonical_mint(&self) -> Pubkey {
self.canonical_mint
}
pub fn trader_usdc_account(&self) -> Pubkey {
self.trader_usdc_account
}
pub fn trader_phoenix_account(&self) -> Pubkey {
self.trader_phoenix_account
}
pub fn amount(&self) -> Option<u64> {
self.amount
}
}
#[derive(Default)]
pub struct EmberWithdrawParamsBuilder {
trader: Option<Pubkey>,
usdc_mint: Option<Pubkey>,
canonical_mint: Option<Pubkey>,
trader_usdc_account: Option<Pubkey>,
trader_phoenix_account: Option<Pubkey>,
amount: Option<Option<u64>>,
}
impl EmberWithdrawParamsBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn trader(mut self, trader: Pubkey) -> Self {
self.trader = Some(trader);
self
}
pub fn usdc_mint(mut self, usdc_mint: Pubkey) -> Self {
self.usdc_mint = Some(usdc_mint);
self
}
pub fn canonical_mint(mut self, canonical_mint: Pubkey) -> Self {
self.canonical_mint = Some(canonical_mint);
self
}
pub fn trader_usdc_account(mut self, trader_usdc_account: Pubkey) -> Self {
self.trader_usdc_account = Some(trader_usdc_account);
self
}
pub fn trader_phoenix_account(mut self, trader_phoenix_account: Pubkey) -> Self {
self.trader_phoenix_account = Some(trader_phoenix_account);
self
}
pub fn amount(mut self, amount: Option<u64>) -> Self {
self.amount = Some(amount);
self
}
pub fn build(self) -> Result<EmberWithdrawParams, PhoenixIxError> {
let amount = self.amount.ok_or(PhoenixIxError::MissingField("amount"))?;
if let Some(amt) = amount {
if amt == 0 {
return Err(PhoenixIxError::InvalidWithdrawAmount);
}
}
Ok(EmberWithdrawParams {
trader: self.trader.ok_or(PhoenixIxError::MissingField("trader"))?,
usdc_mint: self
.usdc_mint
.ok_or(PhoenixIxError::MissingField("usdc_mint"))?,
canonical_mint: self
.canonical_mint
.ok_or(PhoenixIxError::MissingField("canonical_mint"))?,
trader_usdc_account: self
.trader_usdc_account
.ok_or(PhoenixIxError::MissingField("trader_usdc_account"))?,
trader_phoenix_account: self
.trader_phoenix_account
.ok_or(PhoenixIxError::MissingField("trader_phoenix_account"))?,
amount,
})
}
}
pub fn create_ember_withdraw_ix(
params: EmberWithdrawParams,
) -> Result<Instruction, PhoenixIxError> {
let data = encode_ember_withdraw(¶ms);
let accounts = build_accounts(¶ms);
Ok(Instruction {
program_id: EMBER_PROGRAM_ID,
accounts,
data,
})
}
fn encode_ember_withdraw(params: &EmberWithdrawParams) -> Vec<u8> {
let mut data = Vec::with_capacity(17);
data.extend_from_slice(&ember_withdraw_discriminant());
match params.amount() {
Some(amount) => {
data.push(1); data.extend_from_slice(&amount.to_le_bytes());
}
None => {
data.push(0); }
}
data
}
fn build_accounts(params: &EmberWithdrawParams) -> Vec<AccountMeta> {
vec![
AccountMeta::readonly_signer(params.trader()),
AccountMeta::readonly(get_ember_state_address()),
AccountMeta::readonly(params.usdc_mint()),
AccountMeta::writable(params.canonical_mint()),
AccountMeta::writable(params.trader_usdc_account()),
AccountMeta::writable(params.trader_phoenix_account()),
AccountMeta::writable(get_ember_vault_address()),
AccountMeta::readonly(SPL_TOKEN_PROGRAM_ID),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_ember_withdraw_ix_with_amount() {
let params = EmberWithdrawParams::builder()
.trader(Pubkey::new_unique())
.usdc_mint(Pubkey::new_unique())
.canonical_mint(Pubkey::new_unique())
.trader_usdc_account(Pubkey::new_unique())
.trader_phoenix_account(Pubkey::new_unique())
.amount(Some(100_000_000))
.build()
.unwrap();
let ix = create_ember_withdraw_ix(params).unwrap();
assert_eq!(ix.program_id, EMBER_PROGRAM_ID);
assert_eq!(ix.accounts.len(), 8);
assert_eq!(&ix.data[..8], &ember_withdraw_discriminant());
assert_eq!(ix.data[8], 1); let amount_bytes: [u8; 8] = ix.data[9..17].try_into().unwrap();
assert_eq!(u64::from_le_bytes(amount_bytes), 100_000_000);
}
#[test]
fn test_create_ember_withdraw_ix_full_withdrawal() {
let params = EmberWithdrawParams::builder()
.trader(Pubkey::new_unique())
.usdc_mint(Pubkey::new_unique())
.canonical_mint(Pubkey::new_unique())
.trader_usdc_account(Pubkey::new_unique())
.trader_phoenix_account(Pubkey::new_unique())
.amount(None) .build()
.unwrap();
let ix = create_ember_withdraw_ix(params).unwrap();
assert_eq!(&ix.data[..8], &ember_withdraw_discriminant());
assert_eq!(ix.data[8], 0); assert_eq!(ix.data.len(), 9); }
#[test]
fn test_ember_withdraw_missing_amount() {
let result = EmberWithdrawParams::builder()
.trader(Pubkey::new_unique())
.usdc_mint(Pubkey::new_unique())
.canonical_mint(Pubkey::new_unique())
.trader_usdc_account(Pubkey::new_unique())
.trader_phoenix_account(Pubkey::new_unique())
.build();
assert!(matches!(
result,
Err(PhoenixIxError::MissingField("amount"))
));
}
#[test]
fn test_ember_withdraw_zero_amount() {
let result = EmberWithdrawParams::builder()
.trader(Pubkey::new_unique())
.usdc_mint(Pubkey::new_unique())
.canonical_mint(Pubkey::new_unique())
.trader_usdc_account(Pubkey::new_unique())
.trader_phoenix_account(Pubkey::new_unique())
.amount(Some(0))
.build();
assert!(matches!(result, Err(PhoenixIxError::InvalidWithdrawAmount)));
}
#[test]
fn test_ember_withdraw_account_order() {
let trader = Pubkey::new_unique();
let usdc_mint = Pubkey::new_unique();
let canonical_mint = Pubkey::new_unique();
let trader_usdc = Pubkey::new_unique();
let trader_phoenix = Pubkey::new_unique();
let params = EmberWithdrawParams::builder()
.trader(trader)
.usdc_mint(usdc_mint)
.canonical_mint(canonical_mint)
.trader_usdc_account(trader_usdc)
.trader_phoenix_account(trader_phoenix)
.amount(Some(1))
.build()
.unwrap();
let ix = create_ember_withdraw_ix(params).unwrap();
assert_eq!(ix.accounts[0].pubkey, trader);
assert!(ix.accounts[0].is_signer);
assert!(!ix.accounts[0].is_writable);
assert_eq!(ix.accounts[1].pubkey, get_ember_state_address());
assert!(!ix.accounts[1].is_signer);
assert!(!ix.accounts[1].is_writable);
assert_eq!(ix.accounts[2].pubkey, usdc_mint);
assert!(!ix.accounts[2].is_writable);
assert_eq!(ix.accounts[3].pubkey, canonical_mint);
assert!(ix.accounts[3].is_writable);
assert_eq!(ix.accounts[4].pubkey, trader_usdc);
assert!(ix.accounts[4].is_writable);
assert_eq!(ix.accounts[5].pubkey, trader_phoenix);
assert!(ix.accounts[5].is_writable);
assert_eq!(ix.accounts[6].pubkey, get_ember_vault_address());
assert!(ix.accounts[6].is_writable);
assert_eq!(ix.accounts[7].pubkey, SPL_TOKEN_PROGRAM_ID);
assert!(!ix.accounts[7].is_writable);
}
}