Skip to content
42 changes: 42 additions & 0 deletions crates/common/src/local_db/query/fetch_trades_by_tx/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use crate::local_db::{
query::{SqlBuildError, SqlStatement, SqlValue},
OrderbookIdentifier,
};
use alloy::primitives::B256;

const QUERY_TEMPLATE: &str = include_str!("query.sql");

pub fn build_fetch_trades_by_tx_stmt(
ob_id: &OrderbookIdentifier,
tx_hash: B256,
) -> Result<SqlStatement, SqlBuildError> {
let mut stmt = SqlStatement::new(QUERY_TEMPLATE);
stmt.push(SqlValue::from(ob_id.chain_id));
stmt.push(SqlValue::from(ob_id.orderbook_address));
stmt.push(SqlValue::from(tx_hash));
Ok(stmt)
}

#[cfg(test)]
mod tests {
use super::*;
use alloy::{
hex,
primitives::{b256, Address},
};

#[test]
fn builds_with_chain_id_and_tx_hash() {
let tx_hash = b256!("0x00000000000000000000000000000000000000000000000000000000deadface");
let stmt =
build_fetch_trades_by_tx_stmt(&OrderbookIdentifier::new(137, Address::ZERO), tx_hash)
.unwrap();
assert_eq!(stmt.params.len(), 3);
assert_eq!(stmt.params[0], SqlValue::U64(137));
assert_eq!(stmt.params[1], SqlValue::Text(Address::ZERO.to_string()));
assert_eq!(
stmt.params[2],
SqlValue::Text(hex::encode_prefixed(tx_hash))
);
}
}
335 changes: 335 additions & 0 deletions crates/common/src/local_db/query/fetch_trades_by_tx/query.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
WITH
params AS (
SELECT
?1 AS chain_id,
?2 AS orderbook_address,
?3 AS transaction_hash
),
take_trades AS (
SELECT
'take' AS trade_kind,
t.chain_id,
t.orderbook_address,
oe.order_hash,
t.order_owner,
t.order_nonce,
t.transaction_hash,
t.log_index,
t.block_number,
t.block_timestamp,
t.sender AS transaction_sender,
io_in.vault_id AS input_vault_id,
io_in.token AS input_token,
t.taker_output AS input_delta,
io_out.vault_id AS output_vault_id,
io_out.token AS output_token,
FLOAT_NEGATE(t.taker_input) AS output_delta
FROM take_orders t
JOIN params p
ON t.chain_id = p.chain_id
AND t.orderbook_address = p.orderbook_address
AND t.transaction_hash = p.transaction_hash
JOIN order_events oe
ON oe.chain_id = t.chain_id
AND oe.orderbook_address = t.orderbook_address
AND oe.order_owner = t.order_owner
AND oe.order_nonce = t.order_nonce
AND oe.event_type = 'AddOrderV3'
AND (
oe.block_number < t.block_number
OR (oe.block_number = t.block_number AND oe.log_index <= t.log_index)
)
AND NOT EXISTS (
SELECT 1
FROM order_events newer
WHERE newer.chain_id = oe.chain_id
AND newer.orderbook_address = oe.orderbook_address
AND newer.order_owner = oe.order_owner
AND newer.order_nonce = oe.order_nonce
AND newer.event_type = 'AddOrderV3'
AND (
newer.block_number < t.block_number
OR (newer.block_number = t.block_number AND newer.log_index <= t.log_index)
)
AND (
newer.block_number > oe.block_number
OR (newer.block_number = oe.block_number AND newer.log_index > oe.log_index)
)
)
JOIN order_ios io_in
ON io_in.chain_id = oe.chain_id
AND io_in.orderbook_address = oe.orderbook_address
AND io_in.transaction_hash = oe.transaction_hash
AND io_in.log_index = oe.log_index
AND io_in.io_index = t.input_io_index
AND io_in.io_type = 'input'
JOIN order_ios io_out
ON io_out.chain_id = oe.chain_id
AND io_out.orderbook_address = oe.orderbook_address
AND io_out.transaction_hash = oe.transaction_hash
AND io_out.log_index = oe.log_index
AND io_out.io_index = t.output_io_index
AND io_out.io_type = 'output'
),
clear_alice AS (
SELECT DISTINCT
'clear' AS trade_kind,
c.chain_id,
c.orderbook_address,
oe.order_hash,
oe.order_owner,
oe.order_nonce,
c.transaction_hash,
c.log_index,
c.block_number,
c.block_timestamp,
c.sender AS transaction_sender,
c.alice_input_vault_id AS input_vault_id,
io_in.token AS input_token,
a.alice_input AS input_delta,
c.alice_output_vault_id AS output_vault_id,
io_out.token AS output_token,
FLOAT_NEGATE(a.alice_output) AS output_delta
FROM clear_v3_events c
JOIN params p
ON c.chain_id = p.chain_id
AND c.orderbook_address = p.orderbook_address
AND c.transaction_hash = p.transaction_hash
JOIN order_events oe
ON oe.chain_id = c.chain_id
AND oe.orderbook_address = c.orderbook_address
AND oe.order_hash = c.alice_order_hash
AND oe.event_type = 'AddOrderV3'
AND (
oe.block_number < c.block_number
OR (oe.block_number = c.block_number AND oe.log_index <= c.log_index)
)
AND NOT EXISTS (
SELECT 1
FROM order_events newer
WHERE newer.chain_id = oe.chain_id
AND newer.orderbook_address = oe.orderbook_address
AND newer.order_hash = oe.order_hash
AND newer.event_type = 'AddOrderV3'
AND (
newer.block_number < c.block_number
OR (newer.block_number = c.block_number AND newer.log_index <= c.log_index)
)
AND (
newer.block_number > oe.block_number
OR (newer.block_number = oe.block_number AND newer.log_index > oe.log_index)
)
)
JOIN after_clear_v2_events a
ON a.chain_id = c.chain_id
AND a.orderbook_address = c.orderbook_address
AND a.transaction_hash = c.transaction_hash
AND a.log_index = (
SELECT MIN(ac.log_index)
FROM after_clear_v2_events ac
WHERE ac.chain_id = c.chain_id
AND ac.orderbook_address = c.orderbook_address
AND ac.transaction_hash = c.transaction_hash
AND ac.log_index > c.log_index
)
JOIN order_ios io_in
ON io_in.chain_id = oe.chain_id
AND io_in.orderbook_address = oe.orderbook_address
AND io_in.transaction_hash = oe.transaction_hash
AND io_in.log_index = oe.log_index
AND io_in.io_index = c.alice_input_io_index
AND io_in.io_type = 'input'
JOIN order_ios io_out
ON io_out.chain_id = oe.chain_id
AND io_out.orderbook_address = oe.orderbook_address
AND io_out.transaction_hash = oe.transaction_hash
AND io_out.log_index = oe.log_index
AND io_out.io_index = c.alice_output_io_index
AND io_out.io_type = 'output'
),
clear_bob AS (
SELECT DISTINCT
'clear' AS trade_kind,
c.chain_id,
c.orderbook_address,
oe.order_hash,
oe.order_owner,
oe.order_nonce,
c.transaction_hash,
c.log_index,
c.block_number,
c.block_timestamp,
c.sender AS transaction_sender,
c.bob_input_vault_id AS input_vault_id,
io_in.token AS input_token,
a.bob_input AS input_delta,
c.bob_output_vault_id AS output_vault_id,
io_out.token AS output_token,
FLOAT_NEGATE(a.bob_output) AS output_delta
FROM clear_v3_events c
JOIN params p
ON c.chain_id = p.chain_id
AND c.orderbook_address = p.orderbook_address
AND c.transaction_hash = p.transaction_hash
JOIN order_events oe
ON oe.chain_id = c.chain_id
AND oe.orderbook_address = c.orderbook_address
AND oe.order_hash = c.bob_order_hash
AND oe.event_type = 'AddOrderV3'
AND (
oe.block_number < c.block_number
OR (oe.block_number = c.block_number AND oe.log_index <= c.log_index)
)
AND NOT EXISTS (
SELECT 1
FROM order_events newer
WHERE newer.chain_id = oe.chain_id
AND newer.orderbook_address = oe.orderbook_address
AND newer.order_hash = oe.order_hash
AND newer.event_type = 'AddOrderV3'
AND (
newer.block_number < c.block_number
OR (newer.block_number = c.block_number AND newer.log_index <= c.log_index)
)
AND (
newer.block_number > oe.block_number
OR (newer.block_number = oe.block_number AND newer.log_index > oe.log_index)
)
)
JOIN after_clear_v2_events a
ON a.chain_id = c.chain_id
AND a.orderbook_address = c.orderbook_address
AND a.transaction_hash = c.transaction_hash
AND a.log_index = (
SELECT MIN(ac.log_index)
FROM after_clear_v2_events ac
WHERE ac.chain_id = c.chain_id
AND ac.orderbook_address = c.orderbook_address
AND ac.transaction_hash = c.transaction_hash
AND ac.log_index > c.log_index
)
JOIN order_ios io_in
ON io_in.chain_id = oe.chain_id
AND io_in.orderbook_address = oe.orderbook_address
AND io_in.transaction_hash = oe.transaction_hash
AND io_in.log_index = oe.log_index
AND io_in.io_index = c.bob_input_io_index
AND io_in.io_type = 'input'
JOIN order_ios io_out
ON io_out.chain_id = oe.chain_id
AND io_out.orderbook_address = oe.orderbook_address
AND io_out.transaction_hash = oe.transaction_hash
AND io_out.log_index = oe.log_index
AND io_out.io_index = c.bob_output_io_index
AND io_out.io_type = 'output'
),
clear_trades AS (
SELECT * FROM clear_alice
UNION ALL
SELECT * FROM clear_bob
),
unioned_trades AS (
SELECT * FROM take_trades
UNION ALL
SELECT * FROM clear_trades
),
trade_rows AS (
SELECT
ut.trade_kind,
ut.chain_id,
ut.orderbook_address,
ut.order_hash,
ut.order_owner,
ut.order_nonce,
ut.transaction_hash,
ut.log_index,
ut.block_number,
ut.block_timestamp,
ut.transaction_sender,
ut.input_vault_id,
ut.input_token,
ut.input_delta,
ut.output_vault_id,
ut.output_token,
ut.output_delta
FROM unioned_trades ut
Comment on lines +236 to +255
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Feb 17, 2026

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

trade_rows CTE is a passthrough with no transformation.

This CTE is a 1:1 projection of unioned_trades with all the same columns listed explicitly. It adds no filtering or computation. Consider referencing unioned_trades directly in the subsequent CTE to reduce verbosity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/common/src/local_db/query/fetch_trades_by_tx/query.sql` around lines
236 - 255, The trade_rows CTE is a direct 1:1 projection of unioned_trades (no
transforms); remove the redundant trade_rows CTE and update any downstream
references (e.g., within CTEs or final SELECTs that currently reference
trade_rows) to reference unioned_trades directly (search for the symbol
trade_rows and replace usages with unioned_trades, ensuring column names still
match).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Intentional — explicitly pins the column set rather than relying on SELECT * ordering from the union.

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.

@findolor, thank you for clarifying! That makes perfect sense—explicitly projecting the columns ensures a stable, deterministic schema and avoids any fragility from implicit SELECT * ordering. I appreciate the explanation.

(\/)
(^.^)
(")_(")  *learning noted*

✏️ Learnings added
Learnt from: findolor
Repo: rainlanguage/rain.orderbook PR: 2465
File: crates/common/src/local_db/query/fetch_trades_by_tx/query.sql:236-255
Timestamp: 2026-02-18T08:41:03.365Z
Learning: In SQL queries within the rain.orderbook codebase (e.g., crates/common/src/local_db/query/fetch_trades_by_tx/query.sql), passthrough CTEs that explicitly list all columns from their source (like `trade_rows` projecting `unioned_trades`) are intentional. They pin the column set explicitly rather than relying on `SELECT *` ordering, ensuring deterministic schema and maintainability.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: findolor
Repo: rainlanguage/rain.orderbook PR: 2376
File: crates/common/src/raindex_client/orders.rs:2-2
Timestamp: 2026-01-07T14:21:44.132Z
Learning: When local DB is enabled in the rain.orderbook codebase, the intended design is to use it exclusively without falling back to the subgraph, even when results are empty. Methods like get_trades_list() in trades.rs and get_vaults_volume() in orders.rs follow this pattern—they return local DB results directly when is_chain_supported_local_db() and local_db is available. Mixing data sources could cause inconsistencies. The get_balance_changes() method in vaults.rs that checks !local_changes.is_empty() before returning is an outlier that needs to be fixed to align with the exclusive local DB pattern.

Learnt from: findolor
Repo: rainlanguage/rain.orderbook PR: 2145
File: crates/common/src/raindex_client/local_db/query/fetch_orders/query.sql:6-7
Timestamp: 2025-10-06T11:28:30.692Z
Learning: In `crates/common/src/raindex_client/local_db/query/fetch_orders/query.sql`, the orderbook_address is currently hardcoded to '0x2f209e5b67A33B8fE96E28f24628dF6Da301c8eB' because the system only supports a single orderbook at the moment. Multiorderbook logic is not yet implemented and will be added in the future.

Learnt from: findolor
Repo: rainlanguage/rain.orderbook PR: 2155
File: crates/common/src/raindex_client/trades.rs:133-152
Timestamp: 2025-10-06T14:13:18.531Z
Learning: In the rain.orderbook codebase, the `page` parameter in `RaindexOrder::get_trades_list` method (in crates/common/src/raindex_client/trades.rs) is kept for backwards compatibility with subgraph logic, but the LocalDb fast-path intentionally returns all trades without implementing pagination.

Learnt from: findolor
Repo: rainlanguage/rain.orderbook PR: 1956
File: crates/common/src/raindex_client/trades.rs:215-223
Timestamp: 2025-07-04T10:24:56.163Z
Learning: In the rain.orderbook codebase, the get_trade_count method implementation that fetches all trades to count them is intentionally consistent with previous implementations and not considered a performance issue, as indicated by findolor for the trades counting functionality in crates/common/src/raindex_client/trades.rs.

Learnt from: findolor
Repo: rainlanguage/rain.orderbook PR: 2442
File: crates/common/src/local_db/query/fetch_vaults/mod.rs:228-244
Timestamp: 2026-02-09T12:31:02.217Z
Learning: In crates/common/src/local_db/query/fetch_vaults/mod.rs tests, whitespace-sensitive SQL string assertions (e.g., checking for "GROUP BY chain_id, orderbook_address, owner, token, vault_id\n") are acceptable for verifying query structure until a mock database testing approach is implemented. These assertions help ensure the SQL template is correctly formed even though they may be fragile to formatting changes.

),
trade_with_snapshots AS (
SELECT
tr.*,
mvb_in.balance AS input_base_balance,
mvb_in.last_block AS input_base_block,
mvb_in.last_log_index AS input_base_log_index,
mvb_out.balance AS output_base_balance,
mvb_out.last_block AS output_base_block,
mvb_out.last_log_index AS output_base_log_index
FROM trade_rows tr
LEFT JOIN running_vault_balances mvb_in
ON mvb_in.chain_id = tr.chain_id
AND mvb_in.orderbook_address = tr.orderbook_address
AND mvb_in.owner = tr.order_owner
AND mvb_in.token = tr.input_token
AND mvb_in.vault_id = tr.input_vault_id
LEFT JOIN running_vault_balances mvb_out
ON mvb_out.chain_id = tr.chain_id
AND mvb_out.orderbook_address = tr.orderbook_address
AND mvb_out.owner = tr.order_owner
AND mvb_out.token = tr.output_token
AND mvb_out.vault_id = tr.output_vault_id
)
SELECT
tws.trade_kind,
tws.orderbook_address AS orderbook,
tws.order_hash,
tws.order_owner,
tws.order_nonce,
tws.transaction_hash,
tws.log_index,
tws.block_number,
tws.block_timestamp,
tws.transaction_sender,
tws.input_vault_id,
tws.input_token,
tok_in.name AS input_token_name,
tok_in.symbol AS input_token_symbol,
tok_in.decimals AS input_token_decimals,
tws.input_delta,
vbc_input.running_balance AS input_running_balance,
tws.output_vault_id,
tws.output_token,
tok_out.name AS output_token_name,
tok_out.symbol AS output_token_symbol,
tok_out.decimals AS output_token_decimals,
tws.output_delta,
vbc_output.running_balance AS output_running_balance,
(
'0x' ||
lower(replace(tws.transaction_hash, '0x', '')) ||
printf('%016x', tws.log_index)
) AS trade_id
FROM trade_with_snapshots tws
LEFT JOIN vault_balance_changes vbc_input
ON vbc_input.chain_id = tws.chain_id
AND vbc_input.orderbook_address = tws.orderbook_address
AND vbc_input.owner = tws.order_owner
AND vbc_input.token = tws.input_token
AND vbc_input.vault_id = tws.input_vault_id
AND vbc_input.block_number = tws.block_number
AND vbc_input.log_index = tws.log_index
LEFT JOIN vault_balance_changes vbc_output
ON vbc_output.chain_id = tws.chain_id
AND vbc_output.orderbook_address = tws.orderbook_address
AND vbc_output.owner = tws.order_owner
AND vbc_output.token = tws.output_token
AND vbc_output.vault_id = tws.output_vault_id
AND vbc_output.block_number = tws.block_number
AND vbc_output.log_index = tws.log_index
LEFT JOIN erc20_tokens tok_in
ON tok_in.chain_id = tws.chain_id
AND tok_in.orderbook_address = tws.orderbook_address
AND tok_in.token_address = tws.input_token
LEFT JOIN erc20_tokens tok_out
ON tok_out.chain_id = tws.chain_id
AND tok_out.orderbook_address = tws.orderbook_address
AND tok_out.token_address = tws.output_token
ORDER BY tws.block_timestamp DESC, tws.block_number DESC, tws.log_index DESC, tws.trade_kind;
1 change: 1 addition & 0 deletions crates/common/src/local_db/query/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod fetch_orders;
pub mod fetch_store_addresses;
pub mod fetch_tables;
pub mod fetch_target_watermark;
pub mod fetch_trades_by_tx;
pub mod fetch_transaction_by_hash;
pub mod fetch_vault_balance_changes;
pub mod fetch_vaults;
Expand Down
1 change: 1 addition & 0 deletions crates/common/src/raindex_client/local_db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub mod executor;
pub mod orders;
pub mod pipeline;
pub mod query;
pub mod trades;
pub mod transactions;
pub mod vaults;

Expand Down
Loading
Loading