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