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