From f808db23a7b940d79c3decaedbf91d785fd05251 Mon Sep 17 00:00:00 2001 From: ilya Date: Mon, 28 Oct 2024 14:46:05 +0000 Subject: [PATCH] Quote with JIT orders API (#3083) # Description Closes task n1 from https://github.com/cowprotocol/services/issues/3082. Introduces a new version of the driver's `/quote` response in the API schema in order to support a gradual migration to the quote with jit orders making the changes backward compatible. The driver changes are expected in the future PRs. # Changes > Update the driver's OpenAPI schema quote object with an enum with the old and new versions. Trade verifier returns error in case a new version is received. ## How to test Existing tests. I was thinking about how to test it properly, but it still requires e2e tests. So, it should be implemented in the next PRs. --- crates/driver/openapi.yml | 148 +++++++++++++++++--- crates/shared/src/trade_finding/external.rs | 105 ++++++++++++-- 2 files changed, 218 insertions(+), 35 deletions(-) diff --git a/crates/driver/openapi.yml b/crates/driver/openapi.yml index 91f8037113..215f42c81e 100644 --- a/crates/driver/openapi.yml +++ b/crates/driver/openapi.yml @@ -48,7 +48,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/QuoteResponse" + $ref: "#/components/schemas/QuoteResponseKind" 400: $ref: "#/components/responses/BadRequest" 429: @@ -292,31 +292,81 @@ components: and bytes 52..56 valid to, type: string example: "0x30cff40d9f60caa68a37f0ee73253ad6ad72b45580c945fe3ab67596476937197854163b1b0d24e77dca702b97b5cc33e0f83dcb626122a6" - QuoteResponse: + QuoteResponseKind: oneOf: - - description: | - Successful Quote + - $ref: "#/components/schemas/LegacyQuoteResponse" + - $ref: "#/components/schemas/QuoteResponse" + - $ref: "#/components/schemas/Error" + LegacyQuoteResponse: + description: | + Successful Quote - The Solver knows how to fill the request with these parameters. + The Solver knows how to fill the request with these parameters. - If the request was of type `buy` then the response's buy amount has the same value as the - request's amount and the sell amount was filled in by the server. Vice versa for type - `sell`. + If the request was of type `buy` then the response's buy amount has the same value as the + request's amount and the sell amount was filled in by the server. Vice versa for type + `sell`. + type: object + properties: + amount: + $ref: "#/components/schemas/TokenAmount" + interactions: + type: array + items: + $ref: "#/components/schemas/Interaction" + solver: + description: The address of the solver that quoted this order. + $ref: "#/components/schemas/Address" + gas: + type: integer + description: How many units of gas this trade is estimated to cost. + txOrigin: + allOf: + - $ref: "#/components/schemas/Address" + description: Which `tx.origin` is required to make a quote simulation pass. + required: + - amount + - interactions + - solver + QuoteResponse: + description: | + Successful Quote with JIT orders support. + + The Solver knows how to fill the request with these parameters. + type: object + properties: + clearingPrices: + description: | + Mapping of hex token address to the uniform clearing price. type: object - properties: - amount: - $ref: "#/components/schemas/TokenAmount" - interactions: - type: array - items: - $ref: "#/components/schemas/Interaction" - solver: - description: The address of the solver that quoted this order. - $ref: "#/components/schemas/Address" - gas: - type: integer - description: How many units of gas this trade is estimated to cost. - - $ref: "#/components/schemas/Error" + additionalProperties: + $ref: "#/components/schemas/BigUint" + preInteractions: + type: array + items: + $ref: "#/components/schemas/Interaction" + interactions: + type: array + items: + $ref: "#/components/schemas/Interaction" + solver: + allOf: + - $ref: "#/components/schemas/Address" + description: The address of the solver that quoted this order. + gas: + type: integer + description: How many units of gas this trade is estimated to cost. + txOrigin: + allOf: + - $ref: "#/components/schemas/Address" + description: Which `tx.origin` is required to make a quote simulation pass. + jitOrders: + type: array + items: + $ref: "#/components/schemas/JitOrder" + required: + - clearingPrices + - solver DateTime: description: An ISO 8601 UTC date time string. type: string @@ -514,6 +564,60 @@ components: $ref: "#/components/schemas/TokenAmount" solver: $ref: "#/components/schemas/Address" + JitOrder: + type: object + properties: + sellToken: + $ref: "#/components/schemas/Address" + buyToken: + $ref: "#/components/schemas/Address" + sellAmount: + $ref: "#/components/schemas/TokenAmount" + buyAmount: + $ref: "#/components/schemas/TokenAmount" + executedAmount: + $ref: "#/components/schemas/TokenAmount" + receiver: + $ref: "#/components/schemas/Address" + validTo: + type: integer + side: + type: string + enum: ["buy", "sell"] + partiallyFillable: + type: boolean + sellTokenSource: + type: string + enum: ["erc20", "internal", "external"] + buyTokenSource: + type: string + enum: ["erc20", "internal"] + appData: + type: string + signature: + description: | + Hex encoded bytes with `0x` prefix. The content depends on the `signingScheme`. + For `presign`, this should contain the address of the owner. + For `eip1271`, the signature should consist of ``. + type: string + signingScheme: + type: string + enum: ["eip712", "ethsign", "presign", "eip1271"] + required: + - sellToken + - buyToken + - sellAmount + - buyAmount + - executedAmount + - receiver + - validTo + - side + - partiallyFillable + - sellTokenSource + - buyTokenSource + - appData + - signature + - signingScheme Error: description: Response on API errors. type: object diff --git a/crates/shared/src/trade_finding/external.rs b/crates/shared/src/trade_finding/external.rs index ecf651f08e..8758847731 100644 --- a/crates/shared/src/trade_finding/external.rs +++ b/crates/shared/src/trade_finding/external.rs @@ -93,15 +93,19 @@ impl ExternalTradeFinder { .text() .await .map_err(|err| PriceEstimationError::EstimatorInternal(anyhow!(err)))?; - serde_json::from_str::(&text) - .map(Trade::from) - .map_err(|err| { - if let Ok(err) = serde_json::from_str::(&text) { - PriceEstimationError::from(err) - } else { - PriceEstimationError::EstimatorInternal(anyhow!(err)) - } - }) + let quote = serde_json::from_str::(&text).map_err(|err| { + if let Ok(err) = serde_json::from_str::(&text) { + PriceEstimationError::from(err) + } else { + PriceEstimationError::EstimatorInternal(anyhow!(err)) + } + })?; + match quote { + dto::QuoteKind::Legacy(quote) => Ok(Trade::from(quote)), + dto::QuoteKind::Regular(_) => Err(PriceEstimationError::EstimatorInternal( + anyhow!("Quote with JIT orders is not currently supported"), + )), + } } .boxed() }; @@ -113,8 +117,8 @@ impl ExternalTradeFinder { } } -impl From for Trade { - fn from(quote: dto::Quote) -> Self { +impl From for Trade { + fn from(quote: dto::LegacyQuote) -> Self { Self { out_amount: quote.amount, gas_estimate: quote.gas, @@ -142,6 +146,16 @@ impl From for PriceEstimationError { } } +impl From for Interaction { + fn from(interaction: dto::Interaction) -> Self { + Self { + target: interaction.target, + value: interaction.value, + data: interaction.call_data, + } + } +} + #[async_trait::async_trait] impl TradeFinding for ExternalTradeFinder { async fn get_quote(&self, query: &Query) -> Result { @@ -166,12 +180,17 @@ impl TradeFinding for ExternalTradeFinder { mod dto { use { + app_data::AppDataHash, bytes_hex::BytesHex, ethcontract::{H160, U256}, - model::order::OrderKind, + model::{ + order::{BuyTokenDestination, OrderKind, SellTokenSource}, + signature::SigningScheme, + }, number::serialization::HexOrDecimalU256, serde::{Deserialize, Serialize}, serde_with::serde_as, + std::collections::HashMap, }; #[serde_as] @@ -186,10 +205,19 @@ mod dto { pub deadline: chrono::DateTime, } + #[serde_as] + #[derive(Clone, Debug, Deserialize)] + #[serde(untagged)] + pub enum QuoteKind { + Legacy(LegacyQuote), + #[allow(unused)] + Regular(Quote), + } + #[serde_as] #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] - pub struct Quote { + pub struct LegacyQuote { #[serde_as(as = "HexOrDecimalU256")] pub amount: U256, pub interactions: Vec, @@ -199,6 +227,24 @@ mod dto { pub tx_origin: Option, } + #[serde_as] + #[derive(Clone, Debug, Deserialize)] + #[serde(rename_all = "camelCase")] + #[allow(unused)] + pub struct Quote { + #[serde_as(as = "HashMap<_, HexOrDecimalU256>")] + pub clearing_prices: HashMap, + #[serde(default)] + pub pre_interactions: Vec, + #[serde(default)] + pub interactions: Vec, + pub solver: H160, + pub gas: Option, + pub tx_origin: Option, + #[serde(default)] + pub jit_orders: Vec, + } + #[serde_as] #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] @@ -210,6 +256,31 @@ mod dto { pub call_data: Vec, } + #[serde_as] + #[derive(Clone, Debug, Eq, PartialEq, Deserialize)] + #[serde(rename_all = "camelCase")] + #[allow(unused)] + pub struct JitOrder { + pub buy_token: H160, + pub sell_token: H160, + #[serde_as(as = "HexOrDecimalU256")] + pub sell_amount: U256, + #[serde_as(as = "HexOrDecimalU256")] + pub buy_amount: U256, + #[serde_as(as = "HexOrDecimalU256")] + pub executed_amount: U256, + pub receiver: H160, + pub valid_to: u32, + pub app_data: AppDataHash, + pub side: Side, + pub partially_fillable: bool, + pub sell_token_source: SellTokenSource, + pub buy_token_destination: BuyTokenDestination, + #[serde_as(as = "BytesHex")] + pub signature: Vec, + pub signing_scheme: SigningScheme, + } + #[serde_as] #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] @@ -217,4 +288,12 @@ mod dto { pub kind: String, pub description: String, } + + #[serde_as] + #[derive(Clone, Debug, Eq, PartialEq, Deserialize)] + #[serde(rename_all = "camelCase")] + pub enum Side { + Buy, + Sell, + } }