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
12const TICK_ARRAY_SIZE: i32 = 88;
14
15fn 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
25fn 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
31fn 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
40pub struct OrcaBuilder {
42 rpc: Arc<RpcClient>,
43}
44
45impl OrcaBuilder {
46 pub fn new(rpc: Arc<RpcClient>) -> Self {
47 Self { rpc }
48 }
49}
50
51const 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 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 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 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 let sqrt_price_limit: u128 = if a_to_b {
140 4295048016u128 } else {
142 79226673515401279992447579055u128 };
144
145 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 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 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}