diff --git a/Cargo.lock b/Cargo.lock index 17ca562be0..90dc68d1b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1683,6 +1683,7 @@ dependencies = [ "futures", "hex", "maplit", + "serde_json", "sqlx", "strum", "tokio", diff --git a/crates/autopilot/src/database/auction.rs b/crates/autopilot/src/database/auction.rs index 30c4b037e4..606f0ce765 100644 --- a/crates/autopilot/src/database/auction.rs +++ b/crates/autopilot/src/database/auction.rs @@ -23,7 +23,7 @@ impl QuoteStoring for Postgres { .start_timer(); let mut ex = self.pool.acquire().await?; - let row = create_quote_row(data); + let row = create_quote_row(data)?; let id = database::quotes::save(&mut ex, &row).await?; Ok(id) } diff --git a/crates/autopilot/src/database/onchain_order_events/mod.rs b/crates/autopilot/src/database/onchain_order_events/mod.rs index 0c757d63ff..9ae6051493 100644 --- a/crates/autopilot/src/database/onchain_order_events/mod.rs +++ b/crates/autopilot/src/database/onchain_order_events/mod.rs @@ -487,6 +487,8 @@ async fn parse_general_onchain_order_placement_data<'a>( sell_amount: u256_to_big_decimal("e.sell_amount), buy_amount: u256_to_big_decimal("e.buy_amount), solver: ByteArray(quote.data.solver.0), + verified: quote.data.verified, + metadata: quote.data.metadata.try_into()?, }), Err(err) => { let err_label = err.to_metrics_label(); @@ -1187,6 +1189,8 @@ mod test { sell_amount: u256_to_big_decimal("e.sell_amount), buy_amount: u256_to_big_decimal("e.buy_amount), solver: ByteArray(quote.data.solver.0), + verified: quote.data.verified, + metadata: quote.data.metadata.try_into().unwrap(), }; assert_eq!(result.1, vec![Some(expected_quote)]); assert_eq!( diff --git a/crates/database/Cargo.toml b/crates/database/Cargo.toml index 572530a842..95a288e8df 100644 --- a/crates/database/Cargo.toml +++ b/crates/database/Cargo.toml @@ -13,6 +13,7 @@ futures = { workspace = true } hex = { workspace = true } sqlx = { workspace = true } strum = { workspace = true } +serde_json = { workspace = true } [dev-dependencies] maplit = { workspace = true } diff --git a/crates/database/src/orders.rs b/crates/database/src/orders.rs index a574dbc882..ebcc249abe 100644 --- a/crates/database/src/orders.rs +++ b/crates/database/src/orders.rs @@ -329,6 +329,8 @@ pub struct Quote { pub sell_amount: BigDecimal, pub buy_amount: BigDecimal, pub solver: Address, + pub verified: bool, + pub metadata: serde_json::Value, } pub async fn insert_quotes(ex: &mut PgConnection, quotes: &[Quote]) -> Result<(), sqlx::Error> { @@ -346,9 +348,11 @@ INSERT INTO order_quotes ( sell_token_price, sell_amount, buy_amount, - solver + solver, + verified, + metadata ) -VALUES ($1, $2, $3, $4, $5, $6, $7)"#; +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)"#; pub async fn insert_quote_and_update_on_conflict( ex: &mut PgConnection, @@ -362,7 +366,7 @@ pub async fn insert_quote_and_update_on_conflict( " ON CONFLICT (order_uid) DO UPDATE SET gas_amount = $2, gas_price = $3, sell_token_price = $4, sell_amount = $5, -buy_amount = $6 +buy_amount = $6, verified = $8, metadata = $9 " ); sqlx::query(QUERY) @@ -373,6 +377,8 @@ buy_amount = $6 .bind("e.sell_amount) .bind("e.buy_amount) .bind(quote.solver) + .bind(quote.verified) + .bind("e.metadata) .execute(ex) .await?; Ok(()) @@ -387,6 +393,8 @@ pub async fn insert_quote(ex: &mut PgConnection, quote: &Quote) -> Result<(), sq .bind("e.sell_amount) .bind("e.buy_amount) .bind(quote.solver) + .bind(quote.verified) + .bind("e.metadata) .execute(ex) .await?; Ok(()) @@ -498,6 +506,8 @@ pub struct FullOrderWithQuote { pub quote_gas_amount: Option, pub quote_gas_price: Option, pub quote_sell_token_price: Option, + pub quote_verified: Option, + pub quote_metadata: Option, pub solver: Option
, } @@ -591,6 +601,8 @@ pub async fn single_full_order_with_quote( ", o_quotes.gas_amount as quote_gas_amount", ", o_quotes.gas_price as quote_gas_price", ", o_quotes.sell_token_price as quote_sell_token_price", + ", o_quotes.verified as quote_verified", + ", o_quotes.metadata as quote_metadata", ", o_quotes.solver as solver", " FROM ", FROM, " LEFT JOIN order_quotes o_quotes ON o.uid = o_quotes.order_uid", @@ -1201,6 +1213,8 @@ mod tests { sell_amount: 4.into(), buy_amount: 5.into(), solver: ByteArray([1; 20]), + verified: false, + metadata: Default::default(), }; insert_quote(&mut db, "e).await.unwrap(); insert_quote_and_update_on_conflict(&mut db, "e) @@ -1253,6 +1267,20 @@ mod tests { let mut db = db.begin().await.unwrap(); crate::clear_DANGER_(&mut db).await.unwrap(); + let metadata: serde_json::Value = serde_json::from_str( + r#"{ "version":"1.0", "interactions": [ { + "target": "0x0102030405060708091011121314151617181920", + "value": "1", + "callData": "0x0A0B0C102030" + },{ + "target": "0xFF02030405060708091011121314151617181920", + "value": "2", + "callData": "0xFF0B0C102030" + }] + }"#, + ) + .unwrap(); + let quote = Quote { order_uid: Default::default(), gas_amount: 1., @@ -1261,6 +1289,8 @@ mod tests { sell_amount: 4.into(), buy_amount: 5.into(), solver: ByteArray([1; 20]), + verified: true, + metadata, }; insert_quote(&mut db, "e).await.unwrap(); let quote_ = read_quote(&mut db, "e.order_uid) @@ -1287,6 +1317,8 @@ mod tests { sell_amount: 4.into(), buy_amount: 5.into(), solver: ByteArray([1; 20]), + verified: false, + metadata: Default::default(), }; insert_quote(&mut db, "e).await.unwrap(); let order_with_quote = single_full_order_with_quote(&mut db, "e.order_uid) @@ -2142,4 +2174,36 @@ mod tests { ] ); } + + #[tokio::test] + #[ignore] + async fn postgres_get_quote_with_no_metadata_and_validity() { + // This test checks backward compatibility + let mut db = PgConnection::connect("postgresql://").await.unwrap(); + let mut db = db.begin().await.unwrap(); + crate::clear_DANGER_(&mut db).await.unwrap(); + + let quote = Quote { + order_uid: Default::default(), + gas_amount: 1., + gas_price: 2., + sell_token_price: 3., + sell_amount: 4.into(), + buy_amount: 5.into(), + solver: ByteArray([1; 20]), + verified: false, + metadata: Default::default(), + }; + + // insert quote with verified and metadata fields stored as NULL + insert_quote_and_update_on_conflict(&mut db, "e) + .await + .unwrap(); + + let quote_ = read_quote(&mut db, "e.order_uid) + .await + .unwrap() + .unwrap(); + assert_eq!(quote, quote_); + } } diff --git a/crates/database/src/quotes.rs b/crates/database/src/quotes.rs index 41f94e50da..a932566c55 100644 --- a/crates/database/src/quotes.rs +++ b/crates/database/src/quotes.rs @@ -34,6 +34,8 @@ pub struct Quote { pub expiration_timestamp: DateTime, pub quote_kind: QuoteKind, pub solver: Address, + pub verified: bool, + pub metadata: serde_json::Value, } /// Stores the quote and returns the id. The id of the quote parameter is not @@ -51,9 +53,11 @@ INSERT INTO quotes ( order_kind, expiration_timestamp, quote_kind, - solver + solver, + verified, + metadata ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id "#; let (id,) = sqlx::query_as(QUERY) @@ -68,6 +72,8 @@ RETURNING id .bind(quote.expiration_timestamp) .bind("e.quote_kind) .bind(quote.solver) + .bind(quote.verified) + .bind("e.metadata) .fetch_one(ex) .await?; Ok(id) @@ -181,6 +187,8 @@ mod tests { expiration_timestamp: now, quote_kind: QuoteKind::Standard, solver: ByteArray([1; 20]), + verified: false, + metadata: Default::default(), }; let id = save(&mut db, "e).await.unwrap(); quote.id = id; @@ -214,6 +222,8 @@ mod tests { expiration_timestamp: now, quote_kind: QuoteKind::Standard, solver: ByteArray([1; 20]), + verified: false, + metadata: Default::default(), }; let token_b = ByteArray([2; 20]); @@ -230,6 +240,8 @@ mod tests { expiration_timestamp: now, quote_kind: QuoteKind::Standard, solver: ByteArray([2; 20]), + verified: false, + metadata: Default::default(), }; // Save two measurements for token_a @@ -401,6 +413,8 @@ mod tests { expiration_timestamp: now, quote_kind: QuoteKind::Eip1271OnchainOrder, solver: ByteArray([1; 20]), + verified: false, + metadata: Default::default(), }; let id = save(&mut db, "e).await.unwrap(); quote.id = id; @@ -422,4 +436,48 @@ mod tests { search_a.quote_kind = QuoteKind::Standard; assert_eq!(find(&mut db, &search_a).await.unwrap(), None,); } + + #[tokio::test] + #[ignore] + async fn postgres_insert_quote_metadata() { + let mut db = PgConnection::connect("postgresql://").await.unwrap(); + let mut db = db.begin().await.unwrap(); + crate::clear_DANGER_(&mut db).await.unwrap(); + + let metadata: serde_json::Value = serde_json::from_str( + r#"{ "version":"1.0", "interactions": [ { + "target": "0x0102030405060708091011121314151617181920", + "value": "1", + "callData": "0x0A0B0C102030" + },{ + "target": "0xFF02030405060708091011121314151617181920", + "value": "2", + "callData": "0xFF0B0C102030" + }] + }"#, + ) + .unwrap(); + + let quote = Quote { + id: Default::default(), + sell_token: ByteArray([1; 20]), + buy_token: ByteArray([2; 20]), + sell_amount: 3.into(), + buy_amount: 4.into(), + gas_amount: 5., + gas_price: 6., + sell_token_price: 7., + order_kind: OrderKind::Sell, + expiration_timestamp: low_precision_now(), + quote_kind: QuoteKind::Standard, + solver: ByteArray([1; 20]), + verified: false, + metadata: metadata.clone(), + }; + // store quote in database + let id = save(&mut db, "e).await.unwrap(); + + let stored_quote = get(&mut db, id).await.unwrap().unwrap(); + assert_eq!(stored_quote.metadata, metadata); + } } diff --git a/crates/e2e/tests/e2e/database.rs b/crates/e2e/tests/e2e/database.rs index 382b74d685..5807361f05 100644 --- a/crates/e2e/tests/e2e/database.rs +++ b/crates/e2e/tests/e2e/database.rs @@ -19,6 +19,17 @@ pub async fn events_of_order(db: &Db, uid: &OrderUid) -> Vec Option<(serde_json::Value,)> { + const QUERY: &str = "SELECT metadata FROM quotes WHERE id = $1"; + let mut db = db.acquire().await.unwrap(); + sqlx::query_as(QUERY) + .bind(quote_id) + .fetch_optional(db.deref_mut()) + .await + .unwrap() +} + #[allow(dead_code)] #[derive(Clone, Debug, sqlx::FromRow)] pub struct AuctionTransaction { diff --git a/crates/e2e/tests/e2e/main.rs b/crates/e2e/tests/e2e/main.rs index 37534b4b70..e525829381 100644 --- a/crates/e2e/tests/e2e/main.rs +++ b/crates/e2e/tests/e2e/main.rs @@ -21,6 +21,7 @@ mod order_cancellation; mod partial_fill; mod partially_fillable_balance; mod partially_fillable_pool; +mod place_order_with_quote; mod protocol_fee; mod quote_verification; mod quoting; diff --git a/crates/e2e/tests/e2e/place_order_with_quote.rs b/crates/e2e/tests/e2e/place_order_with_quote.rs new file mode 100644 index 0000000000..09d3bb55d3 --- /dev/null +++ b/crates/e2e/tests/e2e/place_order_with_quote.rs @@ -0,0 +1,107 @@ +use { + driver::domain::eth::NonZeroU256, + e2e::{nodes::local_node::TestNodeApi, setup::*, tx, tx_value}, + ethcontract::U256, + model::{ + order::{OrderCreation, OrderKind}, + quote::{OrderQuoteRequest, OrderQuoteSide, SellAmount}, + signature::EcdsaSigningScheme, + }, + secp256k1::SecretKey, + shared::ethrpc::Web3, + std::ops::DerefMut, + web3::signing::SecretKeyRef, +}; + +#[tokio::test] +#[ignore] +async fn local_node_test() { + run_test(place_order_with_quote).await; +} + +async fn place_order_with_quote(web3: Web3) { + let mut onchain = OnchainComponents::deploy(web3.clone()).await; + + let [solver] = onchain.make_solvers(to_wei(10)).await; + let [trader] = onchain.make_accounts(to_wei(10)).await; + let [token] = onchain + .deploy_tokens_with_weth_uni_v2_pools(to_wei(1_000), to_wei(1_000)) + .await; + + tx!( + trader.account(), + onchain + .contracts() + .weth + .approve(onchain.contracts().allowance, to_wei(3)) + ); + tx_value!( + trader.account(), + to_wei(3), + onchain.contracts().weth.deposit() + ); + + tracing::info!("Starting services."); + let services = Services::new(&onchain).await; + services.start_protocol(solver.clone()).await; + + // Disable auto-mine so we don't accidentally mine a settlement + web3.api::>() + .disable_automine() + .await + .expect("Must be able to disable automine"); + + tracing::info!("Quoting"); + let quote_sell_amount = to_wei(1); + let quote_request = OrderQuoteRequest { + from: trader.address(), + sell_token: onchain.contracts().weth.address(), + buy_token: token.address(), + side: OrderQuoteSide::Sell { + sell_amount: SellAmount::BeforeFee { + value: NonZeroU256::try_from(quote_sell_amount).unwrap(), + }, + }, + ..Default::default() + }; + let quote_response = services.submit_quote("e_request).await.unwrap(); + tracing::debug!(?quote_response); + assert!(quote_response.id.is_some()); + + let quote_metadata = + crate::database::quote_metadata(services.db(), quote_response.id.unwrap()).await; + assert!(quote_metadata.is_some()); + tracing::debug!(?quote_metadata); + + tracing::info!("Placing order"); + let balance = token.balance_of(trader.address()).call().await.unwrap(); + assert_eq!(balance, 0.into()); + let order = OrderCreation { + quote_id: quote_response.id, + sell_token: onchain.contracts().weth.address(), + sell_amount: quote_sell_amount, + buy_token: token.address(), + buy_amount: quote_response.quote.buy_amount, + valid_to: model::time::now_in_epoch_seconds() + 300, + kind: OrderKind::Sell, + ..Default::default() + } + .sign( + EcdsaSigningScheme::Eip712, + &onchain.contracts().domain_separator, + SecretKeyRef::from(&SecretKey::from_slice(trader.private_key()).unwrap()), + ); + let order_uid = services.create_order(&order).await.unwrap(); + + tracing::info!("Order quote verification"); + let order_quote = database::orders::read_quote( + services.db().acquire().await.unwrap().deref_mut(), + &database::byte_array::ByteArray(order_uid.0), + ) + .await + .unwrap(); + assert!(order_quote.is_some()); + // compare quote metadata and order quote metadata + let order_quote_metadata = order_quote.unwrap().metadata; + assert_eq!(quote_metadata.unwrap().0, order_quote_metadata); +} diff --git a/crates/e2e/tests/e2e/quote_verification.rs b/crates/e2e/tests/e2e/quote_verification.rs index 149afbfc7d..3b50d7ecc3 100644 --- a/crates/e2e/tests/e2e/quote_verification.rs +++ b/crates/e2e/tests/e2e/quote_verification.rs @@ -4,6 +4,7 @@ use { ethcontract::{H160, U256}, ethrpc::Web3, model::{ + interaction::InteractionData, order::{BuyTokenDestination, OrderKind, SellTokenSource}, quote::{OrderQuoteRequest, OrderQuoteSide, SellAmount}, }, @@ -19,7 +20,7 @@ use { Estimate, Verification, }, - trade_finding::{Interaction, LegacyTrade, TradeKind}, + trade_finding::{Interaction, LegacyTrade, QuoteExecution, TradeKind}, }, std::{str::FromStr, sync::Arc}, }; @@ -175,6 +176,13 @@ async fn test_bypass_verification_for_rfq_quotes(web3: Web3) { gas: 225000, solver: H160::from_str("0xe3067c7c27c1038de4e8ad95a83b927d23dfbd99").unwrap(), verified: true, + execution: QuoteExecution { + interactions: vec![InteractionData { + target: H160::from_str("0xdef1c0ded9bec7f1a1670819833240f027b25eff").unwrap(), + value: 0.into(), + call_data: hex::decode("aa77476c000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c599000000000000000000000000000000000000000000000000e357b42c3a9d8ccf0000000000000000000000000000000000000000000000000000000004d0e79e000000000000000000000000a69babef1ca67a37ffaf7a485dfff3382056e78c0000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000066360af101ffffffffffffffffffffffffffffffffffffff0f3f47f166360a8d0000003f0000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000001c66b3383f287dd9c85ad90e7c5a576ea4ba1bdf5a001d794a9afa379e6b2517b47e487a1aef32e75af432cbdbd301ada42754eaeac21ec4ca744afd92732f47540000000000000000000000000000000000000000000000000000000004d0c80f").unwrap() + }], + }, }; // `tx_origin: 0x0000` is currently used to bypass quote verification due to an diff --git a/crates/orderbook/src/api/post_order.rs b/crates/orderbook/src/api/post_order.rs index 2208bff6fd..e4d5bff45a 100644 --- a/crates/orderbook/src/api/post_order.rs +++ b/crates/orderbook/src/api/post_order.rs @@ -262,6 +262,10 @@ impl IntoWarpReply for AddOrderError { super::error("InvalidReplacement", err.to_string()), StatusCode::UNAUTHORIZED, ), + AddOrderError::MetadataSerializationFailed(err) => reply::with_status( + super::error("MetadataSerializationFailed", err.to_string()), + StatusCode::INTERNAL_SERVER_ERROR, + ), } } } diff --git a/crates/orderbook/src/database/orders.rs b/crates/orderbook/src/database/orders.rs index 2814664825..4a1d78fd30 100644 --- a/crates/orderbook/src/database/orders.rs +++ b/crates/orderbook/src/database/orders.rs @@ -1,5 +1,6 @@ use { super::Postgres, + crate::orderbook::AddOrderError, anyhow::{Context as _, Result}, app_data::AppDataHash, async_trait::async_trait, @@ -90,19 +91,29 @@ pub struct OrderWithQuote { } impl OrderWithQuote { - pub fn new(order: Order, quote: Option) -> Self { - Self { - quote: quote.map(|quote| orders::Quote { - order_uid: ByteArray(order.metadata.uid.0), - gas_amount: quote.data.fee_parameters.gas_amount, - gas_price: quote.data.fee_parameters.gas_price, - sell_token_price: quote.data.fee_parameters.sell_token_price, - sell_amount: u256_to_big_decimal("e.sell_amount), - buy_amount: u256_to_big_decimal("e.buy_amount), - solver: ByteArray(quote.data.solver.0), - }), + pub fn try_new(order: Order, quote: Option) -> Result { + Ok(Self { + quote: quote + .map(|quote| { + Ok::(orders::Quote { + order_uid: ByteArray(order.metadata.uid.0), + gas_amount: quote.data.fee_parameters.gas_amount, + gas_price: quote.data.fee_parameters.gas_price, + sell_token_price: quote.data.fee_parameters.sell_token_price, + sell_amount: u256_to_big_decimal("e.sell_amount), + buy_amount: u256_to_big_decimal("e.buy_amount), + solver: ByteArray(quote.data.solver.0), + verified: quote.data.verified, + metadata: quote + .data + .metadata + .try_into() + .map_err(AddOrderError::MetadataSerializationFailed)?, + }) + }) + .transpose()?, order, - } + }) } } @@ -112,6 +123,7 @@ pub enum InsertionError { DbError(sqlx::Error), /// Full app data to be inserted doesn't match existing. AppDataMismatch(Vec), + MetadataSerializationFailed(serde_json::Error), } impl From for InsertionError { @@ -233,11 +245,17 @@ async fn insert_quote( sell_amount: u256_to_big_decimal("e.sell_amount), buy_amount: u256_to_big_decimal("e.buy_amount), solver: ByteArray(quote.data.solver.0), + verified: quote.data.verified, + metadata: quote + .data + .metadata + .clone() + .try_into() + .map_err(InsertionError::MetadataSerializationFailed)?, }; database::orders::insert_quote(ex, "e) .await - .map_err(InsertionError::DbError)?; - Ok(()) + .map_err(InsertionError::DbError) } #[async_trait::async_trait] @@ -362,6 +380,8 @@ impl OrderStoring for Postgres { order_with_quote.quote_gas_amount, order_with_quote.quote_gas_price, order_with_quote.quote_sell_token_price, + order_with_quote.quote_verified, + order_with_quote.quote_metadata, order_with_quote.solver, ) { ( @@ -370,6 +390,8 @@ impl OrderStoring for Postgres { Some(gas_amount), Some(gas_price), Some(sell_token_price), + Some(verified), + Some(metadata), Some(solver), ) => Some(orders::Quote { order_uid: order_with_quote.full_order.uid, @@ -379,6 +401,8 @@ impl OrderStoring for Postgres { sell_amount, buy_amount, solver, + verified, + metadata, }), _ => None, }; @@ -668,6 +692,7 @@ mod tests { signature::{Signature, SigningScheme}, }, primitive_types::U256, + shared::order_quoting::{QuoteData, QuoteMetadataV1}, std::sync::atomic::{AtomicI64, Ordering}, }; @@ -1170,4 +1195,55 @@ mod tests { u256_to_big_decimal("e.buy_amount) ); } + + #[tokio::test] + #[ignore] + async fn postgres_insert_orders_with_interactions_and_verified() { + let db = Postgres::new("postgresql://").unwrap(); + database::clear_DANGER(&db.pool).await.unwrap(); + + let uid = OrderUid([0x42; 56]); + let order = Order { + data: OrderData { + valid_to: u32::MAX, + ..Default::default() + }, + metadata: OrderMetadata { + uid, + ..Default::default() + }, + ..Default::default() + }; + + let quote = Quote { + id: Some(5), + sell_amount: U256::from(1), + buy_amount: U256::from(2), + data: QuoteData { + verified: true, + metadata: QuoteMetadataV1 { + interactions: vec![ + InteractionData { + target: H160([1; 20]), + value: U256::from(100), + call_data: vec![1, 20], + }, + InteractionData { + target: H160([2; 20]), + value: U256::from(10), + call_data: vec![2, 20], + }, + ], + } + .into(), + ..Default::default() + }, + ..Default::default() + }; + db.insert_order(&order, Some(quote)).await.unwrap(); + + let single_order_with_quote = db.single_order_with_quote(&uid).await.unwrap().unwrap(); + assert_eq!(single_order_with_quote.order, order); + assert!(single_order_with_quote.quote.unwrap().verified); + } } diff --git a/crates/orderbook/src/database/quotes.rs b/crates/orderbook/src/database/quotes.rs index 0cb149cc86..8e2608e0c1 100644 --- a/crates/orderbook/src/database/quotes.rs +++ b/crates/orderbook/src/database/quotes.rs @@ -18,7 +18,7 @@ impl QuoteStoring for Postgres { .start_timer(); let mut ex = self.pool.acquire().await?; - let row = create_quote_row(data); + let row = create_quote_row(data)?; let id = database::quotes::save(&mut ex, &row).await?; Ok(id) } diff --git a/crates/orderbook/src/orderbook.rs b/crates/orderbook/src/orderbook.rs index fc637a1c2f..2b4a6251c4 100644 --- a/crates/orderbook/src/orderbook.rs +++ b/crates/orderbook/src/orderbook.rs @@ -141,6 +141,8 @@ pub enum AddOrderError { provided: String, existing: String, }, + #[error("quote metadata failed to serialize as json, error: {0}")] + MetadataSerializationFailed(serde_json::Error), } impl AddOrderError { @@ -161,6 +163,9 @@ impl AddOrderError { s.into_owned() }, }, + InsertionError::MetadataSerializationFailed(err) => { + AddOrderError::MetadataSerializationFailed(err) + } } } } @@ -249,17 +254,18 @@ impl Orderbook { self.replace_order(order, old_order, quote).await } else { let quote_id = quote.as_ref().and_then(|quote| quote.id); + let order_uid = order.metadata.uid; self.database .insert_order(&order, quote.clone()) .await .map_err(|err| AddOrderError::from_insertion(err, &order))?; Metrics::on_order_operation( - &OrderWithQuote::new(order.clone(), quote), + &OrderWithQuote::try_new(order, quote)?, OrderOperation::Created, ); - Ok((order.metadata.uid, quote_id)) + Ok((order_uid, quote_id)) } } @@ -402,6 +408,7 @@ impl Orderbook { } let quote_id = quote.as_ref().and_then(|quote| quote.id); + let order_uid = validated_new_order.metadata.uid; self.database .replace_order( @@ -413,11 +420,11 @@ impl Orderbook { .map_err(|err| AddOrderError::from_insertion(err, &validated_new_order))?; Metrics::on_order_operation(&old_order, OrderOperation::Cancelled); Metrics::on_order_operation( - &OrderWithQuote::new(validated_new_order.clone(), quote), + &OrderWithQuote::try_new(validated_new_order, quote)?, OrderOperation::Created, ); - Ok((validated_new_order.metadata.uid, quote_id)) + Ok((order_uid, quote_id)) } pub async fn get_order(&self, uid: &OrderUid) -> Result> { diff --git a/crates/shared/src/event_storing_helpers.rs b/crates/shared/src/event_storing_helpers.rs index 1f1938b2c6..e3684df76f 100644 --- a/crates/shared/src/event_storing_helpers.rs +++ b/crates/shared/src/event_storing_helpers.rs @@ -3,6 +3,7 @@ use { db_order_conversions::order_kind_into, order_quoting::{quote_kind_from_signing_scheme, QuoteData, QuoteSearchParameters}, }, + anyhow::Result, chrono::{DateTime, Utc}, database::{ byte_array::ByteArray, @@ -11,8 +12,8 @@ use { number::conversions::u256_to_big_decimal, }; -pub fn create_quote_row(data: QuoteData) -> DbQuote { - DbQuote { +pub fn create_quote_row(data: QuoteData) -> Result { + Ok(DbQuote { id: Default::default(), sell_token: ByteArray(data.sell_token.0), buy_token: ByteArray(data.buy_token.0), @@ -25,7 +26,9 @@ pub fn create_quote_row(data: QuoteData) -> DbQuote { expiration_timestamp: data.expiration, quote_kind: data.quote_kind, solver: ByteArray(data.solver.0), - } + verified: data.verified, + metadata: data.metadata.try_into()?, + }) } pub fn create_db_search_parameters( diff --git a/crates/shared/src/order_quoting.rs b/crates/shared/src/order_quoting.rs index eac107f379..6942655449 100644 --- a/crates/shared/src/order_quoting.rs +++ b/crates/shared/src/order_quoting.rs @@ -170,6 +170,8 @@ pub struct QuoteData { pub solver: H160, /// Were we able to verify that this quote is accurate? pub verified: bool, + /// Additional data associated with the quote. + pub metadata: QuoteMetadata, } impl TryFrom for QuoteData { @@ -195,6 +197,7 @@ impl TryFrom for QuoteData { // Even if the quote was verified at the time of creation // it might no longer be accurate. verified: false, + metadata: row.metadata.try_into()?, }) } } @@ -442,6 +445,10 @@ impl OrderQuoter { quote_kind, solver: trade_estimate.solver, verified: trade_estimate.verified, + metadata: QuoteMetadataV1 { + interactions: trade_estimate.execution.interactions, + } + .into(), }; Ok(quote) @@ -629,6 +636,62 @@ pub fn quote_kind_from_signing_scheme(scheme: &QuoteSigningScheme) -> QuoteKind } } +/// Used to store quote metadata in the database. +/// Versioning is used for the backward compatibility. +/// In case new metadata needs to be associated with a quote create a new +/// variant version and apply serde rename attribute with proper number. +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase", tag = "version")] +pub enum QuoteMetadata { + #[serde(rename = "1.0")] + V1(QuoteMetadataV1), +} + +// Handles deserialization of empty json value {} in metadata column. +#[derive(Clone, Debug, PartialEq, serde::Deserialize)] +#[serde(untagged)] +enum QuoteMetadataDeserializationHelper { + Data(QuoteMetadata), + Empty {}, +} + +impl TryInto for QuoteMetadata { + type Error = serde_json::Error; + + fn try_into(self) -> std::result::Result { + serde_json::to_value(self) + } +} + +impl TryFrom for QuoteMetadata { + type Error = serde_json::Error; + + fn try_from(value: serde_json::Value) -> std::result::Result { + Ok(match serde_json::from_value(value)? { + QuoteMetadataDeserializationHelper::Data(value) => value, + QuoteMetadataDeserializationHelper::Empty {} => Default::default(), + }) + } +} + +impl Default for QuoteMetadata { + fn default() -> Self { + Self::V1(Default::default()) + } +} + +impl From for QuoteMetadata { + fn from(val: QuoteMetadataV1) -> Self { + QuoteMetadata::V1(val) + } +} + +#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct QuoteMetadataV1 { + /// Data provided by the solver in response to /quote request. + pub interactions: Vec, +} + #[cfg(test)] mod tests { use { @@ -727,6 +790,7 @@ mod tests { gas: 3, solver: H160([1; 20]), verified: false, + execution: Default::default(), }) } .boxed() @@ -768,6 +832,7 @@ mod tests { quote_kind: QuoteKind::Standard, solver: H160([1; 20]), verified: false, + metadata: Default::default(), })) .returning(|_| Ok(1337)); @@ -804,6 +869,7 @@ mod tests { quote_kind: QuoteKind::Standard, solver: H160([1; 20]), verified: false, + metadata: Default::default(), }, sell_amount: 70.into(), buy_amount: 29.into(), @@ -862,6 +928,7 @@ mod tests { gas: 3, solver: H160([1; 20]), verified: false, + execution: Default::default(), }) } .boxed() @@ -903,6 +970,7 @@ mod tests { quote_kind: QuoteKind::Standard, solver: H160([1; 20]), verified: false, + metadata: Default::default(), })) .returning(|_| Ok(1337)); @@ -939,6 +1007,7 @@ mod tests { quote_kind: QuoteKind::Standard, solver: H160([1; 20]), verified: false, + metadata: Default::default(), }, sell_amount: 100.into(), buy_amount: 42.into(), @@ -992,6 +1061,7 @@ mod tests { gas: 3, solver: H160([1; 20]), verified: false, + execution: Default::default(), }) } .boxed() @@ -1033,6 +1103,7 @@ mod tests { quote_kind: QuoteKind::Standard, solver: H160([1; 20]), verified: false, + metadata: Default::default(), })) .returning(|_| Ok(1337)); @@ -1069,6 +1140,7 @@ mod tests { quote_kind: QuoteKind::Standard, solver: H160([1; 20]), verified: false, + metadata: Default::default(), }, sell_amount: 100.into(), buy_amount: 42.into(), @@ -1108,6 +1180,7 @@ mod tests { gas: 200, solver: H160([1; 20]), verified: false, + execution: Default::default(), }) } .boxed() @@ -1179,6 +1252,7 @@ mod tests { gas: 200, solver: H160([1; 20]), verified: false, + execution: Default::default(), }) } .boxed() @@ -1255,6 +1329,7 @@ mod tests { quote_kind: QuoteKind::Standard, solver: H160([1; 20]), verified: false, + metadata: Default::default(), })) }); @@ -1288,6 +1363,7 @@ mod tests { quote_kind: QuoteKind::Standard, solver: H160([1; 20]), verified: false, + metadata: Default::default(), }, sell_amount: 85.into(), // Allows for "out-of-price" buy amounts. This means that order @@ -1335,6 +1411,7 @@ mod tests { quote_kind: QuoteKind::Standard, solver: H160([1; 20]), verified: false, + metadata: Default::default(), })) }); @@ -1368,6 +1445,7 @@ mod tests { quote_kind: QuoteKind::Standard, solver: H160([1; 20]), verified: false, + metadata: Default::default(), }, sell_amount: 100.into(), buy_amount: 42.into(), @@ -1416,6 +1494,7 @@ mod tests { quote_kind: QuoteKind::Standard, solver: H160([1; 20]), verified: false, + metadata: Default::default(), }, ))) }); @@ -1450,6 +1529,7 @@ mod tests { quote_kind: QuoteKind::Standard, solver: H160([1; 20]), verified: false, + metadata: Default::default(), }, sell_amount: 100.into(), buy_amount: 42.into(), @@ -1547,4 +1627,62 @@ mod tests { FindQuoteError::NotFound(None), )); } + + #[test] + fn check_quote_metadata_format() { + let q: QuoteMetadata = QuoteMetadataV1 { + interactions: vec![ + InteractionData { + target: H160::from([1; 20]), + value: U256::one(), + call_data: vec![1], + }, + InteractionData { + target: H160::from([2; 20]), + value: U256::from(2), + call_data: vec![2], + }, + ], + } + .into(); + let v = serde_json::to_value(q).unwrap(); + + let req: serde_json::Value = serde_json::from_str( + r#" + {"version":"1.0", + "interactions":[ + {"target":"0x0101010101010101010101010101010101010101","value":"1","callData":"0x01"}, + {"target":"0x0202020202020202020202020202020202020202","value":"2","callData":"0x02"} + ]}"#, + ) + .unwrap(); + + assert_eq!(req, v); + } + + #[test] + fn check_quote_metadata_deserialize_from_empty_json() { + let empty_json: serde_json::Value = serde_json::from_str("{}").unwrap(); + let metadata: QuoteMetadata = empty_json.try_into().unwrap(); + // Empty json is converted to QuoteMetadata default value + assert_eq!(metadata, QuoteMetadata::default()); + } + + #[test] + fn check_quote_metadata_deserialize_from_v1_json() { + let v1: serde_json::Value = serde_json::from_str( + r#" + {"version":"1.0", + "interactions":[ + {"target":"0x0101010101010101010101010101010101010101","value":"1","callData":"0x01"}, + {"target":"0x0202020202020202020202020202020202020202","value":"2","callData":"0x02"} + ]}"#, + ) + .unwrap(); + let metadata: QuoteMetadata = v1.try_into().unwrap(); + + match metadata { + QuoteMetadata::V1(v1) => assert_eq!(v1.interactions.len(), 2), + } + } } diff --git a/crates/shared/src/price_estimation/competition/mod.rs b/crates/shared/src/price_estimation/competition/mod.rs index 2703e3397a..3800f53ddf 100644 --- a/crates/shared/src/price_estimation/competition/mod.rs +++ b/crates/shared/src/price_estimation/competition/mod.rs @@ -287,17 +287,17 @@ mod tests { }; let first = setup_estimator(vec![ - Ok(estimates[0]), - Ok(estimates[0]), - Ok(estimates[0]), + Ok(estimates[0].clone()), + Ok(estimates[0].clone()), + Ok(estimates[0].clone()), Err(PriceEstimationError::ProtocolInternal(anyhow!("a"))), Err(PriceEstimationError::NoLiquidity), ]); let second = setup_estimator(vec![ Err(PriceEstimationError::ProtocolInternal(anyhow!(""))), - Ok(estimates[1]), - Ok(estimates[1]), + Ok(estimates[1].clone()), + Ok(estimates[1].clone()), Err(PriceEstimationError::ProtocolInternal(anyhow!("b"))), Err(PriceEstimationError::UnsupportedToken { token: H160([0; 20]), diff --git a/crates/shared/src/price_estimation/mod.rs b/crates/shared/src/price_estimation/mod.rs index 5db9810d01..c077f83906 100644 --- a/crates/shared/src/price_estimation/mod.rs +++ b/crates/shared/src/price_estimation/mod.rs @@ -2,7 +2,7 @@ use { self::trade_verifier::balance_overrides, crate::{ arguments::{display_option, display_secret_option, ExternalSolver}, - trade_finding::Interaction, + trade_finding::{Interaction, QuoteExecution}, }, anyhow::Result, bigdecimal::BigDecimal, @@ -461,7 +461,7 @@ pub struct Verification { pub buy_token_destination: BuyTokenDestination, } -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Deserialize)] +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)] pub struct Estimate { pub out_amount: U256, /// full gas cost when settling this order alone on gp @@ -470,6 +470,8 @@ pub struct Estimate { pub solver: H160, /// Did we verify the correctness of this estimate's properties? pub verified: bool, + /// Data associated with this estimation. + pub execution: QuoteExecution, } impl Estimate { @@ -530,7 +532,7 @@ pub mod mocks { pub struct FakePriceEstimator(pub Estimate); impl PriceEstimating for FakePriceEstimator { fn estimate(&self, _query: Arc) -> BoxFuture<'_, PriceEstimateResult> { - async { Ok(self.0) }.boxed() + async { Ok(self.0.clone()) }.boxed() } } diff --git a/crates/shared/src/price_estimation/native/mod.rs b/crates/shared/src/price_estimation/native/mod.rs index e0928ddb8d..31ca840575 100644 --- a/crates/shared/src/price_estimation/native/mod.rs +++ b/crates/shared/src/price_estimation/native/mod.rs @@ -130,6 +130,7 @@ mod tests { gas: 0, solver: H160([1; 20]), verified: false, + execution: Default::default(), }) } .boxed() diff --git a/crates/shared/src/price_estimation/sanitized.rs b/crates/shared/src/price_estimation/sanitized.rs index 29c9f44aff..926703a4b8 100644 --- a/crates/shared/src/price_estimation/sanitized.rs +++ b/crates/shared/src/price_estimation/sanitized.rs @@ -67,6 +67,7 @@ impl PriceEstimating for SanitizedPriceEstimator { gas: 0, solver: Default::default(), verified: true, + execution: Default::default(), }; tracing::debug!(?query, ?estimation, "generate trivial price estimation"); return Ok(estimation); @@ -79,6 +80,7 @@ impl PriceEstimating for SanitizedPriceEstimator { gas: GAS_PER_WETH_UNWRAP, solver: Default::default(), verified: true, + execution: Default::default(), }; tracing::debug!(?query, ?estimation, "generate trivial unwrap estimation"); return Ok(estimation); @@ -91,6 +93,7 @@ impl PriceEstimating for SanitizedPriceEstimator { gas: GAS_PER_WETH_WRAP, solver: Default::default(), verified: true, + execution: Default::default(), }; tracing::debug!(?query, ?estimation, "generate trivial wrap estimation"); return Ok(estimation); @@ -185,6 +188,7 @@ mod tests { gas: 100, solver: Default::default(), verified: false, + execution: Default::default(), }), ), // `sanitized_estimator` will replace `buy_token` with `native_token` before querying @@ -206,6 +210,7 @@ mod tests { gas: GAS_PER_WETH_UNWRAP + 100, solver: Default::default(), verified: false, + execution: Default::default(), }), ), // Will cause buffer overflow of gas price in `sanitized_estimator`. @@ -241,6 +246,7 @@ mod tests { gas: GAS_PER_WETH_WRAP + 100, solver: Default::default(), verified: false, + execution: Default::default(), }), ), // Can be estimated by `sanitized_estimator` because `buy_token` and `sell_token` are @@ -259,6 +265,7 @@ mod tests { gas: 0, solver: Default::default(), verified: true, + execution: Default::default(), }), ), // Can be estimated by `sanitized_estimator` because both tokens are the native token. @@ -276,6 +283,7 @@ mod tests { gas: 0, solver: Default::default(), verified: true, + execution: Default::default(), }), ), // Can be estimated by `sanitized_estimator` because it is a native token unwrap. @@ -294,6 +302,7 @@ mod tests { gas: GAS_PER_WETH_UNWRAP, solver: Default::default(), verified: true, + execution: Default::default(), }), ), // Can be estimated by `sanitized_estimator` because it is a native token wrap. @@ -312,6 +321,7 @@ mod tests { gas: GAS_PER_WETH_WRAP, solver: Default::default(), verified: true, + execution: Default::default(), }), ), // Will throw `UnsupportedToken` error in `sanitized_estimator`. @@ -376,6 +386,7 @@ mod tests { gas: 100, solver: Default::default(), verified: false, + execution: Default::default(), }) } .boxed() @@ -391,6 +402,7 @@ mod tests { gas: 100, solver: Default::default(), verified: false, + execution: Default::default(), }) } .boxed() @@ -406,6 +418,7 @@ mod tests { gas: u64::MAX, solver: Default::default(), verified: false, + execution: Default::default(), }) } .boxed() @@ -421,6 +434,7 @@ mod tests { gas: 100, solver: Default::default(), verified: false, + execution: Default::default(), }) } .boxed() diff --git a/crates/shared/src/price_estimation/trade_finder.rs b/crates/shared/src/price_estimation/trade_finder.rs index 8bbe471f36..dcfd7a8d30 100644 --- a/crates/shared/src/price_estimation/trade_finder.rs +++ b/crates/shared/src/price_estimation/trade_finder.rs @@ -89,6 +89,7 @@ impl Inner { gas: quote.gas_estimate, solver: quote.solver, verified: false, + execution: quote.execution, }) } } diff --git a/crates/shared/src/price_estimation/trade_verifier.rs b/crates/shared/src/price_estimation/trade_verifier.rs index 496cecdd4f..7098695c91 100644 --- a/crates/shared/src/price_estimation/trade_verifier.rs +++ b/crates/shared/src/price_estimation/trade_verifier.rs @@ -9,8 +9,10 @@ use { encoded_settlement::{encode_trade, EncodedSettlement, EncodedTrade}, interaction::EncodedInteraction, trade_finding::{ - external::{dto, dto::JitOrder}, + external::dto::{self, JitOrder}, + map_interactions_data, Interaction, + QuoteExecution, TradeKind, }, }, @@ -216,6 +218,9 @@ impl TradeVerifier { gas: trade.gas_estimate().context("no gas estimate")?, solver: trade.solver(), verified: true, + execution: QuoteExecution { + interactions: map_interactions_data(&trade.interactions()), + }, }; tracing::warn!( ?estimate, @@ -272,12 +277,7 @@ impl TradeVerifier { "verified quote", ); - ensure_quote_accuracy( - &self.quote_inaccuracy_limit, - query, - trade.solver(), - &summary, - ) + ensure_quote_accuracy(&self.quote_inaccuracy_limit, query, trade, &summary) } /// Configures all the state overrides that are needed to mock the given @@ -429,6 +429,9 @@ impl TradeVerifying for TradeVerifier { gas, solver: trade.solver(), verified: false, + execution: QuoteExecution { + interactions: map_interactions_data(&trade.interactions()), + }, }; tracing::warn!( ?err, @@ -742,7 +745,7 @@ impl SettleOutput { fn ensure_quote_accuracy( inaccuracy_limit: &BigRational, query: &PriceQuery, - solver: H160, + trade: &TradeKind, summary: &SettleOutput, ) -> std::result::Result { // amounts verified by the simulation @@ -773,8 +776,11 @@ fn ensure_quote_accuracy( Ok(Estimate { out_amount: summary.out_amount, gas: summary.gas_used.as_u64(), - solver, + solver: trade.solver(), verified: true, + execution: QuoteExecution { + interactions: map_interactions_data(&trade.interactions()), + }, }) } @@ -828,7 +834,8 @@ mod tests { out_amount: 2_000.into(), tokens_lost, }; - let estimate = ensure_quote_accuracy(&low_threshold, &query, H160::zero(), &summary); + let estimate = + ensure_quote_accuracy(&low_threshold, &query, &TradeKind::default(), &summary); assert!(matches!(estimate, Err(Error::SimulationFailed(_)))); // sell token is lost @@ -840,7 +847,9 @@ mod tests { out_amount: 2_000.into(), tokens_lost, }; - let estimate = ensure_quote_accuracy(&low_threshold, &query, H160::zero(), &summary); + + let estimate = + ensure_quote_accuracy(&low_threshold, &query, &TradeKind::default(), &summary); assert!(matches!(estimate, Err(Error::SimulationFailed(_)))); // everything is in-place @@ -853,7 +862,8 @@ mod tests { out_amount: 2_000.into(), tokens_lost, }; - let estimate = ensure_quote_accuracy(&low_threshold, &query, H160::zero(), &summary); + let estimate = + ensure_quote_accuracy(&low_threshold, &query, &TradeKind::default(), &summary); assert!(estimate.is_ok()); let tokens_lost = hashmap! { @@ -867,11 +877,13 @@ mod tests { tokens_lost, }; - let estimate = ensure_quote_accuracy(&low_threshold, &query, H160::zero(), &sell_more); + let estimate = + ensure_quote_accuracy(&low_threshold, &query, &Default::default(), &sell_more); assert!(matches!(estimate, Err(Error::TooInaccurate))); // passes with slightly higher tolerance - let estimate = ensure_quote_accuracy(&high_threshold, &query, H160::zero(), &sell_more); + let estimate = + ensure_quote_accuracy(&high_threshold, &query, &Default::default(), &sell_more); assert!(estimate.is_ok()); let tokens_lost = hashmap! { @@ -885,11 +897,13 @@ mod tests { tokens_lost, }; - let estimate = ensure_quote_accuracy(&low_threshold, &query, H160::zero(), &pay_out_more); + let estimate = + ensure_quote_accuracy(&low_threshold, &query, &Default::default(), &pay_out_more); assert!(matches!(estimate, Err(Error::TooInaccurate))); // passes with slightly higher tolerance - let estimate = ensure_quote_accuracy(&high_threshold, &query, H160::zero(), &pay_out_more); + let estimate = + ensure_quote_accuracy(&high_threshold, &query, &Default::default(), &pay_out_more); assert!(estimate.is_ok()); let tokens_lost = hashmap! { @@ -903,7 +917,8 @@ mod tests { tokens_lost, }; // Ending up with surplus in the buffers is always fine - let estimate = ensure_quote_accuracy(&low_threshold, &query, H160::zero(), &sell_less); + let estimate = + ensure_quote_accuracy(&low_threshold, &query, &Default::default(), &sell_less); assert!(estimate.is_ok()); let tokens_lost = hashmap! { @@ -917,7 +932,8 @@ mod tests { tokens_lost, }; // Ending up with surplus in the buffers is always fine - let estimate = ensure_quote_accuracy(&low_threshold, &query, H160::zero(), &pay_out_less); + let estimate = + ensure_quote_accuracy(&low_threshold, &query, &Default::default(), &pay_out_less); assert!(estimate.is_ok()); } } diff --git a/crates/shared/src/trade_finding/external.rs b/crates/shared/src/trade_finding/external.rs index 8aef3be293..95e820eb70 100644 --- a/crates/shared/src/trade_finding/external.rs +++ b/crates/shared/src/trade_finding/external.rs @@ -5,9 +5,11 @@ use { price_estimation::{PriceEstimationError, Query}, request_sharing::{BoxRequestSharing, RequestSharing}, trade_finding::{ + map_interactions_data, Interaction, LegacyTrade, Quote, + QuoteExecution, Trade, TradeError, TradeFinding, @@ -220,6 +222,9 @@ impl TradeFinding for ExternalTradeFinder { .map_err(TradeError::Other)?, gas_estimate, solver: trade.solver(), + execution: QuoteExecution { + interactions: map_interactions_data(&trade.interactions()), + }, }) } diff --git a/crates/shared/src/trade_finding/mod.rs b/crates/shared/src/trade_finding/mod.rs index 4f0fbbacf0..0c0cc9a5ce 100644 --- a/crates/shared/src/trade_finding/mod.rs +++ b/crates/shared/src/trade_finding/mod.rs @@ -15,7 +15,7 @@ use { model::{interaction::InteractionData, order::OrderKind}, num::CheckedDiv, number::conversions::big_rational_to_u256, - serde::Serialize, + serde::{Deserialize, Serialize}, std::{collections::HashMap, ops::Mul}, thiserror::Error, }; @@ -36,6 +36,13 @@ pub struct Quote { pub out_amount: U256, pub gas_estimate: u64, pub solver: H160, + pub execution: QuoteExecution, +} + +/// Quote execution metadata. +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)] +pub struct QuoteExecution { + pub interactions: Vec, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -44,6 +51,12 @@ pub enum TradeKind { Regular(Trade), } +impl Default for TradeKind { + fn default() -> Self { + Self::Legacy(LegacyTrade::default()) + } +} + impl TradeKind { pub fn gas_estimate(&self) -> Option { match self { @@ -181,6 +194,14 @@ impl Interaction { pub fn encode(&self) -> EncodedInteraction { (self.target, self.value, Bytes(self.data.clone())) } + + pub fn to_interaction_data(&self) -> InteractionData { + InteractionData { + target: self.target, + value: self.value, + call_data: self.data.clone(), + } + } } impl From for Interaction { @@ -248,6 +269,14 @@ pub fn map_interactions(interactions: &[InteractionData]) -> Vec { interactions.iter().cloned().map(Into::into).collect() } +pub fn map_interactions_data(interactions: &[Interaction]) -> Vec { + interactions + .iter() + .cloned() + .map(|i| i.to_interaction_data()) + .collect() +} + #[cfg(test)] mod tests { use super::*; @@ -266,6 +295,6 @@ mod tests { interaction_debug, "Interaction { target: 0x0000000000000000000000000000000000000000, value: 1, data: \ 0x010203040506 }" - ); + ) } } diff --git a/database/README.md b/database/README.md index 0a9e8a0bcb..abb2c53b29 100644 --- a/database/README.md +++ b/database/README.md @@ -229,7 +229,7 @@ Indexes: Quotes that an order was created with. These quotes get stored persistently and can be used to evaluate how accurate the quoted fee predicted the execution cost that actually happened on-chain. - Colmun | Type | Nullable | Details + Column | Type | Nullable | Details --------------------|---------|----------|-------- order\_uid | bytea | not null | order that this quote belongs to gas\_amount | double | not null | estimated gas used by the quote used to create this order with @@ -238,6 +238,8 @@ Quotes that an order was created with. These quotes get stored persistently and sell\_amount | numeric | not null | sell\_amount of the quote used to create the order with buy\_amount | numeric | not null | buy\_amount of the quote used to create the order with solver | bytea | not null | public address of the solver that provided this quote + verified | boolean | not null | information if quote was verified + metadata | json | not null | additional data associated with the quote in json format Indexes: - PRIMARY KEY: btree(`order_uid`) @@ -339,6 +341,8 @@ Stores quotes in order to determine whether it makes sense to allow a user to cr id | bigint | not null | unique identifier of this quote quote\_kind | [enum](#quotekind) | not null | quotekind for which this quote is considered valid solver | bytea | not null | public address of the solver that provided this quote + verified | boolean | not null | information if quote was verified + metadata | json | not null | additional data associated with the quote in json format Indexes: - PRIMARY KEY: btree(`id`) diff --git a/database/sql/U075__add_metadata_to_quotes_and_order_quotes.sql b/database/sql/U075__add_metadata_to_quotes_and_order_quotes.sql new file mode 100644 index 0000000000..e0f15152ff --- /dev/null +++ b/database/sql/U075__add_metadata_to_quotes_and_order_quotes.sql @@ -0,0 +1,11 @@ +-- This script reverts changes applied in V075__add_metadata_to_quotes_and_order_quotes.sql migration script. + +-- Step 1: Drop two columns from the quotes table +ALTER TABLE quotes + DROP COLUMN verified, + DROP COLUMN metadata; + +-- Step 2: Drop two columns from the order_quotes table +ALTER TABLE order_quotes + DROP COLUMN verified, + DROP COLUMN metadata; diff --git a/database/sql/V075__add_metadata_to_quotes_and_order_quotes.sql b/database/sql/V075__add_metadata_to_quotes_and_order_quotes.sql new file mode 100644 index 0000000000..363c098f90 --- /dev/null +++ b/database/sql/V075__add_metadata_to_quotes_and_order_quotes.sql @@ -0,0 +1,29 @@ +-- This migration script is not reversible. + +-- Step 1: Add two new columns to the quotes table +ALTER TABLE quotes + ADD COLUMN verified boolean, + ADD COLUMN metadata json; + +-- Step 2: Update existing data with non-null values +UPDATE quotes SET verified = false, metadata = '{}'::json; + +-- Step 3: Add NOT NULL constraint to newly added columns +ALTER TABLE quotes + ALTER COLUMN verified SET NOT NULL, + ALTER COLUMN metadata SET NOT NULL; + + +-- Step 4: Add two new columns to the order_quotes table +ALTER TABLE order_quotes + ADD COLUMN verified boolean, + ADD COLUMN metadata json; + +-- Step 5: Update existing data with non-null values +UPDATE order_quotes SET verified = false, metadata = '{}'::json; + +-- Step 6: Add NOT NULL constraint to newly added columns +ALTER TABLE order_quotes + ALTER COLUMN verified SET NOT NULL, + ALTER COLUMN metadata SET NOT NULL; +