Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
105 changes: 61 additions & 44 deletions crates/autopilot/src/database/onchain_order_events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -318,7 +318,7 @@ impl<T: Send + Sync + Clone, W: Send + Sync> OnchainOrderParser<T, W> {
.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?;

Expand Down Expand Up @@ -356,9 +356,9 @@ impl<T: Send + Sync + Clone, W: Send + Sync> OnchainOrderParser<T, W> {
.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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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<app_data::Hook>| {
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()),
Expand All @@ -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)]
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion crates/database/src/jit_orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
26 changes: 20 additions & 6 deletions crates/database/src/orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ pub struct Order {
pub buy_token_balance: BuyTokenDestination,
pub cancellation_timestamp: Option<DateTime<Utc>>,
pub class: OrderClass,
pub valid_from: Option<i64>,
}

#[instrument(skip_all)]
Expand Down Expand Up @@ -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,
Expand All @@ -159,7 +161,8 @@ VALUES (
COALESCE((SELECT valid_to FROM ethflow_orders WHERE uid = $1), $21)
ELSE
$21
END
END,
$22
)
"#;

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -526,6 +530,7 @@ pub struct FullOrder {
pub executed_fee: BigDecimal,
pub executed_fee_token: Address,
pub full_app_data: Option<Vec<u8>>,
pub valid_from: Option<i64>,
}

impl FullOrder {
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions crates/e2e/tests/e2e/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ mod trades_v2;
mod uncovered_order;
mod univ2;
mod user_surplus;
mod valid_from;
mod wrapper;
32 changes: 1 addition & 31 deletions crates/e2e/tests/e2e/quote_fastpath_flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
);
}
Loading
Loading