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_MAKER_OWNER: 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_MAKER_OWNER,
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_MAKER_OWNER..POSITION_OFFSET_MAKER_OWNER + 32]
114 .copy_from_slice(maker.as_ref());
115 account(program, data)
116 }
117
118 fn request(
119 program: Pubkey,
120 kind: VaultRequestKind,
121 vault: Pubkey,
122 user: Pubkey,
123 amount: u64,
124 ) -> Account {
125 let mut data = vec![0; VaultRequest::LEN];
126 data[..2].copy_from_slice(&[kind.discriminator(), 1]);
127 data[8..40].copy_from_slice(user.as_ref());
128 data[40..72].copy_from_slice(vault.as_ref());
129 data[72..80].copy_from_slice(&amount.to_le_bytes());
130 account(program, data)
131 }
132
133 #[tokio::test]
134 async fn unfunded_positions_are_the_vaults_open_maker_positions_on_chain() {
135 let (program, vault) = (Pubkey::new_unique(), Pubkey::new_unique());
136 let open = Pubkey::new_unique();
137 let chain = ChainClient::with_accounts(
138 program,
139 [
140 (open, position(program, vault, POSITION_OPEN, 2)),
141 (Pubkey::new_unique(), position(program, vault, 2, 2)),
142 (
143 Pubkey::new_unique(),
144 position(program, Pubkey::new_unique(), POSITION_OPEN, 2),
145 ),
146 (
147 Pubkey::new_unique(),
148 position(Pubkey::new_unique(), vault, POSITION_OPEN, 2),
149 ),
150 ],
151 );
152 let unfunded = chain.unfunded_positions(&vault).await.unwrap();
153 let [(address, info)] = unfunded.as_slice() else {
154 panic!("one open maker position: {unfunded:?}");
155 };
156 assert_eq!(*address, open);
157 assert_eq!(info.status, PositionStatus::Open);
158 assert_eq!(info.maker_owner, vault);
159
160 let stale_layout = ChainClient::with_accounts(
161 program,
162 [(
163 Pubkey::new_unique(),
164 position(program, vault, POSITION_OPEN, 1),
165 )],
166 );
167 assert!(matches!(
168 stale_layout.unfunded_positions(&vault).await,
169 Err(ChainError::InvalidAccountData)
170 ));
171 }
172
173 #[tokio::test]
174 async fn pending_capital_requests_are_the_vaults_nonzero_requests_on_chain() {
175 let (program, vault) = (Pubkey::new_unique(), Pubkey::new_unique());
176 let (first, second) = (Pubkey::new_unique(), Pubkey::new_unique());
177 let chain = ChainClient::with_accounts(
178 program,
179 [
180 (
181 Pubkey::new_unique(),
182 request(program, VaultRequestKind::Deposit, vault, first, 500),
183 ),
184 (
185 Pubkey::new_unique(),
186 request(program, VaultRequestKind::Deposit, vault, second, 0),
187 ),
188 (
189 Pubkey::new_unique(),
190 request(
191 program,
192 VaultRequestKind::Deposit,
193 Pubkey::new_unique(),
194 second,
195 9,
196 ),
197 ),
198 (
199 Pubkey::new_unique(),
200 request(program, VaultRequestKind::Withdraw, vault, second, 40),
201 ),
202 ],
203 );
204 assert_eq!(
205 chain.pending_capital_requests(&vault).await.unwrap(),
206 PendingCapitalRequests {
207 deposits: vec![(first, 500)],
208 withdrawals: vec![(second, 40)],
209 }
210 );
211
212 let mut malformed = request(program, VaultRequestKind::Withdraw, vault, first, 1);
213 malformed.data[1] = 9;
214 let chain = ChainClient::with_accounts(program, [(Pubkey::new_unique(), malformed)]);
215 assert!(matches!(
216 chain.pending_capital_requests(&vault).await,
217 Err(ChainError::Vault(_))
218 ));
219 }
220}