dig_rpc_types/fullnode.rs
1//! Fullnode RPC method catalogue.
2//!
3//! One `{Method}Request` + `{Method}Response` struct pair per method. The
4//! catalogue follows the table in
5//! [`dig-network/docs/resources/02-subsystems/08-binaries/supplement/02-rpc-method-matrix.md`](https://github.com/DIG-Network/dig-network).
6//!
7//! Method classification:
8//!
9//! | Class | Examples | Role requirement |
10//! |---|---|---|
11//! | Blockchain state | `get_blockchain_state`, `get_network_info`, `healthz` | Explorer+ |
12//! | Blocks | `get_block`, `get_block_by_height`, `get_block_records` | Explorer+ |
13//! | Coins | `get_coin_record`, `get_coin_records_by_hint`, `get_coin_records_by_puzzle_hash` | Explorer+ |
14//! | Mempool | `get_mempool`, `push_tx` | Explorer+ (read), Admin (write) |
15//! | Peers | `get_connections`, `ban_peer` | Admin |
16//! | Checkpoint | `submit_partial_checkpoint_signature`, `get_checkpoint_pool` | Validator |
17//! | Validator set | `get_validator`, `get_active_validators`, `get_current_proposer` | Explorer+ |
18//! | Admin | `stop_node`, `get_recovery_status`, `get_version` | Admin |
19//!
20//! # Method name convention
21//!
22//! Method names on the JSON-RPC wire are snake_case strings, declared via
23//! the associated `METHOD` constant on each request struct. Request / response
24//! struct names follow PascalCase of the method with `Request` / `Response`
25//! suffixes — `GetBlockchainStateRequest` etc.
26
27use serde::{Deserialize, Serialize};
28
29use crate::types::{Amount, BlockSummary, HashHex, PubkeyHex, SignatureHex, ValidatorSummary};
30
31// ===========================================================================
32// Blockchain state
33// ===========================================================================
34
35/// `get_blockchain_state` — overall chain state.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct GetBlockchainStateRequest;
38
39impl GetBlockchainStateRequest {
40 /// The wire method name.
41 pub const METHOD: &'static str = "get_blockchain_state";
42}
43
44/// Response for [`GetBlockchainStateRequest`].
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct GetBlockchainStateResponse {
47 /// Current canonical tip height.
48 pub height: u64,
49 /// Current canonical tip block hash.
50 pub tip_hash: HashHex,
51 /// Whether the node is synced to the chain tip.
52 pub synced: bool,
53 /// Progress 0.0..=1.0 while syncing.
54 pub sync_progress: f32,
55 /// The most recent finalised (L3) epoch.
56 pub finalized_epoch: u64,
57 /// Height floor below which no reorg may cross.
58 pub sealed_height: u64,
59}
60
61/// `get_network_info` — genesis / network identity.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct GetNetworkInfoRequest;
64
65impl GetNetworkInfoRequest {
66 /// The wire method name.
67 pub const METHOD: &'static str = "get_network_info";
68}
69
70/// Response for [`GetNetworkInfoRequest`].
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct GetNetworkInfoResponse {
73 /// Network identifier (e.g., `"mainnet"`, `"testnet11"`).
74 pub network_id: String,
75 /// 32-byte genesis challenge.
76 pub genesis_challenge: HashHex,
77 /// Seconds since genesis.
78 pub chain_age_seconds: u64,
79}
80
81/// `healthz` — liveness probe.
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct HealthzRequest;
84
85impl HealthzRequest {
86 /// The wire method name.
87 pub const METHOD: &'static str = "healthz";
88}
89
90/// Response for [`HealthzRequest`].
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct HealthzResponse {
93 /// Whether the node reports itself healthy.
94 pub ok: bool,
95}
96
97// ===========================================================================
98// Blocks
99// ===========================================================================
100
101/// `get_block` — fetch a full block by hash.
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct GetBlockRequest {
104 /// Block hash.
105 pub hash: HashHex,
106}
107
108impl GetBlockRequest {
109 /// The wire method name.
110 pub const METHOD: &'static str = "get_block";
111}
112
113/// Response for [`GetBlockRequest`]. `None` means not found.
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct GetBlockResponse {
116 /// The block, or `None` if not present.
117 pub block: Option<BlockFull>,
118}
119
120/// `get_block_by_height` — fetch a full canonical block by height.
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct GetBlockByHeightRequest {
123 /// Canonical block height.
124 pub height: u64,
125}
126
127impl GetBlockByHeightRequest {
128 /// The wire method name.
129 pub const METHOD: &'static str = "get_block_by_height";
130}
131
132/// Response for [`GetBlockByHeightRequest`]. `None` means out-of-range.
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct GetBlockByHeightResponse {
135 /// The block, or `None` if out-of-range / not canonical.
136 pub block: Option<BlockFull>,
137}
138
139/// `get_block_records` — compact summaries of a contiguous height range.
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct GetBlockRecordsRequest {
142 /// First height (inclusive).
143 pub start_height: u64,
144 /// Number of records to return. Server caps at 1000.
145 pub count: u32,
146}
147
148impl GetBlockRecordsRequest {
149 /// The wire method name.
150 pub const METHOD: &'static str = "get_block_records";
151}
152
153/// Response for [`GetBlockRecordsRequest`]. `records.len()` may be less
154/// than `count` at the chain tip.
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct GetBlockRecordsResponse {
157 /// Block summaries in ascending height order.
158 pub records: Vec<BlockSummary>,
159}
160
161/// Full block envelope used by `get_block*` responses.
162///
163/// The header is JSON-native; the body is hex-encoded Chia-streamable
164/// bytes. Consumers that need to decode the body must route through
165/// `dig-block`'s streamable decoder.
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct BlockFull {
168 /// Parsed block header (JSON-native).
169 pub header: BlockHeaderWire,
170 /// Hex-encoded chia-streamable body bytes.
171 pub body_hex: String,
172 /// Whether this block is on the canonical chain.
173 pub canonical: bool,
174}
175
176/// Block header in wire form.
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct BlockHeaderWire {
179 /// L2 block height.
180 pub height: u64,
181 /// Canonical hash of this header.
182 pub hash: HashHex,
183 /// Parent block hash.
184 pub parent_hash: HashHex,
185 /// Unix timestamp (seconds).
186 pub timestamp: u64,
187 /// Proposer's BLS public key.
188 pub proposer: PubkeyHex,
189 /// Coin-state SMT root at the end of this block.
190 pub state_root: HashHex,
191 /// Transaction-receipts root.
192 pub receipts_root: HashHex,
193 /// Cumulative attestation weight.
194 pub weight: u64,
195 /// Chia-style cumulative iteration count (128 bits).
196 pub total_iters: u128,
197 /// Proposer's signature over the header's canonical preimage.
198 pub signature: SignatureHex,
199}
200
201// ===========================================================================
202// Coins
203// ===========================================================================
204
205/// `get_coin_record` — fetch a single coin record by coin id.
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct GetCoinRecordRequest {
208 /// Coin id (32-byte hash of `parent_coin || puzzle_hash || amount`).
209 pub coin_id: HashHex,
210}
211
212impl GetCoinRecordRequest {
213 /// The wire method name.
214 pub const METHOD: &'static str = "get_coin_record";
215}
216
217/// Response for [`GetCoinRecordRequest`]. `None` means not found.
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct GetCoinRecordResponse {
220 /// The coin record, or `None` if not present.
221 pub record: Option<CoinRecordWire>,
222}
223
224/// `get_coin_records_by_hint` — look up coins by CLVM hint.
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct GetCoinRecordsByHintRequest {
227 /// 32-byte hint value.
228 pub hint: HashHex,
229 /// Include already-spent coins?
230 pub include_spent_coins: bool,
231 /// Confirmed-height lower bound.
232 #[serde(skip_serializing_if = "Option::is_none")]
233 pub start_height: Option<u64>,
234 /// Confirmed-height upper bound.
235 #[serde(skip_serializing_if = "Option::is_none")]
236 pub end_height: Option<u64>,
237}
238
239impl GetCoinRecordsByHintRequest {
240 /// The wire method name.
241 pub const METHOD: &'static str = "get_coin_records_by_hint";
242}
243
244/// Response for [`GetCoinRecordsByHintRequest`].
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct GetCoinRecordsByHintResponse {
247 /// Coin records matching the filter.
248 pub records: Vec<CoinRecordWire>,
249}
250
251/// `get_coin_records_by_puzzle_hash` — look up coins owned by a puzzle.
252#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct GetCoinRecordsByPuzzleHashRequest {
254 /// 32-byte puzzle hash (address target).
255 pub puzzle_hash: HashHex,
256 /// Include already-spent coins?
257 pub include_spent_coins: bool,
258 /// Confirmed-height lower bound.
259 #[serde(skip_serializing_if = "Option::is_none")]
260 pub start_height: Option<u64>,
261 /// Confirmed-height upper bound.
262 #[serde(skip_serializing_if = "Option::is_none")]
263 pub end_height: Option<u64>,
264}
265
266impl GetCoinRecordsByPuzzleHashRequest {
267 /// The wire method name.
268 pub const METHOD: &'static str = "get_coin_records_by_puzzle_hash";
269}
270
271/// Response for [`GetCoinRecordsByPuzzleHashRequest`].
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct GetCoinRecordsByPuzzleHashResponse {
274 /// Coin records matching the filter.
275 pub records: Vec<CoinRecordWire>,
276}
277
278/// Coin record in wire form.
279#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct CoinRecordWire {
281 /// Coin id (derived hash).
282 pub coin_id: HashHex,
283 /// Parent coin id.
284 pub parent_coin_info: HashHex,
285 /// Puzzle hash (address target).
286 pub puzzle_hash: HashHex,
287 /// Amount in mojos.
288 pub amount: Amount,
289 /// Height at which this coin was created.
290 pub confirmed_block_height: u64,
291 /// Height at which this coin was spent; `0` if unspent.
292 pub spent_block_height: u64,
293 /// True iff this is a coinbase (reward) coin.
294 pub coinbase: bool,
295 /// Timestamp from the confirming block.
296 pub timestamp: u64,
297}
298
299// ===========================================================================
300// Mempool
301// ===========================================================================
302
303/// `get_mempool` — summary of pending transactions.
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct GetMempoolRequest;
306
307impl GetMempoolRequest {
308 /// The wire method name.
309 pub const METHOD: &'static str = "get_mempool";
310}
311
312/// Response for [`GetMempoolRequest`].
313#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct GetMempoolResponse {
315 /// Sum of CLVM cost across all items.
316 pub total_cost: u64,
317 /// Sum of fees across all items.
318 pub total_fees: Amount,
319 /// Summary entries, in eviction-priority order.
320 pub items: Vec<MempoolItem>,
321}
322
323/// One entry in the mempool summary.
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct MempoolItem {
326 /// Spend-bundle identifier (hash).
327 pub spend_bundle_name: HashHex,
328 /// CLVM cost for this bundle.
329 pub cost: u64,
330 /// Fee paid by this bundle, in mojos.
331 pub fee: Amount,
332 /// Whether this bundle is a CPFP ancestor / descendant.
333 pub is_cpfp: bool,
334}
335
336/// `push_tx` — submit a spend bundle to the mempool.
337#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct PushTxRequest {
339 /// Hex-encoded chia-streamable spend bundle.
340 pub spend_bundle_hex: String,
341}
342
343impl PushTxRequest {
344 /// The wire method name.
345 pub const METHOD: &'static str = "push_tx";
346}
347
348/// Response for [`PushTxRequest`].
349#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct PushTxResponse {
351 /// Admission outcome.
352 pub status: PushTxStatus,
353 /// Optional details (reason code / error message).
354 #[serde(skip_serializing_if = "Option::is_none")]
355 pub details: Option<String>,
356}
357
358/// Admission outcome of [`PushTxRequest`].
359#[non_exhaustive]
360#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
361#[serde(rename_all = "snake_case")]
362pub enum PushTxStatus {
363 /// Admitted to the mempool.
364 Success,
365 /// Rejected; see `details`.
366 Rejected,
367 /// Already present in the mempool.
368 AlreadyExists,
369}
370
371// ===========================================================================
372// Peers
373// ===========================================================================
374
375/// `get_connections` — enumerate connected peers.
376#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct GetConnectionsRequest;
378
379impl GetConnectionsRequest {
380 /// The wire method name.
381 pub const METHOD: &'static str = "get_connections";
382}
383
384/// Response for [`GetConnectionsRequest`].
385#[derive(Debug, Clone, Serialize, Deserialize)]
386pub struct GetConnectionsResponse {
387 /// Connected peers.
388 pub peers: Vec<PeerInfoWire>,
389}
390
391/// Peer info in wire form.
392#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct PeerInfoWire {
394 /// Peer id — SHA256 of remote cert pubkey.
395 pub peer_id: HashHex,
396 /// Peer IP address + port.
397 pub remote_addr: String,
398 /// Peer node type (e.g., `"full_node"`, `"validator"`).
399 pub node_type: String,
400 /// Unix epoch seconds when the connection was established.
401 pub connected_since: u64,
402 /// Accumulated penalty points for abuse.
403 pub penalty: u32,
404}
405
406/// `ban_peer` — evict and ban a peer.
407#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct BanPeerRequest {
409 /// Peer id to ban.
410 pub peer_id: HashHex,
411 /// Reason string (stored in the audit log).
412 pub reason: String,
413 /// Ban duration in seconds; `0` = indefinite.
414 pub duration_secs: u64,
415}
416
417impl BanPeerRequest {
418 /// The wire method name.
419 pub const METHOD: &'static str = "ban_peer";
420}
421
422/// Response for [`BanPeerRequest`].
423#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct BanPeerResponse {
425 /// Whether the peer is now banned.
426 pub banned: bool,
427}
428
429// ===========================================================================
430// Checkpoint
431// ===========================================================================
432
433/// `submit_partial_checkpoint_signature` — validator submits a partial sig
434/// for the current epoch.
435#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct SubmitPartialCheckpointSignatureRequest {
437 /// Epoch the signature is for.
438 pub epoch: u64,
439 /// Signer's validator index in the VMR.
440 pub validator_index: u32,
441 /// BLS partial signature over the checkpoint digest.
442 pub partial_sig: SignatureHex,
443}
444
445impl SubmitPartialCheckpointSignatureRequest {
446 /// The wire method name.
447 pub const METHOD: &'static str = "submit_partial_checkpoint_signature";
448}
449
450/// Response for [`SubmitPartialCheckpointSignatureRequest`].
451#[derive(Debug, Clone, Serialize, Deserialize)]
452pub struct SubmitPartialCheckpointSignatureResponse {
453 /// Whether the signature was admitted to the pool.
454 pub accepted: bool,
455 /// Optional rejection reason.
456 #[serde(skip_serializing_if = "Option::is_none")]
457 pub reason: Option<String>,
458}
459
460/// `get_checkpoint_pool` — status of the checkpoint aggregation pool.
461#[derive(Debug, Clone, Serialize, Deserialize)]
462pub struct GetCheckpointPoolRequest {
463 /// Specific epoch to inspect. `None` returns all open epochs.
464 #[serde(skip_serializing_if = "Option::is_none")]
465 pub epoch: Option<u64>,
466}
467
468impl GetCheckpointPoolRequest {
469 /// The wire method name.
470 pub const METHOD: &'static str = "get_checkpoint_pool";
471}
472
473/// Response for [`GetCheckpointPoolRequest`].
474#[derive(Debug, Clone, Serialize, Deserialize)]
475pub struct GetCheckpointPoolResponse {
476 /// Per-epoch status entries.
477 pub epochs: Vec<CheckpointEpochStatus>,
478}
479
480/// Per-epoch aggregation status.
481#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct CheckpointEpochStatus {
483 /// Epoch number.
484 pub epoch: u64,
485 /// Number of partials received.
486 pub partials_count: u32,
487 /// Threshold required (`2k + 1` where `n = validator_count`).
488 pub threshold: u32,
489 /// Whether the aggregation succeeded.
490 pub aggregated: bool,
491}
492
493// ===========================================================================
494// Validator set
495// ===========================================================================
496
497/// `get_validator` — look up a validator by pubkey.
498#[derive(Debug, Clone, Serialize, Deserialize)]
499pub struct GetValidatorRequest {
500 /// Validator's BLS pubkey.
501 pub pubkey: PubkeyHex,
502}
503
504impl GetValidatorRequest {
505 /// The wire method name.
506 pub const METHOD: &'static str = "get_validator";
507}
508
509/// Response for [`GetValidatorRequest`]. `None` if no such validator.
510#[derive(Debug, Clone, Serialize, Deserialize)]
511pub struct GetValidatorResponse {
512 /// Validator summary, or `None` if not found.
513 pub validator: Option<ValidatorSummary>,
514}
515
516/// `get_active_validators` — page through the current active set.
517#[derive(Debug, Clone, Serialize, Deserialize)]
518pub struct GetActiveValidatorsRequest {
519 /// Maximum results. Server caps at 1000.
520 pub limit: u32,
521 /// Results to skip.
522 pub offset: u32,
523}
524
525impl GetActiveValidatorsRequest {
526 /// The wire method name.
527 pub const METHOD: &'static str = "get_active_validators";
528}
529
530/// Response for [`GetActiveValidatorsRequest`].
531#[derive(Debug, Clone, Serialize, Deserialize)]
532pub struct GetActiveValidatorsResponse {
533 /// Active validators in this page.
534 pub validators: Vec<ValidatorSummary>,
535 /// Total active validators across the full set.
536 pub total: u32,
537}
538
539/// `get_current_proposer` — who is elected proposer at a height.
540#[derive(Debug, Clone, Serialize, Deserialize)]
541pub struct GetCurrentProposerRequest {
542 /// The L2 height to query.
543 pub height: u64,
544}
545
546impl GetCurrentProposerRequest {
547 /// The wire method name.
548 pub const METHOD: &'static str = "get_current_proposer";
549}
550
551/// Response for [`GetCurrentProposerRequest`].
552#[derive(Debug, Clone, Serialize, Deserialize)]
553pub struct GetCurrentProposerResponse {
554 /// Elected proposer, or `None` if the height is out of range.
555 pub proposer: Option<ValidatorSummary>,
556}
557
558// ===========================================================================
559// Admin
560// ===========================================================================
561
562/// `stop_node` — request a graceful shutdown.
563#[derive(Debug, Clone, Serialize, Deserialize)]
564pub struct StopNodeRequest {
565 /// Optional reason (written to audit log).
566 #[serde(skip_serializing_if = "Option::is_none")]
567 pub reason: Option<String>,
568}
569
570impl StopNodeRequest {
571 /// The wire method name.
572 pub const METHOD: &'static str = "stop_node";
573}
574
575/// Response for [`StopNodeRequest`].
576#[derive(Debug, Clone, Serialize, Deserialize)]
577pub struct StopNodeResponse {
578 /// Whether shutdown has been initiated.
579 pub accepted: bool,
580}
581
582/// `get_recovery_status` — fullnode recovery-journal state.
583#[derive(Debug, Clone, Serialize, Deserialize)]
584pub struct GetRecoveryStatusRequest;
585
586impl GetRecoveryStatusRequest {
587 /// The wire method name.
588 pub const METHOD: &'static str = "get_recovery_status";
589}
590
591/// Response for [`GetRecoveryStatusRequest`].
592#[derive(Debug, Clone, Serialize, Deserialize)]
593pub struct GetRecoveryStatusResponse {
594 /// Current recovery mode.
595 pub mode: RecoveryMode,
596 /// Last anomaly observed, if any.
597 #[serde(skip_serializing_if = "Option::is_none")]
598 pub last_anomaly: Option<String>,
599 /// Number of recovery attempts in the current epoch.
600 pub attempts: u32,
601}
602
603/// Recovery state machine values.
604#[non_exhaustive]
605#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
606#[serde(rename_all = "snake_case")]
607pub enum RecoveryMode {
608 /// Normal operation.
609 Running,
610 /// Rolling back after an anomaly.
611 Recovering,
612 /// Exceeded the max recovery attempts per epoch.
613 LoopBreakerOpen,
614}
615
616/// `get_version` — binary identification.
617#[derive(Debug, Clone, Serialize, Deserialize)]
618pub struct GetVersionRequest;
619
620impl GetVersionRequest {
621 /// The wire method name.
622 pub const METHOD: &'static str = "get_version";
623}
624
625/// Response for [`GetVersionRequest`].
626#[derive(Debug, Clone, Serialize, Deserialize)]
627pub struct GetVersionResponse {
628 /// Human-readable version string (`"1.2.3-pre"`).
629 pub version: String,
630 /// Build commit SHA.
631 pub build_commit: String,
632}
633
634#[cfg(test)]
635mod tests {
636 use super::*;
637
638 /// **Proves:** every request struct's `METHOD` constant is
639 /// lower-snake-case — no hyphens, no camelCase leaks.
640 ///
641 /// **Why it matters:** JSON-RPC method names are case-sensitive. A
642 /// regression that introduced PascalCase or kebab-case for one method
643 /// would break that single method on every deployed client.
644 ///
645 /// **Catches:** an accidental `pub const METHOD: &str = "GetBlock";`
646 /// that slipped through review.
647 #[test]
648 fn method_names_are_snake_case() {
649 let method_names = [
650 GetBlockchainStateRequest::METHOD,
651 GetNetworkInfoRequest::METHOD,
652 HealthzRequest::METHOD,
653 GetBlockRequest::METHOD,
654 GetBlockByHeightRequest::METHOD,
655 GetBlockRecordsRequest::METHOD,
656 GetCoinRecordRequest::METHOD,
657 GetCoinRecordsByHintRequest::METHOD,
658 GetCoinRecordsByPuzzleHashRequest::METHOD,
659 GetMempoolRequest::METHOD,
660 PushTxRequest::METHOD,
661 GetConnectionsRequest::METHOD,
662 BanPeerRequest::METHOD,
663 SubmitPartialCheckpointSignatureRequest::METHOD,
664 GetCheckpointPoolRequest::METHOD,
665 GetValidatorRequest::METHOD,
666 GetActiveValidatorsRequest::METHOD,
667 GetCurrentProposerRequest::METHOD,
668 StopNodeRequest::METHOD,
669 GetRecoveryStatusRequest::METHOD,
670 GetVersionRequest::METHOD,
671 ];
672
673 for m in method_names {
674 assert!(
675 m.chars()
676 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'),
677 "method name {m:?} is not snake_case",
678 );
679 assert!(!m.is_empty(), "empty method name");
680 assert!(!m.starts_with('_'), "method {m:?} starts with underscore");
681 }
682 }
683
684 /// **Proves:** all method names are pairwise distinct.
685 ///
686 /// **Why it matters:** Two different request types cannot share a
687 /// method name — the server's dispatcher would have no way to tell
688 /// them apart. Catching collisions at test time prevents a whole class
689 /// of integration bugs.
690 ///
691 /// **Catches:** copy-paste of a method name without updating it;
692 /// shadowing a fullnode method with a same-named validator method
693 /// (they're namespaced in code but collide on the wire).
694 #[test]
695 fn method_names_are_unique() {
696 let method_names = [
697 GetBlockchainStateRequest::METHOD,
698 GetNetworkInfoRequest::METHOD,
699 HealthzRequest::METHOD,
700 GetBlockRequest::METHOD,
701 GetBlockByHeightRequest::METHOD,
702 GetBlockRecordsRequest::METHOD,
703 GetCoinRecordRequest::METHOD,
704 GetCoinRecordsByHintRequest::METHOD,
705 GetCoinRecordsByPuzzleHashRequest::METHOD,
706 GetMempoolRequest::METHOD,
707 PushTxRequest::METHOD,
708 GetConnectionsRequest::METHOD,
709 BanPeerRequest::METHOD,
710 SubmitPartialCheckpointSignatureRequest::METHOD,
711 GetCheckpointPoolRequest::METHOD,
712 GetValidatorRequest::METHOD,
713 GetActiveValidatorsRequest::METHOD,
714 GetCurrentProposerRequest::METHOD,
715 StopNodeRequest::METHOD,
716 GetRecoveryStatusRequest::METHOD,
717 GetVersionRequest::METHOD,
718 ];
719
720 let mut seen = std::collections::HashSet::new();
721 for m in method_names {
722 assert!(seen.insert(m), "duplicate method name {m:?}");
723 }
724 }
725
726 /// **Proves:** `GetBlockchainStateResponse` round-trips through JSON
727 /// unchanged.
728 ///
729 /// **Why it matters:** This is the most-called RPC method — smoke test
730 /// that the full struct decodes cleanly.
731 ///
732 /// **Catches:** dropping a field; re-typing `sync_progress` from `f32`
733 /// to `f64` (JSON numbers are all f64 in JS; this is fine but the test
734 /// pins the Rust-side type).
735 #[test]
736 fn get_blockchain_state_roundtrip() {
737 let r = GetBlockchainStateResponse {
738 height: 100,
739 tip_hash: HashHex::new([1u8; 32]),
740 synced: true,
741 sync_progress: 1.0,
742 finalized_epoch: 3,
743 sealed_height: 96,
744 };
745 let j = serde_json::to_string(&r).unwrap();
746 let back: GetBlockchainStateResponse = serde_json::from_str(&j).unwrap();
747 assert_eq!(back.height, r.height);
748 assert_eq!(back.tip_hash, r.tip_hash);
749 assert_eq!(back.synced, r.synced);
750 }
751
752 /// **Proves:** `PushTxStatus` serializes in snake_case
753 /// (`"success"`, `"rejected"`, `"already_exists"`).
754 ///
755 /// **Why it matters:** Clients pattern-match on these strings. The
756 /// already-declared `#[serde(rename_all = "snake_case")]` is what makes
757 /// them stable.
758 ///
759 /// **Catches:** a regression that drops the rename attribute, or a
760 /// typo in a variant name (`AlreadyExist` vs `AlreadyExists`).
761 #[test]
762 fn push_tx_status_snake_case() {
763 let s = serde_json::to_string(&PushTxStatus::Success).unwrap();
764 assert_eq!(s, "\"success\"");
765
766 let s = serde_json::to_string(&PushTxStatus::AlreadyExists).unwrap();
767 assert_eq!(s, "\"already_exists\"");
768 }
769
770 /// **Proves:** a request with optional fields omitted encodes without
771 /// the `null` sentinel (because of `skip_serializing_if`).
772 ///
773 /// **Why it matters:** JSON-RPC clients vary in how they interpret
774 /// `null` vs absent fields. We consistently omit absent optionals to
775 /// match Chia's own RPC behaviour.
776 ///
777 /// **Catches:** dropping `skip_serializing_if = "Option::is_none"`
778 /// from an optional field — the wire form would gain `"field":null`
779 /// and break deduplication of requests that rely on canonical form.
780 #[test]
781 fn optional_fields_omitted_when_none() {
782 let req = GetCoinRecordsByHintRequest {
783 hint: HashHex::new([0u8; 32]),
784 include_spent_coins: false,
785 start_height: None,
786 end_height: None,
787 };
788 let s = serde_json::to_string(&req).unwrap();
789 assert!(
790 !s.contains("start_height"),
791 "start_height should be omitted, got: {s}"
792 );
793 assert!(
794 !s.contains("end_height"),
795 "end_height should be omitted, got: {s}"
796 );
797 }
798}