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 MakerDepositIxArgs, MakerWithdrawIxArgs, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID,
12 build_maker_deposit_ixs, build_maker_withdraw_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 MakerDepositArgs {
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 MakerWithdrawArgs {
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 holder: 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_sender(
133 ObservedSender(shared_sender(rpc_url)),
134 solana_rpc_client::rpc_client::RpcClientConfig::with_commitment(commitment),
135 );
136 Self {
137 rpc,
138 program_id,
139 commitment,
140 #[cfg(feature = "ws-client")]
141 confirmation_ws_url: None,
142 }
143 }
144
145 pub async fn maker_deposit(
147 &self,
148 args: MakerDepositArgs,
149 maker_owner: &(dyn Signer + Sync),
150 fee_payer: Option<&(dyn Signer + Sync)>,
151 ) -> Result<Signature, ChainError> {
152 let payer = fee_payer.map_or(maker_owner, |payer| payer);
153 let ixs = self.build_maker_deposit_ixs(&args, payer.pubkey()).await?;
154 self.send_instructions_with_options(ixs, payer, &[maker_owner], SendOptions::default())
155 .await
156 }
157
158 pub async fn maker_withdraw(
160 &self,
161 args: MakerWithdrawArgs,
162 maker_owner: &(dyn Signer + Sync),
163 fee_payer: Option<&(dyn Signer + Sync)>,
164 ) -> Result<Signature, ChainError> {
165 let payer = fee_payer.map_or(maker_owner, |payer| payer);
166 let ixs = self.build_maker_withdraw_ixs(&args, payer.pubkey()).await?;
167 self.send_instructions_with_options(ixs, payer, &[maker_owner], SendOptions::default())
168 .await
169 }
170
171 pub async fn fund_position(
173 &self,
174 args: FundPositionArgs,
175 maker_owner: &(dyn Signer + Sync),
176 fee_payer: Option<&(dyn Signer + Sync)>,
177 ) -> Result<Signature, ChainError> {
178 let payer = fee_payer.map_or(maker_owner, |payer| payer);
179 let prepared = self.prepare_fund_position(&args, payer.pubkey()).await?;
180 let (signature, blockhash) = self
181 .submit_instructions(
182 prepared.instructions,
183 payer,
184 &[maker_owner],
185 SendOptions::default(),
186 prepared.native_budget,
187 None,
188 )
189 .await?;
190 self.confirm_submitted(signature, blockhash).await
191 }
192
193 pub async fn build_maker_deposit_ixs(
194 &self,
195 args: &MakerDepositArgs,
196 fee_payer: Pubkey,
197 ) -> Result<Vec<Instruction>, ChainError> {
198 let token_program = self
199 .resolve_token_program(&args.premium_mint, args.token_program)
200 .await?;
201 build_maker_deposit_ixs(
202 &self.program_id,
203 &MakerDepositIxArgs {
204 maker_owner: args.maker_owner,
205 amount: args.amount,
206 premium_mint: args.premium_mint,
207 token_program,
208 create_atas: args.create_atas,
209 },
210 fee_payer,
211 )
212 .map_err(ChainError::from)
213 }
214
215 pub async fn build_maker_withdraw_ixs(
216 &self,
217 args: &MakerWithdrawArgs,
218 fee_payer: Pubkey,
219 ) -> Result<Vec<Instruction>, ChainError> {
220 let token_program = self
221 .resolve_token_program(&args.premium_mint, args.token_program)
222 .await?;
223 build_maker_withdraw_ixs(
224 &self.program_id,
225 &MakerWithdrawIxArgs {
226 maker_owner: args.maker_owner,
227 amount: args.amount,
228 premium_mint: args.premium_mint,
229 token_program,
230 create_atas: args.create_atas,
231 },
232 fee_payer,
233 )
234 .map_err(ChainError::from)
235 }
236
237 pub async fn build_fund_position_ixs(
238 &self,
239 args: &FundPositionArgs,
240 fee_payer: Pubkey,
241 ) -> Result<Vec<Instruction>, ChainError> {
242 Ok(self
243 .prepare_fund_position(args, fee_payer)
244 .await?
245 .instructions)
246 }
247
248 async fn prepare_fund_position(
249 &self,
250 args: &FundPositionArgs,
251 fee_payer: Pubkey,
252 ) -> Result<PreparedFundPosition, ChainError> {
253 let position_data = self.fetch_account(&args.position_pda).await?;
254 let pos = PositionInfo::from_account(&position_data, &self.program_id)?;
255 let market_data = self.fetch_account(&pos.market_pda).await?;
256 let market = MarketInfo::from_account(&market_data, &self.program_id)?;
257
258 let snapshot = super::batch::PositionSnapshot {
259 address: args.position_pda,
260 market_address: pos.market_pda,
261 position: pos.clone(),
262 market: market.clone(),
263 };
264 let mut fund_ixs = snapshot.fund_instructions(
265 &self.program_id,
266 args.holder,
267 fee_payer,
268 args.create_atas,
269 )?;
270
271 let NativeSolFunding::WrapIfNeeded { reserve_lamports } = args.native_sol else {
272 return Ok(PreparedFundPosition {
273 instructions: fund_ixs,
274 native_budget: None,
275 });
276 };
277 let (settlement_mint, settlement_program) = match pos.terms.position_type() {
278 crate::types::PositionType::CoveredCall => {
279 (market.quote_mint, market.quote_token_program_id)
280 }
281 crate::types::PositionType::CashSecuredPut => {
282 (market.underlying_mint, market.underlying_token_program_id)
283 }
284 _ => return Err(ChainError::InvalidSettlementState),
285 };
286 if !is_native_mint(&settlement_mint) {
287 return Ok(PreparedFundPosition {
288 instructions: fund_ixs,
289 native_budget: None,
290 });
291 }
292 let expected_settlement = required_settlement(
293 pos.terms.to_chain().position_type().into(),
294 pos.terms.strike().value(),
295 pos.quantity.value(),
296 market.quote_decimals.value(),
297 market.underlying_decimals.value(),
298 )?;
299 if expected_settlement == 0 {
300 return Err(ChainError::InvalidAccountData);
301 }
302 if settlement_program != crate::chain::wsol::TOKEN_PROGRAM_ID {
303 return Err(ChainError::InvalidNativeTokenProgram(settlement_program));
304 }
305 let funding_ata = crate::chain::ix::derive_associated_token_address(
306 &args.holder,
307 &settlement_mint,
308 &settlement_program,
309 );
310 let position_funding_ata = crate::chain::ix::derive_associated_token_address(
311 &args.position_pda,
312 &settlement_mint,
313 &settlement_program,
314 );
315 let (funding_account, additional_ata_rent_lamports) =
316 if args.create_atas && fee_payer == args.holder {
317 let [position_account, funding_account]: [Option<Account>; 2] = self
318 .rpc
319 .get_multiple_accounts_with_commitment(
320 &[position_funding_ata, funding_ata],
321 self.commitment,
322 )
323 .await?
324 .value
325 .try_into()
326 .map_err(|_| ChainError::InvalidAccountData)?;
327 let rent = if position_account.is_none() {
328 token_account_rent_lamports(&self.rpc).await?
329 } else {
330 0
331 };
332 (funding_account, rent)
333 } else {
334 (
335 self.rpc
336 .get_account_with_commitment(&funding_ata, self.commitment)
337 .await?
338 .value,
339 0,
340 )
341 };
342 match plan_wsol_wrap(
343 &self.rpc,
344 funding_account,
345 WsolWrapRequest {
346 owner: args.holder,
347 funding_ata,
348 token_program: settlement_program,
349 expected_settlement,
350 reserve_lamports,
351 allow_account_creation: args.create_atas,
352 fee_payer,
353 additional_ata_rent_lamports,
354 },
355 )
356 .await?
357 {
358 WrapPlan::NotNeeded => Ok(PreparedFundPosition {
359 instructions: fund_ixs,
360 native_budget: None,
361 }),
362 WrapPlan::Wrap {
363 instructions: wrap_ixs,
364 budget,
365 } => {
366 let deposit_ix = fund_ixs.pop().ok_or(ChainError::InvalidAccountData)?;
367 fund_ixs.extend(wrap_ixs);
368 fund_ixs.push(deposit_ix);
369 Ok(PreparedFundPosition {
370 instructions: fund_ixs,
371 native_budget: Some(budget),
372 })
373 }
374 }
375 }
376
377 pub async fn fetch_position_info(
378 &self,
379 position_pda: &Pubkey,
380 ) -> Result<PositionInfo, ChainError> {
381 let data = self.fetch_account(position_pda).await?;
382 PositionInfo::from_account(&data, &self.program_id)
383 }
384
385 pub async fn fetch_market_info(&self, market_pda: &Pubkey) -> Result<MarketInfo, ChainError> {
386 let market_data = self.fetch_account(market_pda).await?;
387 MarketInfo::from_account(&market_data, &self.program_id)
388 }
389
390 pub async fn fetch_market_quote_mint(&self, market_pda: &Pubkey) -> Result<Pubkey, ChainError> {
391 Ok(self.fetch_market_info(market_pda).await?.quote_mint)
392 }
393
394 pub async fn hot_authority(&self) -> Result<Pubkey, ChainError> {
399 let (global_config, bump) =
400 Pubkey::find_program_address(&[b"global_config"], &self.program_id);
401 let account = self.fetch_account(&global_config).await?;
402 let config = GlobalConfigInfo::from_account(&account, &self.program_id)?;
403 if config.bump != bump {
404 return Err(ChainError::Identity("global config PDA"));
405 }
406 Ok(config.hot_authority)
407 }
408
409 pub async fn fetch_token_balance(&self, token_account: &Pubkey) -> Result<u64, ChainError> {
411 let result = self.rpc.get_token_account_balance(token_account).await?;
412 result
413 .amount
414 .parse::<u64>()
415 .map_err(|_| ChainError::InvalidAccountData)
416 }
417
418 pub async fn address_lookup_tables(
420 &self,
421 keys: &[Pubkey],
422 ) -> Result<Vec<AddressLookupTableAccount>, ChainError> {
423 let accounts = self
424 .rpc
425 .get_multiple_accounts_with_commitment(keys, self.commitment)
426 .await?
427 .value;
428 keys.iter()
429 .zip(accounts)
430 .map(|(key, account)| match account {
431 Some(account) => parse_lookup_table(*key, &account),
432 None => Err(ChainError::MissingLookupTable(*key)),
433 })
434 .collect()
435 }
436
437 async fn resolve_token_program(
438 &self,
439 mint: &Pubkey,
440 token_program: Option<Pubkey>,
441 ) -> Result<Pubkey, ChainError> {
442 if let Some(program) = token_program {
443 return ensure_supported_token_program(program);
444 }
445 let account = self.rpc.get_account(mint).await?;
446 ensure_supported_token_program(account.owner)
447 }
448
449 pub(crate) async fn fetch_account(&self, pubkey: &Pubkey) -> Result<Account, ChainError> {
450 self.rpc.get_account(pubkey).await.map_err(ChainError::from)
451 }
452
453 pub(crate) async fn program_accounts(
455 &self,
456 filters: Vec<solana_rpc_client::api::filter::RpcFilterType>,
457 ) -> Result<Vec<(Pubkey, Account)>, ChainError> {
458 use solana_rpc_client::api::config::{
459 RpcAccountInfoConfig, RpcProgramAccountsConfig, UiAccountEncoding,
460 };
461 self.rpc
462 .get_program_ui_accounts_with_config(
463 &self.program_id,
464 RpcProgramAccountsConfig {
465 filters: Some(filters),
466 account_config: RpcAccountInfoConfig {
467 encoding: Some(UiAccountEncoding::Base64),
468 commitment: Some(self.commitment),
469 ..RpcAccountInfoConfig::default()
470 },
471 ..RpcProgramAccountsConfig::default()
472 },
473 )
474 .await?
475 .into_iter()
476 .map(|(address, account)| {
477 account
478 .decode::<Account>()
479 .map(|account| (address, account))
480 .ok_or(ChainError::InvalidAccountData)
481 })
482 .collect()
483 }
484
485 pub(crate) async fn fetch_optional_account(
486 &self,
487 pubkey: &Pubkey,
488 ) -> Result<Option<Account>, ChainError> {
489 Ok(self
490 .rpc
491 .get_account_with_commitment(pubkey, self.commitment)
492 .await?
493 .value)
494 }
495
496 pub async fn send_instructions_with_options(
498 &self,
499 instructions: Vec<Instruction>,
500 fee_payer: &(dyn Signer + Sync),
501 extra_signers: &[&(dyn Signer + Sync)],
502 options: SendOptions,
503 ) -> Result<Signature, ChainError> {
504 let (signature, blockhash) = self
505 .submit_instructions(instructions, fee_payer, extra_signers, options, None, None)
506 .await?;
507 self.confirm_submitted(signature, blockhash).await
508 }
509
510 pub async fn send_instructions_with_blockhash(
512 &self,
513 instructions: Vec<Instruction>,
514 fee_payer: &(dyn Signer + Sync),
515 extra_signers: &[&(dyn Signer + Sync)],
516 options: SendOptions,
517 blockhash: solana_sdk::hash::Hash,
518 ) -> Result<Signature, ChainError> {
519 self.submit_instructions(
520 instructions,
521 fee_payer,
522 extra_signers,
523 options,
524 None,
525 Some(blockhash),
526 )
527 .await
528 .map(|(signature, _)| signature)
529 }
530
531 async fn submit_instructions(
532 &self,
533 mut instructions: Vec<Instruction>,
534 fee_payer: &(dyn Signer + Sync),
535 extra_signers: &[&(dyn Signer + Sync)],
536 options: SendOptions,
537 native_budget: Option<NativeSolBudget>,
538 blockhash: Option<solana_sdk::hash::Hash>,
539 ) -> Result<(Signature, solana_sdk::hash::Hash), ChainError> {
540 if let Some(limit) = options.compute_unit_limit {
541 instructions.insert(
542 0,
543 solana_compute_budget_interface::ComputeBudgetInstruction::set_compute_unit_limit(
544 limit,
545 ),
546 );
547 }
548 let recent = match blockhash {
549 Some(hash) => hash,
550 None => self.rpc.get_latest_blockhash().await?,
551 };
552
553 let mut tx = Transaction::new_with_payer(&instructions, Some(&fee_payer.pubkey()));
554 tx.message.recent_blockhash = recent;
555 if let Some(budget) = native_budget {
556 let fee_lamports = if fee_payer.pubkey() == budget.owner {
557 self.rpc.get_fee_for_message(&tx.message).await?
558 } else {
559 0
560 };
561 let available_lamports = self.rpc.get_balance(&budget.owner).await?;
562 budget.validate(available_lamports, fee_lamports)?;
563 }
564 let mut signers: Vec<&(dyn Signer + Sync)> = Vec::with_capacity(1 + extra_signers.len());
565 signers.push(fee_payer);
566 for signer in extra_signers {
567 if signer.pubkey() != fee_payer.pubkey() {
568 signers.push(*signer);
569 }
570 }
571 sign_transaction(&mut tx, &signers, recent)?;
572 let signature = self.rpc.send_transaction(&tx).await?;
573 Ok((signature, recent))
574 }
575 async fn confirm_submitted(
576 &self,
577 signature: Signature,
578 blockhash: solana_sdk::hash::Hash,
579 ) -> Result<Signature, ChainError> {
580 #[cfg(feature = "ws-client")]
581 if let Some(url) = &self.confirmation_ws_url {
582 let statuses = self
583 .confirm_signatures(url, &[signature], std::time::Duration::from_secs(60))
584 .await?;
585 return match statuses.as_slice() {
586 [super::ConfirmationStatus::Confirmed] => Ok(signature),
587 [super::ConfirmationStatus::Failed(error)] => Err(ChainError::Confirmation(
588 format!("transaction {signature} failed: {error}"),
589 )),
590 _ => Err(ChainError::Confirmation(format!(
591 "transaction {signature} confirmation unresolved"
592 ))),
593 };
594 }
595 self.rpc
596 .confirm_transaction_with_spinner(&signature, &blockhash, self.commitment)
597 .await?;
598 Ok(signature)
599 }
600}
601
602fn sign_transaction(
603 transaction: &mut Transaction,
604 signers: &[&(dyn Signer + Sync)],
605 recent_blockhash: solana_sdk::hash::Hash,
606) -> Result<(), ChainError> {
607 transaction.try_sign(signers, recent_blockhash)?;
608 Ok(())
609}
610
611fn ensure_supported_token_program(program: Pubkey) -> Result<Pubkey, ChainError> {
612 if program == TOKEN_PROGRAM_ID || program == TOKEN_2022_PROGRAM_ID {
613 Ok(program)
614 } else {
615 Err(ChainError::InvalidAccountData)
616 }
617}
618
619#[cfg(test)]
620#[path = "rpc_tests.rs"]
621mod tests;
622
623#[cfg(test)]
624#[path = "rpc_mock.rs"]
625mod mock;
626
627fn shared_sender(url: &str) -> std::sync::Arc<solana_rpc_client::http_sender::HttpSender> {
628 static SENDERS: std::sync::LazyLock<
629 std::sync::Mutex<
630 std::collections::HashMap<
631 String,
632 std::sync::Arc<solana_rpc_client::http_sender::HttpSender>,
633 >,
634 >,
635 > = std::sync::LazyLock::new(Default::default);
636 let mut senders = match SENDERS.lock() {
637 Ok(s) => s,
638 Err(p) => p.into_inner(),
639 };
640 senders
641 .entry(url.to_owned())
642 .or_insert_with(|| {
643 std::sync::Arc::new(solana_rpc_client::http_sender::HttpSender::new(url))
644 })
645 .clone()
646}
647struct ObservedSender(std::sync::Arc<solana_rpc_client::http_sender::HttpSender>);
648#[async_trait::async_trait]
649impl solana_rpc_client::rpc_sender::RpcSender for ObservedSender {
650 async fn send(
651 &self,
652 request: solana_rpc_client_api::request::RpcRequest,
653 params: serde_json::Value,
654 ) -> solana_rpc_client_api::client_error::Result<serde_json::Value> {
655 crate::io_observer::record(crate::io_observer::Event::Rpc {
656 method: &request.to_string(),
657 });
658 self.0.send(request, params).await
659 }
660 fn get_transport_stats(&self) -> solana_rpc_client::rpc_sender::RpcTransportStats {
661 self.0.get_transport_stats()
662 }
663 fn url(&self) -> String {
664 self.0.url()
665 }
666}