1use monero_daemon_rpc::{prelude::ProvidesTransactions, MoneroDaemon};
5use monero_oxide::block::Block;
6use monero_simple_request_rpc::SimpleRequestTransport;
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: MoneroDaemon<SimpleRequestTransport>,
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: SimpleRequestTransport::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) -> &MoneroDaemon<SimpleRequestTransport> {
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(
85 "get_block",
86 Some(
87 json!(
88 {
89 "height": height,
90 "fill_pow_hash": true
91 }
92 )
93 .to_string(),
94 ),
95 usize::MAX,
96 )
97 .await
98 .unwrap();
99
100 let result: Result = serde_json::from_str(&result).unwrap();
101
102 assert!(
104 !result.block_header.pow_hash.is_empty(),
105 "untrusted node detected, `pow_hash` will not show on these nodes - use a trusted node!"
106 );
107
108 let reward = result.block_header.reward;
109
110 let (block_hash, block_blob, block) = spawn_blocking(|| {
111 let block_blob = hex::decode(result.blob).unwrap();
112 let block = Block::read(&mut block_blob.as_slice()).unwrap();
113 (block.hash(), block_blob, block)
114 })
115 .await
116 .unwrap();
117
118 let txs: Vec<VerifiedTransactionInformation> = self
119 .get_transaction_verification_data(&block.transactions)
120 .await
121 .collect();
122
123 let block_header = result.block_header;
124 let block_hash_2 = <[u8; 32]>::try_from(hex::decode(&block_header.hash).unwrap()).unwrap();
125 let pow_hash = <[u8; 32]>::try_from(hex::decode(&block_header.pow_hash).unwrap()).unwrap();
126
127 assert_eq!(block_hash, block_hash_2);
129
130 let total_tx_fees = txs.iter().map(|tx| tx.fee).sum::<u64>();
131 let generated_coins = block
132 .miner_transaction()
133 .prefix()
134 .outputs
135 .iter()
136 .map(|output| output.amount.expect("miner_tx amount was None"))
137 .sum::<u64>()
138 - total_tx_fees;
139 assert_eq!(
140 reward,
141 generated_coins + total_tx_fees,
142 "generated_coins ({generated_coins}) + total_tx_fees ({total_tx_fees}) != reward ({reward})"
143 );
144
145 VerifiedBlockInformation {
146 block,
147 block_blob,
148 txs,
149 block_hash,
150 pow_hash,
151 generated_coins,
152 height: block_header.height,
153 weight: block_header.block_weight,
154 long_term_weight: block_header.long_term_weight,
155 cumulative_difficulty: block_header.cumulative_difficulty,
156 }
157 }
158
159 pub async fn get_transaction_verification_data<'a>(
165 &self,
166 tx_hashes: &'a [[u8; 32]],
167 ) -> impl Iterator<Item = VerifiedTransactionInformation> + 'a {
168 self.rpc
169 .transactions(tx_hashes)
170 .await
171 .unwrap()
172 .into_iter()
173 .enumerate()
174 .map(|(i, tx)| {
175 let tx_hash = tx.hash();
176 assert_eq!(tx_hash, tx_hashes[i]);
177 let tx_weight = tx.weight();
178 let fee = tx_fee(&tx);
179 let (tx_pruned, prunable) = tx.pruned_with_prunable();
180
181 VerifiedTransactionInformation {
182 tx_weight,
183 tx_pruned: tx_pruned.serialize(),
184 tx_prunable_blob: prunable,
185 tx_hash,
186 fee,
187 tx: tx_pruned,
188 }
189 })
190 }
191}
192
193#[cfg(test)]
195mod tests {
196 use hex_literal::hex;
197
198 use super::*;
199
200 #[ignore] #[tokio::test]
203 async fn localhost() {
204 assert_eq!(HttpRpcClient::new(None).await.address(), LOCALHOST_RPC_URL);
205 }
206
207 #[ignore] #[tokio::test]
210 async fn get() {
211 #[expect(clippy::too_many_arguments)]
212 async fn assert_eq(
213 rpc: &HttpRpcClient,
214 height: usize,
215 block_hash: [u8; 32],
216 pow_hash: [u8; 32],
217 generated_coins: u64,
218 weight: usize,
219 long_term_weight: usize,
220 cumulative_difficulty: u128,
221 tx_count: usize,
222 ) {
223 let block = rpc.get_verified_block_information(height).await;
224
225 println!("block height: {height}");
226 assert_eq!(block.txs.len(), tx_count);
227 println!("{block:#?}");
228
229 assert_eq!(block.block_hash, block_hash);
230 assert_eq!(block.pow_hash, pow_hash);
231 assert_eq!(block.height, height);
232 assert_eq!(block.generated_coins, generated_coins);
233 assert_eq!(block.weight, weight);
234 assert_eq!(block.long_term_weight, long_term_weight);
235 assert_eq!(block.cumulative_difficulty, cumulative_difficulty);
236 }
237
238 let rpc = HttpRpcClient::new(None).await;
239
240 assert_eq(
241 &rpc,
242 0, hex!("418015bb9ae982a1975da7d79277c2705727a56894ba0fb246adaabb1f4632e3"), hex!("8a7b1a780e99eec31a9425b7d89c283421b2042a337d5700dfd4a7d6eb7bd774"), 17592186044415, 80, 80, 1, 0, )
251 .await;
252
253 assert_eq(
254 &rpc,
255 1,
256 hex!("771fbcd656ec1464d3a02ead5e18644030007a0fc664c0a964d30922821a8148"),
257 hex!("5aeebb3de73859d92f3f82fdb97286d81264ecb72a42e4b9f1e6d62eb682d7c0"),
258 17592169267200,
259 383,
260 383,
261 2,
262 0,
263 )
264 .await;
265
266 assert_eq(
267 &rpc,
268 202612,
269 hex!("bbd604d2ba11ba27935e006ed39c9bfdd99b76bf4a50654bc1e1e61217962698"),
270 hex!("84f64766475d51837ac9efbef1926486e58563c95a19fef4aec3254f03000000"),
271 13138270467918,
272 55503,
273 55503,
274 126654460829362,
275 513,
276 )
277 .await;
278
279 assert_eq(
280 &rpc,
281 1731606,
282 hex!("f910435a5477ca27be1986c080d5476aeab52d0c07cf3d9c72513213350d25d4"),
283 hex!("7c78b5b67a112a66ea69ea51477492057dba9cfeaa2942ee7372c61800000000"),
284 3403774022163,
285 6597,
286 6597,
287 23558910234058343,
288 3,
289 )
290 .await;
291
292 assert_eq(
293 &rpc,
294 2751506,
295 hex!("43bd1f2b6556dcafa413d8372974af59e4e8f37dbf74dc6b2a9b7212d0577428"),
296 hex!("10b473b5d097d6bfa0656616951840724dfe38c6fb9c4adf8158800300000000"),
297 600000000000,
298 106,
299 176470,
300 236046001376524168,
301 0,
302 )
303 .await;
304
305 assert_eq(
306 &rpc,
307 3132285,
308 hex!("a999c6ba4d2993541ba9d81561bb8293baa83b122f8aa9ab65b3c463224397d8"),
309 hex!("4eaa3b3d4dc888644bc14dc4895ca0b008586e30b186fbaa009d330100000000"),
310 600000000000,
311 133498,
312 176470,
313 348189741564698577,
314 57,
315 )
316 .await;
317 }
318}