Skip to main content

scematica_executor/
orca.rs

1use crate::SwapInstructionBuilder;
2use anyhow::Result;
3use async_trait::async_trait;
4use scematica_core::{dex::program_ids, types::DexKind};
5use solana_client::nonblocking::rpc_client::RpcClient;
6use solana_sdk::{
7    instruction::{AccountMeta, Instruction},
8    pubkey::Pubkey,
9};
10use std::sync::Arc;
11
12/// Number of ticks per tick array in Orca Whirlpool
13const TICK_ARRAY_SIZE: i32 = 88;
14
15/// Derive a Whirlpool tick array PDA for a given start tick index.
16fn derive_tick_array_pda(whirlpool: &Pubkey, start_tick_index: i32) -> Pubkey {
17    let start_bytes = start_tick_index.to_string();
18    let (pda, _) = Pubkey::find_program_address(
19        &[b"tick_array", whirlpool.as_ref(), start_bytes.as_bytes()],
20        &program_ids::ORCA_WHIRLPOOL,
21    );
22    pda
23}
24
25/// Round a tick index down to the nearest tick array start index.
26fn tick_array_start_index(tick_index: i32, tick_spacing: u16) -> i32 {
27    let ticks_in_array = TICK_ARRAY_SIZE * tick_spacing as i32;
28    tick_index.div_euclid(ticks_in_array) * ticks_in_array
29}
30
31/// Derive the oracle PDA for a Whirlpool.
32fn derive_oracle_pda(whirlpool: &Pubkey) -> Pubkey {
33    let (pda, _) = Pubkey::find_program_address(
34        &[b"oracle", whirlpool.as_ref()],
35        &program_ids::ORCA_WHIRLPOOL,
36    );
37    pda
38}
39
40/// Orca Whirlpool swap instruction builder
41pub struct OrcaBuilder {
42    rpc: Arc<RpcClient>,
43}
44
45impl OrcaBuilder {
46    pub fn new(rpc: Arc<RpcClient>) -> Self {
47        Self { rpc }
48    }
49}
50
51/// Orca Whirlpool swap discriminator: sha256("global:swap")[0..8]
52const WHIRLPOOL_SWAP_DISCRIMINATOR: [u8; 8] = [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x27, 0x43];
53
54fn orca_swap_data(amount: u64, other_amount_threshold: u64, sqrt_price_limit: u128, amount_specified_is_input: bool, a_to_b: bool) -> Vec<u8> {
55    let mut data = WHIRLPOOL_SWAP_DISCRIMINATOR.to_vec();
56    data.extend_from_slice(&amount.to_le_bytes());
57    data.extend_from_slice(&other_amount_threshold.to_le_bytes());
58    data.extend_from_slice(&sqrt_price_limit.to_le_bytes());
59    data.push(amount_specified_is_input as u8);
60    data.push(a_to_b as u8);
61    data
62}
63
64#[async_trait]
65impl SwapInstructionBuilder for OrcaBuilder {
66    fn dex(&self) -> DexKind {
67        DexKind::Orca
68    }
69
70    async fn build_swap(
71        &self,
72        pool: &Pubkey,
73        owner: &Pubkey,
74        token_in: &Pubkey,
75        _token_out: &Pubkey,
76        ata_in: &Pubkey,
77        ata_out: &Pubkey,
78        amount_in: u64,
79        min_amount_out: u64,
80    ) -> Result<Vec<Instruction>> {
81        // ── Fetch Whirlpool state (Req 4.2, 4.3) ────────────────────────────
82        let data = self
83            .rpc
84            .get_account_data(pool)
85            .await
86            .map_err(|e| anyhow::anyhow!("OrcaBuilder: RPC error fetching pool {}: {}", pool, e))?;
87
88        if data.len() < 272 {
89            anyhow::bail!(
90                "OrcaBuilder: Whirlpool data too short: {} < 272 bytes",
91                data.len()
92            );
93        }
94
95        // ── Extract fields (Req 4.4) ─────────────────────────────────────────
96        // Layout (Anchor-serialized, 8-byte discriminator prefix):
97        //   8+0  : whirlpools_config (Pubkey, 32)
98        //   8+32 : whirlpool_bump ([u8; 1])
99        //   8+33 : padding / whirlpool_bump_seed
100        //   8+9  : tick_spacing (u16)  — NOTE: offset within the struct body
101        //   8+37 : tick_current_index (i32)
102        //   8+101: token_mint_a (Pubkey, 32)
103        //   8+133: token_vault_a (Pubkey, 32)
104        //   8+181: token_mint_b (Pubkey, 32)
105        //   8+213: token_vault_b (Pubkey, 32)
106        // tick_spacing and tick_current_index are used in task 3.3 for tick array PDA derivation
107        let tick_spacing = u16::from_le_bytes([data[8 + 9], data[8 + 10]]);
108        let tick_current_index = i32::from_le_bytes([
109            data[8 + 37],
110            data[8 + 38],
111            data[8 + 39],
112            data[8 + 40],
113        ]);
114
115        let token_mint_a = Pubkey::try_from(&data[8 + 101..8 + 133])
116            .map_err(|_| anyhow::anyhow!("OrcaBuilder: failed to parse token_mint_a"))?;
117        let token_vault_a = Pubkey::try_from(&data[8 + 133..8 + 165])
118            .map_err(|_| anyhow::anyhow!("OrcaBuilder: failed to parse token_vault_a"))?;
119        let token_mint_b = Pubkey::try_from(&data[8 + 181..8 + 213])
120            .map_err(|_| anyhow::anyhow!("OrcaBuilder: failed to parse token_mint_b"))?;
121        let token_vault_b = Pubkey::try_from(&data[8 + 213..8 + 245])
122            .map_err(|_| anyhow::anyhow!("OrcaBuilder: failed to parse token_vault_b"))?;
123
124        // ── Determine swap direction (Req 4.5, 4.6) ─────────────────────────
125        let a_to_b = if token_in == &token_mint_a {
126            true
127        } else if token_in == &token_mint_b {
128            false
129        } else {
130            anyhow::bail!(
131                "OrcaBuilder: token_in {} matches neither token_mint_a {} nor token_mint_b {}",
132                token_in,
133                token_mint_a,
134                token_mint_b
135            );
136        };
137
138        // ── sqrt_price_limit (Req 4.7, 4.8) ─────────────────────────────────
139        let sqrt_price_limit: u128 = if a_to_b {
140            4295048016u128 // MIN_SQRT_PRICE
141        } else {
142            79226673515401279992447579055u128 // MAX_SQRT_PRICE
143        };
144
145        // ── User token accounts depend on direction (Req 4.11) ───────────────
146        let (user_token_a, user_token_b) = if a_to_b {
147            (ata_in, ata_out)
148        } else {
149            (ata_out, ata_in)
150        };
151
152        // Derive tick arrays: 3 consecutive arrays starting from current tick, in swap direction.
153        let start_0 = tick_array_start_index(tick_current_index, tick_spacing);
154        let ticks_in_array = TICK_ARRAY_SIZE * tick_spacing as i32;
155        let (start_1, start_2) = if a_to_b {
156            (start_0 - ticks_in_array, start_0 - 2 * ticks_in_array)
157        } else {
158            (start_0 + ticks_in_array, start_0 + 2 * ticks_in_array)
159        };
160        let tick_array_0 = derive_tick_array_pda(pool, start_0);
161        let tick_array_1 = derive_tick_array_pda(pool, start_1);
162        let tick_array_2 = derive_tick_array_pda(pool, start_2);
163        let oracle = derive_oracle_pda(pool);
164
165        // Orca Whirlpool swap accounts
166        let accounts = vec![
167            AccountMeta::new_readonly(spl_token::id(), false),
168            AccountMeta::new_readonly(*owner, true),
169            AccountMeta::new(*pool, false),
170            AccountMeta::new(*user_token_a, false),
171            AccountMeta::new(token_vault_a, false),
172            AccountMeta::new(*user_token_b, false),
173            AccountMeta::new(token_vault_b, false),
174            AccountMeta::new(tick_array_0, false),
175            AccountMeta::new(tick_array_1, false),
176            AccountMeta::new(tick_array_2, false),
177            AccountMeta::new_readonly(oracle, false),
178        ];
179
180        Ok(vec![Instruction {
181            program_id: program_ids::ORCA_WHIRLPOOL,
182            accounts,
183            data: orca_swap_data(amount_in, min_amount_out, sqrt_price_limit, true, a_to_b),
184        }])
185    }
186}