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
66mod dto;
77
@@ -14,12 +14,29 @@ use {
1414const QUOTE_PATH : & str = "swap/v1/quote" ;
1515const SWAP_INSTRUCTIONS_PATH : & str = "swap/v1/swap-instructions" ;
1616
17+ /// Jupiter's quote direction, rendered as the `swapMode` query value.
18+ #[ derive( Clone , Copy ) ]
19+ enum SwapMode {
20+ ExactIn ,
21+ ExactOut ,
22+ }
23+
24+ impl SwapMode {
25+ fn as_str ( self ) -> & ' static str {
26+ match self {
27+ SwapMode :: ExactIn => "ExactIn" ,
28+ SwapMode :: ExactOut => "ExactOut" ,
29+ }
30+ }
31+ }
32+
1733/// Adapter over the Jupiter swap API.
1834pub struct Jupiter {
1935 client : reqwest:: Client ,
2036 endpoint : reqwest:: Url ,
2137 api_key : Option < String > ,
2238 slippage_bps : u16 ,
39+ enable_buy_orders : bool ,
2340}
2441
2542impl Jupiter {
@@ -29,17 +46,19 @@ impl Jupiter {
2946 endpoint : config. endpoint . clone ( ) ,
3047 api_key : config. api_key . clone ( ) ,
3148 slippage_bps : config. slippage_bps ,
49+ enable_buy_orders : config. enable_buy_orders ,
3250 } )
3351 }
3452
3553 /// Quote `order` for the settlement signer `taker` and return the swap to
3654 /// run inside the settlement transaction.
3755 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 ?;
56+ let swap_mode = match order. side {
57+ Side :: Sell => SwapMode :: ExactIn ,
58+ Side :: Buy if self . enable_buy_orders => SwapMode :: ExactOut ,
59+ Side :: Buy => return Err ( Error :: OrderNotSupported ) ,
60+ } ;
61+ let quote = self . quote ( order, swap_mode) . await ?;
4362 let in_amount = amount_field ( & quote, "inAmount" ) ?;
4463 let out_amount = amount_field ( & quote, "outAmount" ) ?;
4564 self . swap_instructions ( & quote, taker, & order. buy_destination )
@@ -49,7 +68,7 @@ impl Jupiter {
4968
5069 /// `GET /swap/v1/quote`. Kept opaque and passed back verbatim to
5170 /// `/swap-instructions`, we only read the amounts.
52- async fn quote ( & self , order : & Order , swap_mode : & str ) -> Result < serde_json:: Value , Error > {
71+ async fn quote ( & self , order : & Order , swap_mode : SwapMode ) -> Result < serde_json:: Value , Error > {
5372 let mut url = self
5473 . endpoint
5574 . join ( QUOTE_PATH )
@@ -58,7 +77,7 @@ impl Jupiter {
5877 . append_pair ( "inputMint" , & order. sell_mint . to_string ( ) )
5978 . append_pair ( "outputMint" , & order. buy_mint . to_string ( ) )
6079 . append_pair ( "amount" , & order. amount . to_string ( ) )
61- . append_pair ( "swapMode" , swap_mode)
80+ . append_pair ( "swapMode" , swap_mode. as_str ( ) )
6281 . append_pair ( "slippageBps" , & self . slippage_bps . to_string ( ) ) ;
6382 self . send ( self . with_key ( self . client . get ( url) ) ) . await
6483 }
@@ -150,32 +169,29 @@ mod tests {
150169 const USDC : & str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" ;
151170 const WSOL : & str = "So11111111111111111111111111111111111111112" ;
152171
153- fn config ( ) -> JupiterConfig {
172+ fn config ( enable_buy_orders : bool ) -> JupiterConfig {
154173 JupiterConfig {
155174 endpoint : "https://api.jup.ag" . parse ( ) . unwrap ( ) ,
156175 api_key : std:: env:: var ( "JUPITER_API_KEY" ) . ok ( ) ,
157176 slippage_bps : 50 ,
177+ enable_buy_orders,
158178 }
159179 }
160180
161- fn sell_order ( ) -> Order {
181+ fn order ( side : Side ) -> Order {
162182 Order {
163183 sell_mint : Pubkey :: from_str ( USDC ) . unwrap ( ) ,
164184 buy_mint : Pubkey :: from_str ( WSOL ) . unwrap ( ) ,
165185 buy_destination : Pubkey :: from_str ( WSOL ) . unwrap ( ) ,
166186 amount : 1_000_000 ,
167- side : Side :: Sell ,
187+ side,
168188 }
169189 }
170190
171191 #[ tokio:: test]
172- async fn buy_unsupported ( ) {
173- let jupiter = Jupiter :: new ( & config ( ) ) . unwrap ( ) ;
174- let order = Order {
175- side : Side :: Buy ,
176- ..sell_order ( )
177- } ;
178- let result = jupiter. swap ( & order, & Pubkey :: new_unique ( ) ) . await ;
192+ async fn buy_disabled ( ) {
193+ let jupiter = Jupiter :: new ( & config ( false ) ) . unwrap ( ) ;
194+ let result = jupiter. swap ( & order ( Side :: Buy ) , & Pubkey :: new_unique ( ) ) . await ;
179195 assert ! ( matches!( result, Err ( Error :: OrderNotSupported ) ) ) ;
180196 }
181197
@@ -217,15 +233,31 @@ mod tests {
217233 #[ tokio:: test]
218234 #[ ignore]
219235 async fn jupiter_live_sell ( ) {
220- let jupiter = Jupiter :: new ( & config ( ) ) . unwrap ( ) ;
236+ let jupiter = Jupiter :: new ( & config ( false ) ) . unwrap ( ) ;
221237 // Any valid pubkey works for building instructions, the swap only runs
222238 // for real once the driver supplies its settlement signer.
223239 let swap = jupiter
224- . swap ( & sell_order ( ) , & Pubkey :: from_str ( WSOL ) . unwrap ( ) )
240+ . swap ( & order ( Side :: Sell ) , & Pubkey :: from_str ( WSOL ) . unwrap ( ) )
225241 . await
226242 . unwrap ( ) ;
227243 assert_eq ! ( swap. in_amount, 1_000_000 ) ;
228244 assert ! ( swap. out_amount > 0 ) ;
229245 assert ! ( !swap. instructions. is_empty( ) ) ;
230246 }
247+
248+ /// Live Jupiter API. Needs network. Keyless works, set `JUPITER_API_KEY`
249+ /// for headroom.
250+ #[ tokio:: test]
251+ #[ ignore]
252+ async fn jupiter_live_buy ( ) {
253+ let jupiter = Jupiter :: new ( & config ( true ) ) . unwrap ( ) ;
254+ // ExactOut: the swap delivers exactly the order's `amount` of buy mint.
255+ let swap = jupiter
256+ . swap ( & order ( Side :: Buy ) , & Pubkey :: from_str ( WSOL ) . unwrap ( ) )
257+ . await
258+ . unwrap ( ) ;
259+ assert_eq ! ( swap. out_amount, 1_000_000 ) ;
260+ assert ! ( swap. in_amount > 0 ) ;
261+ assert ! ( !swap. instructions. is_empty( ) ) ;
262+ }
231263}
0 commit comments