api.rs (24693B)
1 use std::collections::{BTreeMap, BTreeSet}; 2 use std::net::SocketAddr; 3 4 use anyhow::Result; 5 use axum::{ 6 Json, 7 extract::{Query, State}, 8 }; 9 10 use crate::{ 11 adapters::p2p::P2pMetrics, 12 app::{NodeStatus, PeerInfo}, 13 domain::{AddressNetwork, OutPoint, Transaction, TransactionV2, TxOutput, decode_hex}, 14 ip_geolocation::IpGeolocation, 15 }; 16 17 use super::types::{LeaderboardEntry, MetricsLeaderboards}; 18 use super::{ 19 BlocksQuery, ConfigResponse, MempoolCounts, MetricsQuery, MetricsResponse, 20 NetworkHealthLocalState, NetworkHealthResponse, Page, PageQuery, PeerPresentation, UiBlock, 21 UiTransaction, WalletTransactionContext, WalletTransactionFilters, WalletTransactionRow, 22 WalletTransactionsQuery, WalletUtxoRow, 23 }; 24 use super::{ 25 DATASET_LIMIT, DATASET_PAGE_LIMIT, EXPLORER_LIMIT, EXPLORER_PAGE_LIMIT, HttpState, 26 add_pending_outputs, add_pending_v2_outputs, metrics_response, network_health, 27 populate_wallet_reward_flow, top_mine_proofs, transaction_v2_input_outpoints, 28 ui_blocks_from_indexes, ui_transaction, ui_transaction_v2, wallet_transaction_rows, 29 wallet_transaction_v2_row, wallet_transaction_v2_rows, 30 }; 31 32 pub(super) async fn api_status(State(state): State<HttpState>) -> Json<NodeStatus> { 33 let mut status = state.node.lock().await.status(); 34 status.stratum = state.stratum.clone(); 35 Json(status) 36 } 37 38 pub(super) async fn api_blocks( 39 State(state): State<HttpState>, 40 Query(query): Query<BlocksQuery>, 41 ) -> Json<Vec<UiBlock>> { 42 let limit = query 43 .limit 44 .unwrap_or(EXPLORER_PAGE_LIMIT) 45 .min(EXPLORER_LIMIT); 46 let (tip_hash, blocks, storage_size_breakdowns, transaction_v2_domain, network) = { 47 let node = state.node.lock().await; 48 let blocks = match query.before_height { 49 Some(before_height) => node.blocks_before(before_height, limit), 50 None => node.recent_blocks(limit), 51 }; 52 let storage_size_breakdowns = node.block_storage_size_breakdowns(&blocks); 53 ( 54 node.chain_tip_hash(), 55 blocks, 56 storage_size_breakdowns, 57 node.ledger().transaction_v2_domain().ok(), 58 AddressNetwork::from_profile_id(&node.ledger().launch_profile().profile_id), 59 ) 60 }; 61 let store = state.ui_data_store.clone(); 62 let view = tokio::task::spawn_blocking(move || store.load_ui_chain_index(&tip_hash)) 63 .await 64 .ok() 65 .and_then(Result::ok) 66 .flatten() 67 .unwrap_or_default(); 68 Json(ui_blocks_from_indexes( 69 blocks, 70 &view.outputs, 71 &view.burn_leader_ranks_by_hash, 72 &storage_size_breakdowns, 73 transaction_v2_domain.as_ref(), 74 network, 75 )) 76 } 77 78 pub(super) async fn api_config(State(state): State<HttpState>) -> Json<ConfigResponse> { 79 Json(ConfigResponse { 80 config: state.ui_config.lock().await.clone(), 81 p2p_inbound_runtime_active: state.gossip.accepts_inbound().await, 82 p2p_runtime_bind_addr: state.gossip.listen_addr().to_string(), 83 stratum_runtime_enabled: state.stratum.enabled, 84 stratum_runtime_listen_addr: state.stratum.listen_addr.clone(), 85 wallet_endpoint_runtime_enabled: state.wallet_endpoint_addr.is_some(), 86 wallet_endpoint_runtime_listen_addr: state 87 .wallet_endpoint_addr 88 .map(|addr| addr.to_string()), 89 }) 90 } 91 92 pub(super) async fn api_mempool( 93 State(state): State<HttpState>, 94 Query(query): Query<PageQuery>, 95 ) -> Json<Page<UiTransaction>> { 96 let (pending, pending_v2, domain, network, confirmed_outputs) = { 97 let node = state.node.lock().await; 98 let pending_v2 = node.pending_transactions_v2(); 99 let confirmed_outputs = pending_v2 100 .iter() 101 .flat_map(transaction_v2_input_outpoints) 102 .filter_map(|outpoint| { 103 node.ledger() 104 .output_for_outpoint(&outpoint) 105 .map(|output| (outpoint, output)) 106 }) 107 .collect::<BTreeMap<_, _>>(); 108 ( 109 node.pending_transactions(), 110 pending_v2, 111 node.ledger().transaction_v2_domain().ok(), 112 AddressNetwork::from_profile_id(&node.ledger().launch_profile().profile_id), 113 confirmed_outputs, 114 ) 115 }; 116 let mut required_outputs = BTreeSet::new(); 117 collect_transaction_input_outpoints(pending.iter(), &mut required_outputs); 118 let mut outputs = load_outputs_for_outpoints(&state, required_outputs) 119 .await 120 .unwrap_or_default(); 121 add_pending_outputs(&mut outputs, &pending); 122 let mut items = pending 123 .iter() 124 .map(|tx| ui_transaction(tx, &outputs)) 125 .collect::<Vec<_>>(); 126 let mut v2_outputs = confirmed_outputs; 127 if let Some(domain) = domain.as_ref() { 128 let _ = add_pending_v2_outputs(&mut v2_outputs, &pending_v2, domain, network); 129 items.extend(pending_v2.iter().filter_map(|transaction| { 130 ui_transaction_v2(transaction, &v2_outputs, domain, network).ok() 131 })); 132 } 133 items.reverse(); 134 Json(page_items(items, query)) 135 } 136 137 pub(super) async fn api_wallet_transactions( 138 State(state): State<HttpState>, 139 Query(query): Query<WalletTransactionsQuery>, 140 ) -> Json<Page<WalletTransactionRow>> { 141 let page_query = query.page(); 142 let offset = page_query.offset.unwrap_or(0); 143 let limit = page_query 144 .limit 145 .unwrap_or(DATASET_PAGE_LIMIT) 146 .clamp(1, DATASET_LIMIT); 147 let filters = WalletTransactionFilters::from_query(query); 148 let (wallet_addresses, pending, pending_v2, domain, network, v2_outputs) = { 149 let node = state.node.lock().await; 150 let status = node.status(); 151 let mut wallet_addresses = node 152 .wallet_owned_addresses() 153 .unwrap_or_else(|_| vec![node.wallet_address().to_string()]); 154 wallet_addresses.push(status.wallet_receive_address); 155 wallet_addresses.sort(); 156 wallet_addresses.dedup(); 157 let pending_v2 = node.pending_transactions_v2(); 158 let v2_outputs = pending_v2 159 .iter() 160 .flat_map(transaction_v2_input_outpoints) 161 .filter_map(|outpoint| { 162 node.ledger() 163 .output_for_outpoint(&outpoint) 164 .map(|output| (outpoint, output)) 165 }) 166 .collect::<BTreeMap<_, _>>(); 167 ( 168 wallet_addresses, 169 node.pending_transactions(), 170 pending_v2, 171 node.ledger().transaction_v2_domain().ok(), 172 AddressNetwork::from_profile_id(&node.ledger().launch_profile().profile_id), 173 v2_outputs, 174 ) 175 }; 176 let mut pending_required_outputs = BTreeSet::new(); 177 collect_transaction_input_outpoints(pending.iter(), &mut pending_required_outputs); 178 let mut pending_outputs = load_outputs_for_outpoints(&state, pending_required_outputs) 179 .await 180 .unwrap_or_default(); 181 add_pending_outputs(&mut pending_outputs, &pending); 182 let mut pending_rows = wallet_transaction_rows( 183 &wallet_addresses, 184 pending.clone(), 185 &[], 186 &pending_outputs, 187 filters, 188 ); 189 if let Some(domain) = domain.as_ref() { 190 let mut v2_outputs = v2_outputs; 191 let _ = add_pending_v2_outputs(&mut v2_outputs, &pending_v2, domain, network); 192 let mut v2_rows = wallet_transaction_v2_rows( 193 &wallet_addresses, 194 &pending_v2, 195 &v2_outputs, 196 filters, 197 domain, 198 network, 199 ); 200 v2_rows.append(&mut pending_rows); 201 pending_rows = v2_rows; 202 } 203 let pending_total = pending_rows.len(); 204 let mut items = pending_rows 205 .into_iter() 206 .skip(offset.min(pending_total)) 207 .take(limit) 208 .collect::<Vec<_>>(); 209 210 let confirmed_offset = offset.saturating_sub(pending_total); 211 let remaining_limit = limit.saturating_sub(items.len()); 212 let kinds = wallet_transaction_filter_kinds(filters); 213 let store = state.ui_data_store.clone(); 214 let wallet_addresses_for_query = wallet_addresses.clone(); 215 let confirmed_fetch_limit = confirmed_offset.saturating_add(remaining_limit); 216 let ((confirmed_rows, confirmed_legacy_total), (confirmed_v2_rows, confirmed_v2_total)) = 217 tokio::task::spawn_blocking(move || -> Result<_> { 218 Ok(( 219 store.load_wallet_transactions_for_addresses( 220 &wallet_addresses_for_query, 221 &kinds, 222 0, 223 confirmed_fetch_limit, 224 )?, 225 store.load_wallet_transactions_v2( 226 &wallet_addresses_for_query, 227 &kinds, 228 0, 229 confirmed_fetch_limit, 230 )?, 231 )) 232 }) 233 .await 234 .ok() 235 .and_then(Result::ok) 236 .unwrap_or_default(); 237 let decoded_confirmed_v2 = domain.as_ref().map_or_else(Vec::new, |expected_domain| { 238 confirmed_v2_rows 239 .into_iter() 240 .filter_map(|row| { 241 let bytes = decode_hex(&row.envelope).ok()?; 242 let (decoded_domain, transaction) = TransactionV2::decode(&bytes).ok()?; 243 (decoded_domain == *expected_domain).then_some((row, transaction)) 244 }) 245 .collect::<Vec<_>>() 246 }); 247 let mut confirmed_required_outputs = BTreeSet::new(); 248 collect_transaction_input_outpoints( 249 confirmed_rows.iter().map(|row| &row.transaction), 250 &mut confirmed_required_outputs, 251 ); 252 for (_, transaction) in &decoded_confirmed_v2 { 253 confirmed_required_outputs.extend(transaction_v2_input_outpoints(transaction)); 254 } 255 let confirmed_outputs = load_outputs_for_outpoints(&state, confirmed_required_outputs) 256 .await 257 .unwrap_or_default(); 258 let reward_blocks = { 259 let node = state.node.lock().await; 260 confirmed_rows 261 .iter() 262 .filter(|row| row.kind == "reward") 263 .filter_map(|row| { 264 let block = node.chain().get(row.block_height as usize)?.clone(); 265 Some((row.block_height, block)) 266 }) 267 .collect::<BTreeMap<_, _>>() 268 }; 269 let mut confirmed_items = confirmed_rows 270 .into_iter() 271 .filter_map(|row| { 272 let sort_key = row.sort_key; 273 let is_reward = row.kind == "reward"; 274 let mut item = super::ui::wallet_transaction_row_for_addresses( 275 &wallet_addresses, 276 &row.transaction, 277 &confirmed_outputs, 278 &WalletTransactionContext { 279 status: "confirmed", 280 block_height: Some(row.block_height), 281 timestamp_ms: Some(row.timestamp_ms), 282 block_finalizer: Some(row.block_finalizer), 283 }, 284 )?; 285 if is_reward { 286 item.kind = "reward"; 287 item.from = "fees".to_string(); 288 item.direction = "reward"; 289 if let Some(block) = reward_blocks.get(&row.block_height) { 290 populate_wallet_reward_flow(&mut item, block); 291 } 292 } 293 Some((sort_key, item)) 294 }) 295 .collect::<Vec<_>>(); 296 if let Some(domain) = domain.as_ref() { 297 confirmed_items.extend(decoded_confirmed_v2.into_iter().filter_map( 298 |(row, transaction)| { 299 let sort_key = row.sort_key; 300 let context = WalletTransactionContext { 301 status: "confirmed", 302 block_height: Some(row.block_height), 303 timestamp_ms: Some(row.timestamp_ms), 304 block_finalizer: Some(row.block_finalizer), 305 }; 306 wallet_transaction_v2_row( 307 &wallet_addresses, 308 &transaction, 309 &confirmed_outputs, 310 domain, 311 network, 312 &context, 313 ) 314 .ok() 315 .flatten() 316 .map(|item| (sort_key, item)) 317 }, 318 )); 319 } 320 confirmed_items.sort_by(|left, right| right.0.cmp(&left.0)); 321 items.extend( 322 confirmed_items 323 .into_iter() 324 .skip(confirmed_offset) 325 .take(remaining_limit) 326 .map(|(_, item)| item), 327 ); 328 let confirmed_total = confirmed_legacy_total.saturating_add(confirmed_v2_total); 329 let total = pending_total + confirmed_total; 330 let next_offset = offset + items.len(); 331 Json(Page { 332 items, 333 offset: offset.min(total), 334 limit, 335 total, 336 has_more: next_offset < total, 337 next_offset: (next_offset < total).then_some(next_offset), 338 }) 339 } 340 341 async fn load_outputs_for_outpoints( 342 state: &HttpState, 343 outpoints: BTreeSet<OutPoint>, 344 ) -> Result<BTreeMap<OutPoint, TxOutput>> { 345 if outpoints.is_empty() { 346 return Ok(BTreeMap::new()); 347 } 348 let store = state.ui_data_store.clone(); 349 tokio::task::spawn_blocking(move || store.load_outputs(&outpoints)) 350 .await 351 .unwrap_or_else(|_| Ok(BTreeMap::new())) 352 } 353 354 async fn current_real_chain_tip(state: &HttpState) -> Option<String> { 355 let node = state.node.lock().await; 356 node.has_real_chain().then(|| node.chain_tip_hash()) 357 } 358 359 fn collect_transaction_input_outpoints<'a>( 360 transactions: impl IntoIterator<Item = &'a Transaction>, 361 outpoints: &mut BTreeSet<OutPoint>, 362 ) { 363 for transaction in transactions { 364 match transaction { 365 Transaction::Transfer { inputs, .. } | Transaction::Burn { inputs, .. } => { 366 outpoints.extend(inputs.iter().map(|input| input.outpoint.clone())); 367 } 368 Transaction::Mine { .. } => {} 369 } 370 } 371 } 372 373 pub(super) async fn api_wallet_utxos( 374 State(state): State<HttpState>, 375 Query(query): Query<PageQuery>, 376 ) -> Json<Page<WalletUtxoRow>> { 377 let (wallet_addresses, pending_spent) = { 378 let node = state.node.lock().await; 379 ( 380 node.wallet_owned_addresses() 381 .unwrap_or_else(|_| vec![node.wallet_address().to_string()]), 382 node.wallet_pending_spent_outpoints(), 383 ) 384 }; 385 let store = state.ui_data_store.clone(); 386 let utxos = tokio::task::spawn_blocking(move || -> Result<Vec<_>> { 387 let mut utxos = Vec::new(); 388 for address in wallet_addresses { 389 utxos.extend(store.load_wallet_utxos(&address)?); 390 } 391 utxos.sort_by(|(left_point, left), (right_point, right)| { 392 right 393 .amount 394 .cmp(&left.amount) 395 .then_with(|| left_point.cmp(right_point)) 396 }); 397 Ok(utxos) 398 }) 399 .await 400 .ok() 401 .and_then(Result::ok) 402 .unwrap_or_default(); 403 Json(page_items( 404 wallet_utxo_rows_from_ui_data(utxos, &pending_spent), 405 query, 406 )) 407 } 408 409 pub(super) async fn api_wallet_selectable_utxos( 410 State(state): State<HttpState>, 411 ) -> Json<Vec<WalletUtxoRow>> { 412 let (wallet, pending_spent) = { 413 let node = state.node.lock().await; 414 ( 415 node.wallet_address().to_string(), 416 node.wallet_pending_spent_outpoints(), 417 ) 418 }; 419 let store = state.ui_data_store.clone(); 420 let utxos = tokio::task::spawn_blocking(move || store.load_wallet_utxos(&wallet)) 421 .await 422 .ok() 423 .and_then(Result::ok) 424 .unwrap_or_default(); 425 Json( 426 wallet_utxo_rows_from_ui_data(utxos, &pending_spent) 427 .into_iter() 428 .filter(|utxo| utxo.spendable) 429 .collect(), 430 ) 431 } 432 433 pub(super) fn page_items<T>(items: Vec<T>, query: PageQuery) -> Page<T> { 434 let total = items.len(); 435 let offset = query.offset.unwrap_or(0).min(total); 436 let limit = query 437 .limit 438 .unwrap_or(DATASET_PAGE_LIMIT) 439 .clamp(1, DATASET_LIMIT); 440 let page_items = items 441 .into_iter() 442 .skip(offset) 443 .take(limit) 444 .collect::<Vec<_>>(); 445 let next_offset = offset + page_items.len(); 446 Page { 447 items: page_items, 448 offset, 449 limit, 450 total, 451 has_more: next_offset < total, 452 next_offset: (next_offset < total).then_some(next_offset), 453 } 454 } 455 456 fn wallet_transaction_filter_kinds(filters: WalletTransactionFilters) -> Vec<&'static str> { 457 let mut kinds = Vec::new(); 458 if filters.transfer { 459 kinds.push("transfer"); 460 } 461 if filters.mine { 462 kinds.push("mine"); 463 } 464 if filters.burn { 465 kinds.push("burn"); 466 } 467 if filters.reward { 468 kinds.push("reward"); 469 } 470 kinds 471 } 472 473 fn wallet_utxo_rows_from_ui_data( 474 utxos: Vec<(crate::domain::OutPoint, crate::domain::TxOutput)>, 475 pending_spent: &BTreeSet<crate::domain::OutPoint>, 476 ) -> Vec<WalletUtxoRow> { 477 utxos 478 .into_iter() 479 .map(|(outpoint, output)| { 480 let spendable = !pending_spent.contains(&outpoint); 481 WalletUtxoRow { 482 outpoint, 483 address: output.address, 484 amount: output.amount, 485 spendable, 486 } 487 }) 488 .collect() 489 } 490 491 pub(super) async fn api_peers( 492 State(state): State<HttpState>, 493 Query(query): Query<PageQuery>, 494 ) -> Json<Page<PeerPresentation>> { 495 let peers = peer_presentations(state.peers.lock().await.list(), IpGeolocation::bundled()); 496 Json(page_items(peers, query)) 497 } 498 499 fn peer_presentations(peers: Vec<PeerInfo>, geolocation: &IpGeolocation) -> Vec<PeerPresentation> { 500 peers 501 .into_iter() 502 .map(|peer| { 503 let country_code = peer 504 .address 505 .parse::<SocketAddr>() 506 .ok() 507 .and_then(|address| geolocation.country_for_ip(address.ip())); 508 PeerPresentation { peer, country_code } 509 }) 510 .collect() 511 } 512 513 pub(super) async fn api_p2p_metrics(State(state): State<HttpState>) -> Json<P2pMetrics> { 514 Json(state.gossip.metrics()) 515 } 516 517 pub(super) async fn api_metrics( 518 State(state): State<HttpState>, 519 Query(query): Query<MetricsQuery>, 520 ) -> Json<MetricsResponse> { 521 let enabled = state.ui_config.lock().await.keep_track_of_metrics; 522 if !enabled { 523 return Json(empty_metrics_response(enabled, false)); 524 } 525 let Some(tip_hash) = current_real_chain_tip(&state).await else { 526 return Json(empty_metrics_response(enabled, false)); 527 }; 528 let store = state.ui_data_store.clone(); 529 let ready = tokio::task::spawn_blocking(move || store.metrics_are_projected_to(&tip_hash)) 530 .await 531 .ok() 532 .and_then(Result::ok) 533 .unwrap_or(false); 534 let store = state.ui_data_store.clone(); 535 let rows = tokio::task::spawn_blocking(move || match query.limit { 536 Some(limit) => store.load_recent_metrics(limit.clamp(1, DATASET_LIMIT)), 537 None => store.load_metrics(), 538 }) 539 .await 540 .ok() 541 .and_then(Result::ok) 542 .unwrap_or_default(); 543 let store = state.ui_data_store.clone(); 544 let leaderboards = tokio::task::spawn_blocking(move || store.load_leaderboards(10)) 545 .await 546 .ok() 547 .and_then(Result::ok) 548 .map(metrics_leaderboards) 549 .unwrap_or_default(); 550 let (chain_storage_bytes, top_mine_proofs) = { 551 let node = state.node.lock().await; 552 ( 553 node.chain_storage_bytes_by_hash().unwrap_or_default(), 554 top_mine_proofs(node.chain(), 10), 555 ) 556 }; 557 if rows.is_empty() { 558 let mut response = empty_metrics_response(enabled, !ready); 559 response.top_mine_proofs = top_mine_proofs; 560 return Json(response); 561 } 562 Json(metrics_response( 563 enabled, 564 !ready, 565 rows, 566 leaderboards, 567 &chain_storage_bytes, 568 top_mine_proofs, 569 )) 570 } 571 572 fn empty_metrics_response(enabled: bool, preparing: bool) -> MetricsResponse { 573 MetricsResponse { 574 enabled, 575 preparing, 576 latest: None, 577 charts: Vec::new(), 578 leaderboards: MetricsLeaderboards::default(), 579 top_mine_proofs: Vec::new(), 580 } 581 } 582 583 fn metrics_leaderboards( 584 leaderboards: crate::adapters::ui_data_store::UiLeaderboards, 585 ) -> MetricsLeaderboards { 586 MetricsLeaderboards { 587 balances: leaderboards 588 .balances 589 .into_iter() 590 .map(metrics_leaderboard_entry) 591 .collect(), 592 miners: leaderboards 593 .miners 594 .into_iter() 595 .map(metrics_leaderboard_entry) 596 .collect(), 597 burners: leaderboards 598 .burners 599 .into_iter() 600 .map(metrics_leaderboard_entry) 601 .collect(), 602 } 603 } 604 605 fn metrics_leaderboard_entry( 606 entry: crate::adapters::ui_data_store::UiLeaderboardEntry, 607 ) -> LeaderboardEntry { 608 LeaderboardEntry { 609 address: entry.address, 610 amount: entry.amount, 611 count: entry.count, 612 } 613 } 614 615 pub(super) async fn api_network_health( 616 State(state): State<HttpState>, 617 ) -> Json<NetworkHealthResponse> { 618 let sync_progress = state.gossip.sync_progress(); 619 let (local, mempool) = { 620 let node = state.node.lock().await; 621 let status = node.status(); 622 let tip = node.chain().last().cloned(); 623 let p2p_metrics = state.gossip.metrics(); 624 let tip_timestamp_ms = tip 625 .as_ref() 626 .filter(|block| block.height > 0) 627 .map(|block| block.timestamp_ms); 628 let mempool = MempoolCounts { 629 plain_transactions: node.pending_transactions().len(), 630 v2_transactions: node.pending_transactions_v2().len(), 631 }; 632 ( 633 NetworkHealthLocalState { 634 height: status.chain.height, 635 tip_hash: status.chain.tip_hash, 636 finalized_height: status.chain.finalized_height, 637 finalized_hash: status.chain.finalized_hash, 638 tip_timestamp_ms, 639 sync_start_height: sync_progress.map(|progress| progress.start_height), 640 sync_validated_height: sync_progress.map(|progress| progress.validated_height), 641 sync_target_height: sync_progress.map(|progress| progress.target_height), 642 pending_transactions: mempool.total(), 643 last_finalizer_mode: tip.as_ref().map(|block| match block.finalizer_mode { 644 crate::domain::FinalizerMode::Ticket => "ticket".to_string(), 645 crate::domain::FinalizerMode::Recovery => "recovery".to_string(), 646 }), 647 last_finalizer_rank: tip.as_ref().map(|block| block.finalizer_rank), 648 last_block_finalizer: tip.as_ref().map(|block| block.miner.clone()), 649 current_leader: status.mining.current_leader, 650 wallet_is_current_leader: status.mining.wallet_is_current_leader, 651 last_auto_finalization_status: status.mining.last_auto_finalization_status, 652 vdf_rounds: status.mining.vdf_rounds, 653 vdf_target_block_ms: status.mining.vdf_target_block_ms, 654 rejected_blocks: p2p_metrics.rejected_blocks, 655 rejected_block_batches: p2p_metrics.rejected_block_batches, 656 rejected_snapshots: p2p_metrics.rejected_snapshots, 657 last_chain_payload_error: p2p_metrics.last_chain_payload_error, 658 }, 659 mempool, 660 ) 661 }; 662 let peers = state.peers.lock().await.list(); 663 Json(network_health(local, &peers, mempool)) 664 } 665 666 #[cfg(test)] 667 mod tests { 668 use super::peer_presentations; 669 use crate::{app::PeerBook, ip_geolocation::IpGeolocation}; 670 671 #[test] 672 fn presents_multiple_peer_countries_without_guessing_hostnames() { 673 let peers = PeerBook::from_addresses(vec![ 674 "8.8.8.8:9444".to_string(), 675 "[2001:4860:4860::8888]:9444".to_string(), 676 "seed.example:9444".to_string(), 677 ]) 678 .list(); 679 let geolocation = IpGeolocation::from_entries(&[ 680 ("8.8.8.0".parse().unwrap(), 24, "NL"), 681 ("2001:4860::".parse().unwrap(), 32, "US"), 682 ]); 683 684 let presented = peer_presentations(peers, &geolocation); 685 assert_eq!(presented[0].country_code.unwrap().as_str(), "NL"); 686 assert_eq!(presented[1].country_code.unwrap().as_str(), "US"); 687 assert_eq!(presented[2].country_code, None); 688 689 let json = serde_json::to_value(presented).unwrap(); 690 assert_eq!(json[0]["country_code"], "NL"); 691 assert_eq!(json[1]["country_code"], "US"); 692 assert!(json[2].get("country_code").is_none()); 693 } 694 }