cuprate_test_utils/rpc/
client.rs

1//! HTTP RPC client.
2
3//---------------------------------------------------------------------------------------------------- Use
4use monero_rpc::Rpc;
5use monero_serai::block::Block;
6use monero_simple_request_rpc::SimpleRequestRpc;
7use serde::Deserialize;
8use serde_json::json;
9use tokio::task::spawn_blocking;
10
11use cuprate_helper::tx::tx_fee;
12use cuprate_types::{VerifiedBlockInformation, VerifiedTransactionInformation};
13
14//---------------------------------------------------------------------------------------------------- Constants
15/// The default URL used for Monero RPC connections.
16pub const LOCALHOST_RPC_URL: &str = "http://127.0.0.1:18081";
17
18//---------------------------------------------------------------------------------------------------- HttpRpcClient
19/// An HTTP RPC client for Monero.
20pub struct HttpRpcClient {
21    address: String,
22    rpc: SimpleRequestRpc,
23}
24
25impl HttpRpcClient {
26    /// Create an [`HttpRpcClient`].
27    ///
28    /// `address` should be an HTTP URL pointing to a `monerod`.
29    ///
30    /// If `None` is provided the default is used: [`LOCALHOST_RPC_URL`].
31    ///
32    /// Note that for [`Self::get_verified_block_information`] to work, the `monerod`
33    /// must be in unrestricted mode such that some fields (e.g. `pow_hash`) appear
34    /// in the JSON response.
35    ///
36    /// # Panics
37    /// This panics if the `address` is invalid or a connection could not be made.
38    pub async fn new(address: Option<String>) -> Self {
39        let address = address.unwrap_or_else(|| LOCALHOST_RPC_URL.to_string());
40
41        Self {
42            rpc: SimpleRequestRpc::new(address.clone()).await.unwrap(),
43            address,
44        }
45    }
46
47    /// The address used for this [`HttpRpcClient`].
48    #[allow(clippy::allow_attributes, dead_code, reason = "expect doesn't work")]
49    const fn address(&self) -> &String {
50        &self.address
51    }
52
53    /// Access to the inner RPC client for other usage.
54    #[expect(dead_code)]
55    const fn rpc(&self) -> &SimpleRequestRpc {
56        &self.rpc
57    }
58
59    /// Request data and map the response to a [`VerifiedBlockInformation`].
60    ///
61    /// # Panics
62    /// This function will panic at any error point, e.g.,
63    /// if the node cannot be connected to, if deserialization fails, etc.
64    pub async fn get_verified_block_information(&self, height: usize) -> VerifiedBlockInformation {
65        #[derive(Debug, Deserialize)]
66        struct Result {
67            blob: String,
68            block_header: BlockHeader,
69        }
70
71        #[derive(Debug, Deserialize)]
72        struct BlockHeader {
73            block_weight: usize,
74            long_term_weight: usize,
75            cumulative_difficulty: u128,
76            hash: String,
77            height: usize,
78            pow_hash: String,
79            reward: u64, // generated_coins + total_tx_fees
80        }
81
82        let result = self
83            .rpc
84            .json_rpc_call::<Result>(
85                "get_block",
86                Some(json!(
87                    {
88                        "height": height,
89                        "fill_pow_hash": true
90                    }
91                )),
92            )
93            .await
94            .unwrap();
95
96        // Make sure this is a trusted, `pow_hash` only works there.
97        assert!(
98        	!result.block_header.pow_hash.is_empty(),
99        	"untrusted node detected, `pow_hash` will not show on these nodes - use a trusted node!"
100        );
101
102        let reward = result.block_header.reward;
103
104        let (block_hash, block_blob, block) = spawn_blocking(|| {
105            let block_blob = hex::decode(result.blob).unwrap();
106            let block = Block::read(&mut block_blob.as_slice()).unwrap();
107            (block.hash(), block_blob, block)
108        })
109        .await
110        .unwrap();
111
112        let txs: Vec<VerifiedTransactionInformation> = self
113            .get_transaction_verification_data(&block.transactions)
114            .await
115            .collect();
116
117        let block_header = result.block_header;
118        let block_hash_2 = <[u8; 32]>::try_from(hex::decode(&block_header.hash).unwrap()).unwrap();
119        let pow_hash = <[u8; 32]>::try_from(hex::decode(&block_header.pow_hash).unwrap()).unwrap();
120
121        // Assert the block hash matches.
122        assert_eq!(block_hash, block_hash_2);
123
124        let total_tx_fees = txs.iter().map(|tx| tx.fee).sum::<u64>();
125        let generated_coins = block
126            .miner_transaction
127            .prefix()
128            .outputs
129            .iter()
130            .map(|output| output.amount.expect("miner_tx amount was None"))
131            .sum::<u64>()
132            - total_tx_fees;
133        assert_eq!(
134            reward,
135            generated_coins + total_tx_fees,
136            "generated_coins ({generated_coins}) + total_tx_fees ({total_tx_fees}) != reward ({reward})"
137        );
138
139        VerifiedBlockInformation {
140            block,
141            block_blob,
142            txs,
143            block_hash,
144            pow_hash,
145            generated_coins,
146            height: block_header.height,
147            weight: block_header.block_weight,
148            long_term_weight: block_header.long_term_weight,
149            cumulative_difficulty: block_header.cumulative_difficulty,
150        }
151    }
152
153    /// Request data and map the response to a [`VerifiedTransactionInformation`].
154    ///
155    /// # Panics
156    /// This function will panic at any error point, e.g.,
157    /// if the node cannot be connected to, if deserialization fails, etc.
158    pub async fn get_transaction_verification_data<'a>(
159        &self,
160        tx_hashes: &'a [[u8; 32]],
161    ) -> impl Iterator<Item = VerifiedTransactionInformation> + 'a {
162        self.rpc
163            .get_transactions(tx_hashes)
164            .await
165            .unwrap()
166            .into_iter()
167            .enumerate()
168            .map(|(i, tx)| {
169                let tx_hash = tx.hash();
170                assert_eq!(tx_hash, tx_hashes[i]);
171                VerifiedTransactionInformation {
172                    tx_blob: tx.serialize(),
173                    tx_weight: tx.weight(),
174                    tx_hash,
175                    fee: tx_fee(&tx),
176                    tx,
177                }
178            })
179    }
180}
181
182//---------------------------------------------------------------------------------------------------- TESTS
183#[cfg(test)]
184mod tests {
185    use hex_literal::hex;
186
187    use super::*;
188
189    /// Assert the default address is localhost.
190    #[tokio::test]
191    async fn localhost() {
192        assert_eq!(HttpRpcClient::new(None).await.address(), LOCALHOST_RPC_URL);
193    }
194
195    /// Assert blocks are correctly received/calculated.
196    #[ignore] // FIXME: doesn't work in CI, we need a real unrestricted node
197    #[tokio::test]
198    async fn get() {
199        #[expect(clippy::too_many_arguments)]
200        async fn assert_eq(
201            rpc: &HttpRpcClient,
202            height: usize,
203            block_hash: [u8; 32],
204            pow_hash: [u8; 32],
205            generated_coins: u64,
206            weight: usize,
207            long_term_weight: usize,
208            cumulative_difficulty: u128,
209            tx_count: usize,
210        ) {
211            let block = rpc.get_verified_block_information(height).await;
212
213            println!("block height: {height}");
214            assert_eq!(block.txs.len(), tx_count);
215            println!("{block:#?}");
216
217            assert_eq!(block.block_hash, block_hash);
218            assert_eq!(block.pow_hash, pow_hash);
219            assert_eq!(block.height, height);
220            assert_eq!(block.generated_coins, generated_coins);
221            assert_eq!(block.weight, weight);
222            assert_eq!(block.long_term_weight, long_term_weight);
223            assert_eq!(block.cumulative_difficulty, cumulative_difficulty);
224        }
225
226        let rpc = HttpRpcClient::new(None).await;
227
228        assert_eq(
229            &rpc,
230            0,                                                                        // height
231            hex!("418015bb9ae982a1975da7d79277c2705727a56894ba0fb246adaabb1f4632e3"), // block_hash
232            hex!("8a7b1a780e99eec31a9425b7d89c283421b2042a337d5700dfd4a7d6eb7bd774"), // pow_hash
233            17592186044415, // generated_coins
234            80,             // weight
235            80,             // long_term_weight
236            1,              // cumulative_difficulty
237            0,              // tx_count (miner_tx excluded)
238        )
239        .await;
240
241        assert_eq(
242            &rpc,
243            1,
244            hex!("771fbcd656ec1464d3a02ead5e18644030007a0fc664c0a964d30922821a8148"),
245            hex!("5aeebb3de73859d92f3f82fdb97286d81264ecb72a42e4b9f1e6d62eb682d7c0"),
246            17592169267200,
247            383,
248            383,
249            2,
250            0,
251        )
252        .await;
253
254        assert_eq(
255            &rpc,
256            202612,
257            hex!("bbd604d2ba11ba27935e006ed39c9bfdd99b76bf4a50654bc1e1e61217962698"),
258            hex!("84f64766475d51837ac9efbef1926486e58563c95a19fef4aec3254f03000000"),
259            13138270467918,
260            55503,
261            55503,
262            126654460829362,
263            513,
264        )
265        .await;
266
267        assert_eq(
268            &rpc,
269            1731606,
270            hex!("f910435a5477ca27be1986c080d5476aeab52d0c07cf3d9c72513213350d25d4"),
271            hex!("7c78b5b67a112a66ea69ea51477492057dba9cfeaa2942ee7372c61800000000"),
272            3403774022163,
273            6597,
274            6597,
275            23558910234058343,
276            3,
277        )
278        .await;
279
280        assert_eq(
281            &rpc,
282            2751506,
283            hex!("43bd1f2b6556dcafa413d8372974af59e4e8f37dbf74dc6b2a9b7212d0577428"),
284            hex!("10b473b5d097d6bfa0656616951840724dfe38c6fb9c4adf8158800300000000"),
285            600000000000,
286            106,
287            176470,
288            236046001376524168,
289            0,
290        )
291        .await;
292
293        assert_eq(
294            &rpc,
295            3132285,
296            hex!("a999c6ba4d2993541ba9d81561bb8293baa83b122f8aa9ab65b3c463224397d8"),
297            hex!("4eaa3b3d4dc888644bc14dc4895ca0b008586e30b186fbaa009d330100000000"),
298            600000000000,
299            133498,
300            176470,
301            348189741564698577,
302            57,
303        )
304        .await;
305    }
306}