Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
watch_file flake.nix
watch_file flake.lock
use flake path:.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.surfpool/
.zed/
.direnv/
target/
runbooks/
txtx.yml
Binary file added cfg/programs/alphaq.so
Binary file not shown.
1 change: 1 addition & 0 deletions crates/router-client/src/generated/types/dex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ pub enum Dex {
Tessera,
GoonFi,
BisonFi,
AlphaQ,
}
246 changes: 246 additions & 0 deletions crates/router/src/adapters/alphaq.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
use anchor_lang::{prelude::*, solana_program::instruction::Instruction};
use anchor_spl::token_interface::{TokenAccount, TokenInterface};
use arrayref::array_ref;
use magnus_shared::pmm_alphaq::{self, ACCOUNTS_LEN, ARGS_LEN};

use super::common::DexProcessor;
use crate::{
adapters::common::{before_check, invoke_process},
error::ErrorCode,
HopAccounts, ALPHAQ_SWAP_SELECTOR, ZERO_ADDRESS,
};

pub struct AlphaqProcessor;
impl DexProcessor for AlphaqProcessor {}

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum Direction {
QuoteToBase,
BaseToQuote,
}

pub struct AlphaqAccounts<'info> {
pub dex_program_id: &'info AccountInfo<'info>,
pub swap_authority: &'info AccountInfo<'info>,
pub market: &'info AccountInfo<'info>,
pub market_param: &'info AccountInfo<'info>,
pub user_base_ta: InterfaceAccount<'info, TokenAccount>,
pub user_quote_ta: InterfaceAccount<'info, TokenAccount>,
pub market_base_ta: InterfaceAccount<'info, TokenAccount>,
pub market_quote_ta: InterfaceAccount<'info, TokenAccount>,
pub market_base_aux: &'info AccountInfo<'info>,
pub market_quote_aux: &'info AccountInfo<'info>,
pub market_quote_aux_2: &'info AccountInfo<'info>,
pub token_program: Interface<'info, TokenInterface>,
pub sysvar_instructions: &'info AccountInfo<'info>,
}

impl<'info> AlphaqAccounts<'info> {
fn parse_accounts(accounts: &'info [AccountInfo<'info>], offset: usize) -> Result<Self> {
let [
dex_program_id,
swap_authority,
market,
market_param,
user_base_ta,
user_quote_ta,
market_base_ta,
market_quote_ta,
market_base_aux,
market_quote_aux,
market_quote_aux_2,
token_program,
sysvar_instructions,
]: &[AccountInfo<'info>; ACCOUNTS_LEN] = array_ref![accounts, offset, ACCOUNTS_LEN];

Ok(Self {
dex_program_id,
swap_authority,
market,
market_param,
user_base_ta: InterfaceAccount::try_from(user_base_ta)?,
user_quote_ta: InterfaceAccount::try_from(user_quote_ta)?,
market_base_ta: InterfaceAccount::try_from(market_base_ta)?,
market_quote_ta: InterfaceAccount::try_from(market_quote_ta)?,
market_base_aux,
market_quote_aux,
market_quote_aux_2,
token_program: Interface::try_from(token_program)?,
sysvar_instructions,
})
}
}

fn infer_direction(accounts: &AlphaqAccounts, amount_in: u64, hop_accounts: &HopAccounts) -> Result<Direction> {
if hop_accounts.from_account != ZERO_ADDRESS {
if hop_accounts.from_account == accounts.user_base_ta.key() {
return Ok(Direction::BaseToQuote);
}
if hop_accounts.from_account == accounts.user_quote_ta.key() {
return Ok(Direction::QuoteToBase);
}
}

if hop_accounts.last_to_account != ZERO_ADDRESS {
if hop_accounts.last_to_account == accounts.user_base_ta.key() {
return Ok(Direction::BaseToQuote);
}
if hop_accounts.last_to_account == accounts.user_quote_ta.key() {
return Ok(Direction::QuoteToBase);
}
}

if hop_accounts.to_account != ZERO_ADDRESS {
if hop_accounts.to_account == accounts.user_quote_ta.key() {
return Ok(Direction::BaseToQuote);
}
if hop_accounts.to_account == accounts.user_base_ta.key() {
return Ok(Direction::QuoteToBase);
}
}

let base_can_fund = accounts.user_base_ta.amount >= amount_in;
let quote_can_fund = accounts.user_quote_ta.amount >= amount_in;
match (base_can_fund, quote_can_fund) {
(true, false) => Ok(Direction::BaseToQuote),
(false, true) => Ok(Direction::QuoteToBase),
// Fall back to base->quote when both can fund (ambiguous without route-level mint context).
(true, true) => Ok(Direction::BaseToQuote),
(false, false) => Err(ErrorCode::InvalidTokenAccount.into()),
}
}

fn build_swap_data(direction: Direction, amount_in: u64, amount_out_min: u64) -> Vec<u8> {
let side = match direction {
Direction::QuoteToBase => 0u8,
Direction::BaseToQuote => 1u8,
};

let mut data = Vec::with_capacity(ARGS_LEN);
data.extend_from_slice(&[ALPHAQ_SWAP_SELECTOR, side]);
data.extend_from_slice(&amount_in.to_le_bytes());
data.extend_from_slice(&amount_out_min.to_le_bytes());
data
}

pub fn swap<'a>(
remaining_accounts: &'a [AccountInfo<'a>],
amount_in: u64,
offset: &mut usize,
hop_accounts: &mut HopAccounts,
hop: usize,
proxy_swap: bool,
owner_seeds: Option<&[&[&[u8]]]>,
) -> Result<u64> {
msg!("Dex::AlphaQ amount_in: {}, offset: {}", amount_in, offset);

require!(remaining_accounts.len() >= *offset + ACCOUNTS_LEN, ErrorCode::InvalidAccountsLength);

let mut swap_accounts = AlphaqAccounts::parse_accounts(remaining_accounts, *offset)?;
if swap_accounts.dex_program_id.key != &pmm_alphaq::id() {
return Err(ErrorCode::InvalidProgramId.into());
}

swap_accounts.market.key().log();

let direction = infer_direction(&swap_accounts, amount_in, hop_accounts)?;
let (swap_source_key, swap_destination_key) = match direction {
Direction::BaseToQuote => (swap_accounts.user_base_ta.key(), swap_accounts.user_quote_ta.key()),
Direction::QuoteToBase => (swap_accounts.user_quote_ta.key(), swap_accounts.user_base_ta.key()),
};
let swap_source_ta_ref = match direction {
Direction::BaseToQuote => &swap_accounts.user_base_ta,
Direction::QuoteToBase => &swap_accounts.user_quote_ta,
};

before_check(swap_accounts.swap_authority, swap_source_ta_ref, swap_destination_key, hop_accounts, hop, proxy_swap, owner_seeds)?;

let data = build_swap_data(direction, amount_in, 0);
require!(data.len() == ARGS_LEN, ErrorCode::InvalidBundleInput);

let accounts = vec![
AccountMeta::new_readonly(swap_accounts.swap_authority.key(), true),
AccountMeta::new_readonly(swap_accounts.market.key(), false),
AccountMeta::new(swap_accounts.market_param.key(), false),
AccountMeta::new(swap_accounts.user_base_ta.key(), false),
AccountMeta::new(swap_accounts.user_quote_ta.key(), false),
AccountMeta::new(swap_accounts.market_base_ta.key(), false),
AccountMeta::new(swap_accounts.market_quote_ta.key(), false),
AccountMeta::new_readonly(swap_accounts.market_base_aux.key(), false),
AccountMeta::new_readonly(swap_accounts.market_quote_aux.key(), false),
AccountMeta::new_readonly(swap_accounts.market_quote_aux_2.key(), false),
AccountMeta::new_readonly(swap_accounts.token_program.key(), false),
AccountMeta::new_readonly(swap_accounts.sysvar_instructions.key(), false),
];

let account_infos = vec![
swap_accounts.swap_authority.to_account_info(),
swap_accounts.market.to_account_info(),
swap_accounts.market_param.to_account_info(),
swap_accounts.user_base_ta.to_account_info(),
swap_accounts.user_quote_ta.to_account_info(),
swap_accounts.market_base_ta.to_account_info(),
swap_accounts.market_quote_ta.to_account_info(),
swap_accounts.market_base_aux.to_account_info(),
swap_accounts.market_quote_aux.to_account_info(),
swap_accounts.market_quote_aux_2.to_account_info(),
swap_accounts.token_program.to_account_info(),
swap_accounts.sysvar_instructions.to_account_info(),
];

let instruction = Instruction { program_id: swap_accounts.dex_program_id.key(), accounts, data };
let dex_processor = &AlphaqProcessor;

let amount_out = match direction {
Direction::BaseToQuote => invoke_process(
amount_in,
dex_processor,
&account_infos,
&mut swap_accounts.user_base_ta,
&mut swap_accounts.user_quote_ta,
hop_accounts,
instruction,
hop,
offset,
ACCOUNTS_LEN,
proxy_swap,
owner_seeds,
)?,
Direction::QuoteToBase => invoke_process(
amount_in,
dex_processor,
&account_infos,
&mut swap_accounts.user_quote_ta,
&mut swap_accounts.user_base_ta,
hop_accounts,
instruction,
hop,
offset,
ACCOUNTS_LEN,
proxy_swap,
owner_seeds,
)?,
};

// Keep the explicit association visible in logs for debugging ambiguous routes.
msg!("AlphaQ direction: {:?}, source: {}, destination: {}", direction, swap_source_key, swap_destination_key);

Ok(amount_out)
}

#[cfg(test)]
mod tests {
use super::{build_swap_data, Direction};

#[test]
fn alphaq_swap_data_layout_matches_sample_quote_to_base() {
let data = build_swap_data(Direction::QuoteToBase, 511_933_129, 0);
assert_eq!(data, vec![0x0c, 0x00, 0xc9, 0x7a, 0x83, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
}

#[test]
fn alphaq_swap_data_layout_matches_sample_base_to_quote() {
let data = build_swap_data(Direction::BaseToQuote, 6_039_648_624, 0);
assert_eq!(data, vec![0x0c, 0x01, 0x70, 0xb9, 0xfd, 0x67, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
}
}
1 change: 1 addition & 0 deletions crates/router/src/adapters/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod alphaq;
pub mod bisonfi;
pub mod common;
pub mod goonfi;
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub const CPSWAP_SELECTOR: &[u8; 8] = &[143, 190, 90, 218, 196, 30, 51, 222];
pub const TESSERA_SWAP_SELECTOR: &[u8; 1] = &[16];
pub const GOONFI_SWAP_SELECTOR: &[u8; 1] = &[2];
pub const BISONFI_SWAP_SELECTOR: u8 = 0x2;
pub const ALPHAQ_SWAP_SELECTOR: u8 = 0x0c;

pub const HUMIDIFI_SWAP_SELECTOR: u8 = 0x4;
const HUMIDIFI_IX_DATA_KEY_SEED: [u8; 32] =
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/instructions/common_swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ fn distribute_swap<'a>(
Dex::Tessera => tessera::swap,
Dex::GoonFi => goonfi::swap,
Dex::BisonFi => bisonfi::swap,
Dex::AlphaQ => alphaq::swap,
};

swap_function(remaining_accounts, amount_in, offset, hop_accounts, hop, proxy_from, owner_seeds)
Expand Down
18 changes: 16 additions & 2 deletions crates/shared/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ pub mod pmm_bisonfi {
pub const ARGS_LEN: usize = 18;
}

pub mod pmm_alphaq {
use anchor_lang::prelude::*;

declare_id!("ALPHAQmeA7bjrVuccPsYPiCvsi428SNwte66Srvs4pHA");
pub const ACCOUNTS_LEN: usize = 13;
pub const ARGS_LEN: usize = 18;
}

pub mod spl_token {
use anchor_lang::prelude::*;

Expand Down Expand Up @@ -122,6 +130,7 @@ pub enum Dex {
Tessera,
GoonFi,
BisonFi,
AlphaQ,
}

impl std::fmt::Display for Dex {
Expand All @@ -138,13 +147,14 @@ impl std::fmt::Display for Dex {
Dex::Tessera => f.write_str("tessera"),
Dex::GoonFi => f.write_str("goonfi"),
Dex::BisonFi => f.write_str("bisonfi"),
Dex::AlphaQ => f.write_str("alphaq"),
}
}
}

impl Dex {
pub const ALL: [Dex; 9] = [Dex::RaydiumClV2, Dex::RaydiumCp, Dex::ObricV2, Dex::SolfiV2, Dex::ZeroFi, Dex::HumidiFi, Dex::Tessera, Dex::GoonFi, Dex::BisonFi];
pub const PMM: [Dex; 7] = [Dex::ObricV2, Dex::SolfiV2, Dex::ZeroFi, Dex::HumidiFi, Dex::Tessera, Dex::GoonFi, Dex::BisonFi];
pub const ALL: [Dex; 10] = [Dex::RaydiumClV2, Dex::RaydiumCp, Dex::ObricV2, Dex::SolfiV2, Dex::ZeroFi, Dex::HumidiFi, Dex::Tessera, Dex::GoonFi, Dex::BisonFi, Dex::AlphaQ];
pub const PMM: [Dex; 8] = [Dex::ObricV2, Dex::SolfiV2, Dex::ZeroFi, Dex::HumidiFi, Dex::Tessera, Dex::GoonFi, Dex::BisonFi, Dex::AlphaQ];

pub fn program_id(&self) -> anchor_lang::solana_program::pubkey::Pubkey {
match self {
Expand All @@ -159,6 +169,7 @@ impl Dex {
Dex::Tessera => crate::pmm_tessera::id(),
Dex::GoonFi => crate::pmm_goonfi::id(),
Dex::BisonFi => crate::pmm_bisonfi::id(),
Dex::AlphaQ => crate::pmm_alphaq::id(),
}
}
}
Expand All @@ -177,6 +188,7 @@ impl FromStr for Dex {
"tessera" => Ok(Dex::Tessera),
"goonfi" => Ok(Dex::GoonFi),
"bisonfi" => Ok(Dex::BisonFi),
"alphaq" => Ok(Dex::AlphaQ),
_ => Err(format!("unknown dex '{}'", s)),
}
}
Expand All @@ -194,6 +206,7 @@ impl From<magnus_router_client::types::Dex> for Dex {
magnus_router_client::types::Dex::Tessera => Dex::Tessera,
magnus_router_client::types::Dex::GoonFi => Dex::GoonFi,
magnus_router_client::types::Dex::BisonFi => Dex::BisonFi,
magnus_router_client::types::Dex::AlphaQ => Dex::AlphaQ,
}
}
}
Expand All @@ -210,6 +223,7 @@ impl From<Dex> for magnus_router_client::types::Dex {
Dex::Tessera => magnus_router_client::types::Dex::Tessera,
Dex::GoonFi => magnus_router_client::types::Dex::GoonFi,
Dex::BisonFi => magnus_router_client::types::Dex::BisonFi,
Dex::AlphaQ => magnus_router_client::types::Dex::AlphaQ,
}
}
}
Expand Down
27 changes: 27 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading