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

Storing quotes longer, in case an on-chain order placement is intended #414

Merged
merged 10 commits into from
Aug 10, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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: 23 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 66 additions & 3 deletions crates/database/src/quotes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ use sqlx::{

pub type QuoteId = i64;

#[derive(Clone, Debug, PartialEq, sqlx::Type)]
#[sqlx(type_name = "OnchainSigningScheme")]
#[sqlx(rename_all = "lowercase")]
pub enum OnchainSigningScheme {
Eip1271,
PreSign,
}

/// One row in the `quotes` table.
#[derive(Clone, Debug, PartialEq, sqlx::FromRow)]
pub struct Quote {
Expand All @@ -20,6 +28,7 @@ pub struct Quote {
pub sell_token_price: f64,
pub order_kind: OrderKind,
pub expiration_timestamp: DateTime<Utc>,
pub onchain_signing_scheme: Option<OnchainSigningScheme>,
}

/// Stores the quote and returns the id. The id of the quote parameter is not used.
Expand All @@ -34,9 +43,10 @@ INSERT INTO quotes (
gas_price,
sell_token_price,
order_kind,
expiration_timestamp
expiration_timestamp,
onchain_signing_scheme
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING id
"#;
let (id,) = sqlx::query_as(QUERY)
Expand All @@ -49,6 +59,7 @@ RETURNING id
.bind(quote.sell_token_price)
.bind(quote.order_kind)
.bind(quote.expiration_timestamp)
.bind(&quote.onchain_signing_scheme)
.fetch_one(ex)
.await?;
Ok(id)
Expand All @@ -73,6 +84,7 @@ pub struct QuoteSearchParameters {
pub buy_amount: BigDecimal,
pub kind: OrderKind,
pub expiration: DateTime<Utc>,
pub onchain_signing_scheme: Option<OnchainSigningScheme>,
}

pub async fn find(
Expand All @@ -91,7 +103,8 @@ WHERE
(order_kind = 'buy' AND buy_amount = $5)
) AND
order_kind = $6 AND
expiration_timestamp >= $7
expiration_timestamp >= $7 AND
onchain_signing_scheme IS NOT DISTINCT FROM $8
josojo marked this conversation as resolved.
Show resolved Hide resolved
ORDER BY gas_amount * gas_price * sell_token_price ASC
LIMIT 1
"#;
Expand All @@ -103,6 +116,7 @@ LIMIT 1
.bind(&params.buy_amount)
.bind(params.kind)
.bind(params.expiration)
.bind(&params.onchain_signing_scheme)
.fetch_optional(ex)
.await
}
Expand Down Expand Up @@ -155,6 +169,7 @@ mod tests {
sell_token_price: 7.,
order_kind: OrderKind::Sell,
expiration_timestamp: now,
onchain_signing_scheme: None,
};
let id = save(&mut db, &quote).await.unwrap();
quote.id = id;
Expand Down Expand Up @@ -186,6 +201,7 @@ mod tests {
gas_price: 1.,
sell_token_price: 1.,
expiration_timestamp: now,
onchain_signing_scheme: None,
};

let token_b = ByteArray([2; 20]);
Expand All @@ -200,6 +216,7 @@ mod tests {
gas_price: 1.,
sell_token_price: 1.,
expiration_timestamp: now,
onchain_signing_scheme: None,
};

// Save two measurements for token_a
Expand Down Expand Up @@ -247,6 +264,7 @@ mod tests {
buy_amount: 1.into(),
kind: quote_a.order_kind,
expiration: now,
onchain_signing_scheme: None,
};
assert_eq!(
find(&mut db, &search_a).await.unwrap().unwrap(),
Expand Down Expand Up @@ -306,6 +324,7 @@ mod tests {
buy_amount: quote_b.buy_amount,
kind: quote_b.order_kind,
expiration: now,
onchain_signing_scheme: None,
};
assert_eq!(
find(&mut db, &search_b).await.unwrap().unwrap(),
Expand Down Expand Up @@ -345,4 +364,48 @@ mod tests {
assert_eq!(find(&mut db, &search_a).await.unwrap(), None);
assert_eq!(find(&mut db, &search_b).await.unwrap(), None);
}

#[tokio::test]
#[ignore]
async fn postgres_save_and_find_quote_and_differentiates_by_signing_scheme() {
let mut db = PgConnection::connect("postgresql://").await.unwrap();
let mut db = db.begin().await.unwrap();
crate::clear_DANGER_(&mut db).await.unwrap();

let now = low_precision_now();
let token_a = ByteArray([1; 20]);
let quote = {
let mut quote = Quote {
id: Default::default(),
sell_token: token_a,
buy_token: ByteArray([3; 20]),
sell_amount: 4.into(),
buy_amount: 5.into(),
gas_amount: 1.,
gas_price: 1.,
sell_token_price: 1.,
order_kind: OrderKind::Sell,
expiration_timestamp: now,
onchain_signing_scheme: Some(OnchainSigningScheme::Eip1271),
};
let id = save(&mut db, &quote).await.unwrap();
quote.id = id;
quote
};
// Token A has readings valid until now and in 30s
let mut search_a = QuoteSearchParameters {
sell_token: quote.sell_token,
buy_token: quote.buy_token,
sell_amount_0: quote.sell_amount.clone(),
sell_amount_1: quote.sell_amount.clone(),
buy_amount: quote.buy_amount.clone(),
kind: quote.order_kind,
expiration: quote.expiration_timestamp,
onchain_signing_scheme: quote.onchain_signing_scheme.clone(),
};

assert_eq!(find(&mut db, &search_a).await.unwrap().unwrap(), quote,);
search_a.onchain_signing_scheme = None;
assert_eq!(find(&mut db, &search_a).await.unwrap(), None,);
}
}
1 change: 1 addition & 0 deletions crates/e2e/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ harness = false
anyhow = "1.0"
autopilot = { path = "../autopilot" }
contracts = { path = "../contracts" }
chrono = "0.4"
criterion = "0.3"
database = { path = "../database" }
ethcontract = { version = "0.17.0", default-features = false }
Expand Down
2 changes: 2 additions & 0 deletions crates/e2e/tests/e2e/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ impl OrderbookServices {
..Default::default()
}),
api_db.clone(),
chrono::Duration::seconds(60i64),
chrono::Duration::seconds(60i64),
));
let balance_fetcher = Arc::new(Web3BalanceFetcher::new(
web3.clone(),
Expand Down
17 changes: 15 additions & 2 deletions crates/model/src/quote.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use crate::{
app_id::AppId,
order::{BuyTokenDestination, OrderKind, SellTokenSource},
signature::SigningScheme,
time, u256_decimal,
};
use chrono::{DateTime, Utc};
Expand All @@ -16,6 +15,20 @@ pub enum PriceQuality {
Optimal,
}

#[derive(Eq, PartialEq, Clone, Copy, Debug, Default, Deserialize, Serialize, Hash)]
#[serde(rename_all = "lowercase")]
pub enum QuoteSigningScheme {
Copy link
Contributor Author

@josojo josojo Aug 5, 2022

Choose a reason for hiding this comment

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

An alternative implementation could look like this:

#[derive( Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum QuoteSigningScheme {
    #[default]
    Eip712,
    EthSign,
    Eip1271AsOnchainOrder,
    /// EIP orders can be off-chain and on-chain orders
    Eip1271 { 
    #[serde(default)]
    intended_onchain_order: bool
    },
    PreSign  { 
    #[serde(default)]
    intended_onchain_order: bool
    },
}

But I never managed to make it backwards compatible with the current API. Hence I decided for the current form. See my playground: https://play.rust-lang.org/?version=nightly&mode=release&edition=2021&gist=db2ec1559dc581fa97c01ad7e2a70ce3

Copy link
Contributor

@nlordell nlordell Aug 6, 2022

Choose a reason for hiding this comment

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

One thing I'm not super a fan of is introducing new artificial signingScheme values that don't actually exist.

Here is a way that should be backwards compatible (basically it allows an additional onchainOrder: bool field in the JSON that gets "matched" up with the signingScheme field): https://play.rust-lang.org/?version=nightly&mode=release&edition=2021&gist=c0cbff908ab7446fc457453299f8e01c

Its almost the same as yours, I just added #[serde(tag = "signingScheme")] to the QuoteSigngingScheme type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wow... the "tag" command is smart!

But unfortunately, it is not fully backwards compatible. Serde default and serde flatten do not work together, but your solution requires both:
serde-rs/serde#1626

Here is your playground with a failing test: https://play.rust-lang.org/?version=nightly&mode=release&edition=2021&gist=68ed5b31ba40052762430b6d8e742002

I would love to take your solution, but I could not find a solution for that failing unit test

Copy link
Contributor

@nlordell nlordell Aug 8, 2022

Choose a reason for hiding this comment

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

Serde default and serde flatten do not work together, but your solution requires both:

So close! Dang, I didn't know.

In this case, I would recommend deserializing to an intermediate structure and then

match (data.signing_scheme, data.onchain_order) {
    (scheme, true) if scheme.is_ecdsa_scheme() => bail!("ECDSA-signed orders cannot be on-chain"),
    // ...
}

Something like: https://play.rust-lang.org/?version=nightly&mode=release&edition=2021&gist=0d309b803690a14fbb9cfe9e8a37b1fe

What's since is that serde generates the code to convert from the intermediary struct to the QuoteSigningScheme struct without "leaking" the abstraction.

#[default]
Eip712,
EthSign,
/// EIP1271 orders can be created via API-call or in the future via on-chain orders
Eip1271,
Eip1271ForOnchainOrder,
/// PreSign orders can be created via API-call or in the future via on-chain orders
PreSign,
PreSignForOnchainOrder,
}

/// The order parameters to quote a price and fee for.
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
Expand All @@ -38,7 +51,7 @@ pub struct OrderQuoteRequest {
#[serde(default)]
pub buy_token_balance: BuyTokenDestination,
#[serde(default)]
pub signing_scheme: SigningScheme,
pub signing_scheme: QuoteSigningScheme,
#[serde(default)]
pub price_quality: PriceQuality,
}
Expand Down
15 changes: 14 additions & 1 deletion crates/model/src/signature.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{bytes_hex, DomainSeparator};
use crate::{bytes_hex, quote::QuoteSigningScheme, DomainSeparator};
use anyhow::{ensure, Context as _, Result};
use primitive_types::{H160, H256};
use serde::{de, Deserialize, Serialize};
Expand All @@ -21,6 +21,19 @@ pub enum SigningScheme {
PreSign,
}

impl From<QuoteSigningScheme> for SigningScheme {
fn from(scheme: QuoteSigningScheme) -> Self {
match scheme {
QuoteSigningScheme::Eip712 => SigningScheme::Eip712,
QuoteSigningScheme::Eip1271 => SigningScheme::Eip1271,
QuoteSigningScheme::Eip1271ForOnchainOrder => SigningScheme::Eip1271,
QuoteSigningScheme::PreSign => SigningScheme::PreSign,
QuoteSigningScheme::PreSignForOnchainOrder => SigningScheme::PreSign,
QuoteSigningScheme::EthSign => SigningScheme::EthSign,
}
}
}

#[derive(Eq, PartialEq, Clone, Deserialize, Serialize, Hash)]
#[serde(into = "JsonSignature", try_from = "JsonSignature")]
pub enum Signature {
Expand Down
12 changes: 8 additions & 4 deletions crates/orderbook/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ components:
Provides the information to calculate the fees.
type: object
properties:
expirationDate:
expiration:
description: |
Expiration date of the offered fee. Order service might not accept
the fee after this expiration date. Encoded as ISO 8601 UTC.
Expand All @@ -552,7 +552,7 @@ components:
description: Absolute amount of fee charged per order in specified sellToken
$ref: "#/components/schemas/TokenAmount"
required:
- expirationDate
- expiration
- amount
OrderType:
description: Is this a buy order or sell order?
Expand Down Expand Up @@ -623,7 +623,7 @@ components:
$ref: "#/components/schemas/BuyTokenDestination"
default: "erc20"
signingScheme:
$ref: "#/components/schemas/SigningScheme"
$ref: "#/components/schemas/QuoteSigningScheme"
default: "eip712"
required:
- sellToken
Expand Down Expand Up @@ -837,6 +837,10 @@ components:
description: How was the order signed?
type: string
enum: [eip712, ethsign, presign]
QuoteSigningScheme:
description: How will the order be signed and is it intended for onchain order placement?
type: string
enum: [eip712, ethsign, presign, eip1271, eip1271asonchainorder, presignasonchainorder]
EcdsaSigningScheme:
description: How was the order signed?
type: string
Expand Down Expand Up @@ -1081,7 +1085,7 @@ components:
$ref: "#/components/schemas/OrderParameters"
from:
$ref: "#/components/schemas/Address"
expirationDate:
expiration:
description: |
Expiration date of the offered fee. Order service might not accept
the fee after this expiration date. Encoded as ISO 8601 UTC.
Expand Down
7 changes: 7 additions & 0 deletions crates/orderbook/src/api/create_order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,13 @@ impl IntoWarpReply for ValidationError {
error("ZeroAmount", "Buy or sell amount is zero."),
StatusCode::BAD_REQUEST,
),
Self::IncompatibleSigningScheme => with_status(
error(
"IncompatibleSigningScheme",
"Signing scheme is not compatible with order placement method.",
),
StatusCode::BAD_REQUEST,
),
Self::Other(err) => with_status(
internal_error(err.context("order_validation")),
StatusCode::INTERNAL_SERVER_ERROR,
Expand Down
Loading