Skip to main content

acta_maker_sdk/chain/
rpc.rs

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    /// Fetch and parse a position account. Returns full position details for pre-flight validation.
292    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    /// Fetch the raw token balance (u64 atoms) of a token account.
310    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;