1use solana_commitment_config::CommitmentConfig;
2use solana_rpc_client::nonblocking::rpc_client::RpcClient;
3use solana_sdk::{
4 account::Account, instruction::Instruction, message::AddressLookupTableAccount, pubkey::Pubkey,
5 signature::Signature, signer::Signer, transaction::Transaction,
6};
7
8use crate::chain::ChainError;
9use crate::chain::accounts::{MarketInfo, PositionInfo};
10use crate::chain::ix::{
11 DepositPremiumIxArgs, FundPositionIxArgs, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID,
12 WithdrawPremiumIxArgs, build_deposit_premium_ixs, build_fund_position_ixs,
13 build_withdraw_premium_ixs,
14};
15use crate::chain::settlement::required_settlement;
16use crate::chain::wsol::{
17 NativeSolBudget, NativeSolFunding, WrapPlan, WsolWrapRequest, is_native_mint, plan_wsol_wrap,
18 token_account_rent_lamports,
19};
20
21const ADDRESS_LOOKUP_TABLE_PROGRAM_ID: Pubkey =
22 solana_sdk::pubkey!("AddressLookupTab1e1111111111111111111111111");
23const LOOKUP_TABLE_META_LEN: usize = 56;
24
25fn parse_lookup_table(
26 key: Pubkey,
27 account: &Account,
28) -> Result<AddressLookupTableAccount, ChainError> {
29 if account.owner != ADDRESS_LOOKUP_TABLE_PROGRAM_ID {
30 return Err(ChainError::AccountOwnerMismatch {
31 expected: ADDRESS_LOOKUP_TABLE_PROGRAM_ID,
32 actual: account.owner,
33 });
34 }
35 let data = &account.data;
36 let (Some(kind), Some(deactivation), Some(addresses)) = (
37 data.get(..4),
38 data.get(4..12),
39 data.get(LOOKUP_TABLE_META_LEN..),
40 ) else {
41 return Err(ChainError::InvalidAccountData);
42 };
43 if kind != 1u32.to_le_bytes() || addresses.len() % 32 != 0 {
44 return Err(ChainError::InvalidAccountData);
45 }
46 if deactivation != u64::MAX.to_le_bytes() {
47 return Err(ChainError::LookupTableDeactivated(key));
48 }
49 Ok(AddressLookupTableAccount {
50 key,
51 addresses: addresses
52 .as_chunks::<32>()
53 .0
54 .iter()
55 .map(|bytes| Pubkey::new_from_array(*bytes))
56 .collect(),
57 })
58}
59
60pub struct ChainClient {
61 rpc: RpcClient,
62 program_id: Pubkey,
63 commitment: CommitmentConfig,
64}
65
66#[derive(Debug, Clone, Copy, Default)]
67pub struct SendOptions {
68 pub compute_unit_limit: Option<u32>,
69}
70
71#[derive(Debug, Clone)]
72pub struct DepositPremiumArgs {
73 pub maker_owner: Pubkey,
74 pub amount: u64,
75 pub premium_mint: Pubkey,
76 pub token_program: Option<Pubkey>,
77 pub create_atas: bool,
78}
79
80#[derive(Debug, Clone)]
81pub struct WithdrawPremiumArgs {
82 pub maker_owner: Pubkey,
83 pub amount: u64,
84 pub premium_mint: Pubkey,
85 pub token_program: Option<Pubkey>,
86 pub create_atas: bool,
87}
88
89#[derive(Debug, Clone)]
90pub struct FundPositionArgs {
91 pub maker_owner: Pubkey,
92 pub position_pda: Pubkey,
93 pub create_atas: bool,
94 pub native_sol: NativeSolFunding,
95}
96
97struct PreparedFundPosition {
98 instructions: Vec<Instruction>,
99 native_budget: Option<NativeSolBudget>,
100}
101
102impl ChainClient {
103 pub fn program_id(&self) -> Pubkey {
104 self.program_id
105 }
106
107 pub fn new(rpc_url: &str, program_id: Pubkey, commitment: CommitmentConfig) -> Self {
108 let rpc = RpcClient::new_with_commitment(rpc_url.to_string(), commitment);
109 Self {
110 rpc,
111 program_id,
112 commitment,
113 }
114 }
115
116 pub async fn deposit_premium(
117 &self,
118 args: DepositPremiumArgs,
119 maker_owner: &(dyn Signer + Sync),
120 fee_payer: Option<&(dyn Signer + Sync)>,
121 ) -> Result<Signature, ChainError> {
122 let payer = fee_payer.unwrap_or(maker_owner);
123 let ixs = self
124 .build_deposit_premium_ixs(&args, payer.pubkey())
125 .await?;
126 self.send_instructions_with_options(ixs, payer, &[maker_owner], SendOptions::default())
127 .await
128 }
129
130 pub async fn withdraw_premium(
131 &self,
132 args: WithdrawPremiumArgs,
133 maker_owner: &(dyn Signer + Sync),
134 fee_payer: Option<&(dyn Signer + Sync)>,
135 ) -> Result<Signature, ChainError> {
136 let payer = fee_payer.unwrap_or(maker_owner);
137 let ixs = self
138 .build_withdraw_premium_ixs(&args, payer.pubkey())
139 .await?;
140 self.send_instructions_with_options(ixs, payer, &[maker_owner], SendOptions::default())
141 .await
142 }
143
144 pub async fn fund_position(
145 &self,
146 args: FundPositionArgs,
147 maker_owner: &(dyn Signer + Sync),
148 fee_payer: Option<&(dyn Signer + Sync)>,
149 ) -> Result<Signature, ChainError> {
150 let payer = fee_payer.unwrap_or(maker_owner);
151 let prepared = self.prepare_fund_position(&args, payer.pubkey()).await?;
152 self.send_instructions(
153 prepared.instructions,
154 payer,
155 &[maker_owner],
156 SendOptions::default(),
157 prepared.native_budget,
158 )
159 .await
160 }
161
162 pub async fn build_deposit_premium_ixs(
163 &self,
164 args: &DepositPremiumArgs,
165 fee_payer: Pubkey,
166 ) -> Result<Vec<Instruction>, ChainError> {
167 let token_program = self
168 .resolve_token_program(&args.premium_mint, args.token_program)
169 .await?;
170 build_deposit_premium_ixs(
171 &self.program_id,
172 &DepositPremiumIxArgs {
173 maker_owner: args.maker_owner,
174 amount: args.amount,
175 premium_mint: args.premium_mint,
176 token_program,
177 create_atas: args.create_atas,
178 },
179 fee_payer,
180 )
181 .map_err(ChainError::from)
182 }
183
184 pub async fn build_withdraw_premium_ixs(
185 &self,
186 args: &WithdrawPremiumArgs,
187 fee_payer: Pubkey,
188 ) -> Result<Vec<Instruction>, ChainError> {
189 let token_program = self
190 .resolve_token_program(&args.premium_mint, args.token_program)
191 .await?;
192 build_withdraw_premium_ixs(
193 &self.program_id,
194 &WithdrawPremiumIxArgs {
195 maker_owner: args.maker_owner,
196 amount: args.amount,
197 premium_mint: args.premium_mint,
198 token_program,
199 create_atas: args.create_atas,
200 },
201 fee_payer,
202 )
203 .map_err(ChainError::from)
204 }
205
206 pub async fn build_fund_position_ixs(
207 &self,
208 args: &FundPositionArgs,
209 fee_payer: Pubkey,
210 ) -> Result<Vec<Instruction>, ChainError> {
211 Ok(self
212 .prepare_fund_position(args, fee_payer)
213 .await?
214 .instructions)
215 }
216
217 async fn prepare_fund_position(
218 &self,
219 args: &FundPositionArgs,
220 fee_payer: Pubkey,
221 ) -> Result<PreparedFundPosition, ChainError> {
222 let position_data = self.fetch_account(&args.position_pda).await?;
223 let pos = PositionInfo::from_account(&position_data, &self.program_id)?;
224 let market_data = self.fetch_account(&pos.market_pda).await?;
225 let market = MarketInfo::from_account(&market_data, &self.program_id)?;
226
227 let mut fund_ixs = build_fund_position_ixs(
228 &self.program_id,
229 &FundPositionIxArgs {
230 maker_owner: args.maker_owner,
231 position_pda: args.position_pda,
232 market_pda: pos.market_pda,
233 position_type: pos.position_type.into(),
234 underlying_mint: market.underlying_mint,
235 quote_mint: market.quote_mint,
236 underlying_token_program_id: market.underlying_token_program_id,
237 quote_token_program_id: market.quote_token_program_id,
238 create_atas: args.create_atas,
239 },
240 fee_payer,
241 )
242 .map_err(ChainError::from)?;
243
244 let NativeSolFunding::WrapIfNeeded { reserve_lamports } = args.native_sol else {
245 return Ok(PreparedFundPosition {
246 instructions: fund_ixs,
247 native_budget: None,
248 });
249 };
250 let (settlement_mint, settlement_program) = match pos.position_type {
251 crate::types::PositionType::CoveredCall => {
252 (market.quote_mint, market.quote_token_program_id)
253 }
254 crate::types::PositionType::CashSecuredPut => {
255 (market.underlying_mint, market.underlying_token_program_id)
256 }
257 };
258 if !is_native_mint(&settlement_mint) {
259 return Ok(PreparedFundPosition {
260 instructions: fund_ixs,
261 native_budget: None,
262 });
263 }
264 let expected_settlement = required_settlement(
265 pos.position_type.into(),
266 pos.strike.value(),
267 pos.quantity.value(),
268 market.quote_decimals.value(),
269 market.underlying_decimals.value(),
270 )?;
271 if expected_settlement == 0 {
272 return Err(ChainError::InvalidAccountData);
273 }
274 if settlement_program != crate::chain::wsol::TOKEN_PROGRAM_ID {
275 return Err(ChainError::InvalidNativeTokenProgram(settlement_program));
276 }
277 let funding_ata = crate::chain::ix::derive_associated_token_address(
278 &args.maker_owner,
279 &settlement_mint,
280 &settlement_program,
281 );
282 let position_funding_ata = crate::chain::ix::derive_associated_token_address(
283 &args.position_pda,
284 &settlement_mint,
285 &settlement_program,
286 );
287 let (funding_account, additional_ata_rent_lamports) =
288 if args.create_atas && fee_payer == args.maker_owner {
289 let [position_account, funding_account]: [Option<Account>; 2] = self
290 .rpc
291 .get_multiple_accounts_with_commitment(
292 &[position_funding_ata, funding_ata],
293 self.commitment,
294 )
295 .await?
296 .value
297 .try_into()
298 .map_err(|_| ChainError::InvalidAccountData)?;
299 let rent = if position_account.is_none() {
300 token_account_rent_lamports(&self.rpc).await?
301 } else {
302 0
303 };
304 (funding_account, rent)
305 } else {
306 (
307 self.rpc
308 .get_account_with_commitment(&funding_ata, self.commitment)
309 .await?
310 .value,
311 0,
312 )
313 };
314 match plan_wsol_wrap(
315 &self.rpc,
316 funding_account,
317 WsolWrapRequest {
318 owner: args.maker_owner,
319 funding_ata,
320 token_program: settlement_program,
321 expected_settlement,
322 reserve_lamports,
323 allow_account_creation: args.create_atas,
324 fee_payer,
325 additional_ata_rent_lamports,
326 },
327 )
328 .await?
329 {
330 WrapPlan::NotNeeded => Ok(PreparedFundPosition {
331 instructions: fund_ixs,
332 native_budget: None,
333 }),
334 WrapPlan::Wrap {
335 instructions: wrap_ixs,
336 budget,
337 } => {
338 let deposit_ix = fund_ixs.pop().ok_or(ChainError::InvalidAccountData)?;
339 fund_ixs.extend(wrap_ixs);
340 fund_ixs.push(deposit_ix);
341 Ok(PreparedFundPosition {
342 instructions: fund_ixs,
343 native_budget: Some(budget),
344 })
345 }
346 }
347 }
348
349 pub async fn fetch_position_info(
350 &self,
351 position_pda: &Pubkey,
352 ) -> Result<PositionInfo, ChainError> {
353 let data = self.fetch_account(position_pda).await?;
354 PositionInfo::from_account(&data, &self.program_id)
355 }
356
357 pub async fn fetch_market_info(&self, market_pda: &Pubkey) -> Result<MarketInfo, ChainError> {
358 let market_data = self.fetch_account(market_pda).await?;
359 MarketInfo::from_account(&market_data, &self.program_id)
360 }
361
362 pub async fn fetch_market_quote_mint(&self, market_pda: &Pubkey) -> Result<Pubkey, ChainError> {
363 Ok(self.fetch_market_info(market_pda).await?.quote_mint)
364 }
365
366 pub async fn fetch_token_balance(&self, token_account: &Pubkey) -> Result<u64, ChainError> {
368 let result = self.rpc.get_token_account_balance(token_account).await?;
369 result
370 .amount
371 .parse::<u64>()
372 .map_err(|_| ChainError::InvalidAccountData)
373 }
374
375 pub async fn address_lookup_tables(
377 &self,
378 keys: &[Pubkey],
379 ) -> Result<Vec<AddressLookupTableAccount>, ChainError> {
380 let accounts = self
381 .rpc
382 .get_multiple_accounts_with_commitment(keys, self.commitment)
383 .await?
384 .value;
385 keys.iter()
386 .zip(accounts)
387 .map(|(key, account)| match account {
388 Some(account) => parse_lookup_table(*key, &account),
389 None => Err(ChainError::MissingLookupTable(*key)),
390 })
391 .collect()
392 }
393
394 async fn resolve_token_program(
395 &self,
396 mint: &Pubkey,
397 token_program: Option<Pubkey>,
398 ) -> Result<Pubkey, ChainError> {
399 if let Some(program) = token_program {
400 return ensure_supported_token_program(program);
401 }
402 let account = self.rpc.get_account(mint).await?;
403 ensure_supported_token_program(account.owner)
404 }
405
406 pub(crate) async fn fetch_account(&self, pubkey: &Pubkey) -> Result<Account, ChainError> {
407 self.rpc.get_account(pubkey).await.map_err(ChainError::from)
408 }
409
410 pub(crate) async fn program_accounts(
412 &self,
413 filters: Vec<solana_rpc_client::api::filter::RpcFilterType>,
414 ) -> Result<Vec<(Pubkey, Account)>, ChainError> {
415 use solana_rpc_client::api::config::{
416 RpcAccountInfoConfig, RpcProgramAccountsConfig, UiAccountEncoding,
417 };
418 self.rpc
419 .get_program_ui_accounts_with_config(
420 &self.program_id,
421 RpcProgramAccountsConfig {
422 filters: Some(filters),
423 account_config: RpcAccountInfoConfig {
424 encoding: Some(UiAccountEncoding::Base64),
425 commitment: Some(self.commitment),
426 ..RpcAccountInfoConfig::default()
427 },
428 ..RpcProgramAccountsConfig::default()
429 },
430 )
431 .await?
432 .into_iter()
433 .map(|(address, account)| {
434 account
435 .decode::<Account>()
436 .map(|account| (address, account))
437 .ok_or(ChainError::InvalidAccountData)
438 })
439 .collect()
440 }
441
442 pub(crate) async fn fetch_optional_account(
443 &self,
444 pubkey: &Pubkey,
445 ) -> Result<Option<Account>, ChainError> {
446 Ok(self
447 .rpc
448 .get_account_with_commitment(pubkey, self.commitment)
449 .await?
450 .value)
451 }
452
453 pub async fn send_instructions_with_options(
454 &self,
455 instructions: Vec<Instruction>,
456 fee_payer: &(dyn Signer + Sync),
457 extra_signers: &[&(dyn Signer + Sync)],
458 options: SendOptions,
459 ) -> Result<Signature, ChainError> {
460 self.send_instructions(instructions, fee_payer, extra_signers, options, None)
461 .await
462 }
463
464 async fn send_instructions(
465 &self,
466 mut instructions: Vec<Instruction>,
467 fee_payer: &(dyn Signer + Sync),
468 extra_signers: &[&(dyn Signer + Sync)],
469 options: SendOptions,
470 native_budget: Option<NativeSolBudget>,
471 ) -> Result<Signature, ChainError> {
472 if let Some(limit) = options.compute_unit_limit {
473 instructions.insert(
474 0,
475 solana_compute_budget_interface::ComputeBudgetInstruction::set_compute_unit_limit(
476 limit,
477 ),
478 );
479 }
480 let recent = self.rpc.get_latest_blockhash().await?;
481
482 let mut tx = Transaction::new_with_payer(&instructions, Some(&fee_payer.pubkey()));
483 tx.message.recent_blockhash = recent;
484 if let Some(budget) = native_budget {
485 let fee_lamports = if fee_payer.pubkey() == budget.owner {
486 self.rpc.get_fee_for_message(&tx.message).await?
487 } else {
488 0
489 };
490 let available_lamports = self.rpc.get_balance(&budget.owner).await?;
491 budget.validate(available_lamports, fee_lamports)?;
492 }
493 let mut signers: Vec<&(dyn Signer + Sync)> = Vec::with_capacity(1 + extra_signers.len());
494 signers.push(fee_payer);
495 for signer in extra_signers {
496 if signer.pubkey() != fee_payer.pubkey() {
497 signers.push(*signer);
498 }
499 }
500 sign_transaction(&mut tx, &signers, recent)?;
501 self.rpc
502 .send_and_confirm_transaction_with_spinner_and_commitment(&tx, self.commitment)
503 .await
504 .map_err(ChainError::from)
505 }
506}
507
508fn sign_transaction(
509 transaction: &mut Transaction,
510 signers: &[&(dyn Signer + Sync)],
511 recent_blockhash: solana_sdk::hash::Hash,
512) -> Result<(), ChainError> {
513 transaction.try_sign(signers, recent_blockhash)?;
514 Ok(())
515}
516
517fn ensure_supported_token_program(program: Pubkey) -> Result<Pubkey, ChainError> {
518 if program == TOKEN_PROGRAM_ID || program == TOKEN_2022_PROGRAM_ID {
519 Ok(program)
520 } else {
521 Err(ChainError::InvalidAccountData)
522 }
523}
524
525#[cfg(test)]
526#[path = "rpc_tests.rs"]
527mod tests;
528
529#[cfg(test)]
530#[path = "rpc_mock.rs"]
531mod mock;