Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add atomic cancel/modify/place #21

Merged
merged 2 commits into from
Jan 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ $ curl localhost:8080/v2/orders -X POST \
Returns solana tx signature on success

### Modify Orders
like place orders but caller must specify either `orderId` or `userOrderId` to indicate which order(s) to modify.
like place orders but caller must use either `orderId` or `userOrderId` to indicate which order(s) to modify.

- `amount` can be modified to flip the order from long/short to bid/ask
- the order market cannot be modified.
Expand All @@ -260,14 +260,18 @@ $ curl localhost:8080/v2/orders -X PATCH \
-H 'content-type: application/json' \
-d '{
"orders": [{
"marketIndex": 0,
"marketType": "perp",
"amount": -1.1,
"price": 80.5,
"userOrderId": 101
},
{
"amount": 1.05,
"marketIndex": 1,
"marketType": "spot",
"amount": 2.05,
"price": 61.0,
"orderId": 32
"userOrderId": 32
}]
}'
```
Expand All @@ -286,10 +290,10 @@ $ curl localhost:8080/v2/orders -X DELETE
```
Returns solana tx signature on success

### Cancel and Place Orders
### Atomic Cancel/Modify/Place Orders

Atomically cancel then place orders without possible downtime.
Request format is an embedded cancel and place request
Atomically cancel, modify, and place orders without possible downtime.
Request format is an embedded cancel modify, and place request

```bash
$ curl localhost:8080/v2/orders/cancelAndPlace -X POST -H 'content-type: application/json' \
Expand All @@ -298,13 +302,22 @@ $ curl localhost:8080/v2/orders/cancelAndPlace -X POST -H 'content-type: applica
"marketIndex": 0,
"marketType": "perp"
},
"modify": {
"orders": [{
"marketIndex": 0,
"marketType": "perp",
"orderId": 555,
"amount": -0.5,
"price": 82.0
}]
},
"place": {
"orders": [
{
"marketIndex": 0,
"marketType": "perp",
"amount": -1.23,
"price": 80.0,
"price": 99.0,
"postOnly": true,
"orderType": "limit",
"immediateOrCancel": false,
Expand Down
86 changes: 43 additions & 43 deletions src/controller.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{borrow::Cow, sync::Arc};

use drift_sdk::{
constants::ProgramData,
dlob::DLOBClient,
types::{Context, MarketType, ModifyOrderParams, SdkError, SdkResult},
DriftClient, Pubkey, TransactionBuilder, Wallet, WsAccountProvider,
Expand All @@ -22,8 +23,6 @@ pub type GatewayResult<T> = Result<T, ControllerError>;
pub enum ControllerError {
#[error("internal error: {0}")]
Sdk(#[from] SdkError),
#[error("order id not found: {0}")]
UnknownOrderId(u32),
#[error("{0}")]
BadRequest(String),
#[error("tx failed ({code}): {reason}")]
Expand Down Expand Up @@ -215,7 +214,8 @@ impl AppState {
)
.payer(self.wallet.signer());

let tx = build_cancel_ix(builder, req.cancel)?
let builder = build_cancel_ix(builder, req.cancel)?;
let tx = build_modify_ix(builder, req.modify, self.client.program_data())?
.place_orders(orders)
.build();

Expand Down Expand Up @@ -265,51 +265,14 @@ impl AppState {
) -> GatewayResult<TxResponse> {
let sub_account = self.wallet.sub_account(sub_account_id);
let account_data = &self.client.get_user_account(&sub_account).await?;
// NB: its possible to let the drift program sort the modifications by userOrderId
// sorting it client side for simplicity
let mut params = Vec::<(u32, ModifyOrderParams)>::with_capacity(req.orders.len());
for order in req.orders {
if let Some(order_id) = order.order_id {
if let Some(onchain_order) =
account_data.orders.iter().find(|x| x.order_id == order_id)
{
let base_decimals = get_market_decimals(
self.client.program_data(),
Market::new(onchain_order.market_index, onchain_order.market_type),
);
params.push((order_id, order.to_order_params(base_decimals)));
continue;
}
} else if let Some(user_order_id) = order.user_order_id {
if let Some(onchain_order) = account_data
.orders
.iter()
.find(|x| x.user_order_id == user_order_id)
{
let base_decimals = get_market_decimals(
self.client.program_data(),
Market::new(onchain_order.market_index, onchain_order.market_type),
);
params.push((onchain_order.order_id, order.to_order_params(base_decimals)));
continue;
}
}

return Err(ControllerError::UnknownOrderId(
order
.order_id
.unwrap_or(order.user_order_id.unwrap_or(0) as u32),
));
}

let tx = TransactionBuilder::new(
let builder = TransactionBuilder::new(
self.client.program_data(),
sub_account,
Cow::Borrowed(account_data),
)
.payer(self.wallet.signer())
.modify_orders(params.as_slice())
.build();
.payer(self.wallet.signer());
let tx = build_modify_ix(builder, req, self.client.program_data())?.build();

self.client
.sign_and_send(&self.wallet, tx)
Expand Down Expand Up @@ -366,6 +329,43 @@ fn build_cancel_ix(
}
}

fn build_modify_ix<'a>(
builder: TransactionBuilder<'a>,
req: ModifyOrdersRequest,
program_data: &ProgramData,
) -> GatewayResult<TransactionBuilder<'a>> {
if req.orders.is_empty() {
return Ok(builder);
}

let by_user_order_ids = req.orders[0].user_order_id.is_some_and(|x| x > 0);
if by_user_order_ids {
let mut params = Vec::<(u8, ModifyOrderParams)>::with_capacity(req.orders.len());
for order in req.orders {
let base_decimals = get_market_decimals(program_data, order.market);
params.push((
order.user_order_id.ok_or(ControllerError::BadRequest(
"userOrderId not set".to_string(),
))?,
order.to_order_params(base_decimals),
));
}
Ok(builder.modify_orders_by_user_id(params.as_slice()))
} else {
let mut params = Vec::<(u32, ModifyOrderParams)>::with_capacity(req.orders.len());
for order in req.orders {
let base_decimals = get_market_decimals(program_data, order.market);
params.push((
order
.order_id
.ok_or(ControllerError::BadRequest("orderId not set".to_string()))?,
order.to_order_params(base_decimals),
));
}
Ok(builder.modify_orders(params.as_slice()))
}
}

/// Initialize a wallet for controller, possible valid configs:
///
/// 1) keypair
Expand Down
8 changes: 0 additions & 8 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,14 +248,6 @@ fn handle_result<T>(result: Result<T, ControllerError>) -> Either<HttpResponse,
}
)))
}
Err(ControllerError::UnknownOrderId(id)) => {
Either::Left(HttpResponse::NotFound().json(json!(
{
"code": 404,
"reason": format!("order: {id}"),
}
)))
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ pub struct ModifyOrdersRequest {
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ModifyOrder {
#[serde(flatten)]
pub market: Market,
amount: Option<Decimal>,
price: Option<Decimal>,
pub user_order_id: Option<u8>,
Expand Down Expand Up @@ -453,6 +455,7 @@ impl TxResponse {
#[derive(Serialize, Deserialize)]
pub struct CancelAndPlaceRequest {
pub cancel: CancelOrdersRequest,
pub modify: ModifyOrdersRequest,
pub place: PlaceOrdersRequest,
}

Expand Down
Loading