1use solana_sdk::{
2 instruction::{AccountMeta, Instruction},
3 pubkey::Pubkey,
4 signature::Keypair,
5 signer::Signer,
6 transaction::VersionedTransaction,
7 message::{v0, VersionedMessage},
8 commitment_config::CommitmentConfig,
9};
10use solana_client::nonblocking::rpc_client::RpcClient;
11use std::str::FromStr;
12use std::sync::Arc;
13use base64::{Engine as _, engine::general_purpose::STANDARD};
14use reqwest::Client;
15use thiserror::Error;
16use crate::types::*;
17
18#[derive(Error, Debug)]
23pub enum VaeaError {
24 #[error("[{code}] {message}")]
25 Protocol { code: VaeaErrorCode, message: String },
26
27 #[error("HTTP request failed: {0}")]
28 Network(#[from] reqwest::Error),
29
30 #[error("Invalid pubkey: {0}")]
31 InvalidPubkey(String),
32
33 #[error("RPC error: {0}")]
34 Rpc(String),
35
36 #[error("Transaction failed: {0}")]
37 Transaction(String),
38}
39
40impl VaeaError {
41 pub fn protocol(code: VaeaErrorCode, msg: impl Into<String>) -> Self {
42 Self::Protocol { code, message: msg.into() }
43 }
44}
45
46pub struct VaeaFlash {
70 api_url: String,
71 source: Source,
72 http: Client,
73 rpc: Option<Arc<RpcClient>>,
74 payer: Option<Arc<Keypair>>,
75}
76
77impl VaeaFlash {
78 pub fn new(api_url: &str, payer: &Keypair) -> Result<Self, VaeaError> {
80 Ok(Self {
81 api_url: api_url.to_string(),
82 source: Source::Sdk,
83 http: Client::new(),
84 rpc: None,
85 payer: Some(Arc::new(Keypair::try_from(payer.to_bytes().as_ref()).unwrap())),
86 })
87 }
88
89 pub fn with_rpc(api_url: &str, rpc_url: &str, payer: &Keypair) -> Result<Self, VaeaError> {
91 Ok(Self {
92 api_url: api_url.to_string(),
93 source: Source::Sdk,
94 http: Client::new(),
95 rpc: Some(Arc::new(RpcClient::new(rpc_url.to_string()))),
96 payer: Some(Arc::new(Keypair::try_from(payer.to_bytes().as_ref()).unwrap())),
97 })
98 }
99
100 pub fn read_only(api_url: &str) -> Self {
102 Self {
103 api_url: api_url.to_string(),
104 source: Source::Sdk,
105 http: Client::new(),
106 rpc: None,
107 payer: None,
108 }
109 }
110
111 pub fn with_source(mut self, source: Source) -> Self {
113 self.source = source;
114 self
115 }
116
117 pub async fn get_capacity(&self) -> Result<CapacityResponse, VaeaError> {
123 self.api_get("/v1/capacity").await
124 }
125
126 pub async fn get_quote(&self, token: &str, amount: f64) -> Result<QuoteResponse, VaeaError> {
128 if amount <= 0.0 {
129 return Err(VaeaError::protocol(VaeaErrorCode::InvalidAmount, "Amount must be > 0"));
130 }
131 let path = format!(
132 "/v1/quote?token={}&amount={}&source={}",
133 token, amount, self.source
134 );
135 self.api_get(&path).await
136 }
137
138 pub async fn build(&self, request: &BuildRequest) -> Result<BuildResponse, VaeaError> {
140 self.api_post("/v1/build", request).await
141 }
142
143 pub async fn get_health(&self) -> Result<HealthResponse, VaeaError> {
145 self.api_get("/v1/health").await
146 }
147
148 pub async fn get_matrix(&self) -> Result<MatrixResponse, VaeaError> {
150 self.api_get("/v1/matrix").await
151 }
152
153 pub async fn get_discovery(&self) -> Result<DiscoverySummary, VaeaError> {
155 self.api_get("/v1/discovery").await
156 }
157
158 pub async fn get_route(
163 &self,
164 token: &str,
165 amount: f64,
166 max_fee_bps: u16,
167 ) -> Result<ResolvedRoute, VaeaError> {
168 if amount <= 0.0 {
169 return Err(VaeaError::protocol(VaeaErrorCode::InvalidAmount, "Amount must be > 0"));
170 }
171 let path = format!(
172 "/v1/vte?token={}&amount={}&source={}&max_fee_bps={}&alternatives=true",
173 token, amount, self.source, max_fee_bps,
174 );
175 self.api_get(&path).await
176 }
177
178 pub async fn get_sources(&self) -> Result<SourcesResponse, VaeaError> {
180 self.api_get("/v1/sources").await
181 }
182
183 pub async fn get_aggregated_capacity(&self) -> Result<AggregatedCapacityResponse, VaeaError> {
185 self.api_get("/v1/capacity/aggregated").await
186 }
187
188 pub async fn borrow(&self, params: &BorrowParams) -> Result<Vec<Instruction>, VaeaError> {
190 let payer = self.payer.as_ref()
191 .ok_or_else(|| VaeaError::protocol(VaeaErrorCode::ApiError, "Payer keypair required"))?;
192
193 if let Some(max_bps) = params.max_fee_bps {
195 let quote = self.get_quote(¶ms.token, params.amount).await?;
196 let actual_bps = (quote.fee_breakdown.total_fee_pct * 100.0) as u16;
197 if actual_bps > max_bps {
198 return Err(VaeaError::protocol(
199 VaeaErrorCode::FeeTooHigh,
200 format!("Fee {} bps exceeds max {} bps", actual_bps, max_bps),
201 ));
202 }
203 }
204
205 let request = BuildRequest {
206 token: params.token.clone(),
207 amount: params.amount,
208 user_pubkey: payer.pubkey().to_string(),
209 source: Some(self.source.to_string()),
210 slippage_bps: params.slippage_bps,
211 max_fee_bps: params.max_fee_bps,
212 };
213
214 let build = self.build(&request).await?;
215 let mut all_ixs = Vec::new();
216
217 for api_ix in &build.prefix_instructions {
218 all_ixs.push(Self::parse_api_instruction(api_ix)?);
219 }
220 for ix in ¶ms.instructions {
221 all_ixs.push(ix.clone());
222 }
223 for api_ix in &build.suffix_instructions {
224 all_ixs.push(Self::parse_api_instruction(api_ix)?);
225 }
226
227 Ok(all_ixs)
228 }
229
230 pub async fn execute(&self, params: BorrowParams) -> Result<String, VaeaError> {
233 let rpc = self.rpc.as_ref()
234 .ok_or_else(|| VaeaError::protocol(VaeaErrorCode::ApiError, "RPC client required for execute(). Use VaeaFlash::with_rpc()"))?;
235 let payer = self.payer.as_ref()
236 .ok_or_else(|| VaeaError::protocol(VaeaErrorCode::ApiError, "Payer keypair required"))?;
237
238 let all_ixs = self.borrow(¶ms).await?;
239
240 let blockhash = rpc.get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())
241 .await
242 .map_err(|e| VaeaError::Rpc(e.to_string()))?
243 .0;
244
245 let lookup_tables = self.fetch_lookup_table(rpc).await;
247
248 let msg = v0::Message::try_compile(
249 &payer.pubkey(),
250 &all_ixs,
251 &lookup_tables,
252 blockhash,
253 ).map_err(|e| VaeaError::Transaction(e.to_string()))?;
254
255 let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[payer.as_ref()])
256 .map_err(|e| VaeaError::Transaction(e.to_string()))?;
257
258 let sig = rpc.send_and_confirm_transaction(&tx)
259 .await
260 .map_err(|e| VaeaError::Transaction(e.to_string()))?;
261
262 Ok(sig.to_string())
263 }
264
265 pub async fn simulate(&self, params: &BorrowParams) -> Result<SimulateResult, VaeaError> {
272 let rpc = self.rpc.as_ref()
273 .ok_or_else(|| VaeaError::protocol(VaeaErrorCode::ApiError, "RPC required for simulate()"))?;
274 let payer = self.payer.as_ref()
275 .ok_or_else(|| VaeaError::protocol(VaeaErrorCode::ApiError, "Payer required for simulate()"))?;
276
277 let all_ixs = self.borrow(params).await?;
278 let blockhash = rpc.get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())
279 .await
280 .map_err(|e| VaeaError::Rpc(e.to_string()))?
281 .0;
282
283 let lookup_tables = self.fetch_lookup_table(rpc).await;
284 let msg = v0::Message::try_compile(
285 &payer.pubkey(),
286 &all_ixs,
287 &lookup_tables,
288 blockhash,
289 ).map_err(|e| VaeaError::Transaction(e.to_string()))?;
290
291 let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[payer.as_ref()])
292 .map_err(|e| VaeaError::Transaction(e.to_string()))?;
293
294 let sim = rpc.simulate_transaction(&tx)
295 .await
296 .map_err(|e| VaeaError::Rpc(e.to_string()))?;
297
298 Ok(SimulateResult {
299 success: sim.value.err.is_none(),
300 error: sim.value.err.map(|e| format!("{:?}", e)),
301 compute_units: sim.value.units_consumed.unwrap_or(0),
302 logs: sim.value.logs.unwrap_or_default(),
303 })
304 }
305
306 pub async fn borrow_multi(&self, params: &BorrowMultiParams) -> Result<Vec<Instruction>, VaeaError> {
313 let payer = self.payer.as_ref()
314 .ok_or_else(|| VaeaError::protocol(VaeaErrorCode::ApiError, "Payer required for borrow_multi()"))?;
315
316 let mut all_builds = Vec::new();
318 for loan in ¶ms.loans {
319 let request = BuildRequest {
320 token: loan.token.clone(),
321 amount: loan.amount,
322 user_pubkey: payer.pubkey().to_string(),
323 source: Some(self.source.to_string()),
324 slippage_bps: params.slippage_bps,
325 max_fee_bps: params.max_fee_bps,
326 };
327 all_builds.push(self.build(&request).await?);
328 }
329
330 let mut all_ixs = Vec::new();
331
332 for build in &all_builds {
334 for api_ix in &build.prefix_instructions {
335 all_ixs.push(Self::parse_api_instruction(api_ix)?);
336 }
337 }
338
339 for ix in ¶ms.instructions {
341 all_ixs.push(ix.clone());
342 }
343
344 for build in all_builds.iter().rev() {
346 for api_ix in &build.suffix_instructions {
347 all_ixs.push(Self::parse_api_instruction(api_ix)?);
348 }
349 }
350
351 Ok(all_ixs)
352 }
353
354 pub async fn is_profitable(
360 &self,
361 params: &crate::profitability::ProfitabilityParams,
362 ) -> Result<crate::profitability::ProfitabilityResult, VaeaError> {
363 let quote = self.get_quote(¶ms.token, params.amount).await?;
364 Ok(crate::profitability::calculate_profitability("e, params))
365 }
366
367 pub fn borrow_local(&self, params: &BorrowParams) -> Result<Vec<Instruction>, VaeaError> {
390 let payer = self.payer.as_ref()
391 .ok_or_else(|| VaeaError::protocol(VaeaErrorCode::ApiError, "Payer required for borrow_local()"))?;
392
393 let tier = match self.source {
394 Source::Sdk => crate::types::FlashTier::Sdk,
395 Source::Ui => crate::types::FlashTier::Ui,
396 Source::Protocol => crate::types::FlashTier::Protocol,
397 };
398
399 let result = crate::local_builder::local_build(crate::local_builder::LocalBuildParams {
400 payer: payer.pubkey(),
401 token: crate::local_builder::TokenId::Symbol(params.token.clone()),
402 amount: params.amount,
403 tier,
404 }).map_err(|e| VaeaError::protocol(VaeaErrorCode::ApiError, e))?;
405
406 let mut all_ixs = vec![result.begin_flash];
407 all_ixs.extend(params.instructions.iter().cloned());
408 all_ixs.push(result.end_flash);
409
410 Ok(all_ixs)
411 }
412
413 pub async fn execute_local(&self, params: BorrowParams) -> Result<String, VaeaError> {
422 let rpc = self.rpc.as_ref()
423 .ok_or_else(|| VaeaError::protocol(VaeaErrorCode::ApiError, "RPC required for execute_local()"))?;
424 let payer = self.payer.as_ref()
425 .ok_or_else(|| VaeaError::protocol(VaeaErrorCode::ApiError, "Payer required for execute_local()"))?;
426
427 let all_ixs = self.borrow_local(¶ms)?;
428
429 let blockhash = rpc.get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())
430 .await
431 .map_err(|e| VaeaError::Rpc(e.to_string()))?
432 .0;
433
434 let lookup_tables = self.fetch_lookup_table(rpc).await;
435
436 let msg = v0::Message::try_compile(
437 &payer.pubkey(),
438 &all_ixs,
439 &lookup_tables,
440 blockhash,
441 ).map_err(|e| VaeaError::Transaction(e.to_string()))?;
442
443 let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[payer.as_ref()])
444 .map_err(|e| VaeaError::Transaction(e.to_string()))?;
445
446 let sig = rpc.send_and_confirm_transaction(&tx)
447 .await
448 .map_err(|e| VaeaError::Transaction(e.to_string()))?;
449
450 Ok(sig.to_string())
451 }
452
453 pub async fn execute_smart(&self, params: BorrowParams) -> Result<String, VaeaError> {
463 match self.get_route(¶ms.token, params.amount, params.max_fee_bps.unwrap_or(0)).await {
465 Ok(route) => {
466 let has_feasible = route.candidates.iter().any(|c| c.feasible);
467 if !has_feasible && !route.candidates.is_empty() {
468 let best = &route.candidates[0];
469 if !best.sufficient_liquidity {
470 return Err(VaeaError::protocol(
471 VaeaErrorCode::InsufficientLiquidity,
472 format!(
473 "Insufficient liquidity for {} {}. Best: {:.2} on {}.",
474 params.amount, route.token_symbol,
475 best.available_liquidity, best.protocol,
476 ),
477 ));
478 }
479 }
480 self.execute(params).await
482 }
483 Err(VaeaError::Network(_)) | Err(VaeaError::Protocol { .. }) => {
484 self.execute_local(params).await
486 }
487 Err(e) => Err(e),
488 }
489 }
490
491 pub async fn read_flash_state(
502 &self,
503 payer_key: &Pubkey,
504 token_mint: &Pubkey,
505 ) -> Result<Option<FlashStateInfo>, VaeaError> {
506 let rpc = self.rpc.as_ref()
507 .ok_or_else(|| VaeaError::protocol(VaeaErrorCode::ApiError, "RPC required for read_flash_state()"))?;
508
509 let program_id = Pubkey::from_str(VAEA_PROGRAM_ID)
510 .map_err(|_| VaeaError::InvalidPubkey(VAEA_PROGRAM_ID.to_string()))?;
511
512 let (flash_state_pda, _) = Pubkey::find_program_address(
513 &[b"flash", payer_key.as_ref(), token_mint.as_ref()],
514 &program_id,
515 );
516
517 let account_info = match rpc.get_account(&flash_state_pda).await {
518 Ok(acc) => acc,
519 Err(_) => return Ok(None),
520 };
521
522 if account_info.data.len() < 99 {
523 return Ok(None);
524 }
525 if account_info.owner != program_id {
526 return Ok(None);
527 }
528
529 let data = &account_info.data;
530 let payer = Pubkey::try_from(&data[8..40])
532 .map_err(|_| VaeaError::protocol(VaeaErrorCode::ApiError, "Invalid payer in FlashState"))?;
533 let mint = Pubkey::try_from(&data[40..72])
534 .map_err(|_| VaeaError::protocol(VaeaErrorCode::ApiError, "Invalid mint in FlashState"))?;
535 let amount = u64::from_le_bytes(data[72..80].try_into().unwrap());
536 let fee = u64::from_le_bytes(data[80..88].try_into().unwrap());
537 let source_tier = data[88];
538 let slot_created = u64::from_le_bytes(data[89..97].try_into().unwrap());
539 let bump = data[97];
540
541 let tier = FlashTier::from_u8(source_tier).unwrap_or(FlashTier::Sdk);
542
543 Ok(Some(FlashStateInfo {
544 payer,
545 token_mint: mint,
546 amount,
547 fee,
548 source_tier,
549 tier,
550 slot_created,
551 bump,
552 }))
553 }
554
555 async fn fetch_lookup_table(&self, rpc: &RpcClient) -> Vec<solana_sdk::address_lookup_table::AddressLookupTableAccount> {
557 use solana_sdk::address_lookup_table::AddressLookupTableAccount;
558 use crate::types::VAEA_LOOKUP_TABLE;
559
560 match rpc.get_account(&VAEA_LOOKUP_TABLE).await {
561 Ok(account) => {
562 match solana_sdk::address_lookup_table::state::AddressLookupTable::deserialize(&account.data) {
563 Ok(table) => vec![AddressLookupTableAccount {
564 key: VAEA_LOOKUP_TABLE,
565 addresses: table.addresses.to_vec(),
566 }],
567 Err(_) => vec![],
568 }
569 }
570 Err(_) => vec![], }
572 }
573
574 fn parse_api_instruction(api_ix: &ApiInstructionData) -> Result<Instruction, VaeaError> {
579 let program_id = Pubkey::from_str(&api_ix.program_id)
580 .map_err(|_| VaeaError::InvalidPubkey(api_ix.program_id.clone()))?;
581
582 let accounts: Vec<AccountMeta> = api_ix.accounts.iter().map(|acc| {
583 let pubkey = Pubkey::from_str(&acc.pubkey)
584 .unwrap_or_default();
585 if acc.is_writable {
586 AccountMeta::new(pubkey, acc.is_signer)
587 } else {
588 AccountMeta::new_readonly(pubkey, acc.is_signer)
589 }
590 }).collect();
591
592 let data = STANDARD.decode(&api_ix.data)
593 .map_err(|e| VaeaError::protocol(VaeaErrorCode::ApiError, format!("Base64 decode: {}", e)))?;
594
595 Ok(Instruction { program_id, accounts, data })
596 }
597
598 async fn api_get<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T, VaeaError> {
600 let url = format!("{}{}", self.api_url, path);
601 let res = self.http.get(&url).send().await?;
602 if !res.status().is_success() {
603 let status = res.status();
604 let body = res.text().await.unwrap_or_default();
605 return Err(VaeaError::protocol(
606 VaeaErrorCode::ApiError,
607 format!("API returned HTTP {}: {}", status, body),
608 ));
609 }
610 Ok(res.json().await?)
611 }
612
613 async fn api_post<T: serde::de::DeserializeOwned, B: serde::Serialize>(&self, path: &str, body: &B) -> Result<T, VaeaError> {
615 let url = format!("{}{}", self.api_url, path);
616 let res = self.http.post(&url).json(body).send().await?;
617 if !res.status().is_success() {
618 let status = res.status();
619 let body = res.text().await.unwrap_or_default();
620 return Err(VaeaError::protocol(
621 VaeaErrorCode::ApiError,
622 format!("API returned HTTP {}: {}", status, body),
623 ));
624 }
625 Ok(res.json().await?)
626 }
627}