Add onchain gas cost to trades and orders APIs#4540
Conversation
Persist the actual settlement gas (gas_used, effective_gas_price) read from the transaction receipt — previously computed by the autopilot but only logged since the settlement_observations table was dropped in V090 — and expose a per-trade `gasCost` (native token wei). Attribution: each trade gets its share of the settlement transaction's gas cost (gas_used * effective_gas_price), split equally across all trades settled in the same transaction. - V111 migration: nullable gas_used + effective_gas_price on settlements - autopilot save_settlement writes them (going-forward only, no backfill) - trades query computes per-trade gas_cost in SQL - model + openapi documentation for the new gasCost field on trades Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose a per-order `gasCost` (native token wei): the sum, across the order's fills, of each trade's share of its settlement transaction's gas cost (gas_used * effective_gas_price), split equally across all trades settled in the same transaction. Builds on the settlement gas now persisted for trades. - order_gas_costs aggregates the per-trade gas attribution per order_uid - orders wired on the single_order / many_orders read paths - model + openapi documentation for the new gasCost field on orders Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Reminder: Please update the DB Readme and comment whether migrations are reversible (include rollback scripts if applicable).
Caused by: |
|
Reminder: Please consider backward compatibility when modifying the API specification.
Caused by: |
|
Claude finished @jmg-duarte's task in 4m 13s —— View job PR Review: Add on-chain gas cost to trades and orders APIsI reviewed the diff against I left 3 inline comments:
The doc-comment mismatches (1 & 2) read like leftovers from an earlier quote-based design — worth fixing since they directly contradict the OpenAPI and the implementation. Minor (non-blocking)
|
There was a problem hiding this comment.
Code Review
This pull request implements tracking and persistence of actual on-chain gas costs for settlements, trades, and orders, including database migrations, query updates, and OpenAPI documentation. The reviewer feedback highlights that the docstrings for the gas_cost fields in both the OrderMetadata and Trade models are incorrect and misleading, as they describe the value as an estimated cost derived from quotes rather than the actual on-chain gas cost.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| /// Estimated network gas cost (in native token wei) of executing this | ||
| /// order, derived from its quote (`gas_amount * gas_price`). `None` when | ||
| /// the order has no stored quote. | ||
| #[serde_as(as = "Option<HexOrDecimalU256>")] | ||
| #[serde(default, skip_serializing_if = "Option::is_none")] | ||
| pub gas_cost: Option<U256>, |
There was a problem hiding this comment.
The docstring for gas_cost is incorrect and misleading. It states that the gas cost is an "Estimated network gas cost... derived from its quote", but the PR actually implements and exposes the actual on-chain gas cost attributed to the order (as documented in the OpenAPI spec and implemented in the database queries). Please update the docstring to accurately reflect that this is the actual on-chain gas cost.
| /// Estimated network gas cost (in native token wei) of executing this | |
| /// order, derived from its quote (`gas_amount * gas_price`). `None` when | |
| /// the order has no stored quote. | |
| #[serde_as(as = "Option<HexOrDecimalU256>")] | |
| #[serde(default, skip_serializing_if = "Option::is_none")] | |
| pub gas_cost: Option<U256>, | |
| /// Actual on-chain gas cost (in native token wei) attributed to this | |
| /// order. Computed as the order's share of the gas cost | |
| /// (gas_used * effective_gas_price) of the settlement transaction(s) | |
| /// that executed it, split equally across all trades settled in the same | |
| /// transaction, and summed across the order's fills. None for orders | |
| /// settled before this data started being recorded, or not yet settled. | |
| #[serde_as(as = "Option<HexOrDecimalU256>")] | |
| #[serde(default, skip_serializing_if = "Option::is_none")] | |
| pub gas_cost: Option<U256>, |
| /// Estimated network gas cost (in native token wei) of executing this | ||
| /// trade, derived from the order's quote (`gas_amount * gas_price`). | ||
| /// `None` when the order has no stored quote (e.g. JIT orders). | ||
| #[serde_as(as = "Option<HexOrDecimalU256>")] | ||
| #[serde(default, skip_serializing_if = "Option::is_none")] | ||
| pub gas_cost: Option<U256>, |
There was a problem hiding this comment.
The docstring for gas_cost is incorrect and misleading. It states that the gas cost is an "Estimated network gas cost... derived from the order's quote", but the PR actually implements and exposes the actual on-chain gas cost attributed to the trade. Please update the docstring to accurately reflect that this is the actual on-chain gas cost.
/// Actual on-chain gas cost (in native token wei) attributed to this
/// trade. Computed as this trade's share of the settlement transaction's
/// gas cost (gas_used * effective_gas_price), split equally across all
/// trades settled in the same transaction. None for trades settled
/// before this data started being recorded.
#[serde_as(as = "Option<HexOrDecimalU256>")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gas_cost: Option<U256>,| /// Estimated network gas cost (in native token wei) of executing this | ||
| /// order, derived from its quote (`gas_amount * gas_price`). `None` when | ||
| /// the order has no stored quote. |
There was a problem hiding this comment.
This doc comment is incorrect and contradicts both the implementation and the OpenAPI spec. The value populated into gas_cost comes from database::trades::order_gas_costs, i.e. the actual on-chain settlement gas cost (gas_used * effective_gas_price from the receipt, split across trades) — not the quote (gas_amount * gas_price). The None condition is also wrong: it's None for orders settled before V111 or not yet settled, not "the order has no stored quote".
| /// Estimated network gas cost (in native token wei) of executing this | |
| /// order, derived from its quote (`gas_amount * gas_price`). `None` when | |
| /// the order has no stored quote. | |
| /// Actual on-chain gas cost (in native token wei) attributed to this order: | |
| /// its share of the settlement transaction(s)' gas cost | |
| /// (`gas_used * effective_gas_price`), split equally across all trades in | |
| /// each settlement and summed across the order's fills. `None` for orders | |
| /// settled before this data was recorded (see V111), or not yet settled. |
| /// Estimated network gas cost (in native token wei) of executing this | ||
| /// trade, derived from the order's quote (`gas_amount * gas_price`). | ||
| /// `None` when the order has no stored quote (e.g. JIT orders). |
There was a problem hiding this comment.
Same issue as the OrderMetadata::gas_cost comment: this is the actual on-chain gas cost (this trade's share of the settlement's gas_used * effective_gas_price), not a quote-derived estimate. The "None ... (e.g. JIT orders)" claim is also misleading — JIT orders have trades and settlements, so they do get a gas cost here; the value is None only for trades settled before V111.
| /// Estimated network gas cost (in native token wei) of executing this | |
| /// trade, derived from the order's quote (`gas_amount * gas_price`). | |
| /// `None` when the order has no stored quote (e.g. JIT orders). | |
| /// Actual on-chain gas cost (in native token wei) attributed to this trade: | |
| /// its share of the settlement transaction's gas cost | |
| /// (`gas_used * effective_gas_price`), split equally across all trades | |
| /// settled in the same transaction. `None` for trades settled before this | |
| /// data was recorded (see V111). |
| ALTER TABLE settlements | ||
| ADD COLUMN gas_used numeric(78, 0), | ||
| ADD COLUMN effective_gas_price numeric(78, 0); |
There was a problem hiding this comment.
Deploy-ordering caveat worth confirming: the orderbook trades / order_gas_costs queries now reference s.gas_used and s.effective_gas_price unconditionally. If a new orderbook pod starts serving before this migration has been applied (the migration is run by autopilot's Flyway, a separate deployment), every trades/orders query will fail with column "gas_used" does not exist. Please make sure V111 is applied before (or atomically with) the new orderbook rollout — see the bot's backward-compatibility reminder above.
MartinquaXD
left a comment
There was a problem hiding this comment.
Main concern is the error prone settlement <> trade association via log indices and the fact that this estimate can be very much off. Was that discussed with the solver / frontend team?
|
|
||
| /// SQL expression computing a single trade's share of its settlement's on-chain | ||
| /// gas cost: the settlement's total cost (`gas_used * effective_gas_price`) | ||
| /// divided equally across all trades settled in the same transaction. Expects |
There was a problem hiding this comment.
This can be wildly inaccurate because a super complicated order can be batched together with a simple order. I don't think there is a way to make this more accurate with reasonable effort - just wanted to flag this in case you weren't aware.
There was a problem hiding this comment.
I knew all this could be innacurate, the idea is to give an estimate (slightly better than "solver reported" as they can simply write a different value)
| WHERE s.block_number = page.block_number | ||
| AND s.log_index > page.log_index |
There was a problem hiding this comment.
I think deciding to not store the tx_hash in the trades table was a blunder back in the day which leads to this ugly code again and again. Also unless I'm missing something this code does not handle trades from multiple settlements in the same block correctly.
WDYT about introducing the tx_hash column on the trades table to simply join on that? Since we can already get the association using the log index this data can even be fully backfilled using a DB migration alone.
| .transpose()?; | ||
|
|
||
| if let Some(order) = order.as_mut() { | ||
| order.metadata.gas_cost = attributed_gas_cost(&mut ex, uid).await?; |
There was a problem hiding this comment.
Feels like this logic should already be handled in single_full_order_with_quote(), no?
|
|
||
| /// Total on-chain gas cost (native token wei) attributed to a single order | ||
| /// across all of its trades. See [`database::trades::order_gas_costs`]. | ||
| async fn attributed_gas_cost( |
There was a problem hiding this comment.
maybe accumulative_gas_cost expresses the concept of the function better?
| Actual on-chain gas cost attributed to this order, in native token | ||
| wei. Computed as the order's share of the gas cost |
There was a problem hiding this comment.
This exposes a ton of implementation details the user doesn't care about, no?
Key pieces of info appear to be:
- it's an estimate (no need to explain how we estimate IMO)
- it's an ETH value (instead of gas units)
- may not be populated for old orders (unsettled orders could just get a 0 here, no?)
Same applies to the other openapi field
| database::jit_orders::get_many_by_uid(&mut ex_jit_orders, &uids) | ||
| )?; | ||
|
|
||
| let gas_costs = database::trades::order_gas_costs(&mut ex_orders, &uids) |
There was a problem hiding this comment.
single_order and many_orders set gas_cost, but user_orders (/account/{owner}/orders) and orders_for_tx (/transactions/{tx}/orders) return None, since they build the order through full_order_into_model_order and skip this step. A client fetching the same order then sees gasCost over /orders/{uid} and null over /account/{owner}/orders. Is it intentional? If the FE lists costs on the account orders page, it reads null there.
| pub async fn order_gas_costs( | ||
| ex: &mut PgConnection, | ||
| order_uids: &[OrderUid], | ||
| ) -> Result<Vec<(OrderUid, BigDecimal)>, sqlx::Error> { |
There was a problem hiding this comment.
Should it be covered with a small DB test to pin it down: one block, two settlements in different txs, a few trades each, then assert each trade's share and the per-order sum.
Description
The removal of UCP means that we no longer have fees and gas costs info in a single place, meaning the FE must calculate them; the issue comes when one notices that we don't keep track of gas costs for settlements (meaning trades and orders don't either).
This PR adds gas cost tracking, when settlements get indexed, they will now be indexed with the gas cost.
The PR does not do backfills on purpose, that must be done as a separate process.
Changes
How to test
Sepolia staging is currently suspended with this code there, please do a trade and review the UI using this link
https://jmgd-ucp.explorer-dev-dxz.pages.dev/
Under costs and fees you should see this:
