Skip to content

Commit 636bc46

Browse files
committed
feat(solana-solvers): buy orders via v1 ExactOut, gated by enable_buy_orders
1 parent 6e3bae1 commit 636bc46

3 files changed

Lines changed: 42 additions & 19 deletions

File tree

crates/solana-solvers/config/example.jupiter.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ endpoint = "https://api.jup.ag"
99
api-key = "your-jupiter-api-key"
1010
# Slippage tolerance in basis points, sent to Jupiter as slippageBps. 50 = 0.5%.
1111
slippage-bps = 50
12+
# Whether to serve buy orders. Off by default.
13+
enable-buy-orders = false

crates/solana-solvers/src/config.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ pub struct JupiterConfig {
2525
/// Slippage tolerance in basis points, sent to Jupiter as `slippageBps`.
2626
/// 50 = 0.5%.
2727
pub slippage_bps: u16,
28+
29+
/// Whether to serve buy orders. Off by default.
30+
#[serde(default)]
31+
pub enable_buy_orders: bool,
2832
}
2933

3034
/// Load and parse the TOML config file.
@@ -50,6 +54,7 @@ mod tests {
5054
assert_eq!(config.dex.endpoint.as_str(), "https://api.jup.ag/");
5155
assert_eq!(config.dex.slippage_bps, 50);
5256
assert!(config.dex.api_key.is_some());
57+
assert!(!config.dex.enable_buy_orders);
5358
}
5459

5560
#[test]

crates/solana-solvers/src/dex/jupiter/mod.rs

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Jupiter swap-API adapter.
22
//!
3-
//! v1 `/quote` + `/swap-instructions` (ExactIn). Triton is a base-URL and key
4-
//! swap behind the same adapter.
3+
//! v1 `/quote` + `/swap-instructions` (ExactIn for sells, ExactOut for buys).
4+
//! Triton is a base-URL and key swap behind the same adapter.
55
66
mod dto;
77

@@ -20,6 +20,7 @@ pub struct Jupiter {
2020
endpoint: reqwest::Url,
2121
api_key: Option<String>,
2222
slippage_bps: u16,
23+
enable_buy_orders: bool,
2324
}
2425

2526
impl Jupiter {
@@ -29,17 +30,19 @@ impl Jupiter {
2930
endpoint: config.endpoint.clone(),
3031
api_key: config.api_key.clone(),
3132
slippage_bps: config.slippage_bps,
33+
enable_buy_orders: config.enable_buy_orders,
3234
})
3335
}
3436

3537
/// Quote `order` for the settlement signer `taker` and return the swap to
3638
/// run inside the settlement transaction.
3739
pub async fn swap(&self, order: &Order, taker: &Pubkey) -> Result<Swap, Error> {
38-
// Buy orders (ExactOut) aren't served here.
39-
if order.side == Side::Buy {
40-
return Err(Error::OrderNotSupported);
41-
}
42-
let quote = self.quote(order, "ExactIn").await?;
40+
let swap_mode = match order.side {
41+
Side::Sell => "ExactIn",
42+
Side::Buy if self.enable_buy_orders => "ExactOut",
43+
Side::Buy => return Err(Error::OrderNotSupported),
44+
};
45+
let quote = self.quote(order, swap_mode).await?;
4346
let in_amount = amount_field(&quote, "inAmount")?;
4447
let out_amount = amount_field(&quote, "outAmount")?;
4548
self.swap_instructions(&quote, taker, &order.buy_destination)
@@ -151,32 +154,29 @@ mod tests {
151154
const USDC: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
152155
const WSOL: &str = "So11111111111111111111111111111111111111112";
153156

154-
fn config() -> JupiterConfig {
157+
fn config(enable_buy_orders: bool) -> JupiterConfig {
155158
JupiterConfig {
156159
endpoint: "https://api.jup.ag".parse().unwrap(),
157160
api_key: std::env::var("JUPITER_API_KEY").ok(),
158161
slippage_bps: 50,
162+
enable_buy_orders,
159163
}
160164
}
161165

162-
fn sell_order() -> Order {
166+
fn order(side: Side) -> Order {
163167
Order {
164168
sell_mint: Pubkey::from_str(USDC).unwrap(),
165169
buy_mint: Pubkey::from_str(WSOL).unwrap(),
166170
buy_destination: Pubkey::from_str(WSOL).unwrap(),
167171
amount: 1_000_000,
168-
side: Side::Sell,
172+
side,
169173
}
170174
}
171175

172176
#[tokio::test]
173-
async fn buy_unsupported() {
174-
let jupiter = Jupiter::new(&config()).unwrap();
175-
let order = Order {
176-
side: Side::Buy,
177-
..sell_order()
178-
};
179-
let result = jupiter.swap(&order, &Pubkey::new_unique()).await;
177+
async fn buy_disabled() {
178+
let jupiter = Jupiter::new(&config(false)).unwrap();
179+
let result = jupiter.swap(&order(Side::Buy), &Pubkey::new_unique()).await;
180180
assert!(matches!(result, Err(Error::OrderNotSupported)));
181181
}
182182

@@ -205,15 +205,31 @@ mod tests {
205205
#[tokio::test]
206206
#[ignore]
207207
async fn jupiter_live_sell() {
208-
let jupiter = Jupiter::new(&config()).unwrap();
208+
let jupiter = Jupiter::new(&config(false)).unwrap();
209209
// Any valid pubkey works for building instructions, the swap only runs
210210
// for real once the driver supplies its settlement signer.
211211
let swap = jupiter
212-
.swap(&sell_order(), &Pubkey::from_str(WSOL).unwrap())
212+
.swap(&order(Side::Sell), &Pubkey::from_str(WSOL).unwrap())
213213
.await
214214
.unwrap();
215215
assert_eq!(swap.in_amount, 1_000_000);
216216
assert!(swap.out_amount > 0);
217217
assert!(!swap.instructions.is_empty());
218218
}
219+
220+
/// Live Jupiter API. Needs network. Keyless works, set `JUPITER_API_KEY`
221+
/// for headroom.
222+
#[tokio::test]
223+
#[ignore]
224+
async fn jupiter_live_buy() {
225+
let jupiter = Jupiter::new(&config(true)).unwrap();
226+
// ExactOut: the swap delivers exactly the order's `amount` of buy mint.
227+
let swap = jupiter
228+
.swap(&order(Side::Buy), &Pubkey::from_str(WSOL).unwrap())
229+
.await
230+
.unwrap();
231+
assert_eq!(swap.out_amount, 1_000_000);
232+
assert!(swap.in_amount > 0);
233+
assert!(!swap.instructions.is_empty());
234+
}
219235
}

0 commit comments

Comments
 (0)