1use pot_o_core::TribeResult;
5use serde::Serialize;
6use solana_client::rpc_client::RpcClient;
7use solana_sdk::pubkey::Pubkey;
8use std::str::FromStr;
9
10pub 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#[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#[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
239fn 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#[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 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
440pub 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 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 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 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}