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_MAKER_OWNER: 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    /// `VaultFundMakerPosition`, 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_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    /// 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_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}