Skip to main content

acta_maker_sdk/chain/
batch.rs

1#[cfg(feature = "chain-rpc")]
2use std::collections::BTreeMap;
3
4use solana_sdk::{account::Account, instruction::Instruction, pubkey::Pubkey};
5
6use super::{ChainError, MarketInfo, PositionInfo, PositionStatus, ix};
7
8#[derive(Debug, Clone)]
9pub struct PositionSnapshot {
10    pub address: Pubkey,
11    pub position: PositionInfo,
12    pub market_address: Pubkey,
13    pub market: MarketInfo,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
17pub struct PremiumToken {
18    pub mint: Pubkey,
19    pub token_program: Pubkey,
20}
21
22#[derive(Debug, Clone)]
23pub struct PremiumBalance {
24    pub token: PremiumToken,
25    pub address: Pubkey,
26    /// None means the ATA does not exist, not a failed read.
27    pub amount: Option<u64>,
28}
29
30#[derive(Debug, Clone)]
31pub struct MakerSnapshot {
32    pub positions: Vec<PositionSnapshot>,
33    pub premium_balances: Vec<PremiumBalance>,
34    pub missing_positions: Vec<Pubkey>,
35}
36
37impl PositionSnapshot {
38    pub fn settlement_token(&self) -> PremiumToken {
39        match self.position.terms.position_type() {
40            crate::PositionType::CoveredCall
41            | crate::PositionType::CallSpread
42            | crate::PositionType::PutSpread
43            | crate::PositionType::CashPut => PremiumToken {
44                mint: self.market.quote_mint,
45                token_program: self.market.quote_token_program_id,
46            },
47            crate::PositionType::CashSecuredPut => PremiumToken {
48                mint: self.market.underlying_mint,
49                token_program: self.market.underlying_token_program_id,
50            },
51        }
52    }
53
54    pub fn required_settlement(&self) -> Result<u64, ChainError> {
55        super::settlement::required_settlement(
56            self.position.terms.to_chain().position_type().into(),
57            self.position.terms.strike().value(),
58            self.position.quantity.value(),
59            self.market.quote_decimals.value(),
60            self.market.underlying_decimals.value(),
61        )
62    }
63
64    /// Pure builder. Prepend explicit wrapping instructions when needed.
65    pub fn fund_instructions(
66        &self,
67        program_id: &Pubkey,
68        maker_owner: Pubkey,
69        fee_payer: Pubkey,
70        create_atas: bool,
71    ) -> Result<Vec<Instruction>, ChainError> {
72        if self.position.side.is_taker_buy() {
73            return Err(ChainError::TakerBuyUnsupported);
74        }
75        if self.market_address != self.position.market_pda {
76            return Err(ChainError::Identity("market"));
77        }
78        if self.position.holder != maker_owner {
79            return Err(ChainError::Identity("maker owner"));
80        }
81        if self.position.status != PositionStatus::Open
82            || self.market.is_put
83                != matches!(
84                    self.position.terms.position_type(),
85                    crate::PositionType::CashSecuredPut
86                )
87        {
88            return Err(ChainError::InvalidAccountData);
89        }
90        Ok(ix::build_fund_position_ixs(
91            program_id,
92            &ix::FundPositionIxArgs {
93                holder: maker_owner,
94                position_pda: self.address,
95                market_pda: self.position.market_pda,
96                position_type: self.position.terms.to_chain().position_type().into(),
97                underlying_mint: self.market.underlying_mint,
98                quote_mint: self.market.quote_mint,
99                underlying_token_program_id: self.market.underlying_token_program_id,
100                quote_token_program_id: self.market.quote_token_program_id,
101                create_atas,
102            },
103            fee_payer,
104        )?)
105    }
106}
107
108pub fn decode_premium_balance(
109    account: &Account,
110    owner: &Pubkey,
111    token: PremiumToken,
112) -> Result<u64, ChainError> {
113    if ![ix::TOKEN_PROGRAM_ID, ix::TOKEN_2022_PROGRAM_ID].contains(&token.token_program)
114        || account.owner != token.token_program
115        || account.data.len() < 165
116        || account.data[..32] != token.mint.to_bytes()
117        || account.data[32..64] != owner.to_bytes()
118        || account.data[108] != 1
119    {
120        return Err(ChainError::InvalidAccountData);
121    }
122    Ok(u64::from_le_bytes(
123        account.data[64..72]
124            .try_into()
125            .map_err(|_| ChainError::InvalidAccountData)?,
126    ))
127}
128
129#[cfg(feature = "chain-rpc")]
130mod rpc {
131    use super::super::rpc::ChainClient;
132    use super::*;
133    use solana_rpc_client::api::filter::{Memcmp, RpcFilterType};
134
135    pub fn maker_position_filters(owner: &Pubkey) -> Vec<RpcFilterType> {
136        use super::super::accounts::{
137            POSITION_DISCRIMINATOR, POSITION_LEN, POSITION_OFFSET_HOLDER,
138        };
139        vec![
140            RpcFilterType::DataSize(POSITION_LEN as u64),
141            RpcFilterType::Memcmp(Memcmp::new_base58_encoded(0, &[POSITION_DISCRIMINATOR])),
142            RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
143                POSITION_OFFSET_HOLDER,
144                owner.as_ref(),
145            )),
146        ]
147    }
148
149    impl ChainClient {
150        /// Chunked reads preserve input order, including missing accounts.
151        pub async fn fetch_accounts(
152            &self,
153            keys: &[Pubkey],
154        ) -> Result<Vec<Option<Account>>, ChainError> {
155            let mut accounts = Vec::with_capacity(keys.len());
156            for chunk in keys.chunks(100) {
157                let values = self
158                    .rpc()
159                    .get_multiple_accounts_with_commitment(chunk, self.rpc().commitment())
160                    .await?
161                    .value;
162                if values.len() != chunk.len() {
163                    return Err(ChainError::InvalidAccountData);
164                }
165                accounts.extend(values);
166            }
167            Ok(accounts)
168        }
169
170        /// Startup/manual recovery only. Includes Open and Funded positions; absence is not proof of closure.
171        pub async fn maker_open_positions(
172            &self,
173            owner: &Pubkey,
174        ) -> Result<Vec<(Pubkey, PositionInfo)>, ChainError> {
175            let mut result = Vec::new();
176            for (key, account) in self.program_accounts(maker_position_filters(owner)).await? {
177                let position = PositionInfo::from_account(&account, &self.program_id())?;
178                if position.holder != *owner {
179                    return Err(ChainError::Identity("maker owner"));
180                }
181                if matches!(
182                    position.status,
183                    PositionStatus::Open | PositionStatus::Funded
184                ) {
185                    result.push((key, position));
186                }
187            }
188            Ok(result)
189        }
190
191        /// Two dependency rounds: positions + known premium ATAs, then unique markets.
192        /// Chunks may observe different slots; this is not an atomic chain snapshot.
193        pub async fn maker_snapshot(
194            &self,
195            owner: &Pubkey,
196            positions: &[Pubkey],
197            premiums: &[PremiumToken],
198        ) -> Result<MakerSnapshot, ChainError> {
199            if premiums.iter().any(|token| {
200                ![ix::TOKEN_PROGRAM_ID, ix::TOKEN_2022_PROGRAM_ID].contains(&token.token_program)
201            }) {
202                return Err(ChainError::InvalidAccountData);
203            }
204            let maker = ix::maker_pda_with_program_id(&self.program_id(), owner).0;
205            let mut keys = positions.to_vec();
206            keys.extend(
207                premiums.iter().map(|t| {
208                    ix::derive_associated_token_address(&maker, &t.mint, &t.token_program)
209                }),
210            );
211            keys.sort_unstable();
212            keys.dedup();
213            let accounts: BTreeMap<_, _> = keys
214                .iter()
215                .copied()
216                .zip(self.fetch_accounts(&keys).await?)
217                .collect();
218            let mut decoded = Vec::new();
219            let mut missing_positions = Vec::new();
220            for key in positions {
221                match &accounts[key] {
222                    Some(account) => {
223                        let position = PositionInfo::from_account(account, &self.program_id())?;
224                        if position.holder != *owner {
225                            return Err(ChainError::Identity("maker owner"));
226                        }
227                        decoded.push((*key, position));
228                    }
229                    None => missing_positions.push(*key),
230                }
231            }
232            let mut market_keys: Vec<_> = decoded.iter().map(|(_, p)| p.market_pda).collect();
233            market_keys.sort_unstable();
234            market_keys.dedup();
235            let markets: BTreeMap<_, _> = market_keys
236                .iter()
237                .copied()
238                .zip(self.fetch_accounts(&market_keys).await?)
239                .map(|(key, account)| {
240                    let account = account.ok_or(ChainError::MissingAccount(key))?;
241                    Ok((key, MarketInfo::from_account(&account, &self.program_id())?))
242                })
243                .collect::<Result<_, ChainError>>()?;
244            let premium_balances = premiums
245                .iter()
246                .map(|token| {
247                    let address = ix::derive_associated_token_address(
248                        &maker,
249                        &token.mint,
250                        &token.token_program,
251                    );
252                    let amount = accounts[&address]
253                        .as_ref()
254                        .map(|a| decode_premium_balance(a, &maker, *token))
255                        .transpose()?;
256                    Ok(PremiumBalance {
257                        token: *token,
258                        address,
259                        amount,
260                    })
261                })
262                .collect::<Result<_, ChainError>>()?;
263            Ok(MakerSnapshot {
264                positions: decoded
265                    .into_iter()
266                    .map(|(address, position)| PositionSnapshot {
267                        market_address: position.market_pda,
268                        market: markets[&position.market_pda].clone(),
269                        address,
270                        position,
271                    })
272                    .collect(),
273                premium_balances,
274                missing_positions,
275            })
276        }
277    }
278}
279#[cfg(feature = "chain-rpc")]
280pub use rpc::maker_position_filters;
281
282#[cfg(all(test, feature = "chain-rpc"))]
283#[path = "batch_tests.rs"]
284mod tests;