Skip to main content

pot_o_extensions/
defi.rs

1//! HTTP API support for tribewarez-staking, tribewarez-swap, tribewarez-vault.
2//! Fetches on-chain account data via Solana RPC and returns JSON-serializable types.
3
4use pot_o_core::TribeResult;
5use serde::Serialize;
6use solana_client::rpc_client::RpcClient;
7use solana_sdk::pubkey::Pubkey;
8use std::str::FromStr;
9
10// Program IDs (from declare_id! in each program)
11pub const STAKING_PROGRAM_ID: &str = "Go2BZRhNLoaVni3QunrKPAXYdHtwZtTXuVspxpdAeDS8";
12pub const SWAP_PROGRAM_ID: &str = "GPGGnKwnvKseSxzPukrNvch1CwYhifTqgj2RdW1P26H3";
13pub const VAULT_PROGRAM_ID: &str = "HmWGA3JAF6basxGCvvGNHAdTBE3qCPhJCeFJAd7r5ra9";
14
15const ANCHOR_DISCRIMINATOR_LEN: usize = 8;
16
17fn pubkey_to_string(p: &Pubkey) -> String {
18    p.to_string()
19}
20
21// ---------------------------------------------------------------------------
22// Staking (mirror account layouts for Borsh deserialize after 8-byte discriminator)
23// ---------------------------------------------------------------------------
24
25#[derive(Debug, Clone, Serialize)]
26pub struct StakingPoolInfo {
27    pub pubkey: String,
28    pub authority: String,
29    pub token_mint: String,
30    pub reward_mint: String,
31    pub pool_token_account: String,
32    pub reward_token_account: String,
33    pub reward_rate: u64,
34    pub lock_duration: i64,
35    pub total_staked: u64,
36    pub total_rewards_distributed: u64,
37    pub is_active: bool,
38    pub created_at: i64,
39}
40
41#[derive(Debug, Clone, Serialize)]
42pub struct StakeAccountInfo {
43    pub pubkey: String,
44    pub owner: String,
45    pub pool: String,
46    pub amount: u64,
47    pub stake_time: i64,
48    pub unlock_time: i64,
49    pub last_reward_time: i64,
50    pub pending_rewards: u64,
51    pub total_rewards_claimed: u64,
52}
53
54fn parse_staking_pool(pubkey: &Pubkey, data: &[u8]) -> TribeResult<StakingPoolInfo> {
55    let data = data
56        .get(ANCHOR_DISCRIMINATOR_LEN..)
57        .ok_or_else(|| pot_o_core::TribeError::ChainBridgeError("account data too short".into()))?;
58    if data.len() < 32 * 5 + 8 * 5 + 2 {
59        return Err(pot_o_core::TribeError::ChainBridgeError(
60            "staking pool account data too short".into(),
61        ));
62    }
63    let mut off = 0;
64    let read_pubkey = |off: &mut usize| {
65        let slice: [u8; 32] = data[*off..*off + 32].try_into().unwrap();
66        *off += 32;
67        Pubkey::new_from_array(slice)
68    };
69    let authority = read_pubkey(&mut off);
70    let token_mint = read_pubkey(&mut off);
71    let reward_mint = read_pubkey(&mut off);
72    let pool_token_account = read_pubkey(&mut off);
73    let reward_token_account = read_pubkey(&mut off);
74    let reward_rate = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
75    off += 8;
76    let lock_duration = i64::from_le_bytes(data[off..off + 8].try_into().unwrap());
77    off += 8;
78    let total_staked = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
79    off += 8;
80    let total_rewards_distributed = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
81    off += 8;
82    let bump = data[off];
83    off += 1;
84    let _ = bump;
85    let is_active = data[off] != 0;
86    off += 1;
87    let created_at = i64::from_le_bytes(data[off..off + 8].try_into().unwrap());
88    Ok(StakingPoolInfo {
89        pubkey: pubkey_to_string(pubkey),
90        authority: pubkey_to_string(&authority),
91        token_mint: pubkey_to_string(&token_mint),
92        reward_mint: pubkey_to_string(&reward_mint),
93        pool_token_account: pubkey_to_string(&pool_token_account),
94        reward_token_account: pubkey_to_string(&reward_token_account),
95        reward_rate,
96        lock_duration,
97        total_staked,
98        total_rewards_distributed,
99        is_active,
100        created_at,
101    })
102}
103
104fn parse_stake_account(pubkey: &Pubkey, data: &[u8]) -> TribeResult<StakeAccountInfo> {
105    let data = data
106        .get(ANCHOR_DISCRIMINATOR_LEN..)
107        .ok_or_else(|| pot_o_core::TribeError::ChainBridgeError("account data too short".into()))?;
108    if data.len() < 32 * 2 + 8 * 6 {
109        return Err(pot_o_core::TribeError::ChainBridgeError(
110            "stake account data too short".into(),
111        ));
112    }
113    let mut off = 0;
114    let read_pubkey = |off: &mut usize| {
115        let slice: [u8; 32] = data[*off..*off + 32].try_into().unwrap();
116        *off += 32;
117        Pubkey::new_from_array(slice)
118    };
119    let owner = read_pubkey(&mut off);
120    let pool = read_pubkey(&mut off);
121    let amount = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
122    off += 8;
123    let stake_time = i64::from_le_bytes(data[off..off + 8].try_into().unwrap());
124    off += 8;
125    let unlock_time = i64::from_le_bytes(data[off..off + 8].try_into().unwrap());
126    off += 8;
127    let last_reward_time = i64::from_le_bytes(data[off..off + 8].try_into().unwrap());
128    off += 8;
129    let pending_rewards = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
130    off += 8;
131    let total_rewards_claimed = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
132    Ok(StakeAccountInfo {
133        pubkey: pubkey_to_string(pubkey),
134        owner: pubkey_to_string(&owner),
135        pool: pubkey_to_string(&pool),
136        amount,
137        stake_time,
138        unlock_time,
139        last_reward_time,
140        pending_rewards,
141        total_rewards_claimed,
142    })
143}
144
145// ---------------------------------------------------------------------------
146// Swap
147// ---------------------------------------------------------------------------
148
149#[derive(Debug, Clone, Serialize)]
150pub struct LiquidityPoolInfo {
151    pub pubkey: String,
152    pub authority: String,
153    pub token_a_mint: String,
154    pub token_b_mint: String,
155    pub token_a_vault: String,
156    pub token_b_vault: String,
157    pub lp_mint: String,
158    pub reserve_a: u64,
159    pub reserve_b: u64,
160    pub total_lp_supply: u64,
161    pub swap_fee_bps: u64,
162    pub protocol_fee_bps: u64,
163    pub collected_fees_a: u64,
164    pub collected_fees_b: u64,
165    pub is_active: bool,
166    pub created_at: i64,
167}
168
169#[derive(Debug, Clone, Serialize)]
170pub struct SwapQuoteInfo {
171    pub pool: String,
172    pub amount_in: u64,
173    pub amount_out: u64,
174    pub fee: u64,
175    pub price_impact_bps: u64,
176}
177
178fn parse_liquidity_pool(pubkey: &Pubkey, data: &[u8]) -> TribeResult<LiquidityPoolInfo> {
179    let data = data
180        .get(ANCHOR_DISCRIMINATOR_LEN..)
181        .ok_or_else(|| pot_o_core::TribeError::ChainBridgeError("account data too short".into()))?;
182    if data.len() < 32 * 6 + 8 * 6 + 2 + 8 {
183        return Err(pot_o_core::TribeError::ChainBridgeError(
184            "liquidity pool account data too short".into(),
185        ));
186    }
187    let mut off = 0;
188    let read_pubkey = |off: &mut usize| {
189        let slice: [u8; 32] = data[*off..*off + 32].try_into().unwrap();
190        *off += 32;
191        Pubkey::new_from_array(slice)
192    };
193    let authority = read_pubkey(&mut off);
194    let token_a_mint = read_pubkey(&mut off);
195    let token_b_mint = read_pubkey(&mut off);
196    let token_a_vault = read_pubkey(&mut off);
197    let token_b_vault = read_pubkey(&mut off);
198    let lp_mint = read_pubkey(&mut off);
199    let reserve_a = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
200    off += 8;
201    let reserve_b = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
202    off += 8;
203    let total_lp_supply = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
204    off += 8;
205    let swap_fee_bps = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
206    off += 8;
207    let protocol_fee_bps = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
208    off += 8;
209    let collected_fees_a = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
210    off += 8;
211    let collected_fees_b = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
212    off += 8;
213    let bump = data[off];
214    off += 1;
215    let _ = bump;
216    let is_active = data[off] != 0;
217    off += 1;
218    let created_at = i64::from_le_bytes(data[off..off + 8].try_into().unwrap());
219    Ok(LiquidityPoolInfo {
220        pubkey: pubkey_to_string(pubkey),
221        authority: pubkey_to_string(&authority),
222        token_a_mint: pubkey_to_string(&token_a_mint),
223        token_b_mint: pubkey_to_string(&token_b_mint),
224        token_a_vault: pubkey_to_string(&token_a_vault),
225        token_b_vault: pubkey_to_string(&token_b_vault),
226        lp_mint: pubkey_to_string(&lp_mint),
227        reserve_a,
228        reserve_b,
229        total_lp_supply,
230        swap_fee_bps,
231        protocol_fee_bps,
232        collected_fees_a,
233        collected_fees_b,
234        is_active,
235        created_at,
236    })
237}
238
239/// Constant product AMM: amount_out = (amount_in * (10000 - fee_bps) * reserve_out) / (reserve_in * 10000 + amount_in * (10000 - fee_bps))
240fn calc_swap_output(amount_in: u64, reserve_in: u64, reserve_out: u64, fee_bps: u64) -> u64 {
241    if reserve_in == 0 {
242        return 0;
243    }
244    let amount_in_with_fee = (amount_in as u128) * ((10000 - fee_bps) as u128);
245    let numerator = amount_in_with_fee * (reserve_out as u128);
246    let denominator = (reserve_in as u128) * 10000 + amount_in_with_fee;
247    (numerator / denominator) as u64
248}
249
250fn calc_fee(amount: u64, fee_bps: u64) -> u64 {
251    ((amount as u128) * (fee_bps as u128) / 10000) as u64
252}
253
254fn calc_price_impact_bps(amount_in: u64, reserve_in: u64) -> u64 {
255    if reserve_in == 0 {
256        return 0;
257    }
258    ((amount_in as u128) * 10000 / (reserve_in as u128)) as u64
259}
260
261// ---------------------------------------------------------------------------
262// Vault
263// ---------------------------------------------------------------------------
264
265#[derive(Debug, Clone, Serialize)]
266pub struct TreasuryInfo {
267    pub pubkey: String,
268    pub authority: String,
269    pub token_mint: String,
270    pub vault_token_account: String,
271    pub total_deposited: u64,
272    pub total_vaults: u64,
273    pub is_active: bool,
274    pub created_at: i64,
275}
276
277#[derive(Debug, Clone, Serialize)]
278pub struct UserVaultInfo {
279    pub pubkey: String,
280    pub owner: String,
281    pub treasury: String,
282    pub name: String,
283    pub balance: u64,
284    pub lock_until: i64,
285    pub created_at: i64,
286    pub last_activity: i64,
287    pub is_locked: bool,
288    pub total_deposited: u64,
289    pub total_withdrawn: u64,
290}
291
292#[derive(Debug, Clone, Serialize)]
293pub struct EscrowInfo {
294    pub pubkey: String,
295    pub depositor: String,
296    pub beneficiary: String,
297    pub token_mint: String,
298    pub escrow_token_account: String,
299    pub amount: u64,
300    pub release_time: i64,
301    pub created_at: i64,
302    pub is_released: bool,
303    pub is_cancelled: bool,
304}
305
306fn parse_treasury(pubkey: &Pubkey, data: &[u8]) -> TribeResult<TreasuryInfo> {
307    let data = data
308        .get(ANCHOR_DISCRIMINATOR_LEN..)
309        .ok_or_else(|| pot_o_core::TribeError::ChainBridgeError("account data too short".into()))?;
310    if data.len() < 32 * 3 + 8 * 2 + 1 + 1 + 8 {
311        return Err(pot_o_core::TribeError::ChainBridgeError(
312            "treasury account data too short".into(),
313        ));
314    }
315    let mut off = 0;
316    let read_pubkey = |off: &mut usize| {
317        let slice: [u8; 32] = data[*off..*off + 32].try_into().unwrap();
318        *off += 32;
319        Pubkey::new_from_array(slice)
320    };
321    let authority = read_pubkey(&mut off);
322    let token_mint = read_pubkey(&mut off);
323    let vault_token_account = read_pubkey(&mut off);
324    let total_deposited = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
325    off += 8;
326    let total_vaults = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
327    off += 8;
328    let _bump = data[off];
329    off += 1;
330    let is_active = data[off] != 0;
331    off += 1;
332    let created_at = i64::from_le_bytes(data[off..off + 8].try_into().unwrap());
333    Ok(TreasuryInfo {
334        pubkey: pubkey_to_string(pubkey),
335        authority: pubkey_to_string(&authority),
336        token_mint: pubkey_to_string(&token_mint),
337        vault_token_account: pubkey_to_string(&vault_token_account),
338        total_deposited,
339        total_vaults,
340        is_active,
341        created_at,
342    })
343}
344
345fn parse_user_vault(pubkey: &Pubkey, data: &[u8]) -> TribeResult<UserVaultInfo> {
346    let data = data
347        .get(ANCHOR_DISCRIMINATOR_LEN..)
348        .ok_or_else(|| pot_o_core::TribeError::ChainBridgeError("account data too short".into()))?;
349    // owner(32) + treasury(32) + name(4+bytes) + balance(8) + lock_until(8) + created_at(8) + last_activity(8) + is_locked(1) + total_deposited(8) + total_withdrawn(8)
350    if data.len() < 32 + 32 + 4 + 8 + 8 + 8 + 8 + 1 + 8 + 8 {
351        return Err(pot_o_core::TribeError::ChainBridgeError(
352            "user vault account data too short".into(),
353        ));
354    }
355    let mut off = 0;
356    let read_pubkey = |off: &mut usize| {
357        let slice: [u8; 32] = data[*off..*off + 32].try_into().unwrap();
358        *off += 32;
359        Pubkey::new_from_array(slice)
360    };
361    let owner = read_pubkey(&mut off);
362    let treasury = read_pubkey(&mut off);
363    let name_len = u32::from_le_bytes(data[off..off + 4].try_into().unwrap()) as usize;
364    off += 4;
365    let name_len = name_len.min(32).min(data.len().saturating_sub(off));
366    let name = String::from_utf8_lossy(&data[off..off + name_len]).to_string();
367    off += name_len;
368    let balance = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
369    off += 8;
370    let lock_until = i64::from_le_bytes(data[off..off + 8].try_into().unwrap());
371    off += 8;
372    let created_at = i64::from_le_bytes(data[off..off + 8].try_into().unwrap());
373    off += 8;
374    let last_activity = i64::from_le_bytes(data[off..off + 8].try_into().unwrap());
375    off += 8;
376    let is_locked = data[off] != 0;
377    off += 1;
378    let total_deposited = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
379    off += 8;
380    let total_withdrawn = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
381    Ok(UserVaultInfo {
382        pubkey: pubkey_to_string(pubkey),
383        owner: pubkey_to_string(&owner),
384        treasury: pubkey_to_string(&treasury),
385        name,
386        balance,
387        lock_until,
388        created_at,
389        last_activity,
390        is_locked,
391        total_deposited,
392        total_withdrawn,
393    })
394}
395
396fn parse_escrow(pubkey: &Pubkey, data: &[u8]) -> TribeResult<EscrowInfo> {
397    let data = data
398        .get(ANCHOR_DISCRIMINATOR_LEN..)
399        .ok_or_else(|| pot_o_core::TribeError::ChainBridgeError("account data too short".into()))?;
400    if data.len() < 32 * 4 + 8 * 3 + 1 + 1 + 1 {
401        return Err(pot_o_core::TribeError::ChainBridgeError(
402            "escrow account data too short".into(),
403        ));
404    }
405    let mut off = 0;
406    let read_pubkey = |off: &mut usize| {
407        let slice: [u8; 32] = data[*off..*off + 32].try_into().unwrap();
408        *off += 32;
409        Pubkey::new_from_array(slice)
410    };
411    let depositor = read_pubkey(&mut off);
412    let beneficiary = read_pubkey(&mut off);
413    let token_mint = read_pubkey(&mut off);
414    let escrow_token_account = read_pubkey(&mut off);
415    let amount = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
416    off += 8;
417    let release_time = i64::from_le_bytes(data[off..off + 8].try_into().unwrap());
418    off += 8;
419    let created_at = i64::from_le_bytes(data[off..off + 8].try_into().unwrap());
420    off += 8;
421    let is_released = data[off] != 0;
422    off += 1;
423    let is_cancelled = data[off] != 0;
424    off += 1;
425    let _bump = data[off];
426    Ok(EscrowInfo {
427        pubkey: pubkey_to_string(pubkey),
428        depositor: pubkey_to_string(&depositor),
429        beneficiary: pubkey_to_string(&beneficiary),
430        token_mint: pubkey_to_string(&token_mint),
431        escrow_token_account: pubkey_to_string(&escrow_token_account),
432        amount,
433        release_time,
434        created_at,
435        is_released,
436        is_cancelled,
437    })
438}
439
440// ---------------------------------------------------------------------------
441// DefiClient - RPC fetches
442// ---------------------------------------------------------------------------
443
444pub struct DefiClient {
445    rpc_url: String,
446    staking_program_id: Pubkey,
447    swap_program_id: Pubkey,
448    vault_program_id: Pubkey,
449}
450
451impl DefiClient {
452    pub fn new(rpc_url: String) -> Self {
453        Self {
454            rpc_url: rpc_url.clone(),
455            staking_program_id: Pubkey::from_str(STAKING_PROGRAM_ID).unwrap(),
456            swap_program_id: Pubkey::from_str(SWAP_PROGRAM_ID).unwrap(),
457            vault_program_id: Pubkey::from_str(VAULT_PROGRAM_ID).unwrap(),
458        }
459    }
460
461    fn get_account(&self, pubkey: &Pubkey) -> TribeResult<Vec<u8>> {
462        let client = RpcClient::new(&self.rpc_url);
463        let account = client.get_account(pubkey).map_err(|e| {
464            pot_o_core::TribeError::ChainBridgeError(format!("rpc get_account: {e}"))
465        })?;
466        Ok(account.data)
467    }
468
469    // --- Staking ---
470    pub fn get_staking_pool(&self, token_mint: &str) -> TribeResult<Option<StakingPoolInfo>> {
471        let mint = Pubkey::from_str(token_mint)
472            .map_err(|e| pot_o_core::TribeError::ChainBridgeError(format!("invalid mint: {e}")))?;
473        let (pda, _) = Pubkey::find_program_address(
474            &[b"staking_pool", mint.as_ref()],
475            &self.staking_program_id,
476        );
477        let data = match self.get_account(&pda) {
478            Ok(d) => d,
479            Err(_) => return Ok(None),
480        };
481        parse_staking_pool(&pda, &data).map(Some)
482    }
483
484    pub fn get_stake_account(
485        &self,
486        pool_pubkey: &str,
487        user_pubkey: &str,
488    ) -> TribeResult<Option<StakeAccountInfo>> {
489        let pool = Pubkey::from_str(pool_pubkey)
490            .map_err(|e| pot_o_core::TribeError::ChainBridgeError(format!("invalid pool: {e}")))?;
491        let user = Pubkey::from_str(user_pubkey)
492            .map_err(|e| pot_o_core::TribeError::ChainBridgeError(format!("invalid user: {e}")))?;
493        let (pda, _) = Pubkey::find_program_address(
494            &[b"stake", pool.as_ref(), user.as_ref()],
495            &self.staking_program_id,
496        );
497        let data = match self.get_account(&pda) {
498            Ok(d) => d,
499            Err(_) => return Ok(None),
500        };
501        parse_stake_account(&pda, &data).map(Some)
502    }
503
504    // --- Swap ---
505    pub fn get_swap_pool(
506        &self,
507        token_a_mint: &str,
508        token_b_mint: &str,
509    ) -> TribeResult<Option<LiquidityPoolInfo>> {
510        let a = Pubkey::from_str(token_a_mint).map_err(|e| {
511            pot_o_core::TribeError::ChainBridgeError(format!("invalid token_a: {e}"))
512        })?;
513        let b = Pubkey::from_str(token_b_mint).map_err(|e| {
514            pot_o_core::TribeError::ChainBridgeError(format!("invalid token_b: {e}"))
515        })?;
516        let (pda, _) =
517            Pubkey::find_program_address(&[b"pool", a.as_ref(), b.as_ref()], &self.swap_program_id);
518        let data = match self.get_account(&pda) {
519            Ok(d) => d,
520            Err(_) => return Ok(None),
521        };
522        parse_liquidity_pool(&pda, &data).map(Some)
523    }
524
525    pub fn get_swap_quote(
526        &self,
527        token_a_mint: &str,
528        token_b_mint: &str,
529        amount_in: u64,
530        is_a_to_b: bool,
531    ) -> TribeResult<Option<SwapQuoteInfo>> {
532        let pool = match self.get_swap_pool(token_a_mint, token_b_mint)? {
533            Some(p) => p,
534            None => return Ok(None),
535        };
536        let (reserve_in, reserve_out) = if is_a_to_b {
537            (pool.reserve_a, pool.reserve_b)
538        } else {
539            (pool.reserve_b, pool.reserve_a)
540        };
541        let amount_out = calc_swap_output(amount_in, reserve_in, reserve_out, pool.swap_fee_bps);
542        let fee = calc_fee(amount_in, pool.swap_fee_bps);
543        let price_impact_bps = calc_price_impact_bps(amount_in, reserve_in);
544        Ok(Some(SwapQuoteInfo {
545            pool: pool.pubkey,
546            amount_in,
547            amount_out,
548            fee,
549            price_impact_bps,
550        }))
551    }
552
553    // --- Vault ---
554    pub fn get_treasury(&self, token_mint: &str) -> TribeResult<Option<TreasuryInfo>> {
555        let mint = Pubkey::from_str(token_mint)
556            .map_err(|e| pot_o_core::TribeError::ChainBridgeError(format!("invalid mint: {e}")))?;
557        let (pda, _) =
558            Pubkey::find_program_address(&[b"treasury", mint.as_ref()], &self.vault_program_id);
559        let data = match self.get_account(&pda) {
560            Ok(d) => d,
561            Err(_) => return Ok(None),
562        };
563        parse_treasury(&pda, &data).map(Some)
564    }
565
566    pub fn get_user_vault(
567        &self,
568        treasury_pubkey: &str,
569        user_pubkey: &str,
570    ) -> TribeResult<Option<UserVaultInfo>> {
571        let treasury = Pubkey::from_str(treasury_pubkey).map_err(|e| {
572            pot_o_core::TribeError::ChainBridgeError(format!("invalid treasury: {e}"))
573        })?;
574        let user = Pubkey::from_str(user_pubkey)
575            .map_err(|e| pot_o_core::TribeError::ChainBridgeError(format!("invalid user: {e}")))?;
576        let (pda, _) = Pubkey::find_program_address(
577            &[b"user_vault", treasury.as_ref(), user.as_ref()],
578            &self.vault_program_id,
579        );
580        let data = match self.get_account(&pda) {
581            Ok(d) => d,
582            Err(_) => return Ok(None),
583        };
584        parse_user_vault(&pda, &data).map(Some)
585    }
586
587    pub fn get_escrow(
588        &self,
589        depositor: &str,
590        beneficiary: &str,
591    ) -> TribeResult<Option<EscrowInfo>> {
592        let dep = Pubkey::from_str(depositor).map_err(|e| {
593            pot_o_core::TribeError::ChainBridgeError(format!("invalid depositor: {e}"))
594        })?;
595        let ben = Pubkey::from_str(beneficiary).map_err(|e| {
596            pot_o_core::TribeError::ChainBridgeError(format!("invalid beneficiary: {e}"))
597        })?;
598        let (pda, _) = Pubkey::find_program_address(
599            &[b"escrow", dep.as_ref(), ben.as_ref()],
600            &self.vault_program_id,
601        );
602        let data = match self.get_account(&pda) {
603            Ok(d) => d,
604            Err(_) => return Ok(None),
605        };
606        parse_escrow(&pda, &data).map(Some)
607    }
608}