Skip to main content

metaflux_client/rest/
explorer.rs

1//! `/explorer` — block / tx lookups.
2//!
3//! Read-only queries with the same MTF-native shape conventions as `/info`:
4//! snake_case JSON, plain-integer numerics.
5
6use serde::{Deserialize, Serialize};
7use serde_json::json;
8
9use crate::error::ClientError;
10use crate::rest::RestClient;
11
12/// `explorer` namespace handle. Constructed via [`RestClient::explorer`].
13#[derive(Debug)]
14pub struct Explorer<'a> {
15    pub(crate) client: &'a RestClient,
16}
17
18/// A block header + tx ids. Full bodies retrievable via `tx_by_hash`.
19#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub struct Block {
22    /// Block height.
23    pub height: u64,
24    /// 32-byte block hash, 0x-prefixed lowercase hex.
25    pub hash: String,
26    /// Parent block hash.
27    pub parent_hash: String,
28    /// Block timestamp (unix ms).
29    pub ts_ms: u64,
30    /// Validator that proposed this block.
31    pub proposer: String,
32    /// Transaction hashes in inclusion order.
33    pub tx_hashes: Vec<String>,
34    /// Application state commitment post-block — an opaque 0x-hex 32-byte hash
35    /// (a pure full-state fold of the server's `Exchange` ledger). Does NOT
36    /// encode height/epoch, so an unchanged-state chain reports a constant
37    /// value. Round-tripped verbatim; the client does not recompute or verify it.
38    pub app_hash: String,
39}
40
41/// A decoded transaction record.
42#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44pub struct Transaction {
45    /// 32-byte tx hash, 0x-prefixed lowercase hex.
46    pub hash: String,
47    /// Block height the tx was included in.
48    pub block_height: u64,
49    /// Action `type` discriminator.
50    pub action_type: String,
51    /// 20-byte signer address, 0x-prefixed hex.
52    pub signer: String,
53    /// EIP-712 nonce used by the signer.
54    pub nonce: u64,
55    /// Outcome: `accepted` / `rejected` / `error`.
56    pub status: String,
57    /// Optional error message (present only when `status != "accepted"`).
58    #[serde(default)]
59    pub error: Option<String>,
60}
61
62impl<'a> Explorer<'a> {
63    /// Look up a block by height.
64    ///
65    /// # Errors
66    /// HTTP / decode / protocol errors per [`crate::ClientError`].
67    pub async fn block_by_height(&self, height: u64) -> Result<Block, ClientError> {
68        self.client
69            .post_json(
70                "/explorer",
71                &json!({ "type": "block_by_height", "height": height }),
72            )
73            .await
74    }
75
76    /// Look up a transaction by 32-byte hash (0x-prefixed lowercase hex).
77    ///
78    /// # Errors
79    /// HTTP / decode / protocol errors per [`crate::ClientError`].
80    pub async fn tx_by_hash(&self, hash: &str) -> Result<Transaction, ClientError> {
81        self.client
82            .post_json("/explorer", &json!({ "type": "tx_by_hash", "hash": hash }))
83            .await
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn block_round_trips() {
93        let b = Block {
94            height: 100,
95            hash: "0xdeadbeef".repeat(8),
96            parent_hash: "0xcafebabe".repeat(8),
97            ts_ms: 1_700_000_000_000,
98            proposer: "0x".to_string() + &"ab".repeat(20),
99            tx_hashes: vec!["0x01".into(), "0x02".into()],
100            app_hash: "0x".to_string() + &"00".repeat(32),
101        };
102        let j = serde_json::to_string(&b).unwrap();
103        let dec: Block = serde_json::from_str(&j).unwrap();
104        assert_eq!(b, dec);
105    }
106
107    #[test]
108    fn transaction_uses_snake_case_fields() {
109        let t = Transaction {
110            hash: "0x01".into(),
111            block_height: 100,
112            action_type: "submit_order".into(),
113            signer: "0xab".into(),
114            nonce: 1_700_000_000_000,
115            status: "accepted".into(),
116            error: None,
117        };
118        let j = serde_json::to_value(&t).unwrap();
119        for key in ["block_height", "action_type"] {
120            assert!(j.get(key).is_some(), "missing {key}");
121        }
122        for key in ["blockHeight", "actionType"] {
123            assert!(j.get(key).is_none(), "wire leak: {key}");
124        }
125    }
126}