1use solana_commitment_config::CommitmentConfig;
2use solana_rpc_client::nonblocking::rpc_client::RpcClient;
3use solana_sdk::{
4 account::Account, instruction::Instruction, pubkey::Pubkey, signature::Signature,
5 signer::Signer, transaction::Transaction,
6};
7
8use crate::chain::ChainError;
9use crate::chain::accounts::{MarketInfo, PositionInfo, parse_market, parse_position};
10use crate::chain::ix::{
11 DepositPremiumIxArgs, FundPositionIxArgs, WithdrawPremiumIxArgs, build_deposit_premium_ixs,
12 build_fund_position_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
20pub struct ChainClient {
21 rpc: RpcClient,
22 program_id: Pubkey,
23 commitment: CommitmentConfig,
24}
25
26#[derive(Debug, Clone, Copy, Default)]
27pub struct SendOptions {
28 pub compute_unit_limit: Option<u32>,
29}
30
31#[derive(Debug, Clone)]
32pub struct DepositPremiumArgs {
33 pub maker_owner: Pubkey,
34 pub amount: u64,
35 pub premium_mint: Pubkey,
36 pub token_program: Option<Pubkey>,
37 pub create_atas: bool,
38}
39
40#[derive(Debug, Clone)]
41pub struct WithdrawPremiumArgs {
42 pub maker_owner: Pubkey,
43 pub amount: u64,
44 pub premium_mint: Pubkey,
45 pub token_program: Option<Pubkey>,
46 pub create_atas: bool,
47}
48
49#[derive(Debug, Clone)]
50pub struct FundPositionArgs {
51 pub maker_owner: Pubkey,
52 pub position_pda: Pubkey,
53 pub create_atas: bool,
54 pub native_sol: NativeSolFunding,
55}
56
57struct PreparedFundPosition {
58 instructions: Vec<Instruction>,
59 native_budget: Option<NativeSolBudget>,
60}
61
62impl ChainClient {
63 pub fn program_id(&self) -> Pubkey {
64 self.program_id
65 }
66
67 pub fn new(rpc_url: &str, program_id: Pubkey, commitment: CommitmentConfig) -> Self {
68 let rpc = RpcClient::new_with_commitment(rpc_url.to_string(), commitment);
69 Self {
70 rpc,
71 program_id,
72 commitment,
73 }
74 }
75
76 pub async fn deposit_premium(
77 &self,
78 args: DepositPremiumArgs,
79 maker_owner: &(dyn Signer + Sync),
80 fee_payer: Option<&(dyn Signer + Sync)>,
81 ) -> Result<Signature, ChainError> {
82 let payer = fee_payer.unwrap_or(maker_owner);
83 let ixs = self
84 .build_deposit_premium_ixs(&args, payer.pubkey())
85 .await?;
86 self.send_instructions_with_options(ixs, payer, &[maker_owner], SendOptions::default())
87 .await
88 }
89
90 pub async fn withdraw_premium(
91 &self,
92 args: WithdrawPremiumArgs,
93 maker_owner: &(dyn Signer + Sync),
94 fee_payer: Option<&(dyn Signer + Sync)>,
95 ) -> Result<Signature, ChainError> {
96 let payer = fee_payer.unwrap_or(maker_owner);
97 let ixs = self
98 .build_withdraw_premium_ixs(&args, payer.pubkey())
99 .await?;
100 self.send_instructions_with_options(ixs, payer, &[maker_owner], SendOptions::default())
101 .await
102 }
103
104 pub async fn fund_position(
105 &self,
106 args: FundPositionArgs,
107 maker_owner: &(dyn Signer + Sync),
108 fee_payer: Option<&(dyn Signer + Sync)>,
109 ) -> Result<Signature, ChainError> {
110 let payer = fee_payer.unwrap_or(maker_owner);
111 let prepared = self.prepare_fund_position(&args, payer.pubkey()).await?;
112 self.send_instructions(
113 prepared.instructions,
114 payer,
115 &[maker_owner],
116 SendOptions::default(),
117 prepared.native_budget,
118 )
119 .await
120 }
121
122 pub async fn build_deposit_premium_ixs(
123 &self,
124 args: &DepositPremiumArgs,
125 fee_payer: Pubkey,
126 ) -> Result<Vec<Instruction>, ChainError> {
127 let token_program = self
128 .resolve_token_program(&args.premium_mint, args.token_program)
129 .await?;
130 build_deposit_premium_ixs(
131 &self.program_id,
132 &DepositPremiumIxArgs {
133 maker_owner: args.maker_owner,
134 amount: args.amount,
135 premium_mint: args.premium_mint,
136 token_program,
137 create_atas: args.create_atas,
138 },
139 fee_payer,
140 )
141 .map_err(ChainError::from)
142 }
143
144 pub async fn build_withdraw_premium_ixs(
145 &self,
146 args: &WithdrawPremiumArgs,
147 fee_payer: Pubkey,
148 ) -> Result<Vec<Instruction>, ChainError> {
149 let token_program = self
150 .resolve_token_program(&args.premium_mint, args.token_program)
151 .await?;
152 build_withdraw_premium_ixs(
153 &self.program_id,
154 &WithdrawPremiumIxArgs {
155 maker_owner: args.maker_owner,
156 amount: args.amount,
157 premium_mint: args.premium_mint,
158 token_program,
159 create_atas: args.create_atas,
160 },
161 fee_payer,
162 )
163 .map_err(ChainError::from)
164 }
165
166 pub async fn build_fund_position_ixs(
167 &self,
168 args: &FundPositionArgs,
169 fee_payer: Pubkey,
170 ) -> Result<Vec<Instruction>, ChainError> {
171 Ok(self
172 .prepare_fund_position(args, fee_payer)
173 .await?
174 .instructions)
175 }
176
177 async fn prepare_fund_position(
178 &self,
179 args: &FundPositionArgs,
180 fee_payer: Pubkey,
181 ) -> Result<PreparedFundPosition, ChainError> {
182 let position_data = self.fetch_account(&args.position_pda).await?;
183 let pos = parse_position(&position_data, &self.program_id)?;
184 let market_data = self.fetch_account(&pos.market_pda).await?;
185 let market = parse_market(&market_data, &self.program_id)?;
186
187 let mut fund_ixs = build_fund_position_ixs(
188 &self.program_id,
189 &FundPositionIxArgs {
190 maker_owner: args.maker_owner,
191 position_pda: args.position_pda,
192 market_pda: pos.market_pda,
193 position_type: pos.position_type.into(),
194 underlying_mint: market.underlying_mint,
195 quote_mint: market.quote_mint,
196 underlying_token_program_id: market.underlying_token_program_id,
197 quote_token_program_id: market.quote_token_program_id,
198 create_atas: args.create_atas,
199 },
200 fee_payer,
201 )
202 .map_err(ChainError::from)?;
203
204 let NativeSolFunding::WrapIfNeeded { reserve_lamports } = args.native_sol else {
205 return Ok(PreparedFundPosition {
206 instructions: fund_ixs,
207 native_budget: None,
208 });
209 };
210 let (settlement_mint, settlement_program) = match pos.position_type {
211 crate::types::PositionType::CoveredCall => {
212 (market.quote_mint, market.quote_token_program_id)
213 }
214 crate::types::PositionType::CashSecuredPut => {
215 (market.underlying_mint, market.underlying_token_program_id)
216 }
217 };
218 if !is_native_mint(&settlement_mint) {
219 return Ok(PreparedFundPosition {
220 instructions: fund_ixs,
221 native_budget: None,
222 });
223 }
224 let expected_settlement = required_settlement(
225 pos.position_type.into(),
226 pos.strike.value(),
227 pos.quantity.value(),
228 market.quote_decimals.value(),
229 market.underlying_decimals.value(),
230 )?;
231 if expected_settlement == 0 {
232 return Err(ChainError::InvalidAccountData);
233 }
234 let funding_ata = crate::chain::ix::derive_associated_token_address(
235 &args.maker_owner,
236 &settlement_mint,
237 &settlement_program,
238 );
239 let position_funding_ata = crate::chain::ix::derive_associated_token_address(
240 &args.position_pda,
241 &settlement_mint,
242 &settlement_program,
243 );
244 let additional_ata_rent_lamports = if args.create_atas
245 && fee_payer == args.maker_owner
246 && self
247 .rpc
248 .get_account_with_commitment(&position_funding_ata, self.commitment)
249 .await?
250 .value
251 .is_none()
252 {
253 token_account_rent_lamports(&self.rpc).await?
254 } else {
255 0
256 };
257 match plan_wsol_wrap(
258 &self.rpc,
259 WsolWrapRequest {
260 owner: args.maker_owner,
261 funding_ata,
262 token_program: settlement_program,
263 expected_settlement,
264 reserve_lamports,
265 allow_account_creation: args.create_atas,
266 fee_payer,
267 additional_ata_rent_lamports,
268 },
269 )
270 .await?
271 {
272 WrapPlan::NotNeeded => Ok(PreparedFundPosition {
273 instructions: fund_ixs,
274 native_budget: None,
275 }),
276 WrapPlan::Wrap {
277 instructions: wrap_ixs,
278 budget,
279 } => {
280 let deposit_ix = fund_ixs.pop().ok_or(ChainError::InvalidAccountData)?;
281 fund_ixs.extend(wrap_ixs);
282 fund_ixs.push(deposit_ix);
283 Ok(PreparedFundPosition {
284 instructions: fund_ixs,
285 native_budget: Some(budget),
286 })
287 }
288 }
289 }
290
291 pub async fn fetch_position_info(
293 &self,
294 position_pda: &Pubkey,
295 ) -> Result<PositionInfo, ChainError> {
296 let data = self.fetch_account(position_pda).await?;
297 parse_position(&data, &self.program_id)
298 }
299
300 pub async fn fetch_market_info(&self, market_pda: &Pubkey) -> Result<MarketInfo, ChainError> {
301 let market_data = self.fetch_account(market_pda).await?;
302 parse_market(&market_data, &self.program_id)
303 }
304
305 pub async fn fetch_market_quote_mint(&self, market_pda: &Pubkey) -> Result<Pubkey, ChainError> {
306 Ok(self.fetch_market_info(market_pda).await?.quote_mint)
307 }
308
309 pub async fn fetch_token_balance(&self, token_account: &Pubkey) -> Result<u64, ChainError> {
311 let result = self.rpc.get_token_account_balance(token_account).await?;
312 result
313 .amount
314 .parse::<u64>()
315 .map_err(|_| ChainError::InvalidAccountData)
316 }
317
318 async fn resolve_token_program(
319 &self,
320 mint: &Pubkey,
321 token_program: Option<Pubkey>,
322 ) -> Result<Pubkey, ChainError> {
323 if let Some(program) = token_program {
324 return ensure_supported_token_program(program);
325 }
326 let account = self.rpc.get_account(mint).await?;
327 ensure_supported_token_program(account.owner)
328 }
329
330 async fn fetch_account(&self, pubkey: &Pubkey) -> Result<Account, ChainError> {
331 self.rpc.get_account(pubkey).await.map_err(ChainError::from)
332 }
333
334 pub async fn send_instructions_with_options(
335 &self,
336 instructions: Vec<Instruction>,
337 fee_payer: &(dyn Signer + Sync),
338 extra_signers: &[&(dyn Signer + Sync)],
339 options: SendOptions,
340 ) -> Result<Signature, ChainError> {
341 self.send_instructions(instructions, fee_payer, extra_signers, options, None)
342 .await
343 }
344
345 async fn send_instructions(
346 &self,
347 mut instructions: Vec<Instruction>,
348 fee_payer: &(dyn Signer + Sync),
349 extra_signers: &[&(dyn Signer + Sync)],
350 options: SendOptions,
351 native_budget: Option<NativeSolBudget>,
352 ) -> Result<Signature, ChainError> {
353 if let Some(limit) = options.compute_unit_limit {
354 instructions.insert(
355 0,
356 solana_compute_budget_interface::ComputeBudgetInstruction::set_compute_unit_limit(
357 limit,
358 ),
359 );
360 }
361 let recent = self.rpc.get_latest_blockhash().await?;
362
363 let mut tx = Transaction::new_with_payer(&instructions, Some(&fee_payer.pubkey()));
364 tx.message.recent_blockhash = recent;
365 if let Some(budget) = native_budget {
366 let fee_lamports = if fee_payer.pubkey() == budget.owner {
367 self.rpc.get_fee_for_message(&tx.message).await?
368 } else {
369 0
370 };
371 let available_lamports = self.rpc.get_balance(&budget.owner).await?;
372 budget.validate(available_lamports, fee_lamports)?;
373 }
374 let mut signers: Vec<&(dyn Signer + Sync)> = Vec::with_capacity(1 + extra_signers.len());
375 signers.push(fee_payer);
376 for signer in extra_signers {
377 if signer.pubkey() != fee_payer.pubkey() {
378 signers.push(*signer);
379 }
380 }
381 sign_transaction(&mut tx, &signers, recent)?;
382 self.rpc
383 .send_and_confirm_transaction_with_spinner_and_commitment(&tx, self.commitment)
384 .await
385 .map_err(ChainError::from)
386 }
387}
388
389fn sign_transaction(
390 transaction: &mut Transaction,
391 signers: &[&(dyn Signer + Sync)],
392 recent_blockhash: solana_sdk::hash::Hash,
393) -> Result<(), ChainError> {
394 transaction.try_sign(signers, recent_blockhash)?;
395 Ok(())
396}
397
398const TOKEN_PROGRAM_ID: Pubkey = solana_sdk::pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
399const TOKEN_2022_PROGRAM_ID: Pubkey =
400 solana_sdk::pubkey!("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb");
401
402fn ensure_supported_token_program(program: Pubkey) -> Result<Pubkey, ChainError> {
403 if program == TOKEN_PROGRAM_ID || program == TOKEN_2022_PROGRAM_ID {
404 Ok(program)
405 } else {
406 Err(ChainError::InvalidAccountData)
407 }
408}
409
410#[cfg(test)]
411#[path = "rpc_tests.rs"]
412mod tests;