diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..20d9ef3 --- /dev/null +++ b/.envrc @@ -0,0 +1,3 @@ +watch_file flake.nix +watch_file flake.lock +use flake path:. diff --git a/.gitignore b/.gitignore index ce77b96..a6e7a28 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .surfpool/ .zed/ +.direnv/ target/ runbooks/ txtx.yml diff --git a/cfg/programs/alphaq.so b/cfg/programs/alphaq.so new file mode 100644 index 0000000..dd2df7b Binary files /dev/null and b/cfg/programs/alphaq.so differ diff --git a/crates/router-client/src/generated/types/dex.rs b/crates/router-client/src/generated/types/dex.rs index 507098c..8470c36 100644 --- a/crates/router-client/src/generated/types/dex.rs +++ b/crates/router-client/src/generated/types/dex.rs @@ -19,4 +19,5 @@ pub enum Dex { Tessera, GoonFi, BisonFi, + AlphaQ, } diff --git a/crates/router/src/adapters/alphaq.rs b/crates/router/src/adapters/alphaq.rs new file mode 100644 index 0000000..f3fa65b --- /dev/null +++ b/crates/router/src/adapters/alphaq.rs @@ -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 { + 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 { + 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 { + 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 { + 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]); + } +} diff --git a/crates/router/src/adapters/mod.rs b/crates/router/src/adapters/mod.rs index 5a61dbd..26ba517 100644 --- a/crates/router/src/adapters/mod.rs +++ b/crates/router/src/adapters/mod.rs @@ -1,3 +1,4 @@ +pub mod alphaq; pub mod bisonfi; pub mod common; pub mod goonfi; diff --git a/crates/router/src/constants.rs b/crates/router/src/constants.rs index 9ceae06..544e264 100644 --- a/crates/router/src/constants.rs +++ b/crates/router/src/constants.rs @@ -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] = diff --git a/crates/router/src/instructions/common_swap.rs b/crates/router/src/instructions/common_swap.rs index 020e91b..84c00de 100644 --- a/crates/router/src/instructions/common_swap.rs +++ b/crates/router/src/instructions/common_swap.rs @@ -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) diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index 6fb6dae..acadad9 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -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::*; @@ -122,6 +130,7 @@ pub enum Dex { Tessera, GoonFi, BisonFi, + AlphaQ, } impl std::fmt::Display for Dex { @@ -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 { @@ -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(), } } } @@ -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)), } } @@ -194,6 +206,7 @@ impl From 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, } } } @@ -210,6 +223,7 @@ impl From 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, } } } diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..e5578c1 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1770562336, + "narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d6c71932130818840fc8fe9509cf50be8c64634f", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..526032c --- /dev/null +++ b/flake.nix @@ -0,0 +1,43 @@ +{ + description = "Magnus development shell"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = + { self, nixpkgs }: + let + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system); + in + { + devShells = forAllSystems ( + system: + let + pkgs = import nixpkgs { inherit system; }; + in + { + default = pkgs.mkShell { + packages = with pkgs; [ + cargo + rustc + rustfmt + clippy + pkg-config + openssl + protobuf + ]; + + OPENSSL_NO_VENDOR = "1"; + RUST_BACKTRACE = "1"; + }; + } + ); + }; +}