dig-rpc-types 0.1.0

JSON-RPC wire types shared by the DIG Network fullnode + validator RPC servers and their clients. Pure types — no I/O, no async, no logic.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
//! Fullnode RPC method catalogue.
//!
//! One `{Method}Request` + `{Method}Response` struct pair per method. The
//! catalogue follows the table in
//! [`dig-network/docs/resources/02-subsystems/08-binaries/supplement/02-rpc-method-matrix.md`](https://github.com/DIG-Network/dig-network).
//!
//! Method classification:
//!
//! | Class | Examples | Role requirement |
//! |---|---|---|
//! | Blockchain state | `get_blockchain_state`, `get_network_info`, `healthz` | Explorer+ |
//! | Blocks | `get_block`, `get_block_by_height`, `get_block_records` | Explorer+ |
//! | Coins | `get_coin_record`, `get_coin_records_by_hint`, `get_coin_records_by_puzzle_hash` | Explorer+ |
//! | Mempool | `get_mempool`, `push_tx` | Explorer+ (read), Admin (write) |
//! | Peers | `get_connections`, `ban_peer` | Admin |
//! | Checkpoint | `submit_partial_checkpoint_signature`, `get_checkpoint_pool` | Validator |
//! | Validator set | `get_validator`, `get_active_validators`, `get_current_proposer` | Explorer+ |
//! | Admin | `stop_node`, `get_recovery_status`, `get_version` | Admin |
//!
//! # Method name convention
//!
//! Method names on the JSON-RPC wire are snake_case strings, declared via
//! the associated `METHOD` constant on each request struct. Request / response
//! struct names follow PascalCase of the method with `Request` / `Response`
//! suffixes — `GetBlockchainStateRequest` etc.

use serde::{Deserialize, Serialize};

use crate::types::{Amount, BlockSummary, HashHex, PubkeyHex, SignatureHex, ValidatorSummary};

// ===========================================================================
// Blockchain state
// ===========================================================================

/// `get_blockchain_state` — overall chain state.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetBlockchainStateRequest;

impl GetBlockchainStateRequest {
    /// The wire method name.
    pub const METHOD: &'static str = "get_blockchain_state";
}

/// Response for [`GetBlockchainStateRequest`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetBlockchainStateResponse {
    /// Current canonical tip height.
    pub height: u64,
    /// Current canonical tip block hash.
    pub tip_hash: HashHex,
    /// Whether the node is synced to the chain tip.
    pub synced: bool,
    /// Progress 0.0..=1.0 while syncing.
    pub sync_progress: f32,
    /// The most recent finalised (L3) epoch.
    pub finalized_epoch: u64,
    /// Height floor below which no reorg may cross.
    pub sealed_height: u64,
}

/// `get_network_info` — genesis / network identity.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetNetworkInfoRequest;

impl GetNetworkInfoRequest {
    /// The wire method name.
    pub const METHOD: &'static str = "get_network_info";
}

/// Response for [`GetNetworkInfoRequest`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetNetworkInfoResponse {
    /// Network identifier (e.g., `"mainnet"`, `"testnet11"`).
    pub network_id: String,
    /// 32-byte genesis challenge.
    pub genesis_challenge: HashHex,
    /// Seconds since genesis.
    pub chain_age_seconds: u64,
}

/// `healthz` — liveness probe.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthzRequest;

impl HealthzRequest {
    /// The wire method name.
    pub const METHOD: &'static str = "healthz";
}

/// Response for [`HealthzRequest`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthzResponse {
    /// Whether the node reports itself healthy.
    pub ok: bool,
}

// ===========================================================================
// Blocks
// ===========================================================================

/// `get_block` — fetch a full block by hash.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetBlockRequest {
    /// Block hash.
    pub hash: HashHex,
}

impl GetBlockRequest {
    /// The wire method name.
    pub const METHOD: &'static str = "get_block";
}

/// Response for [`GetBlockRequest`]. `None` means not found.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetBlockResponse {
    /// The block, or `None` if not present.
    pub block: Option<BlockFull>,
}

/// `get_block_by_height` — fetch a full canonical block by height.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetBlockByHeightRequest {
    /// Canonical block height.
    pub height: u64,
}

impl GetBlockByHeightRequest {
    /// The wire method name.
    pub const METHOD: &'static str = "get_block_by_height";
}

/// Response for [`GetBlockByHeightRequest`]. `None` means out-of-range.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetBlockByHeightResponse {
    /// The block, or `None` if out-of-range / not canonical.
    pub block: Option<BlockFull>,
}

/// `get_block_records` — compact summaries of a contiguous height range.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetBlockRecordsRequest {
    /// First height (inclusive).
    pub start_height: u64,
    /// Number of records to return. Server caps at 1000.
    pub count: u32,
}

impl GetBlockRecordsRequest {
    /// The wire method name.
    pub const METHOD: &'static str = "get_block_records";
}

/// Response for [`GetBlockRecordsRequest`]. `records.len()` may be less
/// than `count` at the chain tip.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetBlockRecordsResponse {
    /// Block summaries in ascending height order.
    pub records: Vec<BlockSummary>,
}

/// Full block envelope used by `get_block*` responses.
///
/// The header is JSON-native; the body is hex-encoded Chia-streamable
/// bytes. Consumers that need to decode the body must route through
/// `dig-block`'s streamable decoder.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockFull {
    /// Parsed block header (JSON-native).
    pub header: BlockHeaderWire,
    /// Hex-encoded chia-streamable body bytes.
    pub body_hex: String,
    /// Whether this block is on the canonical chain.
    pub canonical: bool,
}

/// Block header in wire form.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockHeaderWire {
    /// L2 block height.
    pub height: u64,
    /// Canonical hash of this header.
    pub hash: HashHex,
    /// Parent block hash.
    pub parent_hash: HashHex,
    /// Unix timestamp (seconds).
    pub timestamp: u64,
    /// Proposer's BLS public key.
    pub proposer: PubkeyHex,
    /// Coin-state SMT root at the end of this block.
    pub state_root: HashHex,
    /// Transaction-receipts root.
    pub receipts_root: HashHex,
    /// Cumulative attestation weight.
    pub weight: u64,
    /// Chia-style cumulative iteration count (128 bits).
    pub total_iters: u128,
    /// Proposer's signature over the header's canonical preimage.
    pub signature: SignatureHex,
}

// ===========================================================================
// Coins
// ===========================================================================

/// `get_coin_record` — fetch a single coin record by coin id.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetCoinRecordRequest {
    /// Coin id (32-byte hash of `parent_coin || puzzle_hash || amount`).
    pub coin_id: HashHex,
}

impl GetCoinRecordRequest {
    /// The wire method name.
    pub const METHOD: &'static str = "get_coin_record";
}

/// Response for [`GetCoinRecordRequest`]. `None` means not found.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetCoinRecordResponse {
    /// The coin record, or `None` if not present.
    pub record: Option<CoinRecordWire>,
}

/// `get_coin_records_by_hint` — look up coins by CLVM hint.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetCoinRecordsByHintRequest {
    /// 32-byte hint value.
    pub hint: HashHex,
    /// Include already-spent coins?
    pub include_spent_coins: bool,
    /// Confirmed-height lower bound.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub start_height: Option<u64>,
    /// Confirmed-height upper bound.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub end_height: Option<u64>,
}

impl GetCoinRecordsByHintRequest {
    /// The wire method name.
    pub const METHOD: &'static str = "get_coin_records_by_hint";
}

/// Response for [`GetCoinRecordsByHintRequest`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetCoinRecordsByHintResponse {
    /// Coin records matching the filter.
    pub records: Vec<CoinRecordWire>,
}

/// `get_coin_records_by_puzzle_hash` — look up coins owned by a puzzle.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetCoinRecordsByPuzzleHashRequest {
    /// 32-byte puzzle hash (address target).
    pub puzzle_hash: HashHex,
    /// Include already-spent coins?
    pub include_spent_coins: bool,
    /// Confirmed-height lower bound.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub start_height: Option<u64>,
    /// Confirmed-height upper bound.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub end_height: Option<u64>,
}

impl GetCoinRecordsByPuzzleHashRequest {
    /// The wire method name.
    pub const METHOD: &'static str = "get_coin_records_by_puzzle_hash";
}

/// Response for [`GetCoinRecordsByPuzzleHashRequest`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetCoinRecordsByPuzzleHashResponse {
    /// Coin records matching the filter.
    pub records: Vec<CoinRecordWire>,
}

/// Coin record in wire form.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoinRecordWire {
    /// Coin id (derived hash).
    pub coin_id: HashHex,
    /// Parent coin id.
    pub parent_coin_info: HashHex,
    /// Puzzle hash (address target).
    pub puzzle_hash: HashHex,
    /// Amount in mojos.
    pub amount: Amount,
    /// Height at which this coin was created.
    pub confirmed_block_height: u64,
    /// Height at which this coin was spent; `0` if unspent.
    pub spent_block_height: u64,
    /// True iff this is a coinbase (reward) coin.
    pub coinbase: bool,
    /// Timestamp from the confirming block.
    pub timestamp: u64,
}

// ===========================================================================
// Mempool
// ===========================================================================

/// `get_mempool` — summary of pending transactions.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetMempoolRequest;

impl GetMempoolRequest {
    /// The wire method name.
    pub const METHOD: &'static str = "get_mempool";
}

/// Response for [`GetMempoolRequest`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetMempoolResponse {
    /// Sum of CLVM cost across all items.
    pub total_cost: u64,
    /// Sum of fees across all items.
    pub total_fees: Amount,
    /// Summary entries, in eviction-priority order.
    pub items: Vec<MempoolItem>,
}

/// One entry in the mempool summary.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MempoolItem {
    /// Spend-bundle identifier (hash).
    pub spend_bundle_name: HashHex,
    /// CLVM cost for this bundle.
    pub cost: u64,
    /// Fee paid by this bundle, in mojos.
    pub fee: Amount,
    /// Whether this bundle is a CPFP ancestor / descendant.
    pub is_cpfp: bool,
}

/// `push_tx` — submit a spend bundle to the mempool.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PushTxRequest {
    /// Hex-encoded chia-streamable spend bundle.
    pub spend_bundle_hex: String,
}

impl PushTxRequest {
    /// The wire method name.
    pub const METHOD: &'static str = "push_tx";
}

/// Response for [`PushTxRequest`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PushTxResponse {
    /// Admission outcome.
    pub status: PushTxStatus,
    /// Optional details (reason code / error message).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub details: Option<String>,
}

/// Admission outcome of [`PushTxRequest`].
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PushTxStatus {
    /// Admitted to the mempool.
    Success,
    /// Rejected; see `details`.
    Rejected,
    /// Already present in the mempool.
    AlreadyExists,
}

// ===========================================================================
// Peers
// ===========================================================================

/// `get_connections` — enumerate connected peers.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetConnectionsRequest;

impl GetConnectionsRequest {
    /// The wire method name.
    pub const METHOD: &'static str = "get_connections";
}

/// Response for [`GetConnectionsRequest`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetConnectionsResponse {
    /// Connected peers.
    pub peers: Vec<PeerInfoWire>,
}

/// Peer info in wire form.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerInfoWire {
    /// Peer id — SHA256 of remote cert pubkey.
    pub peer_id: HashHex,
    /// Peer IP address + port.
    pub remote_addr: String,
    /// Peer node type (e.g., `"full_node"`, `"validator"`).
    pub node_type: String,
    /// Unix epoch seconds when the connection was established.
    pub connected_since: u64,
    /// Accumulated penalty points for abuse.
    pub penalty: u32,
}

/// `ban_peer` — evict and ban a peer.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BanPeerRequest {
    /// Peer id to ban.
    pub peer_id: HashHex,
    /// Reason string (stored in the audit log).
    pub reason: String,
    /// Ban duration in seconds; `0` = indefinite.
    pub duration_secs: u64,
}

impl BanPeerRequest {
    /// The wire method name.
    pub const METHOD: &'static str = "ban_peer";
}

/// Response for [`BanPeerRequest`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BanPeerResponse {
    /// Whether the peer is now banned.
    pub banned: bool,
}

// ===========================================================================
// Checkpoint
// ===========================================================================

/// `submit_partial_checkpoint_signature` — validator submits a partial sig
/// for the current epoch.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubmitPartialCheckpointSignatureRequest {
    /// Epoch the signature is for.
    pub epoch: u64,
    /// Signer's validator index in the VMR.
    pub validator_index: u32,
    /// BLS partial signature over the checkpoint digest.
    pub partial_sig: SignatureHex,
}

impl SubmitPartialCheckpointSignatureRequest {
    /// The wire method name.
    pub const METHOD: &'static str = "submit_partial_checkpoint_signature";
}

/// Response for [`SubmitPartialCheckpointSignatureRequest`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubmitPartialCheckpointSignatureResponse {
    /// Whether the signature was admitted to the pool.
    pub accepted: bool,
    /// Optional rejection reason.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
}

/// `get_checkpoint_pool` — status of the checkpoint aggregation pool.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetCheckpointPoolRequest {
    /// Specific epoch to inspect. `None` returns all open epochs.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub epoch: Option<u64>,
}

impl GetCheckpointPoolRequest {
    /// The wire method name.
    pub const METHOD: &'static str = "get_checkpoint_pool";
}

/// Response for [`GetCheckpointPoolRequest`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetCheckpointPoolResponse {
    /// Per-epoch status entries.
    pub epochs: Vec<CheckpointEpochStatus>,
}

/// Per-epoch aggregation status.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckpointEpochStatus {
    /// Epoch number.
    pub epoch: u64,
    /// Number of partials received.
    pub partials_count: u32,
    /// Threshold required (`2k + 1` where `n = validator_count`).
    pub threshold: u32,
    /// Whether the aggregation succeeded.
    pub aggregated: bool,
}

// ===========================================================================
// Validator set
// ===========================================================================

/// `get_validator` — look up a validator by pubkey.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetValidatorRequest {
    /// Validator's BLS pubkey.
    pub pubkey: PubkeyHex,
}

impl GetValidatorRequest {
    /// The wire method name.
    pub const METHOD: &'static str = "get_validator";
}

/// Response for [`GetValidatorRequest`]. `None` if no such validator.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetValidatorResponse {
    /// Validator summary, or `None` if not found.
    pub validator: Option<ValidatorSummary>,
}

/// `get_active_validators` — page through the current active set.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetActiveValidatorsRequest {
    /// Maximum results. Server caps at 1000.
    pub limit: u32,
    /// Results to skip.
    pub offset: u32,
}

impl GetActiveValidatorsRequest {
    /// The wire method name.
    pub const METHOD: &'static str = "get_active_validators";
}

/// Response for [`GetActiveValidatorsRequest`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetActiveValidatorsResponse {
    /// Active validators in this page.
    pub validators: Vec<ValidatorSummary>,
    /// Total active validators across the full set.
    pub total: u32,
}

/// `get_current_proposer` — who is elected proposer at a height.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetCurrentProposerRequest {
    /// The L2 height to query.
    pub height: u64,
}

impl GetCurrentProposerRequest {
    /// The wire method name.
    pub const METHOD: &'static str = "get_current_proposer";
}

/// Response for [`GetCurrentProposerRequest`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetCurrentProposerResponse {
    /// Elected proposer, or `None` if the height is out of range.
    pub proposer: Option<ValidatorSummary>,
}

// ===========================================================================
// Admin
// ===========================================================================

/// `stop_node` — request a graceful shutdown.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StopNodeRequest {
    /// Optional reason (written to audit log).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
}

impl StopNodeRequest {
    /// The wire method name.
    pub const METHOD: &'static str = "stop_node";
}

/// Response for [`StopNodeRequest`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StopNodeResponse {
    /// Whether shutdown has been initiated.
    pub accepted: bool,
}

/// `get_recovery_status` — fullnode recovery-journal state.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetRecoveryStatusRequest;

impl GetRecoveryStatusRequest {
    /// The wire method name.
    pub const METHOD: &'static str = "get_recovery_status";
}

/// Response for [`GetRecoveryStatusRequest`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetRecoveryStatusResponse {
    /// Current recovery mode.
    pub mode: RecoveryMode,
    /// Last anomaly observed, if any.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_anomaly: Option<String>,
    /// Number of recovery attempts in the current epoch.
    pub attempts: u32,
}

/// Recovery state machine values.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RecoveryMode {
    /// Normal operation.
    Running,
    /// Rolling back after an anomaly.
    Recovering,
    /// Exceeded the max recovery attempts per epoch.
    LoopBreakerOpen,
}

/// `get_version` — binary identification.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetVersionRequest;

impl GetVersionRequest {
    /// The wire method name.
    pub const METHOD: &'static str = "get_version";
}

/// Response for [`GetVersionRequest`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetVersionResponse {
    /// Human-readable version string (`"1.2.3-pre"`).
    pub version: String,
    /// Build commit SHA.
    pub build_commit: String,
}

#[cfg(test)]
mod tests {
    use super::*;

    /// **Proves:** every request struct's `METHOD` constant is
    /// lower-snake-case — no hyphens, no camelCase leaks.
    ///
    /// **Why it matters:** JSON-RPC method names are case-sensitive. A
    /// regression that introduced PascalCase or kebab-case for one method
    /// would break that single method on every deployed client.
    ///
    /// **Catches:** an accidental `pub const METHOD: &str = "GetBlock";`
    /// that slipped through review.
    #[test]
    fn method_names_are_snake_case() {
        let method_names = [
            GetBlockchainStateRequest::METHOD,
            GetNetworkInfoRequest::METHOD,
            HealthzRequest::METHOD,
            GetBlockRequest::METHOD,
            GetBlockByHeightRequest::METHOD,
            GetBlockRecordsRequest::METHOD,
            GetCoinRecordRequest::METHOD,
            GetCoinRecordsByHintRequest::METHOD,
            GetCoinRecordsByPuzzleHashRequest::METHOD,
            GetMempoolRequest::METHOD,
            PushTxRequest::METHOD,
            GetConnectionsRequest::METHOD,
            BanPeerRequest::METHOD,
            SubmitPartialCheckpointSignatureRequest::METHOD,
            GetCheckpointPoolRequest::METHOD,
            GetValidatorRequest::METHOD,
            GetActiveValidatorsRequest::METHOD,
            GetCurrentProposerRequest::METHOD,
            StopNodeRequest::METHOD,
            GetRecoveryStatusRequest::METHOD,
            GetVersionRequest::METHOD,
        ];

        for m in method_names {
            assert!(
                m.chars()
                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'),
                "method name {m:?} is not snake_case",
            );
            assert!(!m.is_empty(), "empty method name");
            assert!(!m.starts_with('_'), "method {m:?} starts with underscore");
        }
    }

    /// **Proves:** all method names are pairwise distinct.
    ///
    /// **Why it matters:** Two different request types cannot share a
    /// method name — the server's dispatcher would have no way to tell
    /// them apart. Catching collisions at test time prevents a whole class
    /// of integration bugs.
    ///
    /// **Catches:** copy-paste of a method name without updating it;
    /// shadowing a fullnode method with a same-named validator method
    /// (they're namespaced in code but collide on the wire).
    #[test]
    fn method_names_are_unique() {
        let method_names = [
            GetBlockchainStateRequest::METHOD,
            GetNetworkInfoRequest::METHOD,
            HealthzRequest::METHOD,
            GetBlockRequest::METHOD,
            GetBlockByHeightRequest::METHOD,
            GetBlockRecordsRequest::METHOD,
            GetCoinRecordRequest::METHOD,
            GetCoinRecordsByHintRequest::METHOD,
            GetCoinRecordsByPuzzleHashRequest::METHOD,
            GetMempoolRequest::METHOD,
            PushTxRequest::METHOD,
            GetConnectionsRequest::METHOD,
            BanPeerRequest::METHOD,
            SubmitPartialCheckpointSignatureRequest::METHOD,
            GetCheckpointPoolRequest::METHOD,
            GetValidatorRequest::METHOD,
            GetActiveValidatorsRequest::METHOD,
            GetCurrentProposerRequest::METHOD,
            StopNodeRequest::METHOD,
            GetRecoveryStatusRequest::METHOD,
            GetVersionRequest::METHOD,
        ];

        let mut seen = std::collections::HashSet::new();
        for m in method_names {
            assert!(seen.insert(m), "duplicate method name {m:?}");
        }
    }

    /// **Proves:** `GetBlockchainStateResponse` round-trips through JSON
    /// unchanged.
    ///
    /// **Why it matters:** This is the most-called RPC method — smoke test
    /// that the full struct decodes cleanly.
    ///
    /// **Catches:** dropping a field; re-typing `sync_progress` from `f32`
    /// to `f64` (JSON numbers are all f64 in JS; this is fine but the test
    /// pins the Rust-side type).
    #[test]
    fn get_blockchain_state_roundtrip() {
        let r = GetBlockchainStateResponse {
            height: 100,
            tip_hash: HashHex::new([1u8; 32]),
            synced: true,
            sync_progress: 1.0,
            finalized_epoch: 3,
            sealed_height: 96,
        };
        let j = serde_json::to_string(&r).unwrap();
        let back: GetBlockchainStateResponse = serde_json::from_str(&j).unwrap();
        assert_eq!(back.height, r.height);
        assert_eq!(back.tip_hash, r.tip_hash);
        assert_eq!(back.synced, r.synced);
    }

    /// **Proves:** `PushTxStatus` serializes in snake_case
    /// (`"success"`, `"rejected"`, `"already_exists"`).
    ///
    /// **Why it matters:** Clients pattern-match on these strings. The
    /// already-declared `#[serde(rename_all = "snake_case")]` is what makes
    /// them stable.
    ///
    /// **Catches:** a regression that drops the rename attribute, or a
    /// typo in a variant name (`AlreadyExist` vs `AlreadyExists`).
    #[test]
    fn push_tx_status_snake_case() {
        let s = serde_json::to_string(&PushTxStatus::Success).unwrap();
        assert_eq!(s, "\"success\"");

        let s = serde_json::to_string(&PushTxStatus::AlreadyExists).unwrap();
        assert_eq!(s, "\"already_exists\"");
    }

    /// **Proves:** a request with optional fields omitted encodes without
    /// the `null` sentinel (because of `skip_serializing_if`).
    ///
    /// **Why it matters:** JSON-RPC clients vary in how they interpret
    /// `null` vs absent fields. We consistently omit absent optionals to
    /// match Chia's own RPC behaviour.
    ///
    /// **Catches:** dropping `skip_serializing_if = "Option::is_none"`
    /// from an optional field — the wire form would gain `"field":null`
    /// and break deduplication of requests that rely on canonical form.
    #[test]
    fn optional_fields_omitted_when_none() {
        let req = GetCoinRecordsByHintRequest {
            hint: HashHex::new([0u8; 32]),
            include_spent_coins: false,
            start_height: None,
            end_height: None,
        };
        let s = serde_json::to_string(&req).unwrap();
        assert!(
            !s.contains("start_height"),
            "start_height should be omitted, got: {s}"
        );
        assert!(
            !s.contains("end_height"),
            "end_height should be omitted, got: {s}"
        );
    }
}