1use std::collections::HashSet;
2
3use chia_bls::SecretKey;
4use chia_protocol::Bytes32;
5use chia_sdk_coinset::{
6 AdditionsAndRemovalsResponse, BlockchainState, BlockchainStateResponse, CoinRecord,
7 GetBlockRecordResponse, GetBlockRecordsResponse, GetBlockSpendsResponse, GetCoinRecordResponse,
8 GetCoinRecordsResponse, GetMempoolItemResponse, GetMempoolItemsResponse,
9 GetNetworkInfoResponse, GetPuzzleAndSolutionResponse, MempoolMinFees, SyncState,
10};
11
12use super::{FullNodeSimulator, SIMULATOR_GENESIS_CHALLENGE, SimCoinRecord, ValidatedBundle};
13
14impl FullNodeSimulator {
15 pub fn height(&self) -> u32 {
16 self.state.height
17 }
18
19 pub fn header_hash(&self) -> Bytes32 {
20 self.state.header_hashes.last().copied().unwrap_or_default()
21 }
22
23 pub fn header_hash_of(&self, height: u32) -> Option<Bytes32> {
24 self.state
25 .header_hashes
26 .get((height as usize).saturating_sub(1))
27 .copied()
28 }
29
30 pub fn get_farming_ph(&self) -> Bytes32 {
31 self.farming_puzzle_hash
32 }
33
34 pub fn get_master_secret_key(&self) -> SecretKey {
35 self.master_secret_key.clone()
36 }
37
38 pub fn get_prefarm_puzzle_hash(&self) -> Bytes32 {
39 self.prefarm_puzzle_hash
40 }
41
42 pub fn get_blockchain_state(&self) -> BlockchainStateResponse {
43 let peak = self.state.blocks.get(&self.header_hash()).map_or_else(
44 || {
45 Self::make_block_record(
46 Bytes32::default(),
47 Bytes32::default(),
48 0,
49 0,
50 Bytes32::default(),
51 0,
52 0,
53 self.farming_puzzle_hash,
54 Vec::new(),
55 )
56 },
57 |block| block.record.clone(),
58 );
59
60 BlockchainStateResponse {
61 blockchain_state: Some(BlockchainState {
62 average_block_time: 1,
63 block_max_cost: 11_000_000_000,
64 difficulty: 1,
65 genesis_challenge_initialized: true,
66 mempool_cost: self.mempool.values().map(|item| item.cost).sum(),
67 mempool_fees: self.mempool.values().map(|item| item.fee).sum(),
68 mempool_max_total_cost: 110_000_000_000,
69 mempool_min_fees: MempoolMinFees { cost_5000000: 0 },
70 mempool_size: self.mempool.len().try_into().unwrap(),
71 node_id: self.node_id,
72 peak,
73 space: 0,
74 sub_slot_iters: 1,
75 sync: SyncState {
76 sync_mode: false,
77 sync_progress_height: self.state.height,
78 sync_tip_height: self.state.height,
79 synced: true,
80 },
81 }),
82 error: None,
83 success: true,
84 }
85 }
86
87 pub fn get_network_info(&self) -> GetNetworkInfoResponse {
88 GetNetworkInfoResponse {
89 network_name: Some("simulator0".to_string()),
90 network_prefix: Some("txch".to_string()),
91 genesis_challenge: Some(SIMULATOR_GENESIS_CHALLENGE),
92 error: None,
93 success: true,
94 }
95 }
96
97 pub fn get_aggsig_additional_data(&self) -> Bytes32 {
98 SIMULATOR_GENESIS_CHALLENGE
99 }
100
101 pub fn get_block_record(&self, header_hash: Bytes32) -> GetBlockRecordResponse {
102 GetBlockRecordResponse {
103 block_record: self
104 .state
105 .blocks
106 .get(&header_hash)
107 .or_else(|| self.orphaned_blocks.get(&header_hash))
108 .map(|block| block.record.clone()),
109 error: None,
110 success: true,
111 }
112 }
113
114 pub fn get_block_record_by_height(&self, height: u32) -> GetBlockRecordResponse {
115 let block_record = self
116 .header_hash_of(height)
117 .and_then(|header_hash| self.state.blocks.get(&header_hash))
118 .map(|block| block.record.clone());
119
120 GetBlockRecordResponse {
121 block_record,
122 error: None,
123 success: true,
124 }
125 }
126
127 pub fn get_block_records(&self, start: u32, end: u32) -> GetBlockRecordsResponse {
128 let block_records = (start..end)
129 .filter_map(|height| self.get_block_record_by_height(height).block_record)
130 .collect();
131
132 GetBlockRecordsResponse {
133 block_records: Some(block_records),
134 error: None,
135 success: true,
136 }
137 }
138
139 pub fn get_additions_and_removals(&self, header_hash: Bytes32) -> AdditionsAndRemovalsResponse {
140 let Some(block) = self
141 .state
142 .blocks
143 .get(&header_hash)
144 .or_else(|| self.orphaned_blocks.get(&header_hash))
145 else {
146 return AdditionsAndRemovalsResponse {
147 additions: None,
148 removals: None,
149 error: Some("block not found".to_string()),
150 success: false,
151 };
152 };
153
154 AdditionsAndRemovalsResponse {
155 additions: Some(self.records_for_ids(&block.additions)),
156 removals: Some(self.records_for_ids(&block.removals)),
157 error: None,
158 success: true,
159 }
160 }
161
162 pub fn get_block_spends(&self, header_hash: Bytes32) -> GetBlockSpendsResponse {
163 GetBlockSpendsResponse {
164 block_spends: self
165 .state
166 .blocks
167 .get(&header_hash)
168 .or_else(|| self.orphaned_blocks.get(&header_hash))
169 .map(|block| block.spends.clone()),
170 error: None,
171 success: true,
172 }
173 }
174
175 pub fn get_coin_record_by_name(&self, name: Bytes32) -> GetCoinRecordResponse {
176 GetCoinRecordResponse {
177 coin_record: self
178 .state
179 .coins
180 .get(&name)
181 .map(|record| record.to_coin_record()),
182 error: None,
183 success: true,
184 }
185 }
186
187 pub fn get_coin_records_by_names(
188 &self,
189 names: &[Bytes32],
190 start_height: Option<u32>,
191 end_height: Option<u32>,
192 include_spent_coins: Option<bool>,
193 ) -> GetCoinRecordsResponse {
194 Self::records_response(
195 self.state
196 .coins
197 .iter()
198 .filter(|(coin_id, _)| names.contains(coin_id))
199 .map(|(_, record)| *record),
200 start_height,
201 end_height,
202 include_spent_coins,
203 )
204 }
205
206 pub fn get_coin_records_by_hint(
207 &self,
208 hint: Bytes32,
209 start_height: Option<u32>,
210 end_height: Option<u32>,
211 include_spent_coins: Option<bool>,
212 ) -> GetCoinRecordsResponse {
213 self.get_coin_records_by_hints(vec![hint], start_height, end_height, include_spent_coins)
214 }
215
216 pub fn get_coin_records_by_hints(
217 &self,
218 hints: Vec<Bytes32>,
219 start_height: Option<u32>,
220 end_height: Option<u32>,
221 include_spent_coins: Option<bool>,
222 ) -> GetCoinRecordsResponse {
223 let hints: HashSet<Bytes32> = hints.into_iter().collect();
224 Self::records_response(
225 self.state
226 .coins
227 .iter()
228 .filter(|(coin_id, _)| {
229 self.state
230 .coin_hints
231 .get(*coin_id)
232 .is_some_and(|hint| hints.contains(hint))
233 })
234 .map(|(_, record)| *record),
235 start_height,
236 end_height,
237 include_spent_coins,
238 )
239 }
240
241 pub fn get_coin_records_by_parent_ids(
242 &self,
243 parent_ids: Vec<Bytes32>,
244 start_height: Option<u32>,
245 end_height: Option<u32>,
246 include_spent_coins: Option<bool>,
247 ) -> GetCoinRecordsResponse {
248 let parent_ids: HashSet<Bytes32> = parent_ids.into_iter().collect();
249 Self::records_response(
250 self.state
251 .coins
252 .values()
253 .filter(|record| parent_ids.contains(&record.coin.parent_coin_info))
254 .copied(),
255 start_height,
256 end_height,
257 include_spent_coins,
258 )
259 }
260
261 pub fn get_coin_records_by_puzzle_hash(
262 &self,
263 puzzle_hash: Bytes32,
264 start_height: Option<u32>,
265 end_height: Option<u32>,
266 include_spent_coins: Option<bool>,
267 ) -> GetCoinRecordsResponse {
268 self.get_coin_records_by_puzzle_hashes(
269 vec![puzzle_hash],
270 start_height,
271 end_height,
272 include_spent_coins,
273 )
274 }
275
276 pub fn get_coin_records_by_puzzle_hashes(
277 &self,
278 puzzle_hashes: Vec<Bytes32>,
279 start_height: Option<u32>,
280 end_height: Option<u32>,
281 include_spent_coins: Option<bool>,
282 ) -> GetCoinRecordsResponse {
283 let puzzle_hashes: HashSet<Bytes32> = puzzle_hashes.into_iter().collect();
284 Self::records_response(
285 self.state
286 .coins
287 .values()
288 .filter(|record| puzzle_hashes.contains(&record.coin.puzzle_hash))
289 .copied(),
290 start_height,
291 end_height,
292 include_spent_coins,
293 )
294 }
295
296 pub fn get_puzzle_and_solution(
297 &self,
298 coin_id: Bytes32,
299 height: Option<u32>,
300 ) -> GetPuzzleAndSolutionResponse {
301 let coin_solution = self.state.coin_spends.get(&coin_id).and_then(|spend| {
302 let record = self.state.coins.get(&coin_id)?;
303 if height.is_none() || record.spent_block_index == height {
304 Some(spend.clone())
305 } else {
306 None
307 }
308 });
309
310 GetPuzzleAndSolutionResponse {
311 coin_solution,
312 error: None,
313 success: true,
314 }
315 }
316
317 pub fn get_mempool_item_by_tx_id(&self, tx_id: Bytes32) -> GetMempoolItemResponse {
318 GetMempoolItemResponse {
319 mempool_item: self
320 .mempool
321 .get(&tx_id)
322 .map(ValidatedBundle::to_mempool_item),
323 error: None,
324 success: true,
325 }
326 }
327
328 pub fn get_mempool_items_by_coin_name(&self, coin_name: Bytes32) -> GetMempoolItemsResponse {
329 GetMempoolItemsResponse {
330 mempool_items: Some(
331 self.mempool
332 .values()
333 .filter(|item| item.removals.contains(&coin_name))
334 .map(ValidatedBundle::to_mempool_item)
335 .collect(),
336 ),
337 error: None,
338 success: true,
339 }
340 }
341
342 pub(super) fn records_for_ids(&self, coin_ids: &[Bytes32]) -> Vec<CoinRecord> {
343 coin_ids
344 .iter()
345 .filter_map(|coin_id| self.state.coins.get(coin_id))
346 .map(|record| record.to_coin_record())
347 .collect()
348 }
349
350 fn records_response(
351 records: impl IntoIterator<Item = SimCoinRecord>,
352 start_height: Option<u32>,
353 end_height: Option<u32>,
354 include_spent_coins: Option<bool>,
355 ) -> GetCoinRecordsResponse {
356 let include_spent = include_spent_coins.unwrap_or(false);
357 let records = records
358 .into_iter()
359 .filter(|record| include_spent || record.spent_block_index.is_none())
360 .filter(|record| {
361 start_height.is_none_or(|start| record.confirmed_block_index >= start)
362 && end_height.is_none_or(|end| record.confirmed_block_index < end)
363 })
364 .map(SimCoinRecord::to_coin_record)
365 .collect();
366
367 GetCoinRecordsResponse {
368 coin_records: Some(records),
369 error: None,
370 success: true,
371 next_cursor: None,
372 truncated: None,
373 }
374 }
375}