diff --git a/crates/cli/src/commands/trade/detail.rs b/crates/cli/src/commands/trade/detail.rs index a4916812f5..24d50a63ed 100644 --- a/crates/cli/src/commands/trade/detail.rs +++ b/crates/cli/src/commands/trade/detail.rs @@ -164,13 +164,15 @@ mod tests { }, "timestamp": "0", "tradeEvent": { + "__typename": "TakeOrder", "sender": encode_prefixed(Address::random()), "transaction": { "id": encode_prefixed(B256::random()), "blockNumber": "0", "timestamp": "0", "from": encode_prefixed(Address::random()) - } + }, + "trades": [] }, "orderbook": { "id": encode_prefixed(B256::random()), diff --git a/crates/cli/src/commands/trade/list.rs b/crates/cli/src/commands/trade/list.rs index 5f413db762..4e9ae2da02 100644 --- a/crates/cli/src/commands/trade/list.rs +++ b/crates/cli/src/commands/trade/list.rs @@ -260,13 +260,15 @@ mod tests { }, "timestamp": "0", "tradeEvent": { + "__typename": "TakeOrder", "sender": encode_prefixed(Address::random()), "transaction": { "id": encode_prefixed(B256::random()), "blockNumber": "0", "timestamp": "0", "from": encode_prefixed(Address::random()) - } + }, + "trades": [] }, "orderbook": { "id": encode_prefixed(B256::random()), diff --git a/crates/common/src/local_db/query/fetch_order_trades/mod.rs b/crates/common/src/local_db/query/fetch_order_trades/mod.rs index f837a23817..daff4ee34a 100644 --- a/crates/common/src/local_db/query/fetch_order_trades/mod.rs +++ b/crates/common/src/local_db/query/fetch_order_trades/mod.rs @@ -34,6 +34,8 @@ pub struct LocalDbOrderTrade { pub output_delta: String, pub output_running_balance: Option, pub trade_id: String, + pub counterparty_order_hash: Option, + pub counterparty_owner: Option, } /// Builds the SQL statement for retrieving order trades within the specified window. diff --git a/crates/common/src/local_db/query/fetch_order_trades/query.sql b/crates/common/src/local_db/query/fetch_order_trades/query.sql index d50b5392f4..19f165e203 100644 --- a/crates/common/src/local_db/query/fetch_order_trades/query.sql +++ b/crates/common/src/local_db/query/fetch_order_trades/query.sql @@ -41,7 +41,9 @@ take_trades AS ( 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 + FLOAT_NEGATE(t.taker_input) AS output_delta, + NULL AS counterparty_order_hash, + t.sender AS counterparty_owner FROM take_orders t JOIN params p ON t.chain_id = p.chain_id @@ -104,7 +106,9 @@ clear_alice AS ( 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 + FLOAT_NEGATE(a.alice_output) AS output_delta, + c.bob_order_hash AS counterparty_order_hash, + c.bob_order_owner AS counterparty_owner FROM clear_v3_events c JOIN params p ON c.chain_id = p.chain_id @@ -178,7 +182,9 @@ clear_bob AS ( 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 + FLOAT_NEGATE(a.bob_output) AS output_delta, + c.alice_order_hash AS counterparty_order_hash, + c.alice_order_owner AS counterparty_owner FROM clear_v3_events c JOIN params p ON c.chain_id = p.chain_id @@ -262,7 +268,9 @@ trade_rows AS ( ut.input_delta, ut.output_vault_id, ut.output_token, - ut.output_delta + ut.output_delta, + ut.counterparty_order_hash, + ut.counterparty_owner FROM unioned_trades ut ), trade_with_snapshots AS ( @@ -317,7 +325,9 @@ SELECT '0x' || lower(replace(tws.transaction_hash, '0x', '')) || printf('%016x', tws.log_index) - ) AS trade_id + ) AS trade_id, + tws.counterparty_order_hash, + tws.counterparty_owner FROM trade_with_snapshots tws LEFT JOIN vault_balance_changes vbc_input ON vbc_input.chain_id = tws.chain_id diff --git a/crates/common/src/raindex_client/orders.rs b/crates/common/src/raindex_client/orders.rs index 664c72f97b..0fc6ee9484 100644 --- a/crates/common/src/raindex_client/orders.rs +++ b/crates/common/src/raindex_client/orders.rs @@ -2825,13 +2825,15 @@ mod tests { "id": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "timestamp": "1632000000", "tradeEvent": { + "__typename": "TakeOrder", "sender": "0x0000000000000000000000000000000000000000", "transaction": { "id": "0x0000000000000000000000000000000000000000000000000000000000000000", "from": "0x0000000000000000000000000000000000000000", "timestamp": "1632000000", "blockNumber": "0" - } + }, + "trades": [] }, "order": { "id": "0x557147dd0daa80d5beff0023fe6a3505469b2b8c4406ce1ab873e1a652572dd4", diff --git a/crates/common/src/raindex_client/trades.rs b/crates/common/src/raindex_client/trades.rs index fd3c56b147..dabd1ecae1 100644 --- a/crates/common/src/raindex_client/trades.rs +++ b/crates/common/src/raindex_client/trades.rs @@ -15,6 +15,16 @@ use std::str::FromStr; #[cfg(target_family = "wasm")] use wasm_bindgen_utils::prelude::js_sys::BigInt; +#[derive(Serialize, Deserialize, Debug, Clone, Tsify)] +#[serde(rename_all = "camelCase")] +pub struct RaindexCounterparty { + #[tsify(optional, type = "Hex")] + pub order_hash: Option, + #[tsify(type = "Address")] + pub owner: Address, +} +impl_wasm_traits!(RaindexCounterparty); + #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] #[wasm_bindgen] @@ -26,6 +36,8 @@ pub struct RaindexTrade { output_vault_balance_change: RaindexVaultBalanceChange, timestamp: U256, orderbook: Address, + trade_event_type: String, + counterparty: Option, } #[cfg(target_family = "wasm")] #[wasm_bindgen] @@ -59,6 +71,14 @@ impl RaindexTrade { pub fn orderbook(&self) -> String { self.orderbook.to_string() } + #[wasm_bindgen(getter = tradeEventType)] + pub fn trade_event_type(&self) -> String { + self.trade_event_type.clone() + } + #[wasm_bindgen(getter)] + pub fn counterparty(&self) -> Option { + self.counterparty.clone() + } } #[cfg(not(target_family = "wasm"))] impl RaindexTrade { @@ -83,6 +103,12 @@ impl RaindexTrade { pub fn orderbook(&self) -> Address { self.orderbook } + pub fn trade_event_type(&self) -> String { + self.trade_event_type.clone() + } + pub fn counterparty(&self) -> Option { + self.counterparty.clone() + } } #[wasm_export] @@ -266,6 +292,28 @@ impl RaindexOrder { impl RaindexTrade { pub fn try_from_sg_trade(chain_id: u32, trade: SgTrade) -> Result { + let trade_event_type = trade.trade_event.__typename.clone(); + + let counterparty = if trade_event_type == "Clear" { + trade + .trade_event + .trades + .iter() + .find(|t| t.id != trade.id) + .map(|sibling| -> Result { + Ok(RaindexCounterparty { + order_hash: Some(Bytes::from_str(&sibling.order.order_hash.0)?), + owner: Address::from_str(&sibling.order.owner.0)?, + }) + }) + .transpose()? + } else { + Some(RaindexCounterparty { + order_hash: None, + owner: Address::from_str(&trade.trade_event.sender.0)?, + }) + }; + Ok(RaindexTrade { id: Bytes::from_str(&trade.id.0)?, order_hash: Bytes::from_str(&trade.order.order_hash.0)?, @@ -282,6 +330,8 @@ impl RaindexTrade { )?, timestamp: U256::from_str(&trade.timestamp.0)?, orderbook: Address::from_str(&trade.orderbook.id.0)?, + trade_event_type, + counterparty, }) } @@ -334,6 +384,26 @@ impl RaindexTrade { trade.block_timestamp, )?; + let trade_event_type = match trade.trade_kind.as_str() { + "clear" => "Clear", + _ => "TakeOrder", + } + .to_string(); + + let counterparty = trade + .counterparty_owner + .as_ref() + .map(|owner_str| { + let owner = Address::from_str(owner_str)?; + let order_hash = trade + .counterparty_order_hash + .as_ref() + .map(|h| Bytes::from_str(h)) + .transpose()?; + Ok::(RaindexCounterparty { order_hash, owner }) + }) + .transpose()?; + Ok(RaindexTrade { id: Bytes::from_str(&trade.trade_id)?, order_hash: trade.order_hash.into(), @@ -342,6 +412,8 @@ impl RaindexTrade { output_vault_balance_change: output_change, timestamp: U256::from(trade.block_timestamp), orderbook: trade.orderbook, + trade_event_type, + counterparty, }) } } @@ -503,6 +575,8 @@ mod test_helpers { output_delta: OUTPUT_DELTA_HEX.into(), output_running_balance: Some(OUTPUT_RUNNING_HEX.into()), trade_id, + counterparty_order_hash: None, + counterparty_owner: Some(transaction_sender.to_string()), }; LocalTradeFixture { @@ -884,13 +958,24 @@ mod test_helpers { json!( { "id": "0x0123", "tradeEvent": { + "__typename": "TakeOrder", "transaction": { "id": "0x0000000000000000000000000000000000000000000000000000000000000123", "from": "0x0000000000000000000000000000000000000000", "blockNumber": "0", "timestamp": "0" }, - "sender": "sender1" + "sender": "0x0000000000000000000000000000000000000001", + "trades": [ + { + "id": "0x0123", + "order": { + "id": "0x0123", + "orderHash": "0x0123", + "owner": "0x0000000000000000000000000000000000000099" + } + } + ] }, "outputVaultBalanceChange": { "id": "0x0000000000000000000000000000000000000000000000000000000000000123", @@ -968,19 +1053,137 @@ mod test_helpers { } }) } + fn get_clear_trade_json() -> Value { + json!({ + "id": "0x0abc", + "tradeEvent": { + "__typename": "Clear", + "transaction": { + "id": "0x0000000000000000000000000000000000000000000000000000000000000abc", + "from": "0x0000000000000000000000000000000000000000", + "blockNumber": "100", + "timestamp": "1700000500" + }, + "sender": "0x0000000000000000000000000000000000000009", + "trades": [ + { + "id": "0x0abc", + "order": { + "id": "0x0abc", + "orderHash": "0x0abc", + "owner": "0x0000000000000000000000000000000000000077" + } + }, + { + "id": "0x0def", + "order": { + "id": "0x0def", + "orderHash": "0x0def", + "owner": "0x0000000000000000000000000000000000000088" + } + } + ] + }, + "outputVaultBalanceChange": { + "id": "0x0000000000000000000000000000000000000000000000000000000000000abc", + "__typename": "TradeVaultBalanceChange", + "amount": NEG2, + "newVaultBalance": F0, + "oldVaultBalance": F0, + "vault": { + "id": "0x0abc", + "vaultId": "0x0abc", + "token": { + "id": "0x12e605bc104e93b45e1ad99f9e555f659051c2bb", + "address": "0x12e605bc104e93b45e1ad99f9e555f659051c2bb", + "name": "Staked FLR", + "symbol": "sFLR", + "decimals": "18" + } + }, + "timestamp": "1700000500", + "transaction": { + "id": "0x0000000000000000000000000000000000000000000000000000000000000abc", + "from": "0x0000000000000000000000000000000000000000", + "blockNumber": "100", + "timestamp": "1700000500" + }, + "orderbook": { + "id": "0x1234567890abcdef1234567890abcdef12345678" + }, + "trade": { + "tradeEvent": { + "__typename": "Clear" + } + } + }, + "order": { + "id": "0x0abc", + "orderHash": "0x0abc" + }, + "inputVaultBalanceChange": { + "id": "0x0abc", + "__typename": "TradeVaultBalanceChange", + "amount": F1, + "newVaultBalance": F0, + "oldVaultBalance": F0, + "vault": { + "id": "0x0abc", + "vaultId": "0x0abc", + "token": { + "id": "0x1d80c49bbbcd1c0911346656b529df9e5c2f783d", + "address": "0x1d80c49bbbcd1c0911346656b529df9e5c2f783d", + "name": "Wrapped Flare", + "symbol": "WFLR", + "decimals": "18" + } + }, + "timestamp": "1700000500", + "transaction": { + "id": "0x0000000000000000000000000000000000000000000000000000000000000abc", + "from": "0x0000000000000000000000000000000000000000", + "blockNumber": "100", + "timestamp": "1700000500" + }, + "orderbook": { + "id": "0x1234567890abcdef1234567890abcdef12345678" + }, + "trade": { + "tradeEvent": { + "__typename": "Clear" + } + } + }, + "timestamp": "1700000500", + "orderbook": { + "id": "0x1234567890abcdef1234567890abcdef12345678" + } + }) + } fn get_trades_json() -> Value { json!([ get_single_trade_json(), { "id": "0x0234", "tradeEvent": { + "__typename": "TakeOrder", "transaction": { "id": "0x0000000000000000000000000000000000000000000000000000000000000234", "from": "0x0000000000000000000000000000000000000001", "blockNumber": "0", "timestamp": "0" }, - "sender": "sender2" + "sender": "0x0000000000000000000000000000000000000002", + "trades": [ + { + "id": "0x0234", + "order": { + "id": "0x0234", + "orderHash": "0x0234", + "owner": "0x0000000000000000000000000000000000000098" + } + } + ] }, "outputVaultBalanceChange": { "id": "0x0234", @@ -1249,9 +1452,27 @@ mod test_helpers { Address::from_str("0x1234567890abcdef1234567890abcdef12345678").unwrap() ); assert_eq!(trade1.order_hash(), Bytes::from_str("0x0123").unwrap()); + assert_eq!(trade1.trade_event_type(), "TakeOrder"); + let cp1 = trade1 + .counterparty() + .expect("TakeOrder should have counterparty"); + assert_eq!(cp1.order_hash, None); + assert_eq!( + cp1.owner, + Address::from_str("0x0000000000000000000000000000000000000001").unwrap() + ); let trade2 = trades[1].clone(); assert_eq!(trade2.id(), Bytes::from_str("0x0234").unwrap()); + assert_eq!(trade2.trade_event_type(), "TakeOrder"); + let cp2 = trade2 + .counterparty() + .expect("TakeOrder should have counterparty"); + assert_eq!(cp2.order_hash, None); + assert_eq!( + cp2.owner, + Address::from_str("0x0000000000000000000000000000000000000002").unwrap() + ); } #[tokio::test] @@ -1425,6 +1646,72 @@ mod test_helpers { Address::from_str("0x1234567890abcdef1234567890abcdef12345678").unwrap() ); assert_eq!(trade.order_hash(), Bytes::from_str("0x0123").unwrap()); + assert_eq!(trade.trade_event_type(), "TakeOrder"); + let cp = trade + .counterparty() + .expect("TakeOrder should have counterparty"); + assert_eq!(cp.order_hash, None); + assert_eq!( + cp.owner, + Address::from_str("0x0000000000000000000000000000000000000001").unwrap() + ); + } + + #[tokio::test] + async fn test_get_order_trade_detail_clear_event() { + let sg_server = MockServer::start_async().await; + sg_server.mock(|when, then| { + when.path("/sg").body_contains("SgOrderTradeDetailQuery"); + then.status(200).json_body_obj(&json!({ + "data": { + "trade": get_clear_trade_json() + } + })); + }); + sg_server.mock(|when, then| { + when.path("/sg").body_contains("SgOrderDetailByHashQuery"); + then.status(200).json_body_obj(&json!({ + "data": { + "orders": [get_order1_json()] + } + })); + }); + + let raindex_client = RaindexClient::new( + vec![get_test_yaml( + &sg_server.url("/sg"), + "http://localhost:3000", + "http://localhost:3000", + "http://localhost:3000", + )], + None, + ) + .unwrap(); + let order = raindex_client + .get_order_by_hash( + &OrderbookIdentifier::new( + 1, + Address::from_str(CHAIN_ID_1_ORDERBOOK_ADDRESS).unwrap(), + ), + b256!("0x0000000000000000000000000000000000000000000000000000000000000123"), + ) + .await + .unwrap(); + let trade = order + .get_trade_detail(Bytes::from_str("0x0abc").unwrap()) + .await + .unwrap(); + + assert_eq!(trade.id(), Bytes::from_str("0x0abc").unwrap()); + assert_eq!(trade.trade_event_type(), "Clear"); + let cp = trade + .counterparty() + .expect("Clear trade should have counterparty from sibling"); + assert_eq!(cp.order_hash, Some(Bytes::from_str("0x0def").unwrap())); + assert_eq!( + cp.owner, + Address::from_str("0x0000000000000000000000000000000000000088").unwrap() + ); } #[tokio::test] diff --git a/crates/common/src/types/order_takes_list_flattened.rs b/crates/common/src/types/order_takes_list_flattened.rs index 411febea20..3ca3c39ed8 100644 --- a/crates/common/src/types/order_takes_list_flattened.rs +++ b/crates/common/src/types/order_takes_list_flattened.rs @@ -71,6 +71,7 @@ mod tests { id: SgBytes("trade001".to_string()), timestamp: SgBigInt("1678886400".to_string()), trade_event: SgTradeEvent { + __typename: "TakeOrder".to_string(), transaction: SgTransaction { id: SgBytes("tx001".to_string()), from: SgBytes("0xfromAddress".to_string()), @@ -78,6 +79,7 @@ mod tests { timestamp: SgBigInt("1678886300".to_string()), }, sender: SgBytes("0xsenderAddress".to_string()), + trades: vec![], }, order: SgTradeStructPartialOrder { id: SgBytes("orderPartial001".to_string()), diff --git a/crates/subgraph/src/orderbook_client/order_trade.rs b/crates/subgraph/src/orderbook_client/order_trade.rs index 41e4f6e5bb..1493310ea1 100644 --- a/crates/subgraph/src/orderbook_client/order_trade.rs +++ b/crates/subgraph/src/orderbook_client/order_trade.rs @@ -80,9 +80,9 @@ impl OrderbookSubgraphClient { mod tests { use super::*; use crate::types::common::{ - SgBigInt, SgBytes, SgErc20, SgOrderbook, SgTradeEvent, SgTradeEventTypename, SgTradeRef, - SgTradeStructPartialOrder, SgTradeVaultBalanceChange, SgTransaction, - SgVaultBalanceChangeVault, + SgBigInt, SgBytes, SgErc20, SgOrderbook, SgSiblingTrade, SgSiblingTradeOrder, SgTradeEvent, + SgTradeEventTypename, SgTradeRef, SgTradeStructPartialOrder, SgTradeVaultBalanceChange, + SgTransaction, SgVaultBalanceChangeVault, }; use crate::utils::float::*; use cynic::Id; @@ -155,10 +155,23 @@ mod tests { } } + fn default_sg_sibling_trade() -> SgSiblingTrade { + SgSiblingTrade { + id: SgBytes("0xtrade_id_default".to_string()), + order: SgSiblingTradeOrder { + id: SgBytes("0xorder_id_for_trade_default".to_string()), + order_hash: SgBytes("0xorder_hash_for_trade_default".to_string()), + owner: SgBytes("0xorder_owner_default".to_string()), + }, + } + } + fn default_sg_trade_event() -> SgTradeEvent { SgTradeEvent { + __typename: "TakeOrder".to_string(), transaction: default_sg_transaction(), sender: SgBytes("0xsender_address_default".to_string()), + trades: vec![default_sg_sibling_trade()], } } @@ -193,11 +206,20 @@ mod tests { ); // Assert TradeEvent + assert_eq!( + actual.trade_event.__typename, expected.trade_event.__typename, + "Trade event __typename mismatch" + ); assert_eq!(actual.trade_event.sender, expected.trade_event.sender); assert_eq!( actual.trade_event.transaction.id, expected.trade_event.transaction.id ); + assert_eq!( + actual.trade_event.trades.len(), + expected.trade_event.trades.len(), + "Trade event sibling trades count mismatch" + ); // Assert SgTradeStructPartialOrder assert_eq!(actual.order.id, expected.order.id); diff --git a/crates/subgraph/src/orderbook_client/performance.rs b/crates/subgraph/src/orderbook_client/performance.rs index 45b185427b..693794dc47 100644 --- a/crates/subgraph/src/orderbook_client/performance.rs +++ b/crates/subgraph/src/orderbook_client/performance.rs @@ -79,6 +79,7 @@ mod tests { id: SgBytes("0xorderbook_default".to_string()), }, trade_event: SgTradeEvent { + __typename: "TakeOrder".to_string(), transaction: SgTransaction { id: SgBytes("0xtx_default".to_string()), from: SgBytes("0xfrom_default".to_string()), @@ -86,6 +87,7 @@ mod tests { timestamp: SgBigInt(timestamp.to_string()), }, sender: SgBytes("0xsender_default".to_string()), + trades: vec![], }, input_vault_balance_change: SgTradeVaultBalanceChange { id: SgBytes("ivbc_default".to_string()), diff --git a/crates/subgraph/src/performance/apy.rs b/crates/subgraph/src/performance/apy.rs index a30a3a35f6..a34021297b 100644 --- a/crates/subgraph/src/performance/apy.rs +++ b/crates/subgraph/src/performance/apy.rs @@ -349,6 +349,7 @@ mod tests { order_hash: bytes.clone(), }, trade_event: SgTradeEvent { + __typename: "TakeOrder".to_string(), sender: bytes.clone(), transaction: SgTransaction { id: bytes.clone(), @@ -356,6 +357,7 @@ mod tests { block_number: bigint.clone(), timestamp: bigint.clone(), }, + trades: vec![], }, timestamp: SgBigInt("1".to_string()), orderbook: SgOrderbook { id: bytes.clone() }, @@ -408,6 +410,7 @@ mod tests { order_hash: bytes.clone(), }, trade_event: SgTradeEvent { + __typename: "TakeOrder".to_string(), sender: bytes.clone(), transaction: SgTransaction { id: bytes.clone(), @@ -415,6 +418,7 @@ mod tests { block_number: bigint.clone(), timestamp: bigint.clone(), }, + trades: vec![], }, timestamp: SgBigInt("2".to_string()), orderbook: SgOrderbook { id: bytes.clone() }, diff --git a/crates/subgraph/src/performance/order_performance.rs b/crates/subgraph/src/performance/order_performance.rs index 140c031cd6..6dc6442692 100644 --- a/crates/subgraph/src/performance/order_performance.rs +++ b/crates/subgraph/src/performance/order_performance.rs @@ -851,6 +851,7 @@ mod test { order_hash: bytes.clone(), }, trade_event: SgTradeEvent { + __typename: "TakeOrder".to_string(), sender: bytes.clone(), transaction: SgTransaction { id: bytes.clone(), @@ -858,6 +859,7 @@ mod test { block_number: bigint.clone(), timestamp: bigint.clone(), }, + trades: vec![], }, timestamp: SgBigInt("1".to_string()), orderbook: SgOrderbook { id: bytes.clone() }, @@ -910,6 +912,7 @@ mod test { order_hash: bytes.clone(), }, trade_event: SgTradeEvent { + __typename: "TakeOrder".to_string(), sender: bytes.clone(), transaction: SgTransaction { id: bytes.clone(), @@ -917,6 +920,7 @@ mod test { block_number: bigint.clone(), timestamp: bigint.clone(), }, + trades: vec![], }, timestamp: SgBigInt("2".to_string()), orderbook: SgOrderbook { id: bytes.clone() }, diff --git a/crates/subgraph/src/performance/vol.rs b/crates/subgraph/src/performance/vol.rs index 3214d789ed..94d48d0ea4 100644 --- a/crates/subgraph/src/performance/vol.rs +++ b/crates/subgraph/src/performance/vol.rs @@ -234,6 +234,7 @@ mod tests { order_hash: bytes.clone(), }, trade_event: SgTradeEvent { + __typename: "TakeOrder".to_string(), sender: bytes.clone(), transaction: SgTransaction { id: bytes.clone(), @@ -241,6 +242,7 @@ mod tests { block_number: bigint.clone(), timestamp: bigint.clone(), }, + trades: vec![], }, timestamp: bigint.clone(), orderbook: SgOrderbook { id: bytes.clone() }, @@ -303,6 +305,7 @@ mod tests { order_hash: bytes.clone(), }, trade_event: SgTradeEvent { + __typename: "TakeOrder".to_string(), sender: bytes.clone(), transaction: SgTransaction { id: bytes.clone(), @@ -310,6 +313,7 @@ mod tests { block_number: bigint.clone(), timestamp: bigint.clone(), }, + trades: vec![], }, timestamp: bigint.clone(), orderbook: SgOrderbook { id: bytes.clone() }, diff --git a/crates/subgraph/src/types/common.rs b/crates/subgraph/src/types/common.rs index d3d6e72e1b..da6a9ca612 100644 --- a/crates/subgraph/src/types/common.rs +++ b/crates/subgraph/src/types/common.rs @@ -385,10 +385,31 @@ pub struct SgClearBounty { } #[derive(cynic::QueryFragment, Debug, Clone, Serialize, Tsify)] +#[serde(rename_all = "camelCase")] #[cynic(graphql_type = "TradeEvent")] pub struct SgTradeEvent { + #[serde(rename = "__typename")] + pub __typename: String, pub transaction: SgTransaction, pub sender: SgBytes, + pub trades: Vec, +} + +#[derive(cynic::QueryFragment, Debug, Clone, Serialize, Tsify)] +#[serde(rename_all = "camelCase")] +#[cynic(graphql_type = "Trade")] +pub struct SgSiblingTrade { + pub id: SgBytes, + pub order: SgSiblingTradeOrder, +} + +#[derive(cynic::QueryFragment, Debug, Clone, Serialize, Tsify)] +#[serde(rename_all = "camelCase")] +#[cynic(graphql_type = "Order")] +pub struct SgSiblingTradeOrder { + pub id: SgBytes, + pub order_hash: SgBytes, + pub owner: SgBytes, } #[derive(cynic::QueryFragment, Debug, Clone, Serialize, Tsify)] @@ -811,4 +832,6 @@ mod impls { impl_wasm_traits!(SgTradeEvent); impl_wasm_traits!(SgTradeEventTypename); impl_wasm_traits!(SgTradeRef); + impl_wasm_traits!(SgSiblingTrade); + impl_wasm_traits!(SgSiblingTradeOrder); } diff --git a/crates/subgraph/src/types/impls.rs b/crates/subgraph/src/types/impls.rs index 5013ce50c9..b0bd76ac00 100644 --- a/crates/subgraph/src/types/impls.rs +++ b/crates/subgraph/src/types/impls.rs @@ -226,6 +226,7 @@ mod tests { SgTrade { id: SgBytes("".to_string()), trade_event: SgTradeEvent { + __typename: "TakeOrder".to_string(), transaction: SgTransaction { id: SgBytes("".to_string()), from: SgBytes("".to_string()), @@ -233,6 +234,7 @@ mod tests { timestamp: SgBigInt("".to_string()), }, sender: SgBytes("".to_string()), + trades: vec![], }, output_vault_balance_change: output_trade_vault_balance_change, input_vault_balance_change: input_trade_vault_balance_change, diff --git a/crates/subgraph/tests/snapshots/order_trade_test__vaults_query_gql_output.snap b/crates/subgraph/tests/snapshots/order_trade_test__vaults_query_gql_output.snap index b3d715895f..58275926b5 100644 --- a/crates/subgraph/tests/snapshots/order_trade_test__vaults_query_gql_output.snap +++ b/crates/subgraph/tests/snapshots/order_trade_test__vaults_query_gql_output.snap @@ -7,6 +7,7 @@ query SgOrderTradeDetailQuery($id: ID!) { trade(id: $id) { id tradeEvent { + __typename transaction { id from @@ -14,6 +15,14 @@ query SgOrderTradeDetailQuery($id: ID!) { timestamp } sender + trades { + id + order { + id + orderHash + owner + } + } } outputVaultBalanceChange { id diff --git a/crates/subgraph/tests/snapshots/order_trades_test__vaults_query_gql_output.snap b/crates/subgraph/tests/snapshots/order_trades_test__vaults_query_gql_output.snap index c2c056004d..2499cb5acd 100644 --- a/crates/subgraph/tests/snapshots/order_trades_test__vaults_query_gql_output.snap +++ b/crates/subgraph/tests/snapshots/order_trades_test__vaults_query_gql_output.snap @@ -7,6 +7,7 @@ query SgOrderTradesListQuery($first: Int, $id: Bytes!, $skip: Int, $timestampGte trades(skip: $skip, first: $first, orderBy: timestamp, orderDirection: desc, where: {order_: {id: $id}, timestamp_gte: $timestampGte, timestamp_lte: $timestampLte}) { id tradeEvent { + __typename transaction { id from @@ -14,6 +15,14 @@ query SgOrderTradesListQuery($first: Int, $id: Bytes!, $skip: Int, $timestampGte timestamp } sender + trades { + id + order { + id + orderHash + owner + } + } } outputVaultBalanceChange { id diff --git a/packages/orderbook/test/js_api/raindexClient.test.ts b/packages/orderbook/test/js_api/raindexClient.test.ts index 1cebdae492..e7d844fa5e 100644 --- a/packages/orderbook/test/js_api/raindexClient.test.ts +++ b/packages/orderbook/test/js_api/raindexClient.test.ts @@ -388,13 +388,15 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Raindex Client', async f id: '0x07db8b3f3e7498f9d4d0e40b98f57c020d3d277516e86023a8200a20464d4895', timestamp: '1632000000', tradeEvent: { + __typename: 'TakeOrder', sender: '0x0000000000000000000000000000000000000000', transaction: { id: BYTES32_ZERO, from: '0x0000000000000000000000000000000000000000', timestamp: '1632000000', blockNumber: '0' - } + }, + trades: [] }, outputVaultBalanceChange: { amount: float100, @@ -465,13 +467,15 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Raindex Client', async f id: '0x07db8b3f3e7498f9d4d0e40b98f57c020d3d277516e86023a8200a20464d4894', timestamp: '1632000000', tradeEvent: { + __typename: 'TakeOrder', sender: '0x0000000000000000000000000000000000000000', transaction: { id: BYTES32_ZERO, from: '0x0000000000000000000000000000000000000000', timestamp: '1632000000', blockNumber: '0' - } + }, + trades: [] }, outputVaultBalanceChange: { amount: '0x0000000000000000000000000000000000000000000000000000000000000001', @@ -544,13 +548,15 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Raindex Client', async f orderHash: BYTES32_0123 }, tradeEvent: { + __typename: 'TakeOrder', sender: '0x0000000000000000000000000000000000000000', transaction: { id: BYTES32_0123, from: '0x0000000000000000000000000000000000000000', blockNumber: '0', timestamp: '0' - } + }, + trades: [] }, timestamp: '0', orderbook: { @@ -1036,6 +1042,12 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Raindex Client', async f result[0].transaction.timestamp, BigInt(mockOrderTradesList[0].tradeEvent.transaction.timestamp) ); + assert.equal(result[0].tradeEventType, 'TakeOrder'); + assert.equal( + result[0].counterparty.owner, + mockOrderTradesList[0].tradeEvent.sender + ); + assert.equal(result[0].counterparty.orderHash, undefined); }); it('should get trade detail', async function () { @@ -1121,6 +1133,9 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Raindex Client', async f BigInt(mockTrade.tradeEvent.transaction.timestamp) ); assert.equal(result.orderbook, mockTrade.orderbook.id.toLowerCase()); + assert.equal(result.tradeEventType, 'TakeOrder'); + assert.equal(result.counterparty.owner, mockTrade.tradeEvent.sender); + assert.equal(result.counterparty.orderHash, undefined); }); it('should get trade count', async function () { diff --git a/packages/ui-components/src/__tests__/OrderTradesListTable.test.ts b/packages/ui-components/src/__tests__/OrderTradesListTable.test.ts index 96c224e8dd..6f42fc34b4 100644 --- a/packages/ui-components/src/__tests__/OrderTradesListTable.test.ts +++ b/packages/ui-components/src/__tests__/OrderTradesListTable.test.ts @@ -68,7 +68,12 @@ const mockTradeOrdersList: RaindexTrade[] = [ }, orderbook: '0x1' }, - orderbook: '0x00' + orderbook: '0x00', + tradeEventType: 'TakeOrder', + counterparty: { + owner: '0xsender_address', + orderHash: undefined + } }, { id: '2', @@ -132,7 +137,12 @@ const mockTradeOrdersList: RaindexTrade[] = [ }, orderbook: '0x1' }, - orderbook: '0x00' + orderbook: '0x00', + tradeEventType: 'TakeOrder', + counterparty: { + owner: '0xsender_address', + orderHash: undefined + } } ] as unknown as RaindexTrade[]; @@ -140,6 +150,8 @@ vi.mock('@tanstack/svelte-query'); const mockOrder: RaindexOrder = { id: '1', + chainId: BigInt(1), + orderbook: '0x00', getTradeCount: vi.fn(), getTradesList: vi.fn() } as unknown as RaindexOrder; @@ -388,7 +400,12 @@ const createMockTrade = (id: string, inputAmount: string, outputAmount: string): }, orderbook: '0x1' }, - orderbook: '0x00' + orderbook: '0x00', + tradeEventType: 'TakeOrder', + counterparty: { + owner: '0xsender_address', + orderHash: undefined + } }) as unknown as RaindexTrade; test('displays dash when output amount is zero (prevents division by zero)', async () => { @@ -480,3 +497,114 @@ test('displays dash when both amounts are zero', async () => { expect(ioRatioCell).not.toHaveTextContent('NaN'); }); }); + +test('renders TakeOrder trade type badge and Taker label', async () => { + const queryClient = new QueryClient(); + + const mockQuery = vi.mocked(await import('@tanstack/svelte-query')); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + mockQuery.createInfiniteQuery = vi.fn((__options, _queryClient) => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + subscribe: (fn: (value: any) => void) => { + fn({ + data: { pages: [mockTradeOrdersList] }, + status: 'success', + isFetching: false, + isFetched: true + }); + return { unsubscribe: () => {} }; + } + })) as Mock; + + render(OrderTradesListTable, { + context: new Map([['$$_queryClient', queryClient]]), + props: { order: mockOrder, rpcs: ['https://example.com'] } + }); + + await waitFor(async () => { + const rows = screen.getAllByTestId('bodyRow'); + rows.forEach((row) => { + expect(row).toHaveTextContent('Take order'); + expect(row).toHaveTextContent('Taker:'); + }); + }); +}); + +test('renders Clear trade with counterparty info', async () => { + const queryClient = new QueryClient(); + const mockClearTrades = [ + { + ...mockTradeOrdersList[0], + id: '3', + tradeEventType: 'Clear', + counterparty: { + owner: '0xcounterparty_owner', + orderHash: '0xcounterparty_order_hash' + } + } + ] as unknown as RaindexTrade[]; + + const mockQuery = vi.mocked(await import('@tanstack/svelte-query')); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + mockQuery.createInfiniteQuery = vi.fn((__options, _queryClient) => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + subscribe: (fn: (value: any) => void) => { + fn({ + data: { pages: [mockClearTrades] }, + status: 'success', + isFetching: false, + isFetched: true + }); + return { unsubscribe: () => {} }; + } + })) as Mock; + + render(OrderTradesListTable, { + context: new Map([['$$_queryClient', queryClient]]), + props: { order: mockOrder, rpcs: ['https://example.com'] } + }); + + await waitFor(async () => { + const rows = screen.getAllByTestId('bodyRow'); + expect(rows[0]).toHaveTextContent('Clear'); + expect(rows[0]).toHaveTextContent('Counterparty:'); + expect(rows[0]).toHaveTextContent('Order:'); + }); +}); + +test('renders dash when counterparty is missing', async () => { + const queryClient = new QueryClient(); + const mockNoCounterpartyTrades = [ + { + ...mockTradeOrdersList[0], + id: '4', + tradeEventType: 'Clear', + counterparty: undefined + } + ] as unknown as RaindexTrade[]; + + const mockQuery = vi.mocked(await import('@tanstack/svelte-query')); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + mockQuery.createInfiniteQuery = vi.fn((__options, _queryClient) => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + subscribe: (fn: (value: any) => void) => { + fn({ + data: { pages: [mockNoCounterpartyTrades] }, + status: 'success', + isFetching: false, + isFetched: true + }); + return { unsubscribe: () => {} }; + } + })) as Mock; + + render(OrderTradesListTable, { + context: new Map([['$$_queryClient', queryClient]]), + props: { order: mockOrder, rpcs: ['https://example.com'] } + }); + + await waitFor(async () => { + const rows = screen.getAllByTestId('bodyRow'); + expect(rows[0]).toHaveTextContent('-'); + }); +}); diff --git a/packages/ui-components/src/lib/components/tables/OrderTradesListTable.svelte b/packages/ui-components/src/lib/components/tables/OrderTradesListTable.svelte index 799745108f..2dc5743cb6 100644 --- a/packages/ui-components/src/lib/components/tables/OrderTradesListTable.svelte +++ b/packages/ui-components/src/lib/components/tables/OrderTradesListTable.svelte @@ -3,7 +3,7 @@ import TanstackAppTable from '../TanstackAppTable.svelte'; import { QKEY_ORDER_TRADES_LIST } from '../../queries/keys'; import { DEFAULT_PAGE_SIZE } from '../../queries/constants'; - import { TableBodyCell, TableHeadCell } from 'flowbite-svelte'; + import { Badge, TableBodyCell, TableHeadCell } from 'flowbite-svelte'; import { formatTimestampSecondsAsLocal } from '../../services/time'; import Hash, { HashType } from '../Hash.svelte'; import { BugOutline } from 'flowbite-svelte-icons'; @@ -11,6 +11,20 @@ import TableTimeFilters from '../charts/TableTimeFilters.svelte'; import Tooltip from '../Tooltip.svelte'; + type BadgeColor = 'blue' | 'pink' | 'dark'; + + function getTypeBadgeColor(type: string): BadgeColor { + const lowerType = type.toLowerCase(); + if (lowerType.includes('clear')) return 'pink'; + if (lowerType.includes('trade') || lowerType.includes('takeorder')) return 'blue'; + return 'dark'; + } + + function getTradeTypeDisplayName(type: string): string { + if (type === 'Clear') return 'Clear'; + return 'Take order'; + } + export let order: RaindexOrder; export let rpcs: string[] | undefined = undefined; export let handleDebugTradeModal: ((hash: string, rpcs: string[]) => void) | undefined = @@ -76,11 +90,12 @@ - Date - Transaction - Input - Output - IO Ratio + Info + Transaction + Counterparty + Input + Output + IO Ratio Actions @@ -91,7 +106,16 @@ {@const oiRatio = Math.abs(outputAmt / inputAmt)} {@const validRatio = Number.isFinite(ioRatio) && Number.isFinite(oiRatio)} - {formatTimestampSecondsAsLocal(BigInt(item.timestamp))} +
+
+ + {getTradeTypeDisplayName(item.tradeEventType)} + +
+ + {formatTimestampSecondsAsLocal(BigInt(item.timestamp))} + +
@@ -105,6 +129,35 @@
+ + {#if item.counterparty} +
+
+ + {item.tradeEventType === 'Clear' ? 'Counterparty:' : 'Taker:'} + + +
+ {#if item.counterparty.orderHash} +
+ Order: + + + +
+ {/if} +
+ {:else} + - + {/if} +
{item.inputVaultBalanceChange.token.symbol}