1use chik_protocol::{Bytes32, SpendBundle};
2use serde::{de::DeserializeOwned, Serialize};
3use std::future::Future;
4
5use super::{
6 AdditionsAndRemovalsResponse, BlockchainStateResponse, GetBlockRecordByHeightResponse,
7 GetBlockRecordResponse, GetBlockRecordsResponse, GetBlockResponse, GetBlockSpendsResponse,
8 GetBlocksResponse, GetCoinRecordResponse, GetCoinRecordsResponse, GetMempoolItemResponse,
9 GetMempoolItemsResponse, GetNetworkInfoResponse, GetPuzzleAndSolutionResponse, PushTxResponse,
10};
11
12pub trait ChikRpcClient {
13 type Error;
14
15 fn base_url(&self) -> &str;
16
17 fn make_post_request<R, B>(
18 &self,
19 endpoint: &str,
20 body: B,
21 ) -> impl Future<Output = Result<R, Self::Error>>
22 where
23 B: Serialize + Send,
24 R: DeserializeOwned + Send;
25
26 fn get_blockchain_state(
27 &self,
28 ) -> impl Future<Output = Result<BlockchainStateResponse, Self::Error>> {
29 self.make_post_request("get_blockchain_state", serde_json::json!({}))
30 }
31
32 fn get_additions_and_removals(
33 &self,
34 header_hash: Bytes32,
35 ) -> impl Future<Output = Result<AdditionsAndRemovalsResponse, Self::Error>> {
36 self.make_post_request(
37 "get_additions_and_removals",
38 serde_json::json!({
39 "header_hash": format!("0x{}", hex::encode(header_hash.to_bytes())),
40 }),
41 )
42 }
43
44 fn get_block(
45 &self,
46 header_hash: Bytes32,
47 ) -> impl Future<Output = Result<GetBlockResponse, Self::Error>> {
48 self.make_post_request(
49 "get_block",
50 serde_json::json!({
51 "header_hash": format!("0x{}", hex::encode(header_hash.to_bytes())),
52 }),
53 )
54 }
55
56 fn get_block_record(
57 &self,
58 header_hash: Bytes32,
59 ) -> impl Future<Output = Result<GetBlockRecordResponse, Self::Error>> {
60 self.make_post_request(
61 "get_block_record",
62 serde_json::json!({
63 "header_hash": format!("0x{}", hex::encode(header_hash.to_bytes())),
64 }),
65 )
66 }
67
68 fn get_block_record_by_height(
69 &self,
70 height: u32,
71 ) -> impl Future<Output = Result<GetBlockRecordByHeightResponse, Self::Error>> {
72 self.make_post_request(
73 "get_block_record_by_height",
74 serde_json::json!({
75 "height": height,
76 }),
77 )
78 }
79
80 fn get_block_records(
81 &self,
82 start_height: u32,
83 end_height: u32,
84 ) -> impl Future<Output = Result<GetBlockRecordsResponse, Self::Error>> {
85 self.make_post_request(
86 "get_block_records",
87 serde_json::json!({
88 "start_height": start_height,
89 "end_height": end_height,
90 }),
91 )
92 }
93
94 fn get_blocks(
95 &self,
96 start: u32,
97 end: u32,
98 exclude_header_hash: bool,
99 exclude_reorged: bool,
100 ) -> impl Future<Output = Result<GetBlocksResponse, Self::Error>> {
101 self.make_post_request(
102 "get_blocks",
103 serde_json::json!({
104 "start": start,
105 "end": end,
106 "exclude_header_hash": exclude_header_hash,
107 "exclude_reorged": exclude_reorged,
108 }),
109 )
110 }
111
112 fn get_block_spends(
113 &self,
114 header_hash: Bytes32,
115 ) -> impl Future<Output = Result<GetBlockSpendsResponse, Self::Error>> {
116 self.make_post_request(
117 "get_block_spends",
118 serde_json::json!({
119 "header_hash": format!("0x{}", hex::encode(header_hash.to_bytes())),
120 }),
121 )
122 }
123
124 fn get_coin_record_by_name(
125 &self,
126 name: Bytes32,
127 ) -> impl Future<Output = Result<GetCoinRecordResponse, Self::Error>> {
128 self.make_post_request(
129 "get_coin_record_by_name",
130 serde_json::json!({
131 "name": format!("0x{}", hex::encode(name.to_bytes())),
132 }),
133 )
134 }
135
136 fn get_coin_records_by_hint(
137 &self,
138 hint: Bytes32,
139 start_height: Option<u32>,
140 end_height: Option<u32>,
141 include_spent_coins: Option<bool>,
142 ) -> impl Future<Output = Result<GetCoinRecordsResponse, Self::Error>> {
143 self.make_post_request(
144 "get_coin_records_by_hint",
145 serde_json::json!({
146 "hint": format!("0x{}", hex::encode(hint.to_bytes())),
147 "start_height": start_height,
148 "end_height": end_height,
149 "include_spent_coins": include_spent_coins,
150 }),
151 )
152 }
153
154 fn get_coin_records_by_names(
155 &self,
156 names: Vec<Bytes32>,
157 start_height: Option<u32>,
158 end_height: Option<u32>,
159 include_spent_coins: Option<bool>,
160 ) -> impl Future<Output = Result<GetCoinRecordsResponse, Self::Error>> {
161 self.make_post_request(
162 "get_coin_records_by_names",
163 serde_json::json!({
164 "names": names.iter().map(|name| format!("0x{}", hex::encode(name.to_bytes()))).collect::<Vec<String>>(),
165 "start_height": start_height,
166 "end_height": end_height,
167 "include_spent_coins": include_spent_coins,
168 }),
169 )
170 }
171
172 fn get_coin_records_by_parent_ids(
173 &self,
174 parent_ids: Vec<Bytes32>,
175 start_height: Option<u32>,
176 end_height: Option<u32>,
177 include_spent_coins: Option<bool>,
178 ) -> impl Future<Output = Result<GetCoinRecordsResponse, Self::Error>> {
179 self.make_post_request(
180 "get_coin_records_by_parent_ids",
181 serde_json::json!({
182 "parent_ids": parent_ids.iter().map(|parent_id| format!("0x{}", hex::encode(parent_id.to_bytes()))).collect::<Vec<String>>(),
183 "start_height": start_height,
184 "end_height": end_height,
185 "include_spent_coins": include_spent_coins,
186 }),
187 )
188 }
189
190 fn get_coin_records_by_puzzle_hash(
191 &self,
192 puzzle_hash: Bytes32,
193 start_height: Option<u32>,
194 end_height: Option<u32>,
195 include_spent_coins: Option<bool>,
196 ) -> impl Future<Output = Result<GetCoinRecordsResponse, Self::Error>> {
197 self.make_post_request(
198 "get_coin_records_by_puzzle_hash",
199 serde_json::json!({
200 "puzzle_hash": format!("0x{}", hex::encode(puzzle_hash.to_bytes())),
201 "start_height": start_height,
202 "end_height": end_height,
203 "include_spent_coins": include_spent_coins,
204 }),
205 )
206 }
207
208 fn get_coin_records_by_puzzle_hashes(
209 &self,
210 puzzle_hashes: Vec<Bytes32>,
211 start_height: Option<u32>,
212 end_height: Option<u32>,
213 include_spent_coins: Option<bool>,
214 ) -> impl Future<Output = Result<GetCoinRecordsResponse, Self::Error>> {
215 self.make_post_request(
216 "get_coin_records_by_puzzle_hashes",
217 serde_json::json!({
218 "puzzle_hashes": puzzle_hashes.iter().map(|puzzle_hash| format!("0x{}", hex::encode(puzzle_hash.to_bytes()))).collect::<Vec<String>>(),
219 "start_height": start_height,
220 "end_height": end_height,
221 "include_spent_coins": include_spent_coins,
222 }),
223 )
224 }
225
226 fn get_puzzle_and_solution(
227 &self,
228 coin_id: Bytes32,
229 height: Option<u32>,
230 ) -> impl Future<Output = Result<GetPuzzleAndSolutionResponse, Self::Error>> {
231 self.make_post_request(
232 "get_puzzle_and_solution",
233 serde_json::json!({
234 "coin_id": format!("0x{}", hex::encode(coin_id.to_bytes())),
235 "height": height,
236 }),
237 )
238 }
239
240 fn push_tx(
241 &self,
242 spend_bundle: SpendBundle,
243 ) -> impl Future<Output = Result<PushTxResponse, Self::Error>> {
244 self.make_post_request(
245 "push_tx",
246 serde_json::json!({
247 "spend_bundle": {
248 "coin_spends": spend_bundle.coin_spends.iter().map(|coin_spend| {
249 serde_json::json!({
250 "coin": {
251 "amount": coin_spend.coin.amount,
252 "parent_coin_info": format!("0x{}", hex::encode(coin_spend.coin.parent_coin_info.to_bytes())),
253 "puzzle_hash": format!("0x{}", hex::encode(coin_spend.coin.puzzle_hash.to_bytes())),
254 },
255 "puzzle_reveal": format!("0x{}", hex::encode(coin_spend.puzzle_reveal.to_vec())),
256 "solution": format!("0x{}", hex::encode(coin_spend.solution.to_vec())),
257 })
258 }).collect::<Vec<serde_json::Value>>(),
259 "aggregated_signature": format!("0x{}", hex::encode(spend_bundle.aggregated_signature.to_bytes())),
260 }
261 }),
262 )
263 }
264
265 fn get_network_info(
266 &self,
267 ) -> impl Future<Output = Result<GetNetworkInfoResponse, Self::Error>> {
268 self.make_post_request("get_network_info", serde_json::json!({}))
269 }
270
271 fn get_mempool_item_by_tx_id(
272 &self,
273 tx_id: Bytes32,
274 ) -> impl Future<Output = Result<GetMempoolItemResponse, Self::Error>> {
275 self.make_post_request(
276 "get_mempool_item_by_tx_id",
277 serde_json::json!({
278 "tx_id": format!("0x{}", hex::encode(tx_id.to_bytes())),
279 }),
280 )
281 }
282
283 fn get_mempool_items_by_coin_name(
284 &self,
285 coin_name: Bytes32,
286 ) -> impl Future<Output = Result<GetMempoolItemsResponse, Self::Error>> {
287 self.make_post_request(
288 "get_mempool_items_by_coin_name",
289 serde_json::json!({
290 "coin_name": format!("0x{}", hex::encode(coin_name.to_bytes())),
291 }),
292 )
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use chik_protocol::Coin;
299 use chik_traits::Streamable;
300 use hex_literal::hex;
301
302 use crate::MockRpcClient;
303
304 use super::*;
305
306 #[tokio::test]
307 async fn test_get_blockchain_state_success() {
308 let mut client = MockRpcClient::new();
309
310 client.mock_response(
311 "http://api.example.com/get_blockchain_state",
312 r#"{"blockchain_state": {"average_block_time": 18, "block_max_cost": 11000000000, "difficulty": 13504, "genesis_challenge_initialized": true, "mempool_cost": 88022711, "mempool_fees": 10, "mempool_max_total_cost": 110000000000, "mempool_min_fees": {"cost_5000000": 0}, "mempool_size": 2, "node_id": "5c8c1640aae6b0ab0f16d5ec01be46aa10ad68f8aa85446fa65f1aee9d6b0b2d", "peak": {"challenge_block_info_hash": "0x3b6cb1a7e32c8c1760ea90a11a369d04755b9d31123aa0890050869bde775150", "challenge_vdf_output": {"data": "0x03009a4b4ab74d6b1d71c1ae4a62252acceb02a517b6fc8acfd7f05632d40da1e4ee2ba51ed5383411ad59749d1f642f41b30224dcb92b8863f8b1ec89eb388dbc346d7a4e9dfdabe42f833e04bc00a4ac123c87261f6ad7477660d579b58364a1160100"}, "deficit": 15, "farmer_puzzle_hash": "0x9fbde16e03f55c85ecf94cb226083fcfe2737d4e629a981e5db3ea0eb9907af4", "fees": 300395698, "finished_challenge_slot_hashes": ["0xa321d872abefa6f935c7ec3a3b72da8f9631edd29d78cd01bc57a3fc9f0a9e46"], "finished_infused_challenge_slot_hashes": ["0xbdca81c482cebcabb532253ef990a618a2e8e8e72dee4038b54c723177d3976f"], "finished_reward_slot_hashes": ["0x77e354091551f4bbf2191513679fc50f72076dff3f2634bdb78882cc9cf43a74"], "header_hash": "0x2b525481f9330f7ca1be1ca6acdd5043362379245b16e18e5897e6203a4add3f", "height": 6515821, "infused_challenge_vdf_output": null, "overflow": false, "pool_puzzle_hash": "0x9fbde16e03f55c85ecf94cb226083fcfe2737d4e629a981e5db3ea0eb9907af4", "prev_hash": "0x5211ea6cbff7175c75355cfa3e10e447a9ee1fbcb14b0e04a6ac8462263590fd", "prev_transaction_block_hash": "0x85bc16981fae8ab0956d065092c537d2d568e9ce9c07011bbfb81bbe19ae66f7", "prev_transaction_block_height": 6515819, "required_iters": 4292961, "reward_claims_incorporated": [{"amount": 875000000000, "parent_coin_info": "0xccd5bb71183532bff220ba46c268991a00000000000000000000000000636c6b", "puzzle_hash": "0xe23046c4362b99b24e027207438717b6d5bd4d440e5a62367e053fa8b409339a"}], "reward_infusion_new_challenge": "0x5defce0af9f85a0fcf144a4f3b0364e81f576bb9c855e0efa58c2b8ba630672a", "signage_point_index": 4, "sub_epoch_summary_included": null, "sub_slot_iters": 578813952, "timestamp": 1737325862, "total_iters": 56509384327521, "weight": 35992319760}, "space": 21810833559006162944, "sub_slot_iters": 578813952, "sync": {"sync_mode": false, "sync_progress_height": 0, "sync_tip_height": 0, "synced": true}}, "success": true}"#,
313 );
314
315 let response = client.get_blockchain_state().await.unwrap();
316 assert!(response.success);
317 assert!(response.error.is_none());
318
319 let state = response.blockchain_state.unwrap();
320 assert_eq!(state.average_block_time, 18);
321 assert_eq!(state.difficulty, 13504);
322 assert_eq!(state.mempool_size, 2);
323
324 let peak = state.peak;
325 assert_eq!(peak.height, 6_515_821);
326 assert_eq!(peak.deficit, 15);
327 assert!(!peak.overflow);
328 }
329
330 #[tokio::test]
331 async fn test_get_blockchain_state_error() {
332 let mut client = MockRpcClient::new();
333
334 client.mock_response(
335 "http://api.example.com/get_blockchain_state",
336 r#"{
337 "success": false,
338 "error": "Failed to connect to full node"
339 }"#,
340 );
341
342 let response = client.get_blockchain_state().await.unwrap();
343 assert!(!response.success);
344 assert_eq!(
345 response.error,
346 Some("Failed to connect to full node".to_string())
347 );
348 assert!(response.blockchain_state.is_none());
349 }
350
351 #[tokio::test]
352 async fn test_get_additions_and_removals_success() {
353 let mut client = MockRpcClient::new();
354
355 client.mock_response(
356 "http://api.example.com/get_additions_and_removals",
357 r#"{
358 "additions": [{
359 "coin": {
360 "amount": 10019626640,
361 "parent_coin_info": "c325057d788bee13367cb8e2d71ff3e209b5e94b31b296322ba1a143053fef5b",
362 "puzzle_hash": "11cd056d9ec93f4612919b445e1ad9afeb7ef7739708c2d16cec4fd2d3cd5e63"
363 },
364 "coinbase": false,
365 "confirmed_block_index": 5910291,
366 "spent": false,
367 "spent_block_index": 0,
368 "timestamp": 1725991066
369 }],
370 "removals": [{
371 "coin": {
372 "amount": 1,
373 "parent_coin_info": "4dda4b8b6017c633794c2b719c3591870b4bc7682930094c11a311112c772ce6",
374 "puzzle_hash": "18cfd81a9a58d598197730b2f2a21ff3b72951577be1dcc6004080ad17069e84"
375 },
376 "coinbase": false,
377 "confirmed_block_index": 5612341,
378 "spent": true,
379 "spent_block_index": 5910291,
380 "timestamp": 1720407964
381 }],
382 "success": true
383 }"#,
384 );
385
386 let header_hash = Bytes32::from([0x88; 32]);
387 let response = client
388 .get_additions_and_removals(header_hash)
389 .await
390 .unwrap();
391
392 assert!(response.success);
393 assert!(response.error.is_none());
394
395 let additions = response.additions.unwrap();
397 assert_eq!(additions.len(), 1);
398 let addition = &additions[0];
399 assert_eq!(
400 addition.coin,
401 Coin::new(
402 Bytes32::new(hex_literal::hex!(
403 "c325057d788bee13367cb8e2d71ff3e209b5e94b31b296322ba1a143053fef5b"
404 )),
405 Bytes32::new(hex_literal::hex!(
406 "11cd056d9ec93f4612919b445e1ad9afeb7ef7739708c2d16cec4fd2d3cd5e63"
407 )),
408 10_019_626_640
409 )
410 );
411 assert!(!addition.coinbase);
412 assert_eq!(addition.confirmed_block_index, 5_910_291);
413 assert!(!addition.spent);
414 assert_eq!(addition.spent_block_index, 0);
415 assert_eq!(addition.timestamp, 1_725_991_066);
416
417 let removals = response.removals.unwrap();
419 assert_eq!(removals.len(), 1);
420 let removal = &removals[0];
421 assert_eq!(
422 removal.coin,
423 Coin::new(
424 Bytes32::new(hex_literal::hex!(
425 "4dda4b8b6017c633794c2b719c3591870b4bc7682930094c11a311112c772ce6"
426 )),
427 Bytes32::new(hex_literal::hex!(
428 "18cfd81a9a58d598197730b2f2a21ff3b72951577be1dcc6004080ad17069e84"
429 )),
430 1
431 )
432 );
433 assert!(!removal.coinbase);
434 assert_eq!(removal.confirmed_block_index, 5_612_341);
435 assert!(removal.spent);
436 assert_eq!(removal.spent_block_index, 5_910_291);
437 assert_eq!(removal.timestamp, 1_720_407_964);
438 }
439
440 #[tokio::test]
441 async fn test_get_additions_and_removals_error() {
442 let mut client = MockRpcClient::new();
443
444 client.mock_response(
445 "http://api.example.com/get_additions_and_removals",
446 r#"{
447 "success": false,
448 "error": "Record not found: [blah blah]"
449 }"#,
450 );
451
452 let header_hash = Bytes32::from([0x88; 32]);
453 let response = client
454 .get_additions_and_removals(header_hash)
455 .await
456 .unwrap();
457
458 assert!(!response.success);
459 assert_eq!(
460 response.error,
461 Some("Record not found: [blah blah]".to_string())
462 );
463 assert!(response.additions.is_none());
464 assert!(response.removals.is_none());
465 }
466
467 #[tokio::test]
468 async fn test_get_block_success() {
469 let mut client = MockRpcClient::new();
470
471 client.mock_response(
472 "http://api.example.com/get_block",
473 r#"{
474 "block": {
475 "challenge_chain_ip_proof": {
476 "normalized_to_identity": false,
477 "witness": "0x02002e9c2d945884802a65e57541f03838956b62b74180777e30961734c7e0272794e869d2280e919e3c02b45370b68da782b0165e0b00dc55f007896e8ec15cc5142d006410b6c75bf8c9eb84a45f0bbd13f3d6fa03cbe91529d1ac710efe7c5d3a0100000000000016f300d4b9b898a5511973a0d069883c34448efc4bda3516e4cede3289b0a2851c9ad2d70100116da23efdfcb4d105de27d1a42b5bb5d0f0c52c96e01043223d4a468acd168f29791287e27e098636447ad2ff57ff5a6b9c6822ae850ff0ac62e791c2f77b2248712abe1a02bbd824949aa2c5af168dec8ec567ed39e8d5731289a2d1ca6c2b0200000000000044e8a09a1b0dcba1f40fe044b05be33c492a9621491efdd01c37240d9e03fc8c895513f10000dff075b7d5bbd49d10da4a539ff4e619664ce5b841f21a39288d477744359bef16dd979891092e3ccc04ff47e119794450cb7ec2ba3f35255d26f8717f64ba25f9d43812c5825ecbe8172b344982ce87e43595a695f59e1e069f747b973cd41c0100000000000022ca40f380ff41cd3e96d5c3259f5f6ae621843fcaa599fe84ae39d8f9cba9909466c31b0000e044c3b598f25dd1b6712ed16413d16c99713b3676d8fba95d22feedbeb180d5eea5e649246475e734adc7e9eaa5395b77754dae938b7dca46e45c802965f92d63d3caa0b4bb7962738c8c5bbf881af7a0a7c49936d67f8398b458ceb6d9c32c01000000000000682fe0a466e45feec21633653f63788ee12441f5154b000b1467896aad5d1e8a2cb87af50200465ad174b651ce5da89280823ffa965620d95ab5bc7a5992752946917414e8b07188342c47908f7a7db96124bf88478a6d2c753140131ff6df10b371ee8096293ded23eb599d8e6a3f24ada092ce16e3143e9d4a8a462e2ac9c218131bf5ed2e02000000000000229b60afba54a3f8a3cc91b454caf0d88acd93c44a39d904842857eb7ab5881a482b23290100b93bcb061cf2a36b5e69270fc435a15814f6279151ed8ebe174761f4f85c2c8fe6c293c6556c43be2efd2ba25feee728cfcd6d3d639c1782ca56272054803345452ae349fd1902a11f4463316fc5ed82b49cc4056fc8a60b2e0378eb79ac88280100000000000067d22090fdfea64f790b44bf9508f10ef8b863bd7911e98b6a86eb5457a4721f6504ece70200b49efd525fe32325ddcd8ba436cee8640864e75c5954f8edb54d99672a8f3e22f8fbe23bd34fb6e481843edebf44cce9ddbc021314f4cd7d7fb311ca2b244b47fdf966dcb3251f2b90c452391096304a429042b1a8c8e9d1e0fd55baa26a53300100000000000020f58083438a0fe47174e2dd54ed55609ebcba9526af1db814e2418155c51aaeef438d490300a1b6746a8add8cac926ebe1d21fc7da9daa798482acfa9502446cc545e2173d74b034548267df9c0e41c65bf689ac4e8bf1abe07e69be42b2eb06eb8391cd00e5f650160f3e9363fb86821ddf04cf2517b09542c327fe4c92272f86b171936050200000000000062f020e5b72f1190221c2fac3a1e09ecac22c72eb21636fec97aa40e16b80fc4673450ed0000e2164613fa906b0d193b5a7af532d811ff0927cb6707dd9d9fb36afed9c3211f645a23593b8b9fe4134e60c4435a57e827c7b6db2118e42ba143af37f3d7ad46b5b289acb00ac86bc1e7812c716f3a7142d6c38a998daa37e217e32cbce29c16010000000000000d0fc0c4efc161c7f4b1dcd004ca6b92871cba1a79e74c4215d09981681b9968a5a408fb02006d800d156b3a4520ecbe5f59def1bfbbfd0852d97ad8f57e8c0ecdae4758825de74caf7ed5532f615c6401e797e60df00bf1f970c16b8034e23c156e02982e325f3434023ec917fe0f4ac2a623868bf7162ad6769399a5df845b3e7d3b8a34420100",
478 "witness_type": 9
479 },
480 "challenge_chain_sp_proof": {
481 "normalized_to_identity": false,
482 "witness": "0x0200e62e311b8380b817c4103f410adbddba77c011639a2fa8c8a157591e47c9b6a9147d5eff546f5a817ec82591e7a30038036c7ba399aafd225caa4557a9b1cd23733fb10305ac767d0188dd470379aeabece89034df58bcabc3dbd592e215dc0a010000000000000d0fc0c4efc161c7f4b1dcd004ca6b92871cba1a79e74c4215d09981681b9968a5a408fb02006d800d156b3a4520ecbe5f59def1bfbbfd0852d97ad8f57e8c0ecdae4758825de74caf7ed5532f615c6401e797e60df00bf1f970c16b8034e23c156e02982e325f3434023ec917fe0f4ac2a623868bf7162ad6769399a5df845b3e7d3b8a34420100",
483 "witness_type": 1
484 },
485 "finished_sub_slots": [],
486 "foliage": {
487 "foliage_block_data": {
488 "extension_data": "0x0000000000000000000000000000000000000000000000000000000003a2c7c9",
489 "farmer_reward_puzzle_hash": "0xd86028d22a28f4d0e4ee63808492630e7829af653fd710c683980959bb7bba1d",
490 "pool_signature": null,
491 "pool_target": {
492 "max_height": 0,
493 "puzzle_hash": "0x3ac292ed271257be352c526f975a1b376752c4ed6e453ea39ed449bd7b6e3c24"
494 },
495 "unfinished_reward_block_hash": "0x8ed67dc350dff7f63b0a5900ca6baacafa4ae33082d253c7182960d0d7de2422"
496 },
497 "foliage_block_data_signature": "0x8c18eba47ec21495a23a585b0b01e6c5418c7e856640c3e0a914e108ddcf55e182e5f0e3830d06108b265f4095411ef40741f333ca7476171a85d4a5637b1e7ddb42018415df1235ebe4bff5d35511695e0af5e0d3a5e74b25db03805e5f621d",
498 "foliage_transaction_block_hash": "0x4a8af14d06506534fa86c337b68716344f98ad6c5596a89ea943360e8c18ce01",
499 "foliage_transaction_block_signature": "0xb9188feb37010a83038774a4dd6a727eb66256baf46baacaf9d83d0d986b3e563f2f87b161961ffe1e4e04d1399e03430254f1e584c591204e199e94cea5cff2c7ca10943f32706d8550b63ae0fd682005c37abe268cef3d87d94fdc0d6c0278",
500 "prev_block_hash": "0x5a98b40a82040091846e703f7bef7e6c5ce4424b9dafbac76303dbf2c3bf0718",
501 "reward_block_hash": "0xa61c2bfbfaade587c93f9136e899b0607a226df606a4ccc4c6d256ceb4747cd9"
502 },
503 "foliage_transaction_block": {
504 "additions_root": "0xfceb313124f5f5c76224261abe7d2d7506c351d51e404c160439548d4fb1ac1b",
505 "filter_hash": "0x63e57b682a28970108b4134988f6a43306eae086d46a3e6abb642dc3fb01bdfc",
506 "prev_transaction_block_hash": "0x638e164bebfe63f4c467a707730718b54870c6b874d116336ab06d6feab5580f",
507 "removals_root": "0x09084ac5ffbfc03369f2769629160923c9401d7d0227d1a58d90fa9013193b69",
508 "timestamp": 1725991066,
509 "transactions_info_hash": "0xf92d7a6f46076be61395e3b2aef394fef384992489633bd8112977a39847403f"
510 },
511 "infused_challenge_chain_ip_proof": {
512 "normalized_to_identity": false,
513 "witness": "0x0000a2bd898a5aa2af9a75675f56dcb9429fe3e1a9d68fe46238e890894949be14b452dfa4968525265eba5a35eaffebc950bbcdf34b7b040ccb53e2005d06bcec040ffcc4871d10be96aca520fb9f5b6e6aa4c4c4ad20a2d63a7de155761804f80a02000000000000781e00ebba70d004de0251bf6d3ee264fdacfc98c86122dfb43ededa850e501288cec5d701005e49a9b084a9764457143ff81e83da474bbf0e0172456167e6671b9935ab1fae0aff07d8a52639277af86ac808bc072adebad38d35994bfe55f004804bdd4426e9870080938dc55d5002e21b731df6c746bd07fa8657582ef321a946bf6f960601000000000001684a60e020aacfe2f0ce689f49d544fe14c92fc6fe03a9b76fd32533df44dbf2aa70190d0100391b54249dae3f289f7266b60bcd57ccde098633f507df872450589c7ede0c697818bd16def7b80d1f3f82c099719777b9f7202b47391e9e5503357f4ffd2d0c14d50ac866f5829f4ea3dbb17a2f5b3a638440d24437d9d7aed369f7f97dec060805",
514 "witness_type": 2
515 },
516 "reward_chain_block": {
517 "challenge_chain_ip_vdf": {
518 "challenge": "0x3b229fc43c8bc35db264000ac30309fdecffd27b353fcf88dd43d61f9d47a497",
519 "number_of_iterations": 288147196,
520 "output": {
521 "data": "0x010086aafeadf030ada0f9fd384c0c8802d3a157600e9d3a15dadf158d6bb1203f53173658e9439957b4b9111a945fdd1ccb8f72a3ce5cef7d03e1d370f0edef3d63b5c3ee46b7425dfd299d00a990dc7150bb046c06703a3d224aee2938153a92360100"
522 }
523 },
524 "challenge_chain_sp_signature": "0xb2611e18db89cd73f6f84601407e3462834fef77bbb74684eabd251bbf7defbe18b5926a8d88c866e784960d9998c2100631ff10afdddfa81a9480e176150ee74b88d99c97cd97094108de890fb9fd660acaace69eb7d1e01005f19c06ad6383",
525 "challenge_chain_sp_vdf": {
526 "challenge": "0x3b229fc43c8bc35db264000ac30309fdecffd27b353fcf88dd43d61f9d47a497",
527 "number_of_iterations": 255066112,
528 "output": {
529 "data": "0x0200a4aec98980f7b1f87ab4fbc45b19425cb5c006ee104424d19b40ba016a1d3bfd0f0ad021b33e58f334e9dd1476d46ed27ce654929020d403a9d69ed659c3a8402531965bd9d43ef0b29781e97ff60db5d59e0a4aa5b8ca7292e935abada2bc150100"
530 }
531 },
532 "height": 5910291,
533 "infused_challenge_chain_ip_vdf": {
534 "challenge": "0x5606978b68f9fe3db577ab132a772e485ef4a96a0a2a53f8ee7f77e627c9c5e8",
535 "number_of_iterations": 34225136,
536 "output": {
537 "data": "0x000007393fe8b0f177674aca1e287bc25818222308585ead5065b6b36c61fe051ef2ae1e45dd06e05c8cbf32d43d275a40d081203c4cbc3b581af52e3dae4e5dea0b0c86fc34324b44afe996936275d405af49d62f32e593bf9e97933c77de698c170200"
538 }
539 },
540 "is_transaction_block": true,
541 "pos_ss_cc_challenge_hash": "0x3b229fc43c8bc35db264000ac30309fdecffd27b353fcf88dd43d61f9d47a497",
542 "proof_of_space": {
543 "challenge": "0xf6204fc238ec82939ad48a34be3b9e73cf427ffa221c934d02a3fde8371e00f6",
544 "plot_public_key": "0x8dd0b25d7aeb522c6e3fd3bec90c232a992b3d12bbb06612e49f703375b0f4c316b1c724c6f084611e14f481e5c46a03",
545 "pool_contract_puzzle_hash": "0x3ac292ed271257be352c526f975a1b376752c4ed6e453ea39ed449bd7b6e3c24",
546 "pool_public_key": null,
547 "proof": "0xd61223e241ef0257ccc668099b52c211d17d3368d9bc424103dc35494c6fe155826c2a57a6af5417f5624e509f03572f2803bd04f66ae3d6bc6ff195c145cb18f5d2f67e8e89212a15509c8fc0f5dc4c866009327fe73112eaef9a2b0b5d069d3528f489714f3f3db0f7ad07e80ea0b81ac14913637bd12afa466bea3e8de02797c92e08e5dd148e9867849c92caebaaea7d60164e2f2a7d30bb11285100991e083bdb3c2de28c2b6bef21499287efb9f635eba2fb9722fd66c309c110ac46a5afd21081f3a8ada06a9b14d5c1369c3077ce6020f24473356d5070cbae959bc935f72d7516819209d595ccdf6dbe382f36b25c70faa8fa03c7a5fa56f4c51de0",
548 "size": 32
549 },
550 "reward_chain_ip_vdf": {
551 "challenge": "0xc11606b3b528b6e45a196ba79e4625a2956ff62c056e22c16c0f0dead1a454b9",
552 "number_of_iterations": 34225136,
553 "output": {
554 "data": "0x0200d0abae11ee4e327bb67ba5baee204da48f23453099cb1ff7c04a7c7b08d03980d01e0ffe7263e5810a11ec99a218ff089165902192de7e4ce6ee8854b67bd489b96cfc124cc0e85c29e9fa23e4debccf59bd6a29415e2a321a219e0bd9cb8f3a0100"
555 }
556 },
557 "reward_chain_sp_signature": "0xa75ec769ecc4109317c8f9be6cf879e076ffbe23645f17482d2e3bcd5ae4ee05365d5a9e36d8b30895cc12494b75b2e20dc18bf343861f81a72581799e36ca9b11c681e2ee48260b9557a5a4930cde62fc494a8085e35696eacbb021392c3a4d",
558 "reward_chain_sp_vdf": {
559 "challenge": "0xc11606b3b528b6e45a196ba79e4625a2956ff62c056e22c16c0f0dead1a454b9",
560 "number_of_iterations": 1144052,
561 "output": {
562 "data": "0x02002623e30e6805d06247ca15976c75c541531329f69627c36631ec53c5661de771965e9894d1fe9075133d716e66eba1b4a00c34a0f2ef58fc149c5406bd296b33e1cf44be99ae72160e75b1c99fed284a68507fd0191c620bb37479cea4526c020200"
563 }
564 },
565 "signage_point_index": 28,
566 "total_iters": 45503147198204,
567 "weight": 27802025488
568 },
569 "reward_chain_ip_proof": {
570 "normalized_to_identity": false,
571 "witness": "0x01007eea0de3c013b51b6c4ba9a878efc51b93ff0173c5d358171f44e02eae59d6859243acbd1105456de342b710f20cb8fadc3af785aa0757c5f87e171dabb918275b2ded625ad5bb8f6c34c78011d0bf928573d479764f47385f859b3b3cf25e250100000000000016f300c81c899285ec7a40ca7a592e224a35de7b4a9f11e2163c8640be948f786572b621020078009e53ff37116fb1cc8e30ee646beb8e7a804a6592fd564e7b36f8fe234916a6d127535a1fa530fc99c0fdb96adbe99c734710763418400f6a402b952359027b45f3b2a9b2c86dd548292b6e12a5217d755e445759caa88d4bb411c1898c00130f000000000044e8a0b68edd518dae1b1a99ff80fa71282bcd0ad45a31876b3371c08c41c522c06487cd020018d5230eb62037be756e0a48487916ce84ea8d92b82bfae3c46bcfb64ff3afce96148274c39edae96a7bb75ab2fad437133ffddc004452dd321cb6505c02608bbdb993f12d4cd48b04bf05a1da25fbd7f0cd65e7521e2a1b56ab89d78cab586d0100000000000022ca40847fb4e0a3784d972bf6499b9602845d314a9794b830d2b206a627bdd29c9f9a1902001bd634a7210d0ee0d9858cdf6c55820291824c2d6c0685e3dad98c1e5aecba3f9a54ae2aedad61faa0d0dc22ff1b5147ccbc13031549adbacca8935af5311210d126fafaa192b20fd6ce521d1f37d5f26be455dac591f76bb37ce1b13eef581d02000000000000682fe0dc8b346cf205fa7fd21e19d988cd029d4a275546982e0a6f0194d64459952862ef0000b8a38ed48a241153b49a04784b4e8e5baf512b02744865b80d769196567aa2203c23eb19d476d7dfae27757de0d10aae45691098f6d6db8da1775ebeabb35d08ad357c37586dd7fde12eaddec7e5536d0548fbe6d0f56b30f6807eab6ed8690508020000000000229b608dae620f251cbdaa5afcdf6730ce465e0cc09926b5ad463b138dc648a48858a1c70100e0e746c2ed703a896f47c78aaa8c20545d0fa5f1588ad477e6b2027f7e9d1c6f25ff8abf765275700e535b1d1442440b2cc525de0ed6d1c127605cb05a68d78e25ad61b026475e104f266f061aedd640c08cbfa603833b46c5532da1510af2650100000000000067d220857041d223eb090f7b31a3dfc1c66accfdda2664131e1cb38e5e126605d0f21fbf0100e48406311dbf8054199fbc47dc5f68cc7c2c71271752f1231e7f79e71c33e1f2029a52e2209977106c3726900859df55230b9cf4627c3aaf947fe5e79ba6016ed10fb143b7f6608b60827c702b23927edccc9833db71994ed329d658a882cb340100000000000020f580e02adb6f55e8df89300d1f4eed9e2759a3ef7c5bfcec8606ccb4d0d7b24522434b0000e4492d9e872c6d39d8e5c2fa12cee3996dc0962ff5a4f5decea3f332c40a2680ce0430ac0547d99997d49b47a5ecf21b096b6a3e6eaa57c51fee52fcd89f1b728d83b6375024c1b00c2918ed71ddcbc362b9950c9573fe9f35747c9b7a434c3f0100000000000062f020ade99b5112bb363da39d85842ddaedaf9ccade1dc3f3f6ca82fa11f15c545b69e3000058285dda67599dd9e33cb468fbce1ac84ee403fd211b68e9f51f737be25172e24e433ff5b1d166e0903fb0692fecc9a57161a5bbeaa470d432b6d29af785cd23859aaea7a41fe774518fc15efa0641e2a6a2a451f6fbc735c51834ee1fcfcf32010000000000000d0fc0e13fdaaf42287e3b4f74a4b69e6087022e3ab1e596f209be7b68159a2f0978351101001595f7da758e3006a16985ccd8867b18b2e840d32ea2eda2398ef0370e150261cef263c1ff5f17a333b747432991f6a2396769b46747620e7365fa88f823bc3735f9513ebc6ae83d0cf04027ad09cef12fbca7a8a8b162127f741f3abe6f89070100",
572 "witness_type": 9
573 },
574 "reward_chain_sp_proof": {
575 "normalized_to_identity": false,
576 "witness": "0x02000db29d4f8513d0cb7c56162478a539ff5b863608514b52dd2ee9cee690b5deec82eafca531e72e2da4473bc0fd616b0b6435217ae0fd4b9fa2f2c6c805e7b215556d9d7ec4db0d9c99a2d9c8ced4879fc9855d01cd68b1c38e105a52f968d822020000000000000d0fc0e13fdaaf42287e3b4f74a4b69e6087022e3ab1e596f209be7b68159a2f0978351101001595f7da758e3006a16985ccd8867b18b2e840d32ea2eda2398ef0370e150261cef263c1ff5f17a333b747432991f6a2396769b46747620e7365fa88f823bc3735f9513ebc6ae83d0cf04027ad09cef12fbca7a8a8b162127f741f3abe6f89070100",
577 "witness_type": 1
578 },
579 "transactions_generator": "0x80",
580 "transactions_generator_ref_list": [],
581 "transactions_info": {
582 "aggregated_signature": "0xa6ffd8e8b1df9d35e11d2dbd29b81827f543963d817c5026619a44a31a8558823a0c18b0b533bca2b68dfcb5000d759200b5e3648420dc108ddd2a455f8a966d2f593d728b7858915e2c5600f8d030354b37a556bb2e3bef9ec7a6af9d15494f",
583 "cost": 162600245,
584 "fees": 205449550,
585 "generator_refs_root": "0x0101010101010101010101010101010101010101010101010101010101010101",
586 "generator_root": "0x6b2c8956c7d31a0ed3a4440cae16f2b7ee6917818909f052867f08e407d3d589",
587 "reward_claims_incorporated": [
588 {
589 "amount": 875000000000,
590 "parent_coin_info": "0xccd5bb71183532bff220ba46c268991a000000000000000000000000005a2f10",
591 "puzzle_hash": "0x3a9ba81f693aac5ba77bc1e42ff55fda4cbaab478bdb1302b5fbe64398f272cd"
592 },
593 {
594 "amount": 125000000000,
595 "parent_coin_info": "0x3ff07eb358e8255a65c30a2dce0e5fbb000000000000000000000000005a2f10",
596 "puzzle_hash": "0xf120223400081b4b58b83eafdbfa536b306c055c56ee6491824e91cc05f88845"
597 }
598 ]
599 }
600 },
601 "success": true
602 }"#,
603 );
604
605 let response = client
606 .get_block(Bytes32::from(hex!(
607 "88a8e404c419e12bb11e809ff7afc8b1fcda77270fe3f157cff8a2fab4f44e8b"
608 )))
609 .await
610 .unwrap();
611
612 assert!(response.success);
613 assert!(response.block.is_some());
614 assert!(response.error.is_none());
615
616 let block = response.block.unwrap();
617 assert_eq!(block.height(), 5_910_291);
618 assert_eq!(block.weight(), 27_802_025_488);
619 assert_eq!(block.total_iters(), 45_503_147_198_204);
620
621 assert!(block.transactions_info.is_some());
622 assert!(
623 block
624 .transactions_info
625 .unwrap()
626 .reward_claims_incorporated
627 .len()
628 == 2
629 );
630 }
631
632 #[tokio::test]
633 async fn test_get_block_record_success() {
634 let mut client = MockRpcClient::new();
635
636 client.mock_response(
637 "http://api.example.com/get_block_record",
638 r#"{
639 "block_record": {
640 "challenge_block_info_hash": "0xc3a285c97ef0fd5b941e2133432159ff6db2599e2f806cb3565781ccf6427689",
641 "challenge_vdf_output": {
642 "data": "0x010086aafeadf030ada0f9fd384c0c8802d3a157600e9d3a15dadf158d6bb1203f53173658e9439957b4b9111a945fdd1ccb8f72a3ce5cef7d03e1d370f0edef3d63b5c3ee46b7425dfd299d00a990dc7150bb046c06703a3d224aee2938153a92360100"
643 },
644 "deficit": 3,
645 "farmer_puzzle_hash": "0xd86028d22a28f4d0e4ee63808492630e7829af653fd710c683980959bb7bba1d",
646 "fees": 205449550,
647 "finished_challenge_slot_hashes": null,
648 "finished_infused_challenge_slot_hashes": null,
649 "finished_reward_slot_hashes": null,
650 "header_hash": "0x88a8e404c419e12bb11e809ff7afc8b1fcda77270fe3f157cff8a2fab4f44e8b",
651 "height": 5910291,
652 "infused_challenge_vdf_output": {
653 "data": "0x000007393fe8b0f177674aca1e287bc25818222308585ead5065b6b36c61fe051ef2ae1e45dd06e05c8cbf32d43d275a40d081203c4cbc3b581af52e3dae4e5dea0b0c86fc34324b44afe996936275d405af49d62f32e593bf9e97933c77de698c170200"
654 },
655 "overflow": false,
656 "pool_puzzle_hash": "0x3ac292ed271257be352c526f975a1b376752c4ed6e453ea39ed449bd7b6e3c24",
657 "prev_hash": "0x5a98b40a82040091846e703f7bef7e6c5ce4424b9dafbac76303dbf2c3bf0718",
658 "prev_transaction_block_hash": "0x638e164bebfe63f4c467a707730718b54870c6b874d116336ab06d6feab5580f",
659 "prev_transaction_block_height": 5910288,
660 "required_iters": 5752572,
661 "reward_claims_incorporated": [
662 {
663 "amount": 875000000000,
664 "parent_coin_info": "0xccd5bb71183532bff220ba46c268991a000000000000000000000000005a2f10",
665 "puzzle_hash": "0x3a9ba81f693aac5ba77bc1e42ff55fda4cbaab478bdb1302b5fbe64398f272cd"
666 },
667 {
668 "amount": 125000000000,
669 "parent_coin_info": "0x3ff07eb358e8255a65c30a2dce0e5fbb000000000000000000000000005a2f10",
670 "puzzle_hash": "0xf120223400081b4b58b83eafdbfa536b306c055c56ee6491824e91cc05f88845"
671 },
672 {
673 "amount": 875000000000,
674 "parent_coin_info": "0xccd5bb71183532bff220ba46c268991a000000000000000000000000005a2f0f",
675 "puzzle_hash": "0x3a9ba81f693aac5ba77bc1e42ff55fda4cbaab478bdb1302b5fbe64398f272cd"
676 },
677 {
678 "amount": 125000000000,
679 "parent_coin_info": "0x3ff07eb358e8255a65c30a2dce0e5fbb000000000000000000000000005a2f0f",
680 "puzzle_hash": "0xf120223400081b4b58b83eafdbfa536b306c055c56ee6491824e91cc05f88845"
681 },
682 {
683 "amount": 875000000000,
684 "parent_coin_info": "0xccd5bb71183532bff220ba46c268991a000000000000000000000000005a2f0e",
685 "puzzle_hash": "0xcd975114b70a116841c7abf214d6c859f1ad5b11b95bb2cfa5a7dfa6883e805f"
686 },
687 {
688 "amount": 125000000000,
689 "parent_coin_info": "0x3ff07eb358e8255a65c30a2dce0e5fbb000000000000000000000000005a2f0e",
690 "puzzle_hash": "0x480454e7d65a9cd6a71944953bcc32ad622b23184091579a1318446fa7301a2d"
691 }
692 ],
693 "reward_infusion_new_challenge": "0xa61c2bfbfaade587c93f9136e899b0607a226df606a4ccc4c6d256ceb4747cd9",
694 "signage_point_index": 28,
695 "sub_epoch_summary_included": null,
696 "sub_slot_iters": 583008256,
697 "timestamp": 1725991066,
698 "total_iters": 45503147198204,
699 "weight": 27802025488
700 },
701 "success": true
702 }"#,
703 );
704
705 let response = client
706 .get_block_record(Bytes32::from(hex!(
707 "88a8e404c419e12bb11e809ff7afc8b1fcda77270fe3f157cff8a2fab4f44e8b"
708 )))
709 .await
710 .unwrap();
711
712 assert!(response.success);
713 assert!(response.block_record.is_some());
714 assert!(response.error.is_none());
715
716 let block_record = response.block_record.unwrap();
717 assert_eq!(block_record.height, 5_910_291);
718 assert_eq!(block_record.weight, 27_802_025_488);
719 assert_eq!(block_record.total_iters, 45_503_147_198_204);
720 assert_eq!(block_record.reward_claims_incorporated.unwrap().len(), 6);
721 }
722
723 #[tokio::test]
724 async fn test_get_puzzle_and_solution() {
725 let mut client = MockRpcClient::new();
726
727 client.mock_response(
728 "http://api.example.com/get_puzzle_and_solution",
729 r#"{
730 "coin_solution": {
731 "coin": {
732 "amount": 7100000,
733 "parent_coin_info": "0xa7658d2add3c2fc83eb07cc2655e4fe4fe630627dd07d3e9a83f9dac03ada8b1",
734 "puzzle_hash": "0xfbacdd2364a53e036af892c9d4e3b593eb12ae4a939776a251abb68de857b3fc"
735 },
736 "puzzle_reveal": "0xff04ff02ff8080",
737 "solution": "0xff0180"
738 },
739 "success": true
740 }"#,
741 );
742
743 let coin_id = Bytes32::from(hex!(
744 "bae7ca992d0062ec050158d00880577afc38a12b5c3af1874d2ce2759eb50ae1"
745 ));
746 let response = client.get_puzzle_and_solution(coin_id, None).await.unwrap();
747 assert!(response.success);
748 assert!(response.coin_solution.is_some());
749 assert!(response.error.is_none());
750
751 let coin_solution = response.coin_solution.unwrap();
752 assert_eq!(coin_solution.coin.coin_id(), coin_id);
753 assert_eq!(
754 coin_solution.puzzle_reveal.to_bytes().unwrap(),
755 hex!("ff04ff02ff8080")
756 );
757 assert_eq!(coin_solution.solution.to_bytes().unwrap(), hex!("ff0180"));
758 }
759}