1use 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
14pub const LOCALHOST_RPC_URL: &str = "http://127.0.0.1:18081";
17
18pub struct HttpRpcClient {
21 address: String,
22 rpc: SimpleRequestRpc,
23}
24
25impl HttpRpcClient {
26 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 #[allow(clippy::allow_attributes, dead_code, reason = "expect doesn't work")]
49 const fn address(&self) -> &String {
50 &self.address
51 }
52
53 #[expect(dead_code)]
55 const fn rpc(&self) -> &SimpleRequestRpc {
56 &self.rpc
57 }
58
59 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, }
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 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_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 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#[cfg(test)]
184mod tests {
185 use hex_literal::hex;
186
187 use super::*;
188
189 #[tokio::test]
191 async fn localhost() {
192 assert_eq!(HttpRpcClient::new(None).await.address(), LOCALHOST_RPC_URL);
193 }
194
195 #[ignore] #[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, hex!("418015bb9ae982a1975da7d79277c2705727a56894ba0fb246adaabb1f4632e3"), hex!("8a7b1a780e99eec31a9425b7d89c283421b2042a337d5700dfd4a7d6eb7bd774"), 17592186044415, 80, 80, 1, 0, )
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}