Skip to main content

acta_maker_sdk/chain/vault/
scan.rs

1use 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/// Queued capital requests: (user, main-mint atoms) deposits and (user, shares) withdrawals.
14#[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    /// Positions the vault bought as a maker that still await
22    /// `FundPositionForVaultHolder`, read from the program's accounts.
23    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    /// The vault's nonzero queued deposits and withdrawals, read from the program's accounts.
53    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}