diff --git a/crates/autopilot/src/database/onchain_order_events/mod.rs b/crates/autopilot/src/database/onchain_order_events/mod.rs index 289ae0fd5e..3e7e5f2471 100644 --- a/crates/autopilot/src/database/onchain_order_events/mod.rs +++ b/crates/autopilot/src/database/onchain_order_events/mod.rs @@ -10,7 +10,7 @@ use { rpc::types::Log, }, anyhow::{Context, Result, anyhow, bail}, - app_data::AppDataHash, + app_data::{AppDataHash, ProtocolAppData}, chrono::{TimeZone, Utc}, contracts::{ CoWSwapOnchainOrders::CoWSwapOnchainOrders::{ @@ -318,7 +318,7 @@ impl OnchainOrderParser { .collect(); let invalidation_events = get_invalidation_events(events)?; let invalided_order_uids = extract_invalidated_order_uids(invalidation_events)?; - let (custom_onchain_data, quotes, broadcasted_order_data, orders, tx_hashes) = self + let (custom_onchain_data, quotes, broadcasted_order_data, mut orders, tx_hashes) = self .extract_custom_and_general_order_data(order_placement_events) .await?; @@ -356,9 +356,9 @@ impl OnchainOrderParser { .await .context("appending quotes for onchain orders failed")?; - insert_order_hooks(transaction, &orders, &self.trampoline) + handle_app_data(transaction, &mut orders, &self.trampoline) .await - .context("failed to insert hooks")?; + .context("failed to handle app data")?; database::orders::insert_orders_and_ignore_conflicts(transaction, orders.as_slice()) .await @@ -628,6 +628,7 @@ fn convert_onchain_order_placement( true => OrderClass::Limit, false => OrderClass::Market, }, + valid_from: None, }; let onchain_order_placement_event = OnchainOrderPlacement { order_uid: ByteArray(order_uid.0), @@ -680,18 +681,49 @@ fn extract_order_data_from_onchain_order_placement_event( Ok((order_data, owner, signing_scheme, order_uid)) } -async fn insert_order_hooks( +async fn handle_app_data( db: &mut PgConnection, - orders: &[Order], + orders: &mut [Order], trampoline: &HooksTrampoline::Instance, ) -> Result<()> { - let mut interactions_to_insert = vec![]; + for order in orders { + let appdata_json = database::app_data::fetch(db, &order.app_data) + .await + .context("failed to fetch appdata")?; + let Some(appdata_json) = appdata_json else { + tracing::debug!(order = ?order.uid, "appdata for order is unknown"); + continue; + }; + let Ok(parsed) = app_data::parse(&appdata_json) else { + tracing::debug!(appdata = %String::from_utf8_lossy(&appdata_json), "could not parse appdata"); + continue; + }; - let execute_via_trampoline = |hooks: Vec| { + store_hooks(db, order, &parsed, trampoline).await?; + order.valid_from = parsed.valid_from.map(|v| v as i64); + } + Ok(()) +} + +async fn store_hooks( + db: &mut PgConnection, + order: &Order, + parsed: &ProtocolAppData, + trampoline: &HooksTrampoline::Instance, +) -> Result<()> { + if parsed.hooks.pre.is_empty() && parsed.hooks.post.is_empty() { + return Ok(()); + } + + let interactions_count = database::orders::next_free_interaction_indices(db, order.uid) + .await + .context("failed to fetch interaction count")?; + + let execute_via_trampoline = |hooks: &[app_data::Hook]| { trampoline .execute( hooks - .into_iter() + .iter() .map(|hook| Hook { target: hook.target, callData: alloy::primitives::Bytes::from(hook.call_data.clone()), @@ -703,52 +735,35 @@ async fn insert_order_hooks( .to_vec() }; - for order in orders { - let appdata_json = database::app_data::fetch(db, &order.app_data) - .await - .context("failed to fetch appdata")?; - let Some(appdata_json) = appdata_json else { - tracing::debug!(order = ?order.uid, "appdata for order is unknown"); - continue; - }; - let Ok(parsed) = app_data::parse(&appdata_json) else { - tracing::debug!(appdata = %String::from_utf8_lossy(&appdata_json), "could not parse appdata"); - continue; - }; - if parsed.hooks.pre.is_empty() && parsed.hooks.post.is_empty() { - continue; // no additional interactions to index - } - - let interactions_count = database::orders::next_free_interaction_indices(db, order.uid) - .await - .context("failed to fetch interaction count")?; - - if !parsed.hooks.pre.is_empty() { - let interaction = database::orders::Interaction { + let mut interactions = vec![]; + if !parsed.hooks.pre.is_empty() { + interactions.push(( + order.uid, + database::orders::Interaction { target: ByteArray(trampoline.address().0.0), value: 0.into(), - data: execute_via_trampoline(parsed.hooks.pre), + data: execute_via_trampoline(&parsed.hooks.pre), index: interactions_count.next_pre_interaction_index, execution: database::orders::ExecutionTime::Pre, - }; - interactions_to_insert.push((order.uid, interaction)); - } - - if !parsed.hooks.post.is_empty() { - let interaction = database::orders::Interaction { + }, + )); + } + if !parsed.hooks.post.is_empty() { + interactions.push(( + order.uid, + database::orders::Interaction { target: ByteArray(trampoline.address().0.0), value: 0.into(), - data: execute_via_trampoline(parsed.hooks.post), + data: execute_via_trampoline(&parsed.hooks.post), index: interactions_count.next_post_interaction_index, execution: database::orders::ExecutionTime::Post, - }; - interactions_to_insert.push((order.uid, interaction)); - } + }, + )); } - database::orders::insert_or_overwrite_interactions(db, &interactions_to_insert) + database::orders::insert_or_overwrite_interactions(db, &interactions) .await - .context("could not insert interactions for orders") + .context("could not insert interactions for order") } #[derive(prometheus_metric_storage::MetricStorage, Clone, Debug)] @@ -1025,6 +1040,7 @@ mod test { sell_token_balance: sell_token_source_into(expected_order_data.sell_token_balance), buy_token_balance: buy_token_destination_into(expected_order_data.buy_token_balance), cancellation_timestamp: None, + valid_from: None, }; assert_eq!(onchain_order_placement, expected_onchain_order_placement); assert_eq!(order, expected_order); @@ -1138,6 +1154,7 @@ mod test { sell_token_balance: sell_token_source_into(expected_order_data.sell_token_balance), buy_token_balance: buy_token_destination_into(expected_order_data.buy_token_balance), cancellation_timestamp: None, + valid_from: None, }; assert_eq!(onchain_order_placement, expected_onchain_order_placement); assert_eq!(order, expected_order); diff --git a/crates/database/src/jit_orders.rs b/crates/database/src/jit_orders.rs index b9d1fde168..9f0a44bd92 100644 --- a/crates/database/src/jit_orders.rs +++ b/crates/database/src/jit_orders.rs @@ -34,7 +34,8 @@ NULL AS onchain_user, NULL AS onchain_placement_error, COALESCE((SELECT SUM(executed_fee) FROM order_execution oe WHERE oe.order_uid = o.uid), 0) as executed_fee, COALESCE((SELECT executed_fee_token FROM order_execution oe WHERE oe.order_uid = o.uid LIMIT 1), o.sell_token) as executed_fee_token, -- TODO surplus token -NULL AS full_app_data +NULL AS full_app_data, +NULL as valid_from "#; pub const FROM: &str = "jit_orders o"; diff --git a/crates/database/src/orders.rs b/crates/database/src/orders.rs index 23d12ae1db..bc77e0c323 100644 --- a/crates/database/src/orders.rs +++ b/crates/database/src/orders.rs @@ -99,6 +99,7 @@ pub struct Order { pub buy_token_balance: BuyTokenDestination, pub cancellation_timestamp: Option>, pub class: OrderClass, + pub valid_from: Option, } #[instrument(skip_all)] @@ -147,7 +148,8 @@ INSERT INTO orders ( buy_token_balance, cancellation_timestamp, class, - true_valid_to + true_valid_to, + valid_from ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, @@ -159,7 +161,8 @@ VALUES ( COALESCE((SELECT valid_to FROM ethflow_orders WHERE uid = $1), $21) ELSE $21 - END + END, + $22 ) "#; @@ -204,6 +207,7 @@ async fn insert_order_execute_sqlx( .bind(order.class) // true_valid_to takes the same value as valid_to when inserting an order .bind(order.valid_to) + .bind(order.valid_from) .execute(ex) .await .map(|result| result.rows_affected() > 0) @@ -526,6 +530,7 @@ pub struct FullOrder { pub executed_fee: BigDecimal, pub executed_fee_token: Address, pub full_app_data: Option>, + pub valid_from: Option, } impl FullOrder { @@ -641,7 +646,8 @@ array(Select (p.target, p.value, p.data) from interactions p where p.order_uid = (SELECT onchain_o.placement_error from onchain_placed_orders onchain_o where onchain_o.uid = o.uid limit 1) as onchain_placement_error, COALESCE((SELECT SUM(executed_fee) FROM order_execution oe WHERE oe.order_uid = o.uid), 0) as executed_fee, COALESCE((SELECT executed_fee_token FROM order_execution oe WHERE oe.order_uid = o.uid LIMIT 1), o.sell_token) as executed_fee_token, -- TODO surplus token -(SELECT full_app_data FROM app_data ad WHERE o.app_data = ad.contract_app_data LIMIT 1) as full_app_data +(SELECT full_app_data FROM app_data ad WHERE o.app_data = ad.contract_app_data LIMIT 1) as full_app_data, +o.valid_from "#; pub const FROM: &str = "orders o"; @@ -752,6 +758,7 @@ pub fn solvable_orders( FROM orders o WHERE o.cancellation_timestamp IS NULL AND o.true_valid_to >= $1 + AND (o.valid_from IS NULL OR o.valid_from <= EXTRACT(EPOCH FROM NOW())::bigint) AND NOT EXISTS (SELECT 1 FROM invalidations i WHERE i.order_uid = o.uid) AND NOT EXISTS (SELECT 1 FROM onchain_order_invalidations oi WHERE oi.uid = o.uid) AND NOT EXISTS (SELECT 1 FROM onchain_placed_orders op WHERE op.uid = o.uid AND op.placement_error IS NOT NULL) @@ -810,7 +817,8 @@ pub fn solvable_orders( NULL AS onchain_placement_error, COALESCE(fee_agg.executed_fee,0) AS executed_fee, COALESCE(fee_agg.executed_fee_token, lo.sell_token) AS executed_fee_token, - ad.full_app_data + ad.full_app_data, + lo.valid_from FROM live_orders lo LEFT JOIN LATERAL ( SELECT NOT signed AS unsigned @@ -859,7 +867,12 @@ pub fn open_orders_by_time_or_uids<'a>( WITH selected_orders AS ( SELECT o.* FROM orders o - WHERE (o.creation_timestamp > $1 OR o.cancellation_timestamp > $1 OR o.uid = ANY($2)) + WHERE (o.creation_timestamp > $1 OR o.cancellation_timestamp > $1 OR o.uid = ANY($2) + -- Pick up orders whose valid_from just crossed NOW() since the last update. + OR (o.valid_from IS NOT NULL + AND o.valid_from > EXTRACT(EPOCH FROM $1)::bigint + AND o.valid_from <= EXTRACT(EPOCH FROM NOW())::bigint)) + AND (o.valid_from IS NULL OR o.valid_from <= EXTRACT(EPOCH FROM NOW())::bigint) ), trades_agg AS ( SELECT t.order_uid, @@ -917,7 +930,8 @@ SELECT opo.onchain_placement_error, COALESCE(fee_agg.executed_fee,0) AS executed_fee, COALESCE(fee_agg.executed_fee_token, so.sell_token) AS executed_fee_token, - ad.full_app_data + ad.full_app_data, + so.valid_from FROM selected_orders so LEFT JOIN LATERAL ( SELECT NOT signed AS unsigned diff --git a/crates/e2e/tests/e2e/main.rs b/crates/e2e/tests/e2e/main.rs index e0d71a1a3d..17d4e3d13d 100644 --- a/crates/e2e/tests/e2e/main.rs +++ b/crates/e2e/tests/e2e/main.rs @@ -50,4 +50,5 @@ mod trades_v2; mod uncovered_order; mod univ2; mod user_surplus; +mod valid_from; mod wrapper; diff --git a/crates/e2e/tests/e2e/quote_fastpath_flags.rs b/crates/e2e/tests/e2e/quote_fastpath_flags.rs index 1e173695af..045afaea4b 100644 --- a/crates/e2e/tests/e2e/quote_fastpath_flags.rs +++ b/crates/e2e/tests/e2e/quote_fastpath_flags.rs @@ -23,8 +23,7 @@ async fn local_node_quote_fastpath_flags_rejected() { } /// Verifies that the orderbook rejects quotes and orders that use the -/// not-yet-supported `fast_path` quote flag or `validFrom`/`enableFastPath` -/// app-data fields. +/// not-yet-supported `enableFastPath` app-data field. async fn quote_fastpath_flags_rejected(web3: Web3) { let mut onchain = OnchainComponents::deploy(web3).await; let [trader] = onchain.make_accounts(1u64.eth()).await; @@ -77,23 +76,6 @@ async fn quote_fastpath_flags_rejected(web3: Web3) { err.1 ); - // --- quote: validFrom in app data --- - let err = services - .submit_quote(&OrderQuoteRequest { - app_data: OrderCreationAppData::Full { - full: r#"{"metadata":{"validFrom":1700000000}}"#.to_string(), - }, - ..base_quote() - }) - .await - .unwrap_err(); - assert_eq!(err.0, StatusCode::BAD_REQUEST); - assert!( - err.1.contains("validFrom"), - "error body should mention validFrom, got: {}", - err.1 - ); - // For order tests, validate_app_data fires before signature verification so // we just need structurally valid (but cryptographically incorrect) orders. let valid_to = model::time::now_in_epoch_seconds() + 300; @@ -129,16 +111,4 @@ async fn quote_fastpath_flags_rejected(web3: Web3) { "error body should mention enableFastPath, got: {}", err.1 ); - - // --- order: validFrom in app data --- - let err = services - .create_order(&make_order(r#"{"metadata":{"validFrom":1700000000}}"#)) - .await - .unwrap_err(); - assert_eq!(err.0, StatusCode::BAD_REQUEST); - assert!( - err.1.contains("validFrom"), - "error body should mention validFrom, got: {}", - err.1 - ); } diff --git a/crates/e2e/tests/e2e/valid_from.rs b/crates/e2e/tests/e2e/valid_from.rs new file mode 100644 index 0000000000..5764831e74 --- /dev/null +++ b/crates/e2e/tests/e2e/valid_from.rs @@ -0,0 +1,126 @@ +use { + crate::ethflow::ExtendedEthFlowOrder, + ::alloy::primitives::{Address, U256}, + contracts::CoWSwapEthFlow, + e2e::setup::*, + ethrpc::{Web3, alloy::CallBuilderExt}, + model::{ + order::{OrderCreation, OrderCreationAppData, OrderKind, OrderStatus, OrderUid}, + signature::EcdsaSigningScheme, + }, + number::units::EthUnit, + std::time::Duration, +}; + +#[tokio::test] +#[ignore] +async fn local_node_valid_from() { + run_test(valid_from_test).await; +} + +async fn valid_from_test(web3: Web3) { + let mut onchain = OnchainComponents::deploy(web3.clone()).await; + + let [solver] = onchain.make_solvers(10u64.eth()).await; + let [trader] = onchain.make_accounts(10u64.eth()).await; + let [token_a, token_b] = onchain + .deploy_tokens_with_weth_uni_v2_pools(1_000u64.eth(), 1_000u64.eth()) + .await; + + token_a.mint(trader.address(), 10u64.eth()).await; + token_a + .approve(onchain.contracts().allowance, 10u64.eth()) + .from(trader.address()) + .send_and_watch() + .await + .unwrap(); + + let services = Services::new(&onchain).await; + services.start_protocol(solver).await; + + let now = model::time::now_in_epoch_seconds(); + let valid_from = now + 3; + + let app_data = format!(r#"{{"metadata":{{"validFrom":{valid_from}}}}}"#); + let order = OrderCreation { + sell_token: *token_a.address(), + sell_amount: 5u64.eth(), + buy_token: *token_b.address(), + buy_amount: 1u64.eth(), + valid_to: now + 300, + kind: OrderKind::Sell, + app_data: OrderCreationAppData::Full { full: app_data }, + ..Default::default() + } + .sign( + EcdsaSigningScheme::Eip712, + &onchain.contracts().domain_separator, + &trader.signer, + ); + let uid = services.create_order(&order).await.unwrap(); + + order_filled_after_valid_from(&onchain, &services, uid, valid_from).await; + + // Now do the same for an ethflow order to verify that `validFrom` is also + // honored when it comes in via the app data attached to an on-chain order. + let now = model::time::now_in_epoch_seconds(); + let ethflow_valid_from = now + 3; + let ethflow_app_data = format!(r#"{{"metadata":{{"validFrom":{ethflow_valid_from}}}}}"#); + let app_data_hash = services + .put_app_data(None, ðflow_app_data) + .await + .unwrap(); + let app_data_hash: [u8; 32] = const_hex::decode(&app_data_hash[2..]) + .unwrap() + .try_into() + .unwrap(); + + let ethflow_contract = onchain.contracts().ethflows.first().unwrap(); + let ethflow_order = ExtendedEthFlowOrder(CoWSwapEthFlow::EthFlowOrder::Data { + buyToken: *token_b.address(), + sellAmount: 1u64.eth(), + buyAmount: U256::ONE, + validTo: now + 3600, + partiallyFillable: false, + quoteId: 0, + feeAmount: U256::ZERO, + receiver: Address::from_slice(&[0x43; 20]), + appData: app_data_hash.into(), + }); + ethflow_order + .mine_order_creation(trader.address(), ethflow_contract) + .await; + let ethflow_uid = ethflow_order + .uid(onchain.contracts(), ethflow_contract) + .await; + + order_filled_after_valid_from(&onchain, &services, ethflow_uid, ethflow_valid_from).await; +} + +async fn order_filled_after_valid_from( + onchain: &OnchainComponents, + services: &Services<'_>, + order_uid: OrderUid, + valid_from: u32, +) { + tokio::time::timeout(TIMEOUT, async { + loop { + onchain.mint_block().await; + let now_in_unix = model::time::now_in_epoch_seconds(); + let Ok(order) = services.get_order(&order_uid).await else { + tokio::time::sleep(Duration::from_millis(200)).await; + continue; + }; + let status = order.metadata.status; + if now_in_unix < valid_from { + assert_eq!(status, OrderStatus::Open); + } else if now_in_unix > valid_from + 1 { + assert_eq!(status, OrderStatus::Fulfilled); + break; + } + tokio::time::sleep(Duration::from_millis(200)).await; + } + }) + .await + .unwrap(); +} diff --git a/crates/model/src/order.rs b/crates/model/src/order.rs index 20508c154b..abd7a600c1 100644 --- a/crates/model/src/order.rs +++ b/crates/model/src/order.rs @@ -731,6 +731,10 @@ pub struct OrderMetadata { /// quote data for reference. #[serde(default, skip_serializing_if = "Option::is_none")] pub quote: Option, + /// Earliest time (UNIX timestamp seconds) the order may enter a regular + /// batch auction. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub valid_from: Option, } /// OrderUid is 56 bytes. When hex encoded as 0x prefixes Json string it is 116. diff --git a/crates/orderbook/openapi.yml b/crates/orderbook/openapi.yml index 6caa18c13a..b9cd48dda6 100644 --- a/crates/orderbook/openapi.yml +++ b/crates/orderbook/openapi.yml @@ -1428,6 +1428,13 @@ components: allOf: - $ref: "#/components/schemas/StoredOrderQuote" nullable: true + validFrom: + description: > + Earliest UNIX timestamp when the order may be put into a regular batch + auction. If this is not set the order is allowed to be in regular auctions + immediately after creation. + type: number + nullable: true required: - creationDate - class diff --git a/crates/orderbook/src/database/orders.rs b/crates/orderbook/src/database/orders.rs index 081336e3ed..9499253441 100644 --- a/crates/orderbook/src/database/orders.rs +++ b/crates/orderbook/src/database/orders.rs @@ -176,6 +176,7 @@ async fn insert_order(order: &Order, ex: &mut PgConnection) -> Result<(), Insert sell_token_balance: sell_token_source_into(order.data.sell_token_balance), buy_token_balance: buy_token_destination_into(order.data.buy_token_balance), cancellation_timestamp: None, + valid_from: order.metadata.valid_from.map(|v| v as i64), }; database::orders::insert_order(ex, &db_order) @@ -625,6 +626,7 @@ fn full_order_with_quote_into_model_order( quote: quote .map(|q| order_quote_into_model(q, status)) .transpose()?, + valid_from: order.valid_from.map(|v| v as u32), }; let data = OrderData { sell_token: Address::new(order.sell_token.0), @@ -732,6 +734,7 @@ mod tests { executed_fee: Default::default(), executed_fee_token: ByteArray([1; 20]), // TODO surplus token full_app_data: Default::default(), + valid_from: None, }; // Open - sell (filled - 0%) diff --git a/crates/orderbook/src/quoter.rs b/crates/orderbook/src/quoter.rs index 6b89021ba8..7adaed0713 100644 --- a/crates/orderbook/src/quoter.rs +++ b/crates/orderbook/src/quoter.rs @@ -276,12 +276,6 @@ impl QuoteHandler { anyhow::anyhow!("'enableFastPath' is not yet supported"), ))); } - if app_data.inner.protocol.valid_from.is_some() { - return Err(OrderQuoteError::AppData(AppDataValidationError::Invalid( - anyhow::anyhow!("'validFrom' is not yet supported"), - ))); - } - // Emit only after validation succeeds so we don't announce requests // that never reach the estimator (invalid app-data / order data return // early above). This is best-effort correlation, not a guarantee: if diff --git a/crates/shared/src/db_order_conversions.rs b/crates/shared/src/db_order_conversions.rs index 96e90bdf6d..8151431248 100644 --- a/crates/shared/src/db_order_conversions.rs +++ b/crates/shared/src/db_order_conversions.rs @@ -100,6 +100,7 @@ pub fn full_order_into_model_order(order: database::orders::FullOrder) -> Result .transpose() .context("full app data isn't utf-8")?, quote: None, + valid_from: order.valid_from.map(|v| v as u32), }; let data = OrderData { sell_token: Address::new(order.sell_token.0), diff --git a/crates/shared/src/order_validation.rs b/crates/shared/src/order_validation.rs index b1998093ee..e62159a2a3 100644 --- a/crates/shared/src/order_validation.rs +++ b/crates/shared/src/order_validation.rs @@ -765,11 +765,6 @@ impl OrderValidating for OrderValidator { "'enableFastPath' is not yet supported" ))); } - if app_data.protocol.valid_from.is_some() { - return Err(AppDataValidationError::Invalid(anyhow::anyhow!( - "'validFrom' is not yet supported" - ))); - } let interactions = self.custom_interactions(&app_data.protocol.hooks); Ok(OrderAppData { @@ -1040,6 +1035,7 @@ impl OrderValidating for OrderValidator { .map(|q| q.try_to_model_order_quote()) .transpose() .map_err(ValidationError::Other)?, + valid_from: app_data.inner.protocol.valid_from, ..Default::default() }, signature: order.signature.clone(), diff --git a/database/README.md b/database/README.md index 01063d9bb7..7833f7b04e 100644 --- a/database/README.md +++ b/database/README.md @@ -267,7 +267,8 @@ Column | Type | Nullable | Details sell\_token\_balance | [enum](#selltokensource) | not null | defines how sell\_tokens need to be transferred into the settlement contract buy\_token\_balance | [enum](#buytokendestination) | not null | defined how buy\_tokens need to be transferred back to the user class | [enum](#orderclass) | not null | determines which special trade semantics will apply to the execution of this order - true_valid_to | timestamptz | not null | timestamp at which order is no longer executable. For regular orders it is the same value as valid_to. Some orders may have multiple valid_to values, such as ethflow: which is initially signed with u32::MAX. Their true validity comes from the Settlement contract's events which is used for liveness checks. + true_valid_to | bigint | not null | UNIX timestamp at which order is no longer executable. For regular orders it is the same value as valid_to. Some orders may have multiple valid_to values, such as ethflow: which is initially signed with u32::MAX. Their true validity comes from the Settlement contract's events which is used for liveness checks. + valid\_from | bigint | nullable | earliest UNIX timestamp where the order may enter regular auctions. if this is NULL the order may be put into auctions immediately. Indexes: - PRIMARY KEY: btree(`uid`) @@ -281,6 +282,8 @@ Indexes: - orders\_true\_valid\_to: btree(`true_valid_to`) - orders_owner_covering: btree(`owner`) INCLUDE (`uid`, `kind`, `buy_amount`, `sell_amount`, `fee_amount`, `buy_token`, `sell_token`) - orders_owner_class_valid_composite: btree(`owner`, `class`, `true_valid_to` DESC) WHERE cancellation_timestamp IS NULL +- `orders_valid_from_idx`: btree(`valid_from`) WHERE valid_from IS NOT NULL + ### fee_policies diff --git a/database/sql/V115__add_valid_from_to_orders.sql b/database/sql/V115__add_valid_from_to_orders.sql new file mode 100644 index 0000000000..12fba22f3b --- /dev/null +++ b/database/sql/V115__add_valid_from_to_orders.sql @@ -0,0 +1,2 @@ +-- valid_from allows orders to be placed now and only become active in the future +ALTER TABLE orders ADD COLUMN valid_from BIGINT; diff --git a/database/sql/V116__index_valid_from_on_orders.sql b/database/sql/V116__index_valid_from_on_orders.sql new file mode 100644 index 0000000000..07f4ee366e --- /dev/null +++ b/database/sql/V116__index_valid_from_on_orders.sql @@ -0,0 +1,5 @@ +-- index to quickly find orders that are already active. most orders are +-- expected to be active immediately so we use a partial index to keep it +-- small +CREATE INDEX CONCURRENTLY orders_valid_from_idx ON orders (valid_from) +WHERE valid_from IS NOT NULL;