Skip to main content

cow_rs/subgraph/
types.rs

1//! Response types for the `CoW` Protocol subgraph.
2//!
3//! All types in this module derive `Serialize` and `Deserialize` with
4//! `camelCase` field names to match the `GraphQL` response format. String
5//! fields are used for large integers (volume, amounts) to avoid overflow.
6//!
7//! # Key types
8//!
9//! | Type | Represents |
10//! |---|---|
11//! | [`Totals`] | Protocol-wide aggregate statistics |
12//! | [`DailyVolume`] / [`HourlyVolume`] | Volume snapshots |
13//! | [`DailyTotal`] / [`HourlyTotal`] | Full per-period statistics |
14//! | [`Token`] | An ERC-20 token indexed by the subgraph |
15//! | [`Trade`] | A single trade within a settlement |
16//! | [`Order`] | An order indexed by the subgraph |
17//! | [`Settlement`] | An on-chain batch settlement |
18//! | [`Pair`] | A trading pair (token0/token1) |
19//! | [`Bundle`] | Current ETH/USD price |
20//! | [`User`] | A trader address with aggregate stats |
21
22use std::fmt;
23
24use serde::{Deserialize, Serialize};
25
26// ── Aggregate stats ───────────────────────────────────────────────────────────
27
28/// Protocol-wide aggregate statistics.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct Totals {
32    /// Number of distinct ERC-20 tokens seen.
33    pub tokens: String,
34    /// Total number of orders.
35    pub orders: String,
36    /// Total number of unique traders.
37    pub traders: String,
38    /// Total number of on-chain batch settlements.
39    pub settlements: String,
40    /// Cumulative volume in USD.
41    pub volume_usd: String,
42    /// Cumulative volume in ETH.
43    pub volume_eth: String,
44    /// Cumulative fees collected in USD.
45    pub fees_usd: String,
46    /// Cumulative fees collected in ETH.
47    pub fees_eth: String,
48}
49
50impl fmt::Display for Totals {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        write!(f, "totals(orders={}, traders={})", self.orders, self.traders)
53    }
54}
55
56/// Per-day volume snapshot.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58#[serde(rename_all = "camelCase")]
59pub struct DailyVolume {
60    /// Unix timestamp (start of day, UTC).
61    pub timestamp: String,
62    /// USD volume for this day.
63    pub volume_usd: String,
64}
65
66impl fmt::Display for DailyVolume {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        write!(f, "daily-vol(ts={}, ${})", self.timestamp, self.volume_usd)
69    }
70}
71
72/// Per-hour volume snapshot.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub struct HourlyVolume {
76    /// Unix timestamp (start of hour, UTC).
77    pub timestamp: String,
78    /// USD volume for this hour.
79    pub volume_usd: String,
80}
81
82impl fmt::Display for HourlyVolume {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        write!(f, "hourly-vol(ts={}, ${})", self.timestamp, self.volume_usd)
85    }
86}
87
88// ── DailyTotal / HourlyTotal (full schema entities) ───────────────────────────
89
90/// Full per-day protocol statistics entity from the subgraph.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct DailyTotal {
94    /// Start-of-day Unix timestamp.
95    pub timestamp: String,
96    /// Total orders settled this day.
97    pub orders: String,
98    /// Unique traders active this day.
99    pub traders: String,
100    /// Unique tokens traded this day.
101    pub tokens: String,
102    /// Number of batch settlements this day.
103    pub settlements: String,
104    /// Total volume in ETH.
105    pub volume_eth: String,
106    /// Total volume in USD.
107    pub volume_usd: String,
108    /// Fees collected in ETH.
109    pub fees_eth: String,
110    /// Fees collected in USD.
111    pub fees_usd: String,
112}
113
114impl fmt::Display for DailyTotal {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        write!(f, "daily-total(ts={}, orders={})", self.timestamp, self.orders)
117    }
118}
119
120/// Full per-hour protocol statistics entity from the subgraph.
121#[derive(Debug, Clone, Serialize, Deserialize)]
122#[serde(rename_all = "camelCase")]
123pub struct HourlyTotal {
124    /// Start-of-hour Unix timestamp.
125    pub timestamp: String,
126    /// Total orders settled this hour.
127    pub orders: String,
128    /// Unique traders active this hour.
129    pub traders: String,
130    /// Unique tokens traded this hour.
131    pub tokens: String,
132    /// Number of batch settlements this hour.
133    pub settlements: String,
134    /// Total volume in ETH.
135    pub volume_eth: String,
136    /// Total volume in USD.
137    pub volume_usd: String,
138    /// Fees collected in ETH.
139    pub fees_eth: String,
140    /// Fees collected in USD.
141    pub fees_usd: String,
142}
143
144impl fmt::Display for HourlyTotal {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        write!(f, "hourly-total(ts={}, orders={})", self.timestamp, self.orders)
147    }
148}
149
150// ── Token ─────────────────────────────────────────────────────────────────────
151
152/// An ERC-20 token indexed by the subgraph.
153#[derive(Debug, Clone, Serialize, Deserialize)]
154#[serde(rename_all = "camelCase")]
155pub struct Token {
156    /// Subgraph entity ID (token address in lowercase hex).
157    pub id: String,
158    /// Checksummed ERC-20 contract address.
159    pub address: String,
160    /// Unix timestamp of the first trade involving this token.
161    pub first_trade_timestamp: String,
162    /// Token name from ERC-20 metadata.
163    pub name: String,
164    /// Token symbol from ERC-20 metadata.
165    pub symbol: String,
166    /// Decimal places (ERC-20 `decimals()`).
167    pub decimals: String,
168    /// Cumulative volume traded (in token units).
169    pub total_volume: String,
170    /// Current price in ETH.
171    pub price_eth: String,
172    /// Current price in USD.
173    pub price_usd: String,
174    /// Total number of trades involving this token.
175    pub number_of_trades: String,
176}
177
178impl Token {
179    /// Returns the token symbol as a string slice.
180    ///
181    /// # Returns
182    ///
183    /// A `&str` referencing the token's symbol field.
184    #[must_use]
185    pub fn symbol_ref(&self) -> &str {
186        &self.symbol
187    }
188
189    /// Returns the token address as a string slice.
190    ///
191    /// # Returns
192    ///
193    /// A `&str` referencing the token's address field.
194    #[must_use]
195    pub fn address_ref(&self) -> &str {
196        &self.address
197    }
198}
199
200impl fmt::Display for Token {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        write!(f, "{} ({})", self.symbol, self.address)
203    }
204}
205
206/// Per-day volume and price statistics for a specific token.
207#[derive(Debug, Clone, Serialize, Deserialize)]
208#[serde(rename_all = "camelCase")]
209pub struct TokenDailyTotal {
210    /// Entity ID: `{tokenAddress}-{dayTimestamp}`.
211    pub id: String,
212    /// Token this snapshot belongs to.
213    pub token: Token,
214    /// Start-of-day Unix timestamp.
215    pub timestamp: String,
216    /// Volume traded in token units.
217    pub total_volume: String,
218    /// Volume in USD.
219    pub total_volume_usd: String,
220    /// Total number of trades.
221    pub total_trades: String,
222    /// Opening price in USD.
223    pub open_price: String,
224    /// Closing price in USD.
225    pub close_price: String,
226    /// Highest price in USD.
227    pub higher_price: String,
228    /// Lowest price in USD.
229    pub lower_price: String,
230    /// Volume-weighted average price in USD.
231    pub average_price: String,
232}
233
234impl fmt::Display for TokenDailyTotal {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        write!(f, "token-daily({}, ts={})", self.token, self.timestamp)
237    }
238}
239
240/// Per-hour volume and price statistics for a specific token.
241#[derive(Debug, Clone, Serialize, Deserialize)]
242#[serde(rename_all = "camelCase")]
243pub struct TokenHourlyTotal {
244    /// Entity ID: `{tokenAddress}-{hourTimestamp}`.
245    pub id: String,
246    /// Token this snapshot belongs to.
247    pub token: Token,
248    /// Start-of-hour Unix timestamp.
249    pub timestamp: String,
250    /// Volume traded in token units.
251    pub total_volume: String,
252    /// Volume in USD.
253    pub total_volume_usd: String,
254    /// Total number of trades.
255    pub total_trades: String,
256    /// Opening price in USD.
257    pub open_price: String,
258    /// Closing price in USD.
259    pub close_price: String,
260    /// Highest price in USD.
261    pub higher_price: String,
262    /// Lowest price in USD.
263    pub lower_price: String,
264    /// Volume-weighted average price in USD.
265    pub average_price: String,
266}
267
268impl fmt::Display for TokenHourlyTotal {
269    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270        write!(f, "token-hourly({}, ts={})", self.token, self.timestamp)
271    }
272}
273
274/// A price-changing event for a token (used to reconstruct OHLC data).
275#[derive(Debug, Clone, Serialize, Deserialize)]
276#[serde(rename_all = "camelCase")]
277pub struct TokenTradingEvent {
278    /// Entity ID.
279    pub id: String,
280    /// Token this event belongs to.
281    pub token: Token,
282    /// USD price at this event.
283    pub price_usd: String,
284    /// Event timestamp.
285    pub timestamp: String,
286}
287
288impl fmt::Display for TokenTradingEvent {
289    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290        write!(f, "trade-event({}, ts={}, price=${})", self.token, self.timestamp, self.price_usd)
291    }
292}
293
294// ── User ──────────────────────────────────────────────────────────────────────
295
296/// A trader address indexed by the subgraph.
297#[derive(Debug, Clone, Serialize, Deserialize)]
298#[serde(rename_all = "camelCase")]
299pub struct User {
300    /// Subgraph entity ID (address in lowercase hex).
301    pub id: String,
302    /// Trader address.
303    pub address: String,
304    /// Unix timestamp of the first trade.
305    pub first_trade_timestamp: String,
306    /// Total number of trades executed.
307    pub number_of_trades: String,
308    /// Cumulative volume of tokens sold, measured in ETH.
309    pub solved_amount_eth: String,
310    /// Cumulative volume of tokens sold, measured in USD.
311    pub solved_amount_usd: String,
312}
313
314impl fmt::Display for User {
315    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
316        f.write_str(&self.address)
317    }
318}
319
320// ── Settlement ────────────────────────────────────────────────────────────────
321
322/// An on-chain batch settlement transaction.
323#[derive(Debug, Clone, Serialize, Deserialize)]
324#[serde(rename_all = "camelCase")]
325pub struct Settlement {
326    /// Entity ID (transaction hash).
327    pub id: String,
328    /// Transaction hash.
329    pub tx_hash: String,
330    /// Timestamp of the first trade in this settlement.
331    pub first_trade_timestamp: String,
332    /// Solver address that submitted the settlement.
333    pub solver: String,
334    /// Gas cost of the settlement transaction.
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub tx_cost: Option<String>,
337    /// Transaction fee paid in ETH.
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub tx_fee_in_eth: Option<String>,
340}
341
342impl Settlement {
343    /// Returns `true` if a gas-cost estimate is available for this settlement.
344    ///
345    /// # Returns
346    ///
347    /// `true` if `tx_cost` is `Some`.
348    #[must_use]
349    pub const fn has_gas_cost(&self) -> bool {
350        self.tx_cost.is_some()
351    }
352
353    /// Returns `true` if a transaction fee (in ETH) is available for this settlement.
354    ///
355    /// # Returns
356    ///
357    /// `true` if `tx_fee_in_eth` is `Some`.
358    #[must_use]
359    pub const fn has_tx_fee(&self) -> bool {
360        self.tx_fee_in_eth.is_some()
361    }
362}
363
364impl fmt::Display for Settlement {
365    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
366        write!(f, "settlement({})", self.tx_hash)
367    }
368}
369
370// ── Trade ─────────────────────────────────────────────────────────────────────
371
372/// A single trade executed within a batch settlement.
373#[derive(Debug, Clone, Serialize, Deserialize)]
374#[serde(rename_all = "camelCase")]
375pub struct Trade {
376    /// Entity ID: `{txHash}-{tradeIndex}`.
377    pub id: String,
378    /// Trade execution timestamp.
379    pub timestamp: String,
380    /// Gas price of the settlement transaction.
381    pub gas_price: String,
382    /// Protocol fee collected for this trade (in sell token).
383    pub fee_amount: String,
384    /// Settlement transaction hash.
385    pub tx_hash: String,
386    /// ID of the [`Settlement`] containing this trade.
387    pub settlement: String,
388    /// Amount of buy token received.
389    pub buy_amount: String,
390    /// Amount of sell token used (after fee).
391    pub sell_amount: String,
392    /// Amount of sell token before the fee was deducted.
393    pub sell_amount_before_fees: String,
394    /// Buy token.
395    pub buy_token: Token,
396    /// Sell token.
397    pub sell_token: Token,
398    /// Trader that placed the order.
399    pub owner: User,
400    /// ID of the [`Order`] this trade fills.
401    pub order: String,
402}
403
404impl Trade {
405    /// Returns `true` if a settlement transaction hash is available for this trade.
406    ///
407    /// # Returns
408    ///
409    /// `true` if the `tx_hash` field is non-empty.
410    #[must_use]
411    pub const fn has_tx_hash(&self) -> bool {
412        !self.tx_hash.is_empty()
413    }
414}
415
416impl fmt::Display for Trade {
417    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418        write!(f, "trade({} {} → {})", self.tx_hash, self.sell_token, self.buy_token)
419    }
420}
421
422// ── Order ─────────────────────────────────────────────────────────────────────
423
424/// An order indexed by the subgraph.
425#[derive(Debug, Clone, Serialize, Deserialize)]
426#[serde(rename_all = "camelCase")]
427pub struct Order {
428    /// Entity ID (order UID hex).
429    pub id: String,
430    /// Order owner / trader.
431    pub owner: User,
432    /// Token to sell.
433    pub sell_token: Token,
434    /// Token to buy.
435    pub buy_token: Token,
436    /// Optional receiver address (if different from owner).
437    #[serde(skip_serializing_if = "Option::is_none")]
438    pub receiver: Option<String>,
439    /// Total sell amount.
440    pub sell_amount: String,
441    /// Total buy amount (minimum for sell orders).
442    pub buy_amount: String,
443    /// Order validity deadline (Unix timestamp).
444    pub valid_to: String,
445    /// App-data hash (hex).
446    pub app_data: String,
447    /// Protocol fee amount.
448    pub fee_amount: String,
449    /// Order kind: `"sell"` or `"buy"`.
450    pub kind: String,
451    /// Whether the order can be partially filled.
452    pub partially_fillable: bool,
453    /// Order status: `"open"`, `"filled"`, `"cancelled"`, or `"expired"`.
454    pub status: String,
455    /// Cumulative sell amount executed so far.
456    pub executed_sell_amount: String,
457    /// Executed sell amount before fees.
458    pub executed_sell_amount_before_fees: String,
459    /// Cumulative buy amount executed so far.
460    pub executed_buy_amount: String,
461    /// Cumulative fee amount executed so far.
462    pub executed_fee_amount: String,
463    /// Timestamp of order cancellation (if any).
464    #[serde(skip_serializing_if = "Option::is_none")]
465    pub invalidate_timestamp: Option<String>,
466    /// Order creation timestamp.
467    pub timestamp: String,
468    /// Transaction hash of the first fill.
469    pub tx_hash: String,
470    /// Whether the signer is a smart contract (`EIP-1271`).
471    pub is_signer_safe: bool,
472    /// Signing scheme used (e.g. `"eip712"`, `"ethsign"`, `"eip1271"`).
473    pub signing_scheme: String,
474    /// The full order UID (bytes).
475    pub uid: String,
476    /// Surplus generated by this order.
477    #[serde(skip_serializing_if = "Option::is_none")]
478    pub surplus: Option<String>,
479}
480
481impl Order {
482    /// Returns `true` if this is a sell order (`kind == "sell"`).
483    ///
484    /// # Returns
485    ///
486    /// `true` if the order kind is `"sell"`.
487    #[must_use]
488    pub fn is_sell(&self) -> bool {
489        self.kind == "sell"
490    }
491
492    /// Returns `true` if this is a buy order (`kind == "buy"`).
493    ///
494    /// # Returns
495    ///
496    /// `true` if the order kind is `"buy"`.
497    #[must_use]
498    pub fn is_buy(&self) -> bool {
499        self.kind == "buy"
500    }
501
502    /// Returns `true` if the order status is `"open"`.
503    ///
504    /// # Returns
505    ///
506    /// `true` if the order status is `"open"`.
507    #[must_use]
508    pub fn is_open(&self) -> bool {
509        self.status == "open"
510    }
511
512    /// Returns `true` if the order status is `"filled"`.
513    ///
514    /// # Returns
515    ///
516    /// `true` if the order status is `"filled"`.
517    #[must_use]
518    pub fn is_filled(&self) -> bool {
519        self.status == "filled"
520    }
521
522    /// Returns `true` if the order status is `"cancelled"`.
523    ///
524    /// # Returns
525    ///
526    /// `true` if the order status is `"cancelled"`.
527    #[must_use]
528    pub fn is_cancelled(&self) -> bool {
529        self.status == "cancelled"
530    }
531
532    /// Returns `true` if the order status is `"expired"`.
533    ///
534    /// # Returns
535    ///
536    /// `true` if the order status is `"expired"`.
537    #[must_use]
538    pub fn is_expired(&self) -> bool {
539        self.status == "expired"
540    }
541
542    /// Returns `true` if the order is in a terminal state (filled, cancelled, or expired).
543    ///
544    /// # Returns
545    ///
546    /// `true` if the order is filled, cancelled, or expired.
547    #[must_use]
548    pub fn is_terminal(&self) -> bool {
549        self.is_filled() || self.is_cancelled() || self.is_expired()
550    }
551
552    /// Returns `true` if a custom receiver address is set (differs from owner).
553    ///
554    /// # Returns
555    ///
556    /// `true` if `receiver` is `Some`.
557    #[must_use]
558    pub const fn has_receiver(&self) -> bool {
559        self.receiver.is_some()
560    }
561
562    /// Returns `true` if a cancellation/invalidation timestamp is recorded.
563    ///
564    /// # Returns
565    ///
566    /// `true` if `invalidate_timestamp` is `Some`.
567    #[must_use]
568    pub const fn has_invalidate_timestamp(&self) -> bool {
569        self.invalidate_timestamp.is_some()
570    }
571
572    /// Returns `true` if a surplus value is available for this order.
573    ///
574    /// # Returns
575    ///
576    /// `true` if `surplus` is `Some`.
577    #[must_use]
578    pub const fn has_surplus(&self) -> bool {
579        self.surplus.is_some()
580    }
581
582    /// Returns `true` if the order may be partially filled.
583    ///
584    /// # Returns
585    ///
586    /// `true` if the `partially_fillable` flag is set.
587    #[must_use]
588    pub const fn is_partially_fillable(&self) -> bool {
589        self.partially_fillable
590    }
591
592    /// Returns `true` if the order signer is a smart contract (`EIP-1271`).
593    ///
594    /// # Returns
595    ///
596    /// `true` if the `is_signer_safe` flag is set.
597    #[must_use]
598    pub const fn is_signer_safe(&self) -> bool {
599        self.is_signer_safe
600    }
601}
602
603impl fmt::Display for Order {
604    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
605        let short_uid = if self.uid.len() > 10 { &self.uid[..10] } else { &self.uid };
606        write!(f, "order({short_uid}… {} {})", self.kind, self.status)
607    }
608}
609
610// ── Pair ──────────────────────────────────────────────────────────────────────
611
612/// A trading pair (two tokens) indexed by the subgraph.
613#[derive(Debug, Clone, Serialize, Deserialize)]
614#[serde(rename_all = "camelCase")]
615pub struct Pair {
616    /// Entity ID: `{token0Address}-{token1Address}`.
617    pub id: String,
618    /// First token (lower address).
619    pub token0: Token,
620    /// Second token (higher address).
621    pub token1: Token,
622    /// Total volume in token0 units.
623    pub volume_token0: String,
624    /// Total volume in token1 units.
625    pub volume_token1: String,
626    /// Total number of trades for this pair.
627    pub number_of_trades: String,
628}
629
630impl fmt::Display for Pair {
631    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
632        write!(f, "pair({}/{})", self.token0, self.token1)
633    }
634}
635
636/// Per-day statistics for a token pair.
637#[derive(Debug, Clone, Serialize, Deserialize)]
638#[serde(rename_all = "camelCase")]
639pub struct PairDaily {
640    /// Entity ID: `{token0Address}-{token1Address}-{dayTimestamp}`.
641    pub id: String,
642    /// First token in the pair (lower address).
643    pub token0: Token,
644    /// Second token in the pair (higher address).
645    pub token1: Token,
646    /// Start-of-day Unix timestamp.
647    pub timestamp: String,
648    /// Volume in token0 units for this day.
649    pub volume_token0: String,
650    /// Volume in token1 units for this day.
651    pub volume_token1: String,
652    /// Number of trades this day.
653    pub number_of_trades: String,
654}
655
656impl fmt::Display for PairDaily {
657    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
658        write!(f, "pair-daily({}/{}, ts={})", self.token0, self.token1, self.timestamp)
659    }
660}
661
662/// Per-hour statistics for a token pair.
663#[derive(Debug, Clone, Serialize, Deserialize)]
664#[serde(rename_all = "camelCase")]
665pub struct PairHourly {
666    /// Entity ID: `{token0Address}-{token1Address}-{hourTimestamp}`.
667    pub id: String,
668    /// First token in the pair.
669    pub token0: Token,
670    /// Second token in the pair.
671    pub token1: Token,
672    /// Start-of-hour Unix timestamp.
673    pub timestamp: String,
674    /// Volume in token0 units.
675    pub volume_token0: String,
676    /// Volume in token1 units.
677    pub volume_token1: String,
678    /// Number of trades this hour.
679    pub number_of_trades: String,
680}
681
682impl fmt::Display for PairHourly {
683    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
684        write!(f, "pair-hourly({}/{}, ts={})", self.token0, self.token1, self.timestamp)
685    }
686}
687
688// ── Bundle ────────────────────────────────────────────────────────────────────
689
690/// Aggregate price bundle — contains the current ETH/USD price.
691///
692/// The subgraph maintains a single `Bundle` entity with `id = "1"`.
693#[derive(Debug, Clone, Serialize, Deserialize)]
694#[serde(rename_all = "camelCase")]
695pub struct Bundle {
696    /// Always `"1"` (singleton entity).
697    pub id: String,
698    /// Current ETH price in USD.
699    pub eth_price_usd: String,
700}
701
702impl Bundle {
703    /// Returns the current ETH/USD price as a string slice.
704    ///
705    /// # Returns
706    ///
707    /// A `&str` referencing the `eth_price_usd` field.
708    #[must_use]
709    pub fn eth_price_usd_ref(&self) -> &str {
710        &self.eth_price_usd
711    }
712}
713
714impl fmt::Display for Bundle {
715    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
716        write!(f, "eth-price=${}", self.eth_price_usd)
717    }
718}
719
720// ── Total (singleton accumulator) ────────────────────────────────────────────
721
722/// Protocol-wide singleton accumulator entity from the subgraph.
723///
724/// Mirrors the `Total` `GraphQL` type. Unlike [`Totals`] (which is the
725/// flattened query-response shape), this type matches the full subgraph
726/// entity including its `id` field.
727#[derive(Debug, Clone, Serialize, Deserialize)]
728#[serde(rename_all = "camelCase")]
729pub struct Total {
730    /// Singleton entity ID (always `"1"`).
731    pub id: String,
732    /// Total number of orders placed.
733    pub orders: String,
734    /// Total number of batch settlements.
735    pub settlements: String,
736    /// Total number of distinct tokens traded.
737    pub tokens: String,
738    /// Total number of unique traders.
739    pub traders: String,
740    /// Total number of trades executed.
741    pub number_of_trades: String,
742    /// Cumulative volume in ETH.
743    #[serde(skip_serializing_if = "Option::is_none")]
744    pub volume_eth: Option<String>,
745    /// Cumulative volume in USD.
746    #[serde(skip_serializing_if = "Option::is_none")]
747    pub volume_usd: Option<String>,
748    /// Cumulative fees in ETH.
749    #[serde(skip_serializing_if = "Option::is_none")]
750    pub fees_eth: Option<String>,
751    /// Cumulative fees in USD.
752    #[serde(skip_serializing_if = "Option::is_none")]
753    pub fees_usd: Option<String>,
754}
755
756impl fmt::Display for Total {
757    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
758        write!(f, "total(orders={}, traders={})", self.orders, self.traders)
759    }
760}
761
762// ── UniswapToken ─────────────────────────────────────────────────────────────
763
764/// A Uniswap token entity indexed by the `CoW` Protocol subgraph.
765///
766/// Mirrors the `UniswapToken` `GraphQL` type.
767#[derive(Debug, Clone, Serialize, Deserialize)]
768#[serde(rename_all = "camelCase")]
769pub struct UniswapToken {
770    /// Entity ID (token address hex).
771    pub id: String,
772    /// Token contract address (bytes).
773    pub address: String,
774    /// Token name from ERC-20 metadata.
775    pub name: String,
776    /// Token symbol from ERC-20 metadata.
777    pub symbol: String,
778    /// Decimal places (ERC-20 `decimals()`).
779    pub decimals: i32,
780    /// Derived price in ETH.
781    #[serde(skip_serializing_if = "Option::is_none")]
782    pub price_eth: Option<String>,
783    /// Derived price in USD.
784    #[serde(skip_serializing_if = "Option::is_none")]
785    pub price_usd: Option<String>,
786}
787
788impl fmt::Display for UniswapToken {
789    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
790        write!(f, "{} ({})", self.symbol, self.address)
791    }
792}
793
794// ── UniswapPool ──────────────────────────────────────────────────────────────
795
796/// A Uniswap pool entity indexed by the `CoW` Protocol subgraph.
797///
798/// Mirrors the `UniswapPool` `GraphQL` type.
799#[derive(Debug, Clone, Serialize, Deserialize)]
800#[serde(rename_all = "camelCase")]
801pub struct UniswapPool {
802    /// Pool contract address.
803    pub id: String,
804    /// In-range liquidity.
805    pub liquidity: String,
806    /// Current tick (may be absent for inactive pools).
807    #[serde(skip_serializing_if = "Option::is_none")]
808    pub tick: Option<String>,
809    /// First token in the pool.
810    pub token0: UniswapToken,
811    /// Price of token0 in terms of token1.
812    pub token0_price: String,
813    /// Second token in the pool.
814    pub token1: UniswapToken,
815    /// Price of token1 in terms of token0.
816    pub token1_price: String,
817    /// Total token0 locked across all ticks.
818    pub total_value_locked_token0: String,
819    /// Total token1 locked across all ticks.
820    pub total_value_locked_token1: String,
821}
822
823impl fmt::Display for UniswapPool {
824    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
825        write!(f, "pool({}, {}/{})", self.id, self.token0, self.token1)
826    }
827}
828
829// ── Subgraph block / meta ────────────────────────────────────────────────────
830
831/// Block information returned by the subgraph `_meta` field.
832///
833/// Mirrors the `_Block_` `GraphQL` type.
834#[derive(Debug, Clone, Serialize, Deserialize)]
835#[serde(rename_all = "camelCase")]
836pub struct SubgraphBlock {
837    /// Block hash (may be `None` if a block-number constraint was used).
838    #[serde(skip_serializing_if = "Option::is_none")]
839    pub hash: Option<String>,
840    /// Block number.
841    pub number: i64,
842    /// Parent block hash.
843    #[serde(skip_serializing_if = "Option::is_none")]
844    pub parent_hash: Option<String>,
845    /// Block timestamp (integer representation).
846    #[serde(skip_serializing_if = "Option::is_none")]
847    pub timestamp: Option<i64>,
848}
849
850impl fmt::Display for SubgraphBlock {
851    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
852        write!(f, "block(#{})", self.number)
853    }
854}
855
856/// Top-level subgraph indexing metadata returned by the `_meta` field.
857///
858/// Mirrors the `_Meta_` `GraphQL` type.
859#[derive(Debug, Clone, Serialize, Deserialize)]
860#[serde(rename_all = "camelCase")]
861pub struct SubgraphMeta {
862    /// Block the subgraph has indexed up to.
863    pub block: SubgraphBlock,
864    /// Subgraph deployment ID.
865    pub deployment: String,
866    /// Whether the subgraph encountered indexing errors at some past block.
867    pub has_indexing_errors: bool,
868}
869
870impl fmt::Display for SubgraphMeta {
871    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
872        write!(f, "meta(deploy={}, block={})", self.deployment, self.block)
873    }
874}
875
876#[cfg(test)]
877mod tests {
878    use super::*;
879
880    // ── Helper constructors ──────────────────────────────────────────────────
881
882    fn sample_token() -> Token {
883        Token {
884            id: "0xabc".into(),
885            address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".into(),
886            first_trade_timestamp: "1700000000".into(),
887            name: "USD Coin".into(),
888            symbol: "USDC".into(),
889            decimals: "6".into(),
890            total_volume: "1000000".into(),
891            price_eth: "0.0003".into(),
892            price_usd: "1.0".into(),
893            number_of_trades: "42".into(),
894        }
895    }
896
897    fn sample_user() -> User {
898        User {
899            id: "0xuser".into(),
900            address: "0xUserAddress".into(),
901            first_trade_timestamp: "1700000000".into(),
902            number_of_trades: "10".into(),
903            solved_amount_eth: "5.0".into(),
904            solved_amount_usd: "10000".into(),
905        }
906    }
907
908    // ── Display impls ────────────────────────────────────────────────────────
909
910    #[test]
911    fn totals_display() {
912        let t = Totals {
913            tokens: "100".into(),
914            orders: "500".into(),
915            traders: "50".into(),
916            settlements: "20".into(),
917            volume_usd: "1000000".into(),
918            volume_eth: "500".into(),
919            fees_usd: "1000".into(),
920            fees_eth: "0.5".into(),
921        };
922        assert_eq!(t.to_string(), "totals(orders=500, traders=50)");
923    }
924
925    #[test]
926    fn daily_volume_display() {
927        let d = DailyVolume { timestamp: "1700000000".into(), volume_usd: "500000".into() };
928        assert_eq!(d.to_string(), "daily-vol(ts=1700000000, $500000)");
929    }
930
931    #[test]
932    fn hourly_volume_display() {
933        let h = HourlyVolume { timestamp: "1700000000".into(), volume_usd: "10000".into() };
934        assert_eq!(h.to_string(), "hourly-vol(ts=1700000000, $10000)");
935    }
936
937    #[test]
938    fn daily_total_display() {
939        let d = DailyTotal {
940            timestamp: "1700000000".into(),
941            orders: "100".into(),
942            traders: "10".into(),
943            tokens: "5".into(),
944            settlements: "3".into(),
945            volume_eth: "50".into(),
946            volume_usd: "100000".into(),
947            fees_eth: "0.1".into(),
948            fees_usd: "200".into(),
949        };
950        assert_eq!(d.to_string(), "daily-total(ts=1700000000, orders=100)");
951    }
952
953    #[test]
954    fn hourly_total_display() {
955        let h = HourlyTotal {
956            timestamp: "1700000000".into(),
957            orders: "10".into(),
958            traders: "5".into(),
959            tokens: "3".into(),
960            settlements: "1".into(),
961            volume_eth: "10".into(),
962            volume_usd: "20000".into(),
963            fees_eth: "0.01".into(),
964            fees_usd: "20".into(),
965        };
966        assert_eq!(h.to_string(), "hourly-total(ts=1700000000, orders=10)");
967    }
968
969    #[test]
970    fn token_display() {
971        let t = sample_token();
972        assert_eq!(t.to_string(), "USDC (0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48)");
973    }
974
975    #[test]
976    fn token_accessors() {
977        let t = sample_token();
978        assert_eq!(t.symbol_ref(), "USDC");
979        assert_eq!(t.address_ref(), "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
980    }
981
982    #[test]
983    fn user_display() {
984        let u = sample_user();
985        assert_eq!(u.to_string(), "0xUserAddress");
986    }
987
988    #[test]
989    fn settlement_display_and_methods() {
990        let s = Settlement {
991            id: "0xtx".into(),
992            tx_hash: "0xdeadbeef".into(),
993            first_trade_timestamp: "1700000000".into(),
994            solver: "0xsolver".into(),
995            tx_cost: Some("1000".into()),
996            tx_fee_in_eth: None,
997        };
998        assert_eq!(s.to_string(), "settlement(0xdeadbeef)");
999        assert!(s.has_gas_cost());
1000        assert!(!s.has_tx_fee());
1001
1002        let s2 = Settlement {
1003            id: "0xtx".into(),
1004            tx_hash: "0xdeadbeef".into(),
1005            first_trade_timestamp: "1700000000".into(),
1006            solver: "0xsolver".into(),
1007            tx_cost: None,
1008            tx_fee_in_eth: Some("0.01".into()),
1009        };
1010        assert!(!s2.has_gas_cost());
1011        assert!(s2.has_tx_fee());
1012    }
1013
1014    #[test]
1015    fn trade_display_and_has_tx_hash() {
1016        let t = Trade {
1017            id: "0x-0".into(),
1018            timestamp: "1700000000".into(),
1019            gas_price: "20".into(),
1020            fee_amount: "100".into(),
1021            tx_hash: "0xabc".into(),
1022            settlement: "0xsettle".into(),
1023            buy_amount: "500".into(),
1024            sell_amount: "1000".into(),
1025            sell_amount_before_fees: "1100".into(),
1026            buy_token: sample_token(),
1027            sell_token: sample_token(),
1028            owner: sample_user(),
1029            order: "0xorder".into(),
1030        };
1031        assert!(t.has_tx_hash());
1032        assert!(t.to_string().contains("0xabc"));
1033    }
1034
1035    #[test]
1036    fn order_methods() {
1037        let o = Order {
1038            id: "0xorderuid1234567890".into(),
1039            owner: sample_user(),
1040            sell_token: sample_token(),
1041            buy_token: sample_token(),
1042            receiver: Some("0xreceiver".into()),
1043            sell_amount: "1000".into(),
1044            buy_amount: "500".into(),
1045            valid_to: "1700000000".into(),
1046            app_data: "0xappdata".into(),
1047            fee_amount: "10".into(),
1048            kind: "sell".into(),
1049            partially_fillable: true,
1050            status: "open".into(),
1051            executed_sell_amount: "0".into(),
1052            executed_sell_amount_before_fees: "0".into(),
1053            executed_buy_amount: "0".into(),
1054            executed_fee_amount: "0".into(),
1055            invalidate_timestamp: None,
1056            timestamp: "1700000000".into(),
1057            tx_hash: "0xtx".into(),
1058            is_signer_safe: false,
1059            signing_scheme: "eip712".into(),
1060            uid: "0xorderuid1234567890abcdef".into(),
1061            surplus: Some("50".into()),
1062        };
1063        assert!(o.is_sell());
1064        assert!(!o.is_buy());
1065        assert!(o.is_open());
1066        assert!(!o.is_filled());
1067        assert!(!o.is_cancelled());
1068        assert!(!o.is_expired());
1069        assert!(!o.is_terminal());
1070        assert!(o.has_receiver());
1071        assert!(!o.has_invalidate_timestamp());
1072        assert!(o.has_surplus());
1073        assert!(o.is_partially_fillable());
1074        assert!(!o.is_signer_safe());
1075        assert!(o.to_string().contains("sell"));
1076        assert!(o.to_string().contains("open"));
1077    }
1078
1079    #[test]
1080    fn order_terminal_states() {
1081        let make = |status: &str| Order {
1082            id: "x".into(),
1083            owner: sample_user(),
1084            sell_token: sample_token(),
1085            buy_token: sample_token(),
1086            receiver: None,
1087            sell_amount: "0".into(),
1088            buy_amount: "0".into(),
1089            valid_to: "0".into(),
1090            app_data: "0x".into(),
1091            fee_amount: "0".into(),
1092            kind: "buy".into(),
1093            partially_fillable: false,
1094            status: status.into(),
1095            executed_sell_amount: "0".into(),
1096            executed_sell_amount_before_fees: "0".into(),
1097            executed_buy_amount: "0".into(),
1098            executed_fee_amount: "0".into(),
1099            invalidate_timestamp: Some("100".into()),
1100            timestamp: "0".into(),
1101            tx_hash: String::new(),
1102            is_signer_safe: true,
1103            signing_scheme: "eip1271".into(),
1104            uid: "short".into(),
1105            surplus: None,
1106        };
1107        assert!(make("filled").is_filled());
1108        assert!(make("filled").is_terminal());
1109        assert!(make("cancelled").is_cancelled());
1110        assert!(make("cancelled").is_terminal());
1111        assert!(make("expired").is_expired());
1112        assert!(make("expired").is_terminal());
1113
1114        let buy_order = make("open");
1115        assert!(buy_order.is_buy());
1116        assert!(buy_order.has_invalidate_timestamp());
1117        assert!(!buy_order.has_surplus());
1118        assert!(!buy_order.is_partially_fillable());
1119        assert!(buy_order.is_signer_safe());
1120    }
1121
1122    #[test]
1123    fn bundle_display_and_accessor() {
1124        let b = Bundle { id: "1".into(), eth_price_usd: "3500.00".into() };
1125        assert_eq!(b.to_string(), "eth-price=$3500.00");
1126        assert_eq!(b.eth_price_usd_ref(), "3500.00");
1127    }
1128
1129    #[test]
1130    fn total_display() {
1131        let t = Total {
1132            id: "1".into(),
1133            orders: "100".into(),
1134            settlements: "10".into(),
1135            tokens: "20".into(),
1136            traders: "30".into(),
1137            number_of_trades: "200".into(),
1138            volume_eth: None,
1139            volume_usd: None,
1140            fees_eth: None,
1141            fees_usd: None,
1142        };
1143        assert_eq!(t.to_string(), "total(orders=100, traders=30)");
1144    }
1145
1146    #[test]
1147    fn subgraph_block_display() {
1148        let b = SubgraphBlock {
1149            hash: Some("0xabc".into()),
1150            number: 12345,
1151            parent_hash: None,
1152            timestamp: Some(1700000000),
1153        };
1154        assert_eq!(b.to_string(), "block(#12345)");
1155    }
1156
1157    #[test]
1158    fn subgraph_meta_display() {
1159        let m = SubgraphMeta {
1160            block: SubgraphBlock { hash: None, number: 999, parent_hash: None, timestamp: None },
1161            deployment: "deploy-123".into(),
1162            has_indexing_errors: false,
1163        };
1164        assert_eq!(m.to_string(), "meta(deploy=deploy-123, block=block(#999))");
1165    }
1166
1167    #[test]
1168    fn uniswap_token_display() {
1169        let t = UniswapToken {
1170            id: "0xabc".into(),
1171            address: "0xABC".into(),
1172            name: "Token".into(),
1173            symbol: "TKN".into(),
1174            decimals: 18,
1175            price_eth: None,
1176            price_usd: None,
1177        };
1178        assert_eq!(t.to_string(), "TKN (0xABC)");
1179    }
1180
1181    #[test]
1182    fn uniswap_pool_display() {
1183        let t0 = UniswapToken {
1184            id: "0xa".into(),
1185            address: "0xA".into(),
1186            name: "A".into(),
1187            symbol: "A".into(),
1188            decimals: 18,
1189            price_eth: None,
1190            price_usd: None,
1191        };
1192        let t1 = UniswapToken {
1193            id: "0xb".into(),
1194            address: "0xB".into(),
1195            name: "B".into(),
1196            symbol: "B".into(),
1197            decimals: 18,
1198            price_eth: None,
1199            price_usd: None,
1200        };
1201        let p = UniswapPool {
1202            id: "0xpool".into(),
1203            liquidity: "1000".into(),
1204            tick: Some("100".into()),
1205            token0: t0,
1206            token0_price: "1.0".into(),
1207            token1: t1,
1208            token1_price: "1.0".into(),
1209            total_value_locked_token0: "500".into(),
1210            total_value_locked_token1: "500".into(),
1211        };
1212        assert_eq!(p.to_string(), "pool(0xpool, A (0xA)/B (0xB))");
1213    }
1214
1215    #[test]
1216    fn pair_display() {
1217        let p = Pair {
1218            id: "0xa-0xb".into(),
1219            token0: sample_token(),
1220            token1: sample_token(),
1221            volume_token0: "100".into(),
1222            volume_token1: "200".into(),
1223            number_of_trades: "5".into(),
1224        };
1225        assert!(p.to_string().starts_with("pair("));
1226    }
1227
1228    #[test]
1229    fn pair_daily_display() {
1230        let pd = PairDaily {
1231            id: "0xa-0xb-123".into(),
1232            token0: sample_token(),
1233            token1: sample_token(),
1234            timestamp: "1700000000".into(),
1235            volume_token0: "100".into(),
1236            volume_token1: "200".into(),
1237            number_of_trades: "5".into(),
1238        };
1239        assert!(pd.to_string().contains("pair-daily("));
1240    }
1241
1242    #[test]
1243    fn pair_hourly_display() {
1244        let ph = PairHourly {
1245            id: "0xa-0xb-123".into(),
1246            token0: sample_token(),
1247            token1: sample_token(),
1248            timestamp: "1700000000".into(),
1249            volume_token0: "100".into(),
1250            volume_token1: "200".into(),
1251            number_of_trades: "5".into(),
1252        };
1253        assert!(ph.to_string().contains("pair-hourly("));
1254    }
1255
1256    #[test]
1257    fn token_daily_total_display() {
1258        let tdt = TokenDailyTotal {
1259            id: "0x-123".into(),
1260            token: sample_token(),
1261            timestamp: "1700000000".into(),
1262            total_volume: "1000".into(),
1263            total_volume_usd: "1000".into(),
1264            total_trades: "10".into(),
1265            open_price: "1.0".into(),
1266            close_price: "1.01".into(),
1267            higher_price: "1.02".into(),
1268            lower_price: "0.99".into(),
1269            average_price: "1.005".into(),
1270        };
1271        assert!(tdt.to_string().contains("token-daily("));
1272    }
1273
1274    #[test]
1275    fn token_hourly_total_display() {
1276        let tht = TokenHourlyTotal {
1277            id: "0x-123".into(),
1278            token: sample_token(),
1279            timestamp: "1700000000".into(),
1280            total_volume: "500".into(),
1281            total_volume_usd: "500".into(),
1282            total_trades: "5".into(),
1283            open_price: "1.0".into(),
1284            close_price: "1.01".into(),
1285            higher_price: "1.02".into(),
1286            lower_price: "0.99".into(),
1287            average_price: "1.005".into(),
1288        };
1289        assert!(tht.to_string().contains("token-hourly("));
1290    }
1291
1292    #[test]
1293    fn token_trading_event_display() {
1294        let e = TokenTradingEvent {
1295            id: "evt1".into(),
1296            token: sample_token(),
1297            price_usd: "1.01".into(),
1298            timestamp: "1700000000".into(),
1299        };
1300        assert!(e.to_string().contains("trade-event("));
1301        assert!(e.to_string().contains("price=$1.01"));
1302    }
1303
1304    // ── Serde roundtrips ─────────────────────────────────────────────────────
1305
1306    #[test]
1307    fn totals_serde_roundtrip() {
1308        let t = Totals {
1309            tokens: "100".into(),
1310            orders: "500".into(),
1311            traders: "50".into(),
1312            settlements: "20".into(),
1313            volume_usd: "1000000".into(),
1314            volume_eth: "500".into(),
1315            fees_usd: "1000".into(),
1316            fees_eth: "0.5".into(),
1317        };
1318        let json = serde_json::to_string(&t).unwrap();
1319        let t2: Totals = serde_json::from_str(&json).unwrap();
1320        assert_eq!(t2.orders, "500");
1321    }
1322
1323    #[test]
1324    fn settlement_serde_skips_none() {
1325        let s = Settlement {
1326            id: "x".into(),
1327            tx_hash: "0x".into(),
1328            first_trade_timestamp: "0".into(),
1329            solver: "0x".into(),
1330            tx_cost: None,
1331            tx_fee_in_eth: None,
1332        };
1333        let json = serde_json::to_string(&s).unwrap();
1334        assert!(!json.contains("txCost"));
1335        assert!(!json.contains("txFeeInEth"));
1336    }
1337
1338    #[test]
1339    fn total_serde_roundtrip_with_optional_fields() {
1340        let t = Total {
1341            id: "1".into(),
1342            orders: "10".into(),
1343            settlements: "5".into(),
1344            tokens: "3".into(),
1345            traders: "2".into(),
1346            number_of_trades: "20".into(),
1347            volume_eth: Some("100".into()),
1348            volume_usd: Some("200000".into()),
1349            fees_eth: None,
1350            fees_usd: None,
1351        };
1352        let json = serde_json::to_string(&t).unwrap();
1353        assert!(json.contains("volumeEth"));
1354        assert!(!json.contains("feesEth"));
1355        let t2: Total = serde_json::from_str(&json).unwrap();
1356        assert_eq!(t2.volume_eth, Some("100".into()));
1357        assert_eq!(t2.fees_eth, None);
1358    }
1359
1360    #[test]
1361    fn bundle_serde_roundtrip() {
1362        let b = Bundle { id: "1".into(), eth_price_usd: "3500".into() };
1363        let json = serde_json::to_string(&b).unwrap();
1364        assert!(json.contains("ethPriceUsd"));
1365        let b2: Bundle = serde_json::from_str(&json).unwrap();
1366        assert_eq!(b2.eth_price_usd, "3500");
1367    }
1368}