acta_maker_sdk/chain/vault/
scan.rs1use solana_rpc_client::api::filter::{Memcmp, RpcFilterType};
2use solana_sdk::pubkey::Pubkey;
3
4use super::{VaultRequest, VaultRequestKind};
5use crate::chain::{ChainClient, ChainError, PositionInfo};
6
7const POSITION_DISCRIMINATOR: u8 = 1;
8const POSITION_LEN: u64 = 424;
9const POSITION_OFFSET_STATUS: usize = 4;
10const POSITION_OFFSET_HOLDER: usize = 40;
11const POSITION_OPEN: u8 = 1;
12
13#[derive(Debug, Clone, Default, PartialEq, Eq)]
15pub struct PendingCapitalRequests {
16 pub deposits: Vec<(Pubkey, u64)>,
17 pub withdrawals: Vec<(Pubkey, u64)>,
18}
19
20impl ChainClient {
21 pub async fn unfunded_positions(
24 &self,
25 vault: &Pubkey,
26 ) -> Result<Vec<(Pubkey, PositionInfo)>, ChainError> {
27 let accounts = self
28 .program_accounts(vec![
29 RpcFilterType::DataSize(POSITION_LEN),
30 RpcFilterType::Memcmp(Memcmp::new_base58_encoded(0, &[POSITION_DISCRIMINATOR])),
31 RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
32 POSITION_OFFSET_STATUS,
33 &[POSITION_OPEN],
34 )),
35 RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
36 POSITION_OFFSET_HOLDER,
37 vault.as_ref(),
38 )),
39 ])
40 .await?;
41 accounts
42 .into_iter()
43 .map(|(address, account)| {
44 Ok((
45 address,
46 PositionInfo::from_account(&account, &self.program_id())?,
47 ))
48 })
49 .collect()
50 }
51
52 pub async fn pending_capital_requests(
54 &self,
55 vault: &Pubkey,
56 ) -> Result<PendingCapitalRequests, ChainError> {
57 Ok(PendingCapitalRequests {
58 deposits: self.requests(vault, VaultRequestKind::Deposit).await?,
59 withdrawals: self.requests(vault, VaultRequestKind::Withdraw).await?,
60 })
61 }
62
63 async fn requests(
64 &self,
65 vault: &Pubkey,
66 kind: VaultRequestKind,
67 ) -> Result<Vec<(Pubkey, u64)>, ChainError> {
68 let accounts = self
69 .program_accounts(vec![
70 RpcFilterType::DataSize(VaultRequest::LEN as u64),
71 RpcFilterType::Memcmp(Memcmp::new_base58_encoded(0, &[kind.discriminator()])),
72 RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
73 VaultRequest::VAULT_OFFSET,
74 vault.as_ref(),
75 )),
76 ])
77 .await?;
78 let mut requests = Vec::with_capacity(accounts.len());
79 for (_, account) in accounts {
80 let request = VaultRequest::from_account(&account, &self.program_id(), kind)?;
81 if request.vault != *vault {
82 return Err(ChainError::Identity("request vault"));
83 }
84 if request.amount > 0 {
85 requests.push((request.user, request.amount));
86 }
87 }
88 Ok(requests)
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use solana_sdk::account::Account;
95
96 use super::*;
97 use crate::chain::PositionStatus;
98
99 fn account(owner: Pubkey, data: Vec<u8>) -> Account {
100 Account {
101 lamports: 1,
102 data,
103 owner,
104 executable: false,
105 rent_epoch: 0,
106 }
107 }
108
109 fn position(program: Pubkey, maker: Pubkey, status: u8, version: u8) -> Account {
110 let mut data = vec![0; POSITION_LEN as usize];
111 data[..2].copy_from_slice(&[POSITION_DISCRIMINATOR, version]);
112 data[POSITION_OFFSET_STATUS] = status;
113 data[POSITION_OFFSET_HOLDER..POSITION_OFFSET_HOLDER + 32].copy_from_slice(maker.as_ref());
114 account(program, data)
115 }
116
117 fn request(
118 program: Pubkey,
119 kind: VaultRequestKind,
120 vault: Pubkey,
121 user: Pubkey,
122 amount: u64,
123 ) -> Account {
124 let mut data = vec![0; VaultRequest::LEN];
125 data[..2].copy_from_slice(&[kind.discriminator(), 1]);
126 data[8..40].copy_from_slice(user.as_ref());
127 data[40..72].copy_from_slice(vault.as_ref());
128 data[72..80].copy_from_slice(&amount.to_le_bytes());
129 account(program, data)
130 }
131
132 #[tokio::test]
133 async fn unfunded_positions_are_the_vaults_open_maker_positions_on_chain() {
134 let (program, vault) = (Pubkey::new_unique(), Pubkey::new_unique());
135 let open = Pubkey::new_unique();
136 let chain = ChainClient::with_accounts(
137 program,
138 [
139 (open, position(program, vault, POSITION_OPEN, 2)),
140 (Pubkey::new_unique(), position(program, vault, 2, 2)),
141 (
142 Pubkey::new_unique(),
143 position(program, Pubkey::new_unique(), POSITION_OPEN, 2),
144 ),
145 (
146 Pubkey::new_unique(),
147 position(Pubkey::new_unique(), vault, POSITION_OPEN, 2),
148 ),
149 ],
150 );
151 let unfunded = chain.unfunded_positions(&vault).await.unwrap();
152 let [(address, info)] = unfunded.as_slice() else {
153 panic!("one open maker position: {unfunded:?}");
154 };
155 assert_eq!(*address, open);
156 assert_eq!(info.status, PositionStatus::Open);
157 assert_eq!(info.holder, vault);
158
159 let stale_layout = ChainClient::with_accounts(
160 program,
161 [(
162 Pubkey::new_unique(),
163 position(program, vault, POSITION_OPEN, 1),
164 )],
165 );
166 assert!(matches!(
167 stale_layout.unfunded_positions(&vault).await,
168 Err(ChainError::InvalidAccountData)
169 ));
170 }
171
172 #[tokio::test]
173 async fn pending_capital_requests_are_the_vaults_nonzero_requests_on_chain() {
174 let (program, vault) = (Pubkey::new_unique(), Pubkey::new_unique());
175 let (first, second) = (Pubkey::new_unique(), Pubkey::new_unique());
176 let chain = ChainClient::with_accounts(
177 program,
178 [
179 (
180 Pubkey::new_unique(),
181 request(program, VaultRequestKind::Deposit, vault, first, 500),
182 ),
183 (
184 Pubkey::new_unique(),
185 request(program, VaultRequestKind::Deposit, vault, second, 0),
186 ),
187 (
188 Pubkey::new_unique(),
189 request(
190 program,
191 VaultRequestKind::Deposit,
192 Pubkey::new_unique(),
193 second,
194 9,
195 ),
196 ),
197 (
198 Pubkey::new_unique(),
199 request(program, VaultRequestKind::Withdraw, vault, second, 40),
200 ),
201 ],
202 );
203 assert_eq!(
204 chain.pending_capital_requests(&vault).await.unwrap(),
205 PendingCapitalRequests {
206 deposits: vec![(first, 500)],
207 withdrawals: vec![(second, 40)],
208 }
209 );
210
211 let mut malformed = request(program, VaultRequestKind::Withdraw, vault, first, 1);
212 malformed.data[1] = 9;
213 let chain = ChainClient::with_accounts(program, [(Pubkey::new_unique(), malformed)]);
214 assert!(matches!(
215 chain.pending_capital_requests(&vault).await,
216 Err(ChainError::Vault(_))
217 ));
218 }
219}