Skip to main content

solagent_plugin_jupiter/
trade.rs

1// Copyright 2025 zTgx
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::JUP_API;
16use anyhow::Result;
17use base64::{engine::general_purpose, Engine as _};
18use serde::{Deserialize, Serialize};
19use solagent_core::{
20    solana_sdk::{
21        commitment_config::CommitmentConfig, program_pack::Pack, pubkey::Pubkey,
22        transaction::VersionedTransaction,
23    },
24    SolanaAgentKit,
25};
26use spl_token::state::Mint;
27use std::str::FromStr;
28
29#[derive(Serialize)]
30struct SwapRequest {
31    #[serde(rename = "quoteResponse")]
32    quote_response: QuoteResponse,
33    #[serde(rename = "userPublicKey")]
34    user_public_key: String,
35    #[serde(rename = "wrapAndUnwrapSol")]
36    wrap_and_unwrap_sol: bool,
37    #[serde(rename = "dynamicComputeUnitLimit")]
38    dynamic_compute_unit_limit: bool,
39    #[serde(rename = "prioritizationFeeLamports")]
40    prioritization_fee_lamports: String,
41    #[serde(rename = "feeAccount")]
42    fee_account: Option<String>,
43}
44#[derive(Deserialize)]
45struct SwapResponse {
46    #[serde(rename = "swapTransaction")]
47    swap_transaction: String,
48}
49
50#[derive(Deserialize, Serialize)]
51struct QuoteResponse {
52    #[serde(flatten)]
53    extra: serde_json::Value,
54}
55
56/// Swap tokens using Jupiter Exchange
57///
58/// # Arguments
59///
60/// * `agent` - SolanaAgentKit instance
61/// * `output_mint` - Target token mint address
62/// * `input_amount` - Amount to swap (in token decimals)
63/// * `input_mint` - Source token mint address (defaults to SOL)
64/// * `slippage_bps` - Slippage tolerance in basis points (default: 300 = 3%)
65///
66/// # Returns
67///
68/// Transaction signature as a string
69pub async fn trade(
70    agent: &SolanaAgentKit,
71    output_mint: &str,
72    input_amount: f64,
73    input_mint: Option<String>,
74    slippage_bps: Option<u32>,
75) -> Result<String, Box<dyn std::error::Error>> {
76    // Convert strings to Pubkeys
77    let output_mint = Pubkey::from_str(output_mint)?;
78    let input_mint = input_mint
79        .as_deref()
80        .map(Pubkey::from_str)
81        .transpose()?
82        .unwrap_or(spl_token::native_mint::id());
83
84    // Use defaults if not provided
85    let slippage_bps = slippage_bps.unwrap_or(300);
86
87    // Check if input token is native SOL
88    let is_native_sol = input_mint == spl_token::native_mint::id();
89
90    // Get input token decimals
91    let input_decimals = if is_native_sol {
92        9
93    } else {
94        let account = agent.connection.get_account(&input_mint)?;
95        let mint = Mint::unpack(&account.data)?;
96        mint.decimals
97    };
98
99    // Calculate scaled amount
100    let scaled_amount = (input_amount * 10f64.powf(input_decimals as f64)) as u64;
101
102    // Build quote URL
103    let quote_url = format!(
104        "{}/quote?inputMint={}&outputMint={}&amount={}&slippageBps={}&onlyDirectRoutes=true&maxAccounts=20",
105        JUP_API, input_mint, output_mint, scaled_amount, slippage_bps
106    );
107
108    // Get quote
109    let client = reqwest::Client::new();
110    let quote_response: QuoteResponse = client.get(&quote_url).send().await?.json().await?;
111
112    // Get swap transaction
113    let swap_request = SwapRequest {
114        quote_response,
115        user_public_key: agent.wallet.pubkey.to_string(),
116        wrap_and_unwrap_sol: true,
117        dynamic_compute_unit_limit: true,
118        prioritization_fee_lamports: "auto".to_string(),
119        fee_account: None,
120    };
121
122    let swap_response: SwapResponse = client
123        .post(format!("{}/swap", JUP_API))
124        .json(&swap_request)
125        .send()
126        .await?
127        .json()
128        .await?;
129
130    let swap_transaction = general_purpose::STANDARD
131        .decode(&swap_response.swap_transaction)
132        .expect("decode swap_transaction");
133
134    let versioned_transaction: VersionedTransaction = bincode::deserialize(&swap_transaction)?;
135
136    let signed_transaction =
137        VersionedTransaction::try_new(versioned_transaction.message, &[&agent.wallet.keypair])?;
138
139    let signature = agent.connection.send_transaction(&signed_transaction)?;
140
141    let latest_blockhash = agent.connection.get_latest_blockhash()?;
142
143    agent.connection.confirm_transaction_with_spinner(
144        &signature,
145        &latest_blockhash,
146        CommitmentConfig::confirmed(),
147    )?;
148
149    Ok(signature.to_string())
150}