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 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 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.maker_owner != 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 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_MAKER_OWNER,
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_MAKER_OWNER,
144 owner.as_ref(),
145 )),
146 ]
147 }
148
149 impl ChainClient {
150 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 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.maker_owner != *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 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.maker_owner != *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;