diff --git a/Cargo.toml b/Cargo.toml index 7824738..83a1aaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "2" members = [ "jupiter-swap-api-client", "example", diff --git a/jupiter-swap-api-client/src/lib.rs b/jupiter-swap-api-client/src/lib.rs index c5a2415..4987da9 100644 --- a/jupiter-swap-api-client/src/lib.rs +++ b/jupiter-swap-api-client/src/lib.rs @@ -4,7 +4,7 @@ use anyhow::{anyhow, Result}; use quote::{InternalQuoteRequest, QuoteRequest, QuoteResponse}; use reqwest::{Client, Response}; use serde::de::DeserializeOwned; -use swap::{SwapInstructionsResponse, SwapInstructionsResponseInternal, SwapRequest, SwapResponse}; +use swap::{SwapInstructionsResponse, SwapRequest, SwapResponse}; pub mod quote; pub mod route_plan_with_metadata; @@ -15,6 +15,7 @@ pub mod transaction_config; #[derive(Clone)] pub struct JupiterSwapApiClient { pub base_path: String, + client: Client, } async fn check_is_success(response: Response) -> Result { @@ -38,14 +39,15 @@ async fn check_status_code_and_deserialize(response: Respon impl JupiterSwapApiClient { pub fn new(base_path: String) -> Self { - Self { base_path } + let client = Client::new(); + Self { base_path, client } } pub async fn quote(&self, quote_request: &QuoteRequest) -> Result { let url = format!("{}/quote", self.base_path); let extra_args = quote_request.quote_args.clone(); let internal_quote_request = InternalQuoteRequest::from(quote_request.clone()); - let response = Client::new() + let response = self.client .get(url) .query(&internal_quote_request) .query(&extra_args) @@ -59,7 +61,7 @@ impl JupiterSwapApiClient { swap_request: &SwapRequest, extra_args: Option>, ) -> Result { - let response = Client::new() + let response = self.client .post(format!("{}/swap", self.base_path)) .query(&extra_args) .json(swap_request) @@ -72,13 +74,12 @@ impl JupiterSwapApiClient { &self, swap_request: &SwapRequest, ) -> Result { - let response = Client::new() + let response = self.client .post(format!("{}/swap-instructions", self.base_path)) .json(swap_request) .send() .await?; - check_status_code_and_deserialize::(response) + check_status_code_and_deserialize::(response) .await - .map(Into::into) } } diff --git a/jupiter-swap-api-client/src/serde_helpers/field_as_base64.rs b/jupiter-swap-api-client/src/serde_helpers/field_as_base64.rs new file mode 100644 index 0000000..c503b19 --- /dev/null +++ b/jupiter-swap-api-client/src/serde_helpers/field_as_base64.rs @@ -0,0 +1,19 @@ +use { + serde::{Deserializer, Serializer}, + serde::{Deserialize, Serialize}, +}; +use base64::{engine::general_purpose::STANDARD, Engine}; + +pub fn serialize(v: &Vec, s: S) -> Result { + String::serialize(&STANDARD.encode(v), s) +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + STANDARD + .decode(String::deserialize(deserializer)?) + .map_err(|e| serde::de::Error::custom(format!("base64 decoding error {:?}", e))) +} + diff --git a/jupiter-swap-api-client/src/serde_helpers/mod.rs b/jupiter-swap-api-client/src/serde_helpers/mod.rs index d1a6d84..29cd6a7 100644 --- a/jupiter-swap-api-client/src/serde_helpers/mod.rs +++ b/jupiter-swap-api-client/src/serde_helpers/mod.rs @@ -1,2 +1,97 @@ +use serde::{Deserialize, Deserializer}; +use solana_sdk::instruction::{AccountMeta, Instruction}; +use solana_sdk::pubkey::Pubkey; + pub mod field_as_string; pub mod option_field_as_string; +pub mod field_as_base64; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct InstructionHelper { + #[serde(with = "field_as_string")] + program_id: Pubkey, + accounts: Vec, + #[serde(with = "field_as_base64")] + data: Vec, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct AccountMetaHelper { + #[serde(with = "field_as_string")] + pubkey: Pubkey, + is_signer: bool, + is_writable: bool, +} + +impl From for Instruction { + fn from(helper: InstructionHelper) -> Self { + Instruction { + program_id: helper.program_id, + accounts: helper + .accounts + .into_iter() + .map(|acc| AccountMeta { + pubkey: acc.pubkey, + is_signer: acc.is_signer, + is_writable: acc.is_writable, + }) + .collect(), + data: helper.data, + } + } +} + +pub mod field_as_instruction { + use super::*; + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(InstructionHelper::deserialize(deserializer)?.into()) + } +} + +pub mod option_field_as_instruction { + use super::*; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Ok(Option::::deserialize(deserializer)?.map(Into::into)) + } +} + +pub mod vec_field_as_instruction { + use super::*; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Ok(Vec::::deserialize(deserializer)? + .into_iter() + .map(Into::into) + .collect()) + } +} + +pub mod vec_field_as_pubkey { + use super::*; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + struct Helper(#[serde(with = "field_as_string")] Pubkey); + + Ok(Vec::::deserialize(deserializer)? + .into_iter() + .map(|v| v.0) + .collect()) + } +} diff --git a/jupiter-swap-api-client/src/swap.rs b/jupiter-swap-api-client/src/swap.rs index c458c65..d130a3a 100644 --- a/jupiter-swap-api-client/src/swap.rs +++ b/jupiter-swap-api-client/src/swap.rs @@ -1,10 +1,15 @@ use crate::{ - quote::QuoteResponse, serde_helpers::field_as_string, transaction_config::TransactionConfig, + quote::QuoteResponse, + serde_helpers::{ + field_as_base64, field_as_string, vec_field_as_pubkey, vec_field_as_instruction, + option_field_as_instruction, field_as_instruction + }, + transaction_config::TransactionConfig, }; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use solana_sdk::{ - instruction::{AccountMeta, Instruction}, + instruction::Instruction, pubkey::Pubkey, }; @@ -50,7 +55,7 @@ pub struct UiSimulationError { #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct SwapResponse { - #[serde(with = "base64_serialize_deserialize")] + #[serde(with = "field_as_base64")] pub swap_transaction: Vec, pub last_valid_block_height: u64, pub prioritization_fee_lamports: u64, @@ -60,38 +65,25 @@ pub struct SwapResponse { pub simulation_error: Option, } -pub mod base64_serialize_deserialize { - use base64::{engine::general_purpose::STANDARD, Engine}; - use serde::{de, Deserializer, Serializer}; - - use super::*; - pub fn serialize(v: &Vec, s: S) -> Result { - let base58 = STANDARD.encode(v); - String::serialize(&base58, s) - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - let field_string = String::deserialize(deserializer)?; - STANDARD - .decode(field_string) - .map_err(|e| de::Error::custom(format!("base64 decoding error: {:?}", e))) - } -} - -#[derive(Debug, Clone)] +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] pub struct SwapInstructionsResponse { + #[serde(with = "option_field_as_instruction")] pub token_ledger_instruction: Option, + #[serde(with = "vec_field_as_instruction")] pub compute_budget_instructions: Vec, + #[serde(with = "vec_field_as_instruction")] pub setup_instructions: Vec, /// Instruction performing the action of swapping + #[serde(with = "field_as_instruction")] pub swap_instruction: Instruction, + #[serde(with = "option_field_as_instruction")] pub cleanup_instruction: Option, /// Other instructions that should be included in the transaction. /// Now, it should only have the Jito tip instruction. + #[serde(with = "vec_field_as_instruction")] pub other_instructions: Vec, + #[serde(with = "vec_field_as_pubkey")] pub address_lookup_table_addresses: Vec, pub prioritization_fee_lamports: u64, pub compute_unit_limit: u32, @@ -99,102 +91,3 @@ pub struct SwapInstructionsResponse { pub dynamic_slippage_report: Option, pub simulation_error: Option, } - -// Duplicate for deserialization -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct SwapInstructionsResponseInternal { - token_ledger_instruction: Option, - compute_budget_instructions: Vec, - setup_instructions: Vec, - /// Instruction performing the action of swapping - swap_instruction: InstructionInternal, - cleanup_instruction: Option, - /// Other instructions that should be included in the transaction. - /// Now, it should only have the Jito tip instruction. - other_instructions: Vec, - address_lookup_table_addresses: Vec, - prioritization_fee_lamports: u64, - compute_unit_limit: u32, - prioritization_type: Option, - dynamic_slippage_report: Option, - simulation_error: Option, -} - -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -struct InstructionInternal { - #[serde(with = "field_as_string")] - pub program_id: Pubkey, - pub accounts: Vec, - #[serde(with = "base64_serialize_deserialize")] - pub data: Vec, -} - -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct AccountMetaInternal { - #[serde(with = "field_as_string")] - pub pubkey: Pubkey, - pub is_signer: bool, - pub is_writable: bool, -} - -impl From for AccountMeta { - fn from(val: AccountMetaInternal) -> Self { - AccountMeta { - pubkey: val.pubkey, - is_signer: val.is_signer, - is_writable: val.is_writable, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -struct PubkeyInternal(#[serde(with = "field_as_string")] Pubkey); - -impl From for Instruction { - fn from(val: InstructionInternal) -> Self { - Instruction { - program_id: val.program_id, - accounts: val.accounts.into_iter().map(Into::into).collect(), - data: val.data, - } - } -} - -impl From for SwapInstructionsResponse { - fn from(value: SwapInstructionsResponseInternal) -> Self { - Self { - token_ledger_instruction: value.token_ledger_instruction.map(Into::into), - compute_budget_instructions: value - .compute_budget_instructions - .into_iter() - .map(Into::into) - .collect(), - setup_instructions: value - .setup_instructions - .into_iter() - .map(Into::into) - .collect(), - swap_instruction: value.swap_instruction.into(), - cleanup_instruction: value.cleanup_instruction.map(Into::into), - other_instructions: value - .other_instructions - .into_iter() - .map(Into::into) - .collect(), - address_lookup_table_addresses: value - .address_lookup_table_addresses - .into_iter() - .map(|p| p.0) - .collect(), - prioritization_fee_lamports: value.prioritization_fee_lamports, - compute_unit_limit: value.compute_unit_limit, - prioritization_type: value.prioritization_type, - dynamic_slippage_report: value.dynamic_slippage_report, - simulation_error: value.simulation_error, - } - } -}