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
12 changes: 10 additions & 2 deletions crates/database/src/jit_orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub async fn get_by_id(
const QUERY: &str = const_format::concatcp!(
"SELECT ",
SELECT,
crate::trades::ORDER_GAS_COST_COLUMN,
" FROM ", FROM,
" WHERE o.uid = $1 ",
);
Expand All @@ -59,8 +60,14 @@ pub async fn get_many_by_uid<'a>(
ex: &'a mut PgConnection,
order_uids: &'a [OrderUid],
) -> Result<Vec<orders::FullOrder>, sqlx::Error> {
const QUERY: &str =
const_format::concatcp!("SELECT ", SELECT, " FROM ", FROM, " WHERE o.uid = ANY($1)");
const QUERY: &str = const_format::concatcp!(
"SELECT ",
SELECT,
crate::trades::ORDER_GAS_COST_COLUMN,
" FROM ",
FROM,
" WHERE o.uid = ANY($1)"
);
sqlx::query_as(QUERY).bind(order_uids).fetch_all(ex).await
}

Expand All @@ -73,6 +80,7 @@ pub async fn get_by_tx(
orders::SETTLEMENT_LOG_INDICES,
"SELECT ",
SELECT,
crate::trades::ORDER_GAS_COST_COLUMN,
" FROM ",
FROM,
" JOIN trades t ON t.order_uid = o.uid",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Worth double-checking the semantics here: ORDER_GAS_COST sums the order's gas across all of its fills globally, but get_by_tx (feeding /transactions/{tx}/orders) is a per-transaction view. For a partially-fillable order settled across multiple transactions, this endpoint will report the order's lifetime gas cost, not the portion attributable to $tx. A frontend summing gasCost over the orders in a tx to get a "transaction gas total" would over-count. Consistent with the OrderMetadata.gas_cost field definition (it's an order-level property), so likely acceptable — just flagging in case the per-tx endpoint is expected to be tx-scoped.

Expand Down
4 changes: 2 additions & 2 deletions crates/database/src/order_history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,13 @@ pub fn user_orders<'a>(
") ",
// Phase 2: fetch full rows for the relevant UIDs only
" (",
" SELECT ", orders::SELECT,
" SELECT ", orders::SELECT, crate::trades::ORDER_GAS_COST_COLUMN,
" FROM ", orders::FROM,
" WHERE o.uid IN (SELECT uid FROM page_uids)",
" )",
" UNION ALL",
" (",
" SELECT ", jit_orders::SELECT,
" SELECT ", jit_orders::SELECT, crate::trades::ORDER_GAS_COST_COLUMN,
" FROM ", jit_orders::FROM,
" WHERE o.uid IN (SELECT uid FROM page_uids)",
// despite already handling duplicates in phase 1 we need to handle
Expand Down
10 changes: 9 additions & 1 deletion crates/database/src/orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,12 @@ pub struct FullOrder {
pub executed_fee: BigDecimal,
pub executed_fee_token: Address,
pub full_app_data: Option<Vec<u8>>,
/// Total on-chain gas cost (native token wei) attributed to the order,
/// summed across its fills. Populated by the order-detail queries that
/// select it (see [`crate::trades::ORDER_GAS_COST`]); `None` for queries
/// that don't, such as the solvable-orders queries.
#[sqlx(default)]
pub gas_cost: Option<BigDecimal>,
}

impl FullOrder {
Expand Down Expand Up @@ -648,6 +654,7 @@ pub const FROM: &str = "orders o";
const FULL_ORDER_WITH_QUOTE: &str = const_format::concatcp!(
"SELECT ",
SELECT,
crate::trades::ORDER_GAS_COST_COLUMN,
", o_quotes.sell_amount as quote_sell_amount",
", o_quotes.buy_amount as quote_buy_amount",
", o_quotes.gas_amount as quote_gas_amount",
Expand Down Expand Up @@ -713,10 +720,11 @@ pub fn full_orders_in_tx<'a>(
ex: &'a mut PgConnection,
tx_hash: &'a TransactionHash,
) -> BoxStream<'a, Result<FullOrder, sqlx::Error>> {
use crate::trades::ORDER_GAS_COST_COLUMN;
const QUERY: &str = const_format::formatcp!(
r#"
{SETTLEMENT_LOG_INDICES}
SELECT {SELECT}
SELECT {SELECT}{ORDER_GAS_COST_COLUMN}
FROM {FROM}
JOIN trades t ON t.order_uid = o.uid
WHERE
Expand Down
248 changes: 233 additions & 15 deletions crates/database/src/trades.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,85 @@ pub struct TradesQueryRow {
pub sell_token: Address,
pub tx_hash: Option<TransactionHash>,
pub auction_id: Option<AuctionId>,
/// This trade's share of the settlement transaction's gas cost in native
/// token wei (`gas_used * effective_gas_price / trades_in_settlement`).
/// `NULL` for settlements observed before gas was persisted (see V116).
pub gas_cost: Option<BigDecimal>,
}

/// CTE definition (to be placed in a `WITH` list) mapping every trade to the
/// settlement that included it and to its share of that settlement's on-chain
/// gas cost. A trade belongs to the first settlement following it in the same
/// block, and a settlement's cost (`gas_used * effective_gas_price`) is split
/// equally across the trades between it and the previous settlement of the
/// block. `gas_cost` is `NULL` for settlements whose gas was not persisted (see
/// migration V116).
///
/// `NOT MATERIALIZED` is load-bearing: the CTE spans the whole `trades` table,
/// so it is only affordable if Postgres inlines it into the referencing query
/// and pushes the caller's filters down into the index scans. A materialized
/// version (the default as soon as a query references the CTE more than once)
/// sequentially scans `trades`.
pub(crate) const TRADE_GAS_COSTS_CTE: &str = r#"trade_gas_costs AS NOT MATERIALIZED (
SELECT
t.order_uid,
t.block_number,
t.log_index,
settlement.tx_hash,
settlement.auction_id,
FLOOR(
(settlement.gas_used * settlement.effective_gas_price)
/ NULLIF(settlement.trades_in_settlement, 0)
) AS gas_cost
FROM trades t
JOIN LATERAL (
SELECT
s.tx_hash,
s.auction_id,
s.gas_used,
s.effective_gas_price,
(
SELECT COUNT(*)
FROM trades tc
WHERE tc.block_number = s.block_number
AND tc.log_index < s.log_index
AND tc.log_index > COALESCE((
SELECT MAX(sp.log_index)
FROM settlements sp
WHERE sp.block_number = s.block_number
AND sp.log_index < s.log_index
), -1)
) AS trades_in_settlement
FROM settlements s
WHERE s.block_number = t.block_number
AND s.log_index > t.log_index
ORDER BY s.log_index ASC
LIMIT 1
) AS settlement ON true
)"#;

/// Scalar subquery yielding a single order's total on-chain gas cost (native
/// token wei) summed across all of its fills, or `NULL` when none of its
/// settlements have persisted gas (see V116). Carries its own copy of
/// [`TRADE_GAS_COSTS_CTE`] so it stays a self-contained expression, and
/// correlates on the order alias `o`, so it can be embedded in the
/// `orders`/`jit_orders` order-detail queries to fetch the gas cost in the same
/// round-trip.
pub(crate) const ORDER_GAS_COST: &str = const_format::concatcp!(
"(WITH ",
TRADE_GAS_COSTS_CTE,
r#"
SELECT SUM(gas.gas_cost)
FROM trade_gas_costs gas
WHERE gas.order_uid = o.uid
)"#,
);

/// Kept out of the shared `SELECT` fragments so only the queries that need the
/// gas cost pay for it.
pub(crate) const ORDER_GAS_COST_COLUMN: &str =
const_format::concatcp!(", ", ORDER_GAS_COST, " AS gas_cost");

pub fn trades<'a>(
ex: &'a mut PgConnection,
owner_filter: Option<&'a Address>,
Expand All @@ -37,24 +114,15 @@ SELECT
t.sell_amount - t.fee_amount as sell_amount_before_fees,
o.owner,
o.buy_token,
o.sell_token,
settlement.tx_hash,
settlement.auction_id"#;

const SETTLEMENT_JOIN: &str = r#"
LEFT OUTER JOIN LATERAL (
SELECT tx_hash, auction_id FROM settlements s
WHERE s.block_number = t.block_number
AND s.log_index > t.log_index
ORDER BY s.log_index ASC
LIMIT 1
) AS settlement ON true"#;
o.sell_token"#;

const QUERY: &str = const_format::concatcp!(
"WITH ",
TRADE_GAS_COSTS_CTE,
", page AS (",
"(",
SELECT,
" FROM trades t",
SETTLEMENT_JOIN,
" JOIN orders o ON o.uid = t.order_uid",
// the uid already contains the owner address and we have
// an index on this expression so this is very efficient
Expand All @@ -67,7 +135,6 @@ LEFT OUTER JOIN LATERAL (
"(",
SELECT,
" FROM trades t",
SETTLEMENT_JOIN,
" JOIN orders o ON o.uid = t.order_uid",
" JOIN onchain_placed_orders onchain_o",
" ON onchain_o.uid = t.order_uid",
Expand Down Expand Up @@ -101,13 +168,34 @@ LEFT OUTER JOIN LATERAL (
SELECT,
" FROM jit o",
" JOIN trades t ON o.uid = t.order_uid",
SETTLEMENT_JOIN,
" ORDER BY t.block_number DESC, t.log_index DESC",
" LIMIT $3 + $4",
")",
" ORDER BY block_number DESC, log_index DESC",
" LIMIT $3",
" OFFSET $4",
")",
// Joined onto the paginated `page` CTE (not the UNION branches) so the
// settlement lookup and gas attribution only run for the returned rows.
r#"
SELECT
page.block_number,
page.log_index,
page.order_uid,
page.buy_amount,
page.sell_amount,
page.sell_amount_before_fees,
page.owner,
page.buy_token,
page.sell_token,
gas.tx_hash,
gas.auction_id,
gas.gas_cost
FROM page
LEFT OUTER JOIN trade_gas_costs gas
ON gas.block_number = page.block_number
AND gas.log_index = page.log_index"#,
" ORDER BY page.block_number DESC, page.log_index DESC",
);

sqlx::query_as(QUERY)
Expand Down Expand Up @@ -191,6 +279,7 @@ mod tests {
onchain_broadcasted_orders::{OnchainOrderPlacement, insert_onchain_order},
orders::Order,
},
bigdecimal::ToPrimitive,
sqlx::Connection,
};

Expand Down Expand Up @@ -744,6 +833,135 @@ mod tests {
);
}

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

// 1 user with 4 orders.
let mut users_and_orders = generate_owners_and_order_ids(&[4]).await;
let (owner, orders) = users_and_orders
.pop()
.expect("users_and_orders should have 1 element");
let order_a = orders[0];
let order_b = orders[1];
let order_c = orders[2];
let order_d = orders[3];

let index = |block: i64, log: i64| EventIndex {
block_number: block,
log_index: log,
};

// Block 0 holds two settlements in different transactions. Trades before
// each settlement event belong to it.
//
// log 0: trade (order_a) ┐
// log 1: trade (order_b) ┴─ settled by A
// log 2: settlement A -> gas_used * price = 100 * 10 = 1000
// log 3: trade (order_a) ┐
// log 4: trade (order_c) ┴─ settled by B
// log 5: settlement B -> gas_used * price = 300 * 10 = 3000
add_order_and_trade(&mut db, owner, order_a, index(0, 0), None, None).await;
add_order_and_trade(&mut db, owner, order_b, index(0, 1), None, None).await;
let settlement_a = add_settlement(
&mut db,
index(0, 2),
Default::default(),
ByteArray([1; 32]),
1,
)
.await;
crate::settlements::update_settlement_gas(
&mut db,
0,
2,
BigDecimal::from(100),
BigDecimal::from(10),
)
.await
.unwrap();
// order_a fills a second time, in settlement B.
add_trade(&mut db, owner, order_a, index(0, 3), None, None).await;
add_order_and_trade(&mut db, owner, order_c, index(0, 4), None, None).await;
let settlement_b = add_settlement(
&mut db,
index(0, 5),
Default::default(),
ByteArray([2; 32]),
2,
)
.await;
crate::settlements::update_settlement_gas(
&mut db,
0,
5,
BigDecimal::from(300),
BigDecimal::from(10),
)
.await
.unwrap();

// A settlement whose gas was never recorded (e.g. observed before V116)
// contributes no gas cost.
add_order_and_trade(&mut db, owner, order_d, index(1, 0), None, None).await;
let settlement_c = add_settlement(
&mut db,
index(1, 1),
Default::default(),
ByteArray([3; 32]),
3,
)
.await;

// Each trade gets an equal share of its settlement's gas cost.
let mut rows = trades(&mut db, None, None, 0, 1000)
.into_inner()
.await
.unwrap();
rows.sort_by_key(|row| (row.block_number, row.log_index));
let gas = |row: &TradesQueryRow| row.gas_cost.as_ref().and_then(|cost| cost.to_u64());

assert_eq!(rows.len(), 5);
// Settlement A: 1000 / 2 trades = 500 each.
assert_eq!(rows[0].order_uid, order_a);
assert_eq!(gas(&rows[0]), Some(500));
assert_eq!(rows[0].tx_hash, Some(settlement_a.transaction_hash));
assert_eq!(rows[1].order_uid, order_b);
assert_eq!(gas(&rows[1]), Some(500));
assert_eq!(rows[1].tx_hash, Some(settlement_a.transaction_hash));
// Settlement B: 3000 / 2 trades = 1500 each.
assert_eq!(rows[2].order_uid, order_a);
assert_eq!(gas(&rows[2]), Some(1500));
assert_eq!(rows[2].tx_hash, Some(settlement_b.transaction_hash));
assert_eq!(rows[3].order_uid, order_c);
assert_eq!(gas(&rows[3]), Some(1500));
assert_eq!(rows[3].tx_hash, Some(settlement_b.transaction_hash));
// Settlement C: gas not recorded -> no share, but still resolves its tx.
assert_eq!(rows[4].order_uid, order_d);
assert_eq!(gas(&rows[4]), None);
assert_eq!(rows[4].tx_hash, Some(settlement_c.transaction_hash));

// The order-detail query attributes the same cost per order, summed
// across the order's fills (see `crate::trades::ORDER_GAS_COST`).
async fn order_gas(ex: &mut PgConnection, uid: &OrderUid) -> Option<u64> {
crate::orders::single_full_order_with_quote(ex, uid)
.await
.unwrap()
.unwrap()
.full_order
.gas_cost
.and_then(|cost| cost.to_u64())
}
assert_eq!(order_gas(&mut db, &order_a).await, Some(2000)); // 500 + 1500
assert_eq!(order_gas(&mut db, &order_b).await, Some(500));
assert_eq!(order_gas(&mut db, &order_c).await, Some(1500));
// order_d's only settlement has no recorded gas.
assert_eq!(order_gas(&mut db, &order_d).await, None);
}

#[tokio::test]
#[ignore]
async fn postgres_token_first_trade_block() {
Expand Down
Loading
Loading