Skip to content
This repository was archived by the owner on Apr 28, 2022. It is now read-only.

New fee and amount quote API #1143

Merged
merged 5 commits into from
Sep 24, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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
169 changes: 153 additions & 16 deletions orderbook/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,40 @@ paths:
$ref: "#/components/schemas/Order"
400:
description: Problem with parameters like limit being too large.
/api/v1/quote:
post:
summary: Quotes a price and fee for the specified order parameters.
description: |
This API endpoint accepts a partial order and computes the minimum fee and
a price estimate for the order. It returns a full order that can be used
directly for signing, and with an included signature, passed directly to
the order creation endpoint.
requestBody:
description: The order parameters to compute a quote for.
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/OrderQuoteRequest"
responses:
200:
description: Quoted order.
content:
application/json:
schema:
$ref: "#/components/schemas/OrderQuote"
400:
description: Error quoting order.
content:
application/json:
schema:
$ref: "#/components/schemas/FeeAndQuoteError"
403:
description: Forbidden, your account is deny-listed
429:
description: Too many order placements
500:
description: Error quoting an order
Copy link
Contributor

@bh2smith bh2smith Sep 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see the difference here between 400 and 500 errors - both are "Error quoting order".

Also, is the pattern for #/components/schemas/FeeAndQuoteError defined here, because it doesn't appear to be.

I think we may also be missing some possibilities here. For example;

  • UnsupportedToken(H160),
  • TransferEthToContract,
  • SameBuyAndSellToken,
  • UnsupportedBuyTokenDestination(BuyTokenDestination),
  • UnsupportedSellTokenSource(SellTokenSource),
  • ZeroAmount,

I can't really see the reason for the 429 here, unless we mean to change this to "RateLimited" too many quotes? but this could easily be worked around.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be cute to encapsulate all these different specifics into the single FeeAndQuoteError which i imagine is the intention here. Something like

pub enum PostQuoteResult {
    QuoteConstructed(OrderQuote),
    FeeAndQuoteError(FeeError),
    Forbidden,
}

pub enum FeeError {
    UnsupportedToken(H160),
    TransferEthToContract,
    SameBuyAndSellToken,
    UnsupportedBuyTokenDestination(BuyTokenDestination),
    UnsupportedSellTokenSource(SellTokenSource),
    ZeroAmount,
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#/components/schemas/FeeAndQuoteError already exists (ans is used by the feeAndQuote route). Since the errors are the same, I just reused the error name for now.

Additionally, we would probably reuse the error type from the existing feeAndQuote route.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds reasonable, but what about all these other responses that seem out of place

        403:
          description: Forbidden, your account is deny-listed
        429:
          description: Too many order placements
        500:
          description: Error quoting an order

as I see it, the only things we will need here are

#[derive(Serialize)]
pub enum OrderQuoteResult {
    QuoteConstructed(OrderQuote),
    FeeAndQuoteError(FeeAndQuoteError),
}

along with some possible rate limiting...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

along with some possible rate limiting...

That is what the 429 is for AFIAU:

        429:
          description: Too many order placements # i.e you quoted too many orders and are being rate limited. We should `s/placements/quotes/` to make the error a bit clearer.

Additionally, 500 is used as a catch all "internal server error" - i.e. something unexpected went wrong.

Finally:

403:
          description: Forbidden, your account is deny-listed

Since the from is passed in, we can also check that the owner is forbidden by address (we have an "owner deny list" already).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed 429 description and added "unexpected" to 500 description.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There appears to be something missing in the description of 429 here in your comment...

We should `s/placements/quotes/` to make the error a bit clearer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was changed to:

        429:
          description: Too many order quotes

components:
schemas:
TransactionHash:
Expand Down Expand Up @@ -493,8 +527,8 @@ components:
description: The current order status
type: string
enum: [presignaturePending, open, fulfilled, cancelled, expired]
OrderCreation:
description: Data a user provides when creating a new order.
OrderParameters:
description: Order parameters.
type: object
properties:
sellToken:
Expand Down Expand Up @@ -533,24 +567,12 @@ components:
partiallyFillable:
description: Is this a fill-or-kill order or a partially fillable order?
type: boolean
signature:
$ref: "#/components/schemas/Signature"
signingScheme:
$ref: "#/components/schemas/SigningScheme"
sellTokenBalance:
$ref: "#/components/schemas/SellTokenSource"
default: "erc20"
buyTokenBalance:
$ref: "#/components/schemas/BuyTokenDestination"
default: "erc20"
from:
description: |
If set, the backend enforces that this address matches what is decoded as the signer of
the signature. This helps catch errors with invalid signature encodings as the backend
might otherwise silently work with an unexpected address that for example does not have
any balance.
$ref: "#/components/schemas/Address"
nullable: true
required:
- sellToken
- buyToken
Expand All @@ -561,8 +583,27 @@ components:
- feeAmount
- kind
- partiallyFillable
- signature
- signingScheme
OrderCreation:
description: Data a user provides when creating a new order.
allOf:
- $ref: "#/components/schemas/OrderParameters"
- type: object
properties:
signingScheme:
$ref: "#/components/schemas/SigningScheme"
signature:
$ref: "#/components/schemas/Signature"
from:
description: |
If set, the backend enforces that this address matches what is decoded as the signer of
the signature. This helps catch errors with invalid signature encodings as the backend
might otherwise silently work with an unexpected address that for example does not have
any balance.
$ref: "#/components/schemas/Address"
nullable: true
required:
- signingScheme
- signature
OrderMetaData:
description: |
Extra order data that is returned to users when querying orders
Expand Down Expand Up @@ -769,3 +810,99 @@ components:
required:
- errorType
- description
OrderQuoteSide:
description: The buy or sell side when quoting an order.
oneOf:
- type: object
description: Quote a sell order given the final total sell amount including fees
properties:
kind:
type: string
enum: [sell]
sellAmountBeforeFee:
description: |
The total amount that is available for the order. From this value, the fee
is deducted and the buy amount is calculated.
$ref: "#/components/schemas/TokenAmount"
required:
- kind
- sellAmountBeforeFee
- type: object
description: Quote a sell order given the sell amount.
properties:
kind:
type: string
enum: [sell]
sellAmountAfterFee:
description: The sell amount for the order.
$ref: "#/components/schemas/TokenAmount"
required:
- kind
- sellAmountAfterFee
- type: object
description: Quote a buy order given an exact buy amount.
properties:
kind:
type: string
enum: [buy]
buyAmountAfterFee:
description: The buy amount for the order.
$ref: "#/components/schemas/TokenAmount"
required:
- kind
- buyAmountAfterFee
OrderQuoteRequest:
description: Request fee and price quote.
allOf:
- $ref: "#/components/schemas/OrderQuoteSide"
- type: object
properties:
sellToken:
description: "ERC20 token to be sold"
$ref: "#/components/schemas/Address"
buyToken:
description: "ERC20 token to be bought"
$ref: "#/components/schemas/Address"
receiver:
description: |
An optional address to receive the proceeds of the trade instead of the
owner (i.e. the order signer).
$ref: "#/components/schemas/Address"
nullable: true
validTo:
description: Unix timestamp until the order is valid. uint32.
type: integer
appData:
description: |
Arbitrary application specific data that can be added to an order. This can
also be used to ensure uniqueness between two orders with otherwise the
exact same parameters.
$ref: "#/components/schemas/AppData"
partiallyFillable:
description: Is this a fill-or-kill order or a partially fillable order?
type: boolean
sellTokenBalance:
$ref: "#/components/schemas/SellTokenSource"
default: "erc20"
buyTokenBalance:
$ref: "#/components/schemas/BuyTokenDestination"
default: "erc20"
required:
- sellToken
- buyToken
- validTo
- appData
- partiallyFillable
OrderQuote:
description: |
An order quoted by the back end that can be directly signed and
submitted to the order creation backend.
allOf:
- $ref: "#/components/schemas/OrderParameters"
- type: object
properties:
from:
description: The expected address of the signer
$ref: "#/components/schemas/Address"
required:
- from
4 changes: 4 additions & 0 deletions orderbook/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod get_orders;
mod get_solvable_orders;
mod get_trades;
mod get_user_orders;
mod post_quote;

use crate::{
database::trades::TradeRetrieving,
Expand Down Expand Up @@ -48,6 +49,7 @@ pub fn handle_all_routes(
let get_fee_and_quote_buy =
get_fee_and_quote::get_fee_and_quote_buy(fee_calculator, price_estimator.clone());
let get_user_orders = get_user_orders::get_user_orders(orderbook);
let post_quote = post_quote::post_quote();
let cors = warp::cors()
.allow_any_origin()
.allow_methods(vec!["GET", "POST", "DELETE", "OPTIONS", "PUT", "PATCH"])
Expand Down Expand Up @@ -77,6 +79,8 @@ pub fn handle_all_routes(
.map(|reply| LabelledReply::new(reply, "get_fee_and_quote_buy")))
.unify()
.or(get_user_orders.map(|reply| LabelledReply::new(reply, "get_user_orders")))
.unify()
.or(post_quote.map(|reply| LabelledReply::new(reply, "get_user_orders")))
.unify(),
);
routes_with_labels
Expand Down
105 changes: 105 additions & 0 deletions orderbook/src/api/post_quote.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use crate::api;
use anyhow::{anyhow, Result};
use ethcontract::{H160, U256};
use model::{
appdata_hexadecimal,
order::{BuyTokenDestination, OrderKind, SellTokenSource},
u256_decimal,
};
use serde::{Deserialize, Serialize};
use std::convert::Infallible;
use warp::{hyper::StatusCode, reply, Filter, Rejection, Reply};

/// The order parameters to quote a price and fee for.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct OrderQuoteRequest {
from: H160,
sell_token: H160,
buy_token: H160,
receiver: Option<H160>,
#[serde(flatten)]
side: OrderQuoteSide,
valid_to: u32,
#[serde(with = "appdata_hexadecimal")]
app_data: [u8; 32],
partially_fillable: bool,
#[serde(default)]
sell_token_balance: SellTokenSource,
#[serde(default)]
buy_token_balance: BuyTokenDestination,
}

#[derive(Debug, Deserialize)]
#[serde(tag = "kind")]
enum OrderQuoteSide {
#[serde(rename_all = "camelCase")]
Sell {
#[serde(flatten)]
sell_amount: SellAmount,
},
#[serde(rename_all = "camelCase")]
Buy {
#[serde(with = "u256_decimal")]
buy_amount_after_fee: U256,
},
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum SellAmount {
BeforeFee {
#[serde(rename = "sell_amount_before_fee")]
value: U256,
},
AfterFee {
#[serde(rename = "sell_amount_after_fee")]
value: U256,
},
}

/// The quoted order by the service.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct OrderQuote {
from: H160,
sell_token: H160,
buy_token: H160,
receiver: Option<H160>,
#[serde(with = "u256_decimal")]
sell_amount: U256,
#[serde(with = "u256_decimal")]
buy_amount: U256,
valid_to: u32,
#[serde(with = "appdata_hexadecimal")]
app_data: [u8; 32],
#[serde(with = "u256_decimal")]
fee_amount: U256,
kind: OrderKind,
partially_fillable: bool,
sell_token_balance: SellTokenSource,
buy_token_balance: BuyTokenDestination,
}

fn post_quote_request() -> impl Filter<Extract = (OrderQuoteRequest,), Error = Rejection> + Clone {
warp::path!("feeAndQuote" / "sell")
.and(warp::post())
.and(api::extract_payload())
}

fn post_order_response(result: Result<OrderQuote>) -> impl Reply {
match result {
Ok(response) => reply::with_status(reply::json(&response), StatusCode::OK),
Err(err) => reply::with_status(
super::error("NotYetImplemented", err.to_string()),
StatusCode::INTERNAL_SERVER_ERROR,
),
}
}

pub fn post_quote() -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone {
post_quote_request().and_then(move |request| async move {
tracing::warn!("unimplemented request {:#?}", request);
Result::<_, Infallible>::Ok(post_order_response(Err(anyhow!("not yet implemented"))))
})
}