Skip to content

Commit

Permalink
Add docker support
Browse files Browse the repository at this point in the history
Tidy ups
  • Loading branch information
jordy25519 committed Dec 15, 2023
1 parent 0449118 commit 791a78c
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 98 deletions.
74 changes: 40 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,30 @@

Self hosted API gateway to easily interact with Drift V2 Protocol

## Run
## Build & Run

```bash
export `DRIFT_GATEWAY_KEY=</PATH/TO/KEY | keyBase58>`
# build
cargo build --release

# --dev to use devnet markets (default is mainnet)
# ensure the RPC node is also using the matching dev or mainnet
# configure the gateway wallet key
export DRIFT_GATEWAY_KEY=</PATH/TO/KEY.json | seedBase58>

# '--dev' to toggle devnet markets (default is mainnet)
# ensure the RPC node is also using the matching devnet or mainnet
drift-gateway --dev https://api.devnet.solana.com

# or mainnet
drift-gateway https://api.mainnet-beta.solana.com
```

with docker
```bash
docker build -f Dockerfile . -t drift-gateway
docker run -e DRIFT_GATEWAY_KEY=<BASE58_SEED> -p 8080:8080 drift-gateway https://api.mainnet-beta.solana.com --host 0.0.0.0
```

## Usage
```bash
Usage: drift-gateway <rpc_host> [--dev] [--host <host>] [--port <port>]

Expand All @@ -34,15 +48,34 @@ Options:
$> curl localhost:8080/v2/markets
```

### Get Orderbook
```bash
$> curl localhost:8080/v2/orderbook -X GET -H 'content-type: application/json' -d '{"market":{"id":0,"type":"perp"}}'
```
to stream orderbooks via websocket DLOB servers are available at:
devnet: `wss://master.dlob.drift.trade/ws`
mainnet: `wss://dlob.drift.trade/ws`
see https://github.com/drift-labs/dlob-server/blob/master/example/wsClient.ts for usage example

### Get Orders
get all orders
```bash
$> curl localhost:8080/v2/orders
```
get orders by market
```bash
$> curl localhost:8080/v2/orders -X GET -H 'content-type: application/json' -d '{"market":{"id":0,"type":"perp"}};
```
### Get Positions
get all positions
```bash
$> curl localhost:8080/v2/positions
```
get positions by market
```bash
$> curl localhost:8080/v2/positions -X GET -H 'content-type: application/json' -d '{"market":{"id":0,"type":"perp"}};
```

### Place Orders
```bash
Expand Down Expand Up @@ -101,33 +134,6 @@ $> curl localhost:8080/v2/orders -X DELETE -H 'content-type: application/json' -
$> curl localhost:8080/v2/orders -X DELETE -H 'content-type: application/json' -d '{"ids":[1,2,3,4]}'
# cancel by user assigned order ids
$> curl localhost:8080/v2/orders -X DELETE -H 'content-type: application/json' -d '{"userIds":[1,2,3,4]}'
# cancel all
$> curl localhost:8080/v2/orders -X DELETE -H 'content-type: application/json'
```

### Get Positions
```bash
curl localhost:8080/v2/positions | jq .

{
"spot": [
{
"amount": "0.400429",
"type": "deposit",
"market_id": 0
},
{
"amount": "9.531343582",
"type": "deposit",
"market_id": 1
}
],
"perp": []
}
```

### Stream Orderbook
```bash
$> curl localhost:8080/v2/orderbooks -N -X GET -H 'content-type: application/json' -d '{"market":{"id":3,"type":"perp"}'
```

# cancel all orders
$> curl localhost:8080/v2/orders -X DELETE
```
54 changes: 13 additions & 41 deletions src/controller.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
use std::{sync::Arc, task::Poll};
use std::sync::Arc;

use actix_web::{web::Bytes, Error};
use drift_sdk::{
dlob::{DLOBClient, OrderbookStream},
dlob::{DLOBClient, L2Orderbook},
types::{Context, MarketType, OrderParams, SdkError, SdkResult},
DriftClient, Pubkey, TransactionBuilder, Wallet, WsAccountProvider,
};
use futures_util::{stream::FuturesUnordered, Stream, StreamExt};
use futures_util::{stream::FuturesUnordered, StreamExt};
use log::error;
use thiserror::Error;

Expand All @@ -16,6 +15,8 @@ use crate::types::{
PlaceOrdersRequest, SpotPosition,
};

pub type GatewayResult<T> = Result<T, ControllerError>;

#[derive(Error, Debug)]
pub enum ControllerError {
#[error("internal server error")]
Expand Down Expand Up @@ -80,7 +81,7 @@ impl AppState {
/// 2) "user ids" are set, cancel all orders by user assigned id
/// 3) ids are given, cancel all orders by id (global, exchange assigned id)
/// 4) catch all. cancel all orders
pub async fn cancel_orders(&self, req: CancelOrdersRequest) -> Result<String, ControllerError> {
pub async fn cancel_orders(&self, req: CancelOrdersRequest) -> GatewayResult<String> {
let user_data = self.client.get_user_account(self.user()).await?;
let builder = TransactionBuilder::new(&self.wallet, &user_data);

Expand Down Expand Up @@ -112,7 +113,7 @@ impl AppState {
pub async fn get_positions(
&self,
req: GetPositionsRequest,
) -> Result<GetPositionsResponse, ControllerError> {
) -> GatewayResult<GetPositionsResponse> {
let (all_spot, all_perp) = self.client.all_positions(self.user()).await?;

// calculating spot token balance requires knowing the 'spot market account' data
Expand Down Expand Up @@ -153,10 +154,7 @@ impl AppState {
}

/// Return orders by market if given, otherwise return all orders
pub async fn get_orders(
&self,
req: GetOrdersRequest,
) -> Result<GetOrdersResponse, ControllerError> {
pub async fn get_orders(&self, req: GetOrdersRequest) -> GatewayResult<GetOrdersResponse> {
let orders = self.client.all_orders(self.user()).await?;
Ok(GetOrdersResponse {
orders: orders
Expand All @@ -183,7 +181,7 @@ impl AppState {
}
}

pub async fn place_orders(&self, req: PlaceOrdersRequest) -> Result<String, ControllerError> {
pub async fn place_orders(&self, req: PlaceOrdersRequest) -> GatewayResult<String> {
let orders = req
.orders
.into_iter()
Expand All @@ -203,7 +201,7 @@ impl AppState {
.map_err(handle_tx_err)
}

pub async fn modify_orders(&self, req: ModifyOrdersRequest) -> Result<String, ControllerError> {
pub async fn modify_orders(&self, req: ModifyOrdersRequest) -> GatewayResult<String> {
let user_data = &self.client.get_user_account(self.user()).await?;

let mut params = Vec::<(u32, OrderParams)>::with_capacity(req.orders.len());
Expand Down Expand Up @@ -239,35 +237,9 @@ impl AppState {
.map_err(handle_tx_err)
}

pub fn stream_orderbook(&self, req: GetOrderbookRequest) -> DlobStream {
let stream = self
.dlob_client
.subscribe(req.market.as_market_id(), Some(1)); // poll book at 1s interval
DlobStream(stream)
}
}

/// Provides JSON serialized orderbook snapshots
pub struct DlobStream(OrderbookStream);
impl Stream for DlobStream {
type Item = Result<Bytes, Error>;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
match self.0.poll_next_unpin(cx) {
std::task::Poll::Pending => std::task::Poll::Pending,
std::task::Poll::Ready(result) => {
let result = result.unwrap();
if let Err(err) = result {
error!("orderbook stream: {err:?}");
return Poll::Ready(None);
}

let msg = serde_json::to_vec(&result.unwrap()).unwrap();
std::task::Poll::Ready(Some(Ok(msg.into())))
}
}
pub async fn get_orderbook(&self, req: GetOrderbookRequest) -> GatewayResult<L2Orderbook> {
let book = self.dlob_client.get_l2(req.market.as_market_id()).await;
Ok(book?)
}
}

Expand Down
33 changes: 23 additions & 10 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ async fn get_orders(
body: actix_web::web::Bytes,
) -> impl Responder {
let mut req = GetOrdersRequest::default();
if body.len() > 0 {
if !body.is_empty() {
match serde_json::from_slice(body.as_ref()) {
Ok(deser) => req = deser,
Err(err) => {
Expand Down Expand Up @@ -64,9 +64,23 @@ async fn modify_orders(
#[delete("/orders")]
async fn cancel_orders(
controller: web::Data<AppState>,
req: Json<CancelOrdersRequest>,
body: actix_web::web::Bytes,
) -> impl Responder {
handle_result(controller.cancel_orders(req.0).await)
let mut req = CancelOrdersRequest::default();
if !body.is_empty() {
match serde_json::from_slice(body.as_ref()) {
Ok(deser) => req = deser,
Err(err) => {
return Either::Left(HttpResponse::BadRequest().json(json!(
{
"code": 400,
"reason": err.to_string(),
}
)))
}
}
};
handle_result(controller.cancel_orders(req).await)
}

#[get("/positions")]
Expand All @@ -76,7 +90,7 @@ async fn get_positions(
) -> impl Responder {
let mut req = GetPositionsRequest::default();
// handle the body manually to allow empty payload `Json` requires some body is set
if body.len() > 0 {
if !body.is_empty() {
match serde_json::from_slice(body.as_ref()) {
Ok(deser) => req = deser,
Err(err) => {
Expand All @@ -93,14 +107,13 @@ async fn get_positions(
handle_result(controller.get_positions(req).await)
}

#[get("/orderbooks")]
async fn get_orderbooks(
#[get("/orderbook")]
async fn get_orderbook(
controller: web::Data<AppState>,
req: Json<GetOrderbookRequest>,
) -> impl Responder {
let dlob = controller.stream_orderbook(req.0);
// there's no graceful shutdown for the stream: https://github.com/actix/actix-web/issues/1313
HttpResponse::Ok().streaming(dlob)
let book = controller.get_orderbook(req.0).await;
handle_result(book)
}

#[actix_web::main]
Expand Down Expand Up @@ -131,7 +144,7 @@ async fn main() -> std::io::Result<()> {
.service(create_orders)
.service(cancel_orders)
.service(modify_orders)
.service(get_orderbooks),
.service(get_orderbook),
)
})
.bind((config.host, config.port))?
Expand Down
27 changes: 14 additions & 13 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,35 @@ pub struct Order {
)]
market_type: MarketType,
amount: Decimal,
filled: Decimal,
price: Decimal,
post_only: bool,
reduce_only: bool,
user_order_id: u8,
order_id: u32,
immediate_or_cancel: bool,
}

impl Order {
pub fn from_sdk_order(value: sdk_types::Order, context: Context) -> Self {
let amount = if let MarketType::Perp = value.market_type {
Decimal::from_i128_with_scale(value.base_asset_amount as i128, BASE_PRECISION.ilog10())
let precision = if let MarketType::Perp = value.market_type {
BASE_PRECISION.ilog10()
} else {
let config =
spot_market_config_by_index(context, value.market_index).expect("market exists");
Decimal::from_i128_with_scale(
value.base_asset_amount as i128,
config.precision_exp as u32,
)
config.precision_exp as u32
};

Order {
market_id: value.market_index,
market_type: value.market_type,
price: Decimal::from_i128_with_scale(value.price as i128, PRICE_PRECISION.ilog10()),
amount,
price: Decimal::new(value.price as i64, PRICE_PRECISION.ilog10()),
amount: Decimal::new(value.base_asset_amount as i64, precision),
filled: Decimal::new(value.base_asset_amount_filled as i64, precision),
immediate_or_cancel: value.immediate_or_cancel,
reduce_only: value.reduce_only,
order_type: value.order_type,
order_id: value.order_id,
post_only: value.post_only,
user_order_id: value.user_order_id,
}
Expand Down Expand Up @@ -123,13 +125,12 @@ pub struct PerpPosition {

impl From<sdk_types::PerpPosition> for PerpPosition {
fn from(value: sdk_types::PerpPosition) -> Self {
let amount =
Decimal::from_i128_with_scale(value.base_asset_amount.into(), BASE_PRECISION.ilog10());
let amount = Decimal::new(value.base_asset_amount, BASE_PRECISION.ilog10());
Self {
amount,
market_id: value.market_index,
average_entry: Decimal::from_i128_with_scale(
(value.quote_entry_amount.abs() / value.base_asset_amount.abs().max(1)) as i128,
average_entry: Decimal::new(
value.quote_entry_amount.abs() / value.base_asset_amount.abs().max(1),
PRICE_PRECISION.ilog10(),
),
}
Expand Down Expand Up @@ -311,7 +312,7 @@ pub struct AllMarketsResponse {
pub perp: Vec<MarketInfo>,
}

#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CancelOrdersRequest {
/// Market to cancel orders
Expand Down

0 comments on commit 791a78c

Please sign in to comment.