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