diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a5c9c12..89c62b6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,6 +60,8 @@ jobs: DRIFT_GATEWAY_KEY: ${{ secrets.DRIFT_GATEWAY_KEY }} TEST_DELEGATED_SIGNER: ${{ secrets.TEST_DELEGATED_SIGNER }} TEST_RPC_ENDPOINT: ${{ secrets.DEVNET_RPC_ENDPOINT }} + TEST_MAINNET_RPC_ENDPOINT: ${{ secrets.MAINNET_RPC_ENDPOINT }} + # --test-threads, limit parallelism to prevent hitting RPC rate-limits run: | cargo -V diff --git a/Cargo.lock b/Cargo.lock index fc9dea3..f14e0cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1582,7 +1582,7 @@ dependencies = [ [[package]] name = "drift-gateway" -version = "1.5.0" +version = "1.5.1" dependencies = [ "actix-web", "argh", @@ -1605,7 +1605,7 @@ dependencies = [ [[package]] name = "drift-idl-gen" version = "0.2.0" -source = "git+https://github.com/drift-labs/drift-rs?rev=4da4966#4da4966b935df31ec5560783cf1c8365e00ef35b" +source = "git+https://github.com/drift-labs/drift-rs?rev=975d81f#975d81f7f6ec693cca0211a2045f0fa1296d4a03" dependencies = [ "proc-macro2", "quote", @@ -1618,7 +1618,7 @@ dependencies = [ [[package]] name = "drift-pubsub-client" version = "0.1.1" -source = "git+https://github.com/drift-labs/drift-rs?rev=4da4966#4da4966b935df31ec5560783cf1c8365e00ef35b" +source = "git+https://github.com/drift-labs/drift-rs?rev=975d81f#975d81f7f6ec693cca0211a2045f0fa1296d4a03" dependencies = [ "futures-util", "gjson", @@ -1638,7 +1638,7 @@ dependencies = [ [[package]] name = "drift-rs" version = "1.0.0-alpha.15" -source = "git+https://github.com/drift-labs/drift-rs?rev=4da4966#4da4966b935df31ec5560783cf1c8365e00ef35b" +source = "git+https://github.com/drift-labs/drift-rs?rev=975d81f#975d81f7f6ec693cca0211a2045f0fa1296d4a03" dependencies = [ "abi_stable", "ahash 0.8.11", diff --git a/Cargo.toml b/Cargo.toml index 1e07edc..aaed0c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "drift-gateway" -version = "1.5.0" +version = "1.5.1" edition = "2021" [dependencies] actix-web = "*" argh = "*" -drift-rs = { git = "https://github.com/drift-labs/drift-rs", rev = "4da4966" } +drift-rs = { git = "https://github.com/drift-labs/drift-rs", rev = "975d81f" } env_logger = "*" futures-util = "*" log = "*" diff --git a/README.md b/README.md index 2cf8446..80d47a8 100644 --- a/README.md +++ b/README.md @@ -743,7 +743,7 @@ curl -X POST "http://localhost:8080/v2/swap" \ "outputMarket": 1, "exactIn": true, "amount": "500.0", - "slippage": 10 + "slippageBps": 10 }' ``` diff --git a/src/controller.rs b/src/controller.rs index 881b01c..8ca57a8 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -22,7 +22,7 @@ use drift_rs::{ priority_fee_subscriber::{PriorityFeeSubscriber, PriorityFeeSubscriberConfig}, types::{ self, accounts::SpotMarket, MarketId, MarketType, ModifyOrderParams, OrderStatus, - RpcSendTransactionConfig, SdkError, SdkResult, VersionedMessage, + ProgramError, RpcSendTransactionConfig, SdkError, SdkResult, VersionedMessage, }, utils::get_http_url, DriftClient, Pubkey, TransactionBuilder, Wallet, @@ -643,9 +643,10 @@ impl AppState { ) }; + let signer = self.wallet.signer(); let (jupiter_swap_info, account_data) = tokio::try_join!( self.client.jupiter_swap_query( - self.wallet.authority(), + &signer, amount, swap_mode, req.slippage_bps, @@ -669,13 +670,14 @@ impl AppState { jupiter_swap_info, in_market, out_market, - &Wallet::derive_associated_token_address(self.wallet.authority(), in_market), - &Wallet::derive_associated_token_address(self.wallet.authority(), out_market), + &Wallet::derive_associated_token_address(&signer, in_market), + &Wallet::derive_associated_token_address(&signer, out_market), None, None, ) .with_priority_fee(ctx.cu_price.unwrap_or(pf), ctx.cu_limit) .build(); + self.send_tx(tx, "swap", ctx.ttl).await } @@ -739,11 +741,12 @@ impl AppState { }), _ => { let err: SdkError = err.into(); - if let Some(code) = err.to_anchor_error_code() { + + if let Some(program_error) = err.to_anchor_error_code() { return Ok(TxEventsResponse::new( Default::default(), false, - Some(format!("program error: {code}")), + Some(format!("program error: {program_error}")), )); } if err.to_out_of_sol_error().is_some() { @@ -753,6 +756,7 @@ impl AppState { Some("ouf of sol, top-up account".into()), )); } + Ok(TxEventsResponse::new( Default::default(), false, @@ -794,6 +798,27 @@ impl AppState { self.priority_fee_subscriber.priority_fee_nth(0.9) } + /// Test tx simulate only + #[cfg(test)] + async fn send_tx( + &self, + tx: VersionedMessage, + reason: &'static str, + _ttl: Option, + ) -> GatewayResult { + match self.client.simulate_tx(tx).await?.err { + Some(err) => { + log::error!("test tx failed: {err:?}"); + Err(ControllerError::TxFailed { + reason: reason.into(), + code: 0, + }) + } + None => Ok(TxResponse::new("".into())), + } + } + + #[cfg(not(test))] async fn send_tx( &self, tx: VersionedMessage, @@ -875,10 +900,16 @@ impl AppState { } fn handle_tx_err(err: SdkError) -> ControllerError { - if let Some(code) = err.to_anchor_error_code() { - ControllerError::TxFailed { - reason: code.name(), - code: code.into(), + if let Some(program_err) = err.to_anchor_error_code() { + match program_err { + ProgramError::Drift(code) => ControllerError::TxFailed { + reason: code.name(), + code: code.into(), + }, + ProgramError::Other { ix_idx, code } => ControllerError::TxFailed { + reason: format!("ix idx: {ix_idx}"), + code, + }, } } else { ControllerError::Sdk(err) diff --git a/src/main.rs b/src/main.rs index 8106176..fe68cf7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -554,15 +554,15 @@ mod tests { Some(delegated_seed), None, Some( - "GiMXQkJXLVjScmQDkoLJShBJpTh9SDPvT2AZQq8NyEBf" + "DxoRJ4f5XRMvXU9SGuM4ZziBFUxbhB3ubur5sVZEvue2" .try_into() .unwrap(), ), ); - let rpc_endpoint = std::env::var("TEST_RPC_ENDPOINT") - .unwrap_or_else(|_| "https://api.devnet.solana.com".to_string()); - let state = AppState::new(&rpc_endpoint, true, wallet, None, None, false, vec![]).await; + let rpc_endpoint = std::env::var("TEST_MAINNET_RPC_ENDPOINT") + .unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string()); + let state = AppState::new(&rpc_endpoint, false, wallet, None, None, false, vec![]).await; let app = test::init_service( App::new() @@ -582,6 +582,82 @@ mod tests { assert!(resp.status().is_success()); } + // likely safe to ignore during development, mainly regression test for CI + #[actix_web::test] + async fn delegated_swap_works() { + let _ = env_logger::try_init(); + let delegated_seed = + std::env::var("TEST_DELEGATED_SIGNER").expect("delegated signing key set"); + let wallet = create_wallet( + Some(delegated_seed), + None, + Some( + "DxoRJ4f5XRMvXU9SGuM4ZziBFUxbhB3ubur5sVZEvue2" + .try_into() + .unwrap(), + ), + ); + + let rpc_endpoint = std::env::var("TEST_MAINNET_RPC_ENDPOINT") + .unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string()); + let state = AppState::new(&rpc_endpoint, false, wallet, None, None, false, vec![]).await; + + let app = + test::init_service(App::new().app_data(web::Data::new(state)).service(swap)).await; + tokio::time::sleep(Duration::from_secs(1)).await; + + let payload = serde_json::json!({ + "inputMarket": 0, + "outputMarket": 1, + "exactIn": true, + "amount": "5.0", + "slippageBps": 10 + }); + + let req = test::TestRequest::default() + .set_payload(serde_json::to_vec(&payload).unwrap()) + .method(Method::POST) + .uri("/swap") + .to_request(); + + let resp = test::call_service(&app, req).await; + dbg!(resp.response().body()); + assert!(resp.status().is_success()); + } + + // likely safe to ignore during development, mainly regression test for CI + #[actix_web::test] + async fn swap_works() { + let _ = env_logger::try_init(); + let wallet = create_wallet(Some(get_seed()), None, None); + + let rpc_endpoint = std::env::var("TEST_MAINNET_RPC_ENDPOINT") + .unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string()); + let state = AppState::new(&rpc_endpoint, false, wallet, None, None, false, vec![]).await; + + let app = + test::init_service(App::new().app_data(web::Data::new(state)).service(swap)).await; + tokio::time::sleep(Duration::from_secs(1)).await; + + let payload = serde_json::json!({ + "inputMarket": 0, + "outputMarket": 1, + "exactIn": false, + "amount": "0.1", + "slippageBps": 10 + }); + + let req = test::TestRequest::default() + .set_payload(serde_json::to_vec(&payload).unwrap()) + .method(Method::POST) + .uri("/swap") + .to_request(); + + let resp = test::call_service(&app, req).await; + dbg!(resp.response().body()); + assert!(resp.status().is_success()); + } + #[actix_web::test] async fn set_leverage_works() { let controller = setup_controller(None).await;