Skip to content

Commit

Permalink
endpoint swap-instructions implementation (#14)
Browse files Browse the repository at this point in the history
* endpoint swap-instructions

* cargo format + bump version

* cargo clippy

* better error

* no need to clone, remained from tests
  • Loading branch information
palinko91 authored Apr 14, 2024
1 parent 269950a commit 400fa5e
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 19 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ license = "WTFPL"
name = "jup-ag"
readme = "README.md"
repository = "https://github.com/mvines/rust-jup-ag"
version = "0.7.0"
version = "0.7.1"

[dependencies]
base64 = "0.13"
Expand Down
47 changes: 47 additions & 0 deletions examples/swap_instructions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use jup_ag::{QuoteConfig, SwapRequest};
use solana_sdk::{pubkey, signature::Keypair, signature::Signer};
use spl_token::{amount_to_ui_amount, ui_amount_to_amount};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let sol = pubkey!("So11111111111111111111111111111111111111112");
let msol = pubkey!("mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So");

let keypair = Keypair::new();

let slippage_bps = 100;
let only_direct_routes = false;
let quotes = jup_ag::quote(
sol,
msol,
ui_amount_to_amount(0.01, 9),
QuoteConfig {
only_direct_routes,
slippage_bps: Some(slippage_bps),
..QuoteConfig::default()
},
)
.await?;

let route = quotes.route_plan[0]
.swap_info
.label
.clone()
.unwrap_or_else(|| "Unknown DEX".to_string());
println!(
"Quote: {} SOL for {} mSOL via {} (worst case with slippage: {}). Impact: {:.2}%",
amount_to_ui_amount(quotes.in_amount, 9),
amount_to_ui_amount(quotes.out_amount, 9),
route,
amount_to_ui_amount(quotes.other_amount_threshold, 9),
quotes.price_impact_pct * 100.
);

let request: SwapRequest = SwapRequest::new(keypair.pubkey(), quotes.clone());

let swap_instructions = jup_ag::swap_instructions(request).await?;

println!("Swap Instructions: {:?}", swap_instructions);

Ok(())
}
104 changes: 104 additions & 0 deletions src/field_instruction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Deserialize Instruction with a custom function
pub mod instruction {
use serde::{Deserialize, Deserializer};
use solana_sdk::{instruction::AccountMeta, instruction::Instruction, pubkey::Pubkey};
use std::str::FromStr;

pub fn deserialize<'de, D>(deserializer: D) -> Result<Instruction, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct InstructionFields {
accounts: Vec<AccountMetaFields>,
data: String,
program_id: String,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct AccountMetaFields {
pubkey: String,
is_signer: bool,
is_writable: bool,
}

let fields = InstructionFields::deserialize(deserializer)?;
let program_id = Pubkey::from_str(&fields.program_id)
.map_err(|e| serde::de::Error::custom(format!("Error parsing programId: {}", e)))?;

let accounts = fields
.accounts
.into_iter()
.map(|acc| {
let pubkey = Pubkey::from_str(&acc.pubkey).map_err(|e| {
serde::de::Error::custom(format!("Error parsing pubkey: {}", e))
})?;
Ok(AccountMeta {
pubkey,
is_signer: acc.is_signer,
is_writable: acc.is_writable,
})
})
.collect::<Result<Vec<AccountMeta>, _>>()?;

let instruction = Instruction {
program_id,
accounts,
data: base64::decode(&fields.data)
.map_err(|e| serde::de::Error::custom(format!("Error decoding data: {}", e)))?,
};

Ok(instruction)
}
}

// Deserialize Option<Instruction> with a custom function
pub mod option_instruction {
use serde::{Deserialize, Deserializer};
use solana_sdk::instruction::Instruction;

pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Instruction>, D::Error>
where
D: Deserializer<'de>,
{
let value: serde_json::Value = Deserialize::deserialize(deserializer)?;

match value {
serde_json::Value::Null => Ok(None),
_ => crate::field_instruction::instruction::deserialize(value)
.map_err(|e| {
serde::de::Error::custom(format!(
"Error deserialize optional instruction: {}",
e
))
})
.map(Some),
}
}
}

// Deserialize Vec<Instruction> with a custom function
pub mod vec_instruction {
use serde::{Deserialize, Deserializer};
use solana_sdk::instruction::Instruction;

pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Instruction>, D::Error>
where
D: Deserializer<'de>,
{
let values: Vec<serde_json::Value> = Deserialize::deserialize(deserializer)?;
let mut instructions = Vec::new();

for value in values {
let instruction: Instruction =
crate::field_instruction::instruction::deserialize(value).map_err(|e| {
serde::de::Error::custom(format!("Error deserialize vec instruction: {}", e))
})?;
instructions.push(instruction);
}

Ok(instructions)
}
}
14 changes: 0 additions & 14 deletions src/field_option_pubkey.rs

This file was deleted.

47 changes: 47 additions & 0 deletions src/field_pubkey.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
pub mod vec {
use {
serde::{de, Deserializer, Serializer},
serde::{Deserialize, Serialize},
solana_sdk::pubkey::Pubkey,
std::str::FromStr,
};

pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Pubkey>, D::Error>
where
D: Deserializer<'de>,
{
let vec_str: Vec<String> = Vec::deserialize(deserializer)?;
let mut vec_pubkey = Vec::new();
for s in vec_str {
let pubkey = Pubkey::from_str(&s).map_err(de::Error::custom)?;
vec_pubkey.push(pubkey);
}
Ok(vec_pubkey)
}

#[allow(dead_code)]
pub fn serialize<S>(vec_pubkey: &[Pubkey], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let vec_str: Vec<String> = vec_pubkey.iter().map(|pubkey| pubkey.to_string()).collect();
vec_str.serialize(serializer)
}
}

pub mod option {
use {
serde::{Serialize, Serializer},
solana_sdk::pubkey::Pubkey,
};

pub fn serialize<S>(t: &Option<Pubkey>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match t {
Some(pubkey) => pubkey.to_string().serialize(serializer),
None => serializer.serialize_none(),
}
}
}
50 changes: 46 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ use solana_sdk::transaction::VersionedTransaction;

use {
serde::{Deserialize, Serialize},
solana_sdk::pubkey::{ParsePubkeyError, Pubkey},
solana_sdk::{
instruction::Instruction,
pubkey::{ParsePubkeyError, Pubkey},
},
std::collections::HashMap,
};

mod field_as_string;
mod field_option_pubkey;
mod field_instruction;
mod field_pubkey;

/// A `Result` alias where the `Err` case is `jup_ag::Error`.
pub type Result<T> = std::result::Result<T, Error>;
Expand Down Expand Up @@ -138,6 +142,25 @@ pub struct Swap {
pub last_valid_block_height: u64,
}

/// Swap instructions
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SwapInstructions {
#[serde(with = "field_instruction::option_instruction")]
pub token_ledger_instruction: Option<Instruction>,
#[serde(with = "field_instruction::vec_instruction")]
pub compute_budget_instructions: Vec<Instruction>,
#[serde(with = "field_instruction::vec_instruction")]
pub setup_instructions: Vec<Instruction>,
#[serde(with = "field_instruction::instruction")]
pub swap_instruction: Instruction,
#[serde(with = "field_instruction::option_instruction")]
pub cleanup_instruction: Option<Instruction>,
#[serde(with = "field_pubkey::vec")]
pub address_lookup_table_addresses: Vec<Pubkey>,
pub prioritization_fee_lamports: u64,
}

/// Hashmap of possible swap routes from input mint to an array of output mints
pub type RouteMap = HashMap<Pubkey, Vec<Pubkey>>;

Expand Down Expand Up @@ -257,12 +280,12 @@ pub struct SwapRequest {
pub user_public_key: Pubkey,
pub wrap_and_unwrap_sol: Option<bool>,
pub use_shared_accounts: Option<bool>,
#[serde(with = "field_option_pubkey")]
#[serde(with = "field_pubkey::option")]
pub fee_account: Option<Pubkey>,
pub compute_unit_price_micro_lamports: Option<u64>,
pub as_legacy_transaction: Option<bool>,
pub use_token_ledger: Option<bool>,
#[serde(with = "field_option_pubkey")]
#[serde(with = "field_pubkey::option")]
pub destination_token_account: Option<Pubkey>,
pub quote_response: Quote,
}
Expand Down Expand Up @@ -318,6 +341,25 @@ pub async fn swap(swap_request: SwapRequest) -> Result<Swap> {
})
}

/// Get swap serialized transaction instructions for a quote
pub async fn swap_instructions(swap_request: SwapRequest) -> Result<SwapInstructions> {
let url = format!("{}/swap-instructions", quote_api_url());

let response = reqwest::Client::builder()
.build()?
.post(url)
.header("Accept", "application/json")
.json(&swap_request)
.send()
.await?;

if !response.status().is_success() {
return Err(Error::JupiterApi(response.text().await?));
}

Ok(response.json::<SwapInstructions>().await?)
}

/// Returns a hash map, input mint as key and an array of valid output mint as values
pub async fn route_map() -> Result<RouteMap> {
let url = format!(
Expand Down

0 comments on commit 400fa5e

Please sign in to comment.