dig-rpc-types 0.2.1

Canonical DIG-node JSON-RPC interface: request/response types, the method enum + tier classification, the error-code taxonomy, and an OpenRPC 1.2.6 document generator. The single source of truth both DIG node implementations depend on. Pure types — no I/O, no async, no server 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
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
//! Request/response wire types for every DIG-node RPC method.
//!
//! Each type is `serde`-derived and models a method's params or result
//! field-for-field with the canonical implementation (the digstore `dig-node`
//! crate). Fields that appear only in one profile or only on the first window of
//! a paged stream are `Option` and doc-flagged.
//!
//! Hex-encoded identifiers (`store_id`, `root`, `retrieval_key`, `peer_id`) are
//! carried as `String` on the wire — lower-case 64-hex — because the interface
//! crate does no crypto and imposes no byte-array dependency. Callers validate
//! length/charset at their boundary.
//!
//! # Two content profiles, one chunk type
//!
//! [`ContentChunk`] models both the node profile (`dig.getContent` on the local
//! dig-node) and the network profile (`rpc.dig.net`). The network-profile-only
//! fields — [`total_length`](ContentChunk::total_length),
//! [`length`](ContentChunk::length), [`program_hash`](ContentChunk::program_hash),
//! [`offset`](ContentChunk::offset) — are `Option` so one type serves both
//! surfaces with no silent split.

use serde::{Deserialize, Serialize};

/// A lower-case 64-hex identifier on the wire (e.g. a `store_id`, `root`,
/// `retrieval_key`, or `peer_id`). A type alias for documentation; validation is
/// the boundary's job.
pub type HexId = String;

// ===========================================================================
// Shared value objects
// ===========================================================================

/// A peer's dialable network endpoint.
///
/// IPv6-first per the ecosystem networking rule: an address list orders
/// global-unicast IPv6 ahead of IPv4 fallback, and a wildcard bind
/// (`[::]`/`0.0.0.0`) is never advertised.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct PeerAddress {
    /// The host — an IPv6 or IPv4 literal (never a wildcard).
    pub host: String,
    /// The TCP port.
    pub port: u16,
    /// How the address was discovered: `direct`, `reflexive`, `mapped`, or
    /// `relay`.
    pub kind: String,
}

/// A content provider: a holder's stable `peer_id` plus its candidate addresses.
///
/// The address list is byte-compatible with [`dig.getPeers`](crate::method::Method::GetPeers)
/// and the DHT provider shape.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct Provider {
    /// The holder's stable `peer_id` = `SHA-256(TLS SPKI DER)`, 64-hex.
    pub peer_id: HexId,
    /// The holder's candidate addresses (IPv6-first).
    pub addresses: Vec<PeerAddress>,
}

/// The content item a redirect points at: `store_id` [+ `root` [+
/// `retrieval_key`]], each lower-case 64-hex — the exact item to re-request.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct ContentRef {
    /// The store launcher id (always present).
    pub store_id: HexId,
    /// The generation root (present for capsule/resource granularity).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub root: Option<HexId>,
    /// The resource retrieval key (present for resource granularity).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub retrieval_key: Option<HexId>,
}

/// The `error.data.redirect` payload of a
/// [`ContentRedirect`](crate::error::ErrorCode::ContentRedirect) (`-32008`).
///
/// The node does not hold the content but located peers that do; the caller
/// re-requests against one of `providers`, echoing `redirect_depth` in its
/// params so the hop budget stays bounded (stop at `max_redirects`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct RedirectInfo {
    /// The content the caller should re-request.
    pub content: ContentRef,
    /// The holders (peer_id + candidate addresses) to re-request against.
    pub providers: Vec<Provider>,
    /// The hop count the caller must echo on its re-request.
    pub redirect_depth: u64,
    /// The redirect budget — stop redirecting when `redirect_depth` reaches this.
    pub max_redirects: u64,
}

// ===========================================================================
// dig.getContent  (PUBLIC-READ, also peer-reachable)
// ===========================================================================

/// Params for [`dig.getContent`](crate::method::Method::GetContent) — a verified
/// resource-window read.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct GetContentParams {
    /// The CHIP-0035 singleton launcher id (64-hex).
    pub store_id: HexId,
    /// `SHA-256(urn)` — the only URN-derived value sent to a node (64-hex).
    pub retrieval_key: HexId,
    /// The generation root (64-hex). Empty / `"latest"` / absent ⇒ resolve the
    /// chain tip.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub root: Option<HexId>,
    /// The window start offset (default 0).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub offset: Option<u64>,
    /// Retrieval mode: `"speed"` (default) or `"privacy"` (onion — target).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub mode: Option<String>,
    /// The redirect budget already consumed (echoed from a `-32008` redirect).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub redirect_depth: Option<u64>,
}

/// One window of a resource's ciphertext — the chunk wire object.
///
/// Serves BOTH the node profile (`dig.getContent` on the local dig-node) and the
/// network profile (`rpc.dig.net`). Node-profile responses omit the
/// network-profile-only fields; the doc on each field says which profile
/// populates it.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct ContentChunk {
    /// This window's bytes, base64. Both profiles.
    pub ciphertext: String,
    /// The resolved generation root (64-hex). Both profiles.
    pub root: HexId,
    /// Whether this window ends the resource. Both profiles.
    pub complete: bool,
    /// The next offset; present iff not complete. Both profiles.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub next_offset: Option<u64>,
    /// Whole-resource merkle proof, base64. First window only (`offset == 0`).
    /// Both profiles.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub inclusion_proof: Option<String>,
    /// Per-chunk ciphertext lengths of the full resource. First window only;
    /// empty ⇒ single chunk. Both profiles.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub chunk_lens: Option<Vec<u64>>,
    /// Where the window was served from: `"local"` (this device's cache) or
    /// `"remote"` (freshly fetched). **Node profile only** — additive tag the
    /// in-process node sets; absent on the network profile.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub source: Option<String>,
    /// The full resource ciphertext length (pre-windowing). **Network profile
    /// only.**
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub total_length: Option<u64>,
    /// This window's byte length. **Network profile only** (the node profile's
    /// length is implicit in `ciphertext`).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub length: Option<u64>,
    /// The window start offset (echoed). **Network profile only.**
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub offset: Option<u64>,
    /// `SHA-256(.dig bytes)` — the on-chain program identity (64-hex).
    /// **Network profile only.**
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub program_hash: Option<HexId>,
}

// ===========================================================================
// dig.getAnchoredRoot  (PUBLIC-READ, also peer-reachable)
// ===========================================================================

/// Params for [`dig.getAnchoredRoot`](crate::method::Method::GetAnchoredRoot).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct GetAnchoredRootParams {
    /// The store launcher id (64-hex).
    pub store_id: HexId,
}

/// Result for [`dig.getAnchoredRoot`](crate::method::Method::GetAnchoredRoot) —
/// the store's current chain-anchored tip root.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct AnchoredRoot {
    /// The store launcher id (echoed, 64-hex).
    pub store_id: HexId,
    /// The chain-anchored tip root (64-hex).
    pub root: HexId,
}

// ===========================================================================
// dig.getCollection / dig.listCollectionItems  (PUBLIC-READ, also peer)
// ===========================================================================

/// Params for [`dig.getCollection`](crate::method::Method::GetCollection).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct GetCollectionParams {
    /// The NFT launcher ids to resolve. Capped at 10,000 (over-cap ⇒ `-32602`).
    pub launcher_ids: Vec<HexId>,
    /// The optional collection creator DID (64-hex).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub did: Option<HexId>,
}

/// Result for [`dig.getCollection`](crate::method::Method::GetCollection) —
/// collection-level facts computed from DIG's own coinset data.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct Collection {
    /// The resolved creator DID (64-hex), if any.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub did: Option<HexId>,
    /// The DID declared by the caller / metadata (64-hex), if any.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub declared_did: Option<HexId>,
    /// The number of launcher ids requested.
    pub item_count: u64,
    /// How many resolved to live NFTs.
    pub resolved_count: u64,
    /// The uniform royalty in basis points, if resolvable.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub royalty_basis_points: Option<u64>,
}

/// Params for
/// [`dig.listCollectionItems`](crate::method::Method::ListCollectionItems).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct ListCollectionItemsParams {
    /// The NFT launcher ids. Capped at 10,000 (over-cap ⇒ `-32602`).
    pub launcher_ids: Vec<HexId>,
    /// Page start (default 0).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub offset: Option<u64>,
    /// Page size (default 50, capped at 200).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub limit: Option<u64>,
}

/// CHIP-0007 NFT metadata for one collection item.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct NftMetadata {
    /// Edition ordinal, if any.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub edition_number: Option<u64>,
    /// Edition total, if any.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub edition_total: Option<u64>,
    /// Data URIs.
    #[serde(default)]
    pub data_uris: Vec<String>,
    /// `SHA-256` of the data (64-hex), if any.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub data_hash: Option<HexId>,
    /// Metadata URIs.
    #[serde(default)]
    pub metadata_uris: Vec<String>,
    /// `SHA-256` of the metadata document (64-hex), if any.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub metadata_hash: Option<HexId>,
    /// License URIs.
    #[serde(default)]
    pub license_uris: Vec<String>,
    /// `SHA-256` of the license (64-hex), if any.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub license_hash: Option<HexId>,
}

/// One resolved collection item — its current on-chain owner, royalty, and
/// CHIP-0007 metadata.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct CollectionItem {
    /// The NFT launcher id (64-hex).
    pub launcher_id: HexId,
    /// The current coin id (64-hex).
    pub coin_id: HexId,
    /// The current owner DID (64-hex), if any.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub owner_did: Option<HexId>,
    /// The royalty puzzle hash (64-hex).
    pub royalty_puzzle_hash: HexId,
    /// The royalty in basis points.
    pub royalty_basis_points: u64,
    /// The current owner puzzle hash (64-hex).
    pub owner_puzzle_hash: HexId,
    /// The CHIP-0007 metadata, if resolvable.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub metadata: Option<NftMetadata>,
}

/// Result for
/// [`dig.listCollectionItems`](crate::method::Method::ListCollectionItems) — a
/// page of resolved items.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct CollectionItemsPage {
    /// This page's items.
    pub items: Vec<CollectionItem>,
    /// The page start (echoed).
    pub offset: u64,
    /// The page size (echoed).
    pub limit: u64,
    /// The total item count across the whole (capped) launcher set.
    pub total: u64,
    /// The next page's offset, or `null` when exhausted.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub next_offset: Option<u64>,
}

// ===========================================================================
// dig.getNetworkInfo  (PEER)
// ===========================================================================

/// The node's relay reservation posture.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct RelayStatus {
    /// The relay endpoint URL (e.g. `wss://relay.dig.net:9450`).
    pub url: String,
    /// Whether a relay reservation is currently held.
    pub reserved: bool,
}

/// Result for [`dig.getNetworkInfo`](crate::method::Method::GetNetworkInfo) —
/// this node's own peer-network posture.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct NetworkInfo {
    /// This node's stable `peer_id` = `SHA-256(TLS SPKI DER)` (64-hex), or
    /// `null` when no identity is configured.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub peer_id: Option<HexId>,
    /// The DIG network id (e.g. `DIG_MAINNET`).
    pub network_id: String,
    /// The first advertised (dialable) candidate address, `host:port`.
    pub listen_addr: String,
    /// The STUN-discovered reflexive address, if known.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub reflexive_addr: Option<String>,
    /// All advertised candidate addresses (IPv6-first).
    pub candidate_addresses: Vec<String>,
    /// Reachability posture: `"direct"` or `"relayed"`.
    pub reachability: String,
    /// The relay reservation posture.
    pub relay: RelayStatus,
}

// ===========================================================================
// dig.getPeers  (PEER)
// ===========================================================================

/// Result for [`dig.getPeers`](crate::method::Method::GetPeers) — the peers this
/// node currently knows (peer exchange over RPC).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct PeersList {
    /// The known peers (peer_id + candidate addresses).
    pub peers: Vec<Provider>,
}

// ===========================================================================
// dig.announce  (PEER)
// ===========================================================================

/// Params for [`dig.announce`](crate::method::Method::Announce).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct AnnounceParams {
    /// The announcing peer's `peer_id` (64-hex).
    pub peer_id: HexId,
    /// The announcing peer's candidate addresses.
    pub addresses: Vec<PeerAddress>,
}

/// Result for [`dig.announce`](crate::method::Method::Announce).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct AnnounceAck {
    /// Whether the announcement was accepted.
    pub accepted: bool,
    /// How many peers this node now knows.
    pub known_peers: u64,
}

// ===========================================================================
// dig.getAvailability  (PEER)
// ===========================================================================

/// One availability query item. Granularity is inferred from which fields are
/// present: `store_id` only ⇒ which roots are held; `+root` ⇒ a capsule; `+root
/// +retrieval_key` ⇒ a resource.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct AvailabilityQuery {
    /// The store launcher id (64-hex, required).
    pub store_id: HexId,
    /// The generation root (64-hex), for capsule/resource granularity.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub root: Option<HexId>,
    /// The resource retrieval key (64-hex), for resource granularity.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub retrieval_key: Option<HexId>,
}

/// Params for [`dig.getAvailability`](crate::method::Method::GetAvailability).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct GetAvailabilityParams {
    /// The items to check. Capped at 512 per batch (past-cap items are dropped).
    pub items: Vec<AvailabilityQuery>,
}

/// One availability answer. Only the fields relevant to the query's granularity
/// are populated.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct AvailabilityAnswer {
    /// Whether this node holds the queried item.
    pub available: bool,
    /// The roots held (store-granularity queries only).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub roots: Option<Vec<HexId>>,
    /// The full resource ciphertext length (resource-granularity only).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub total_length: Option<u64>,
    /// The chunk count (resource-granularity only).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub chunk_count: Option<u64>,
    /// Whether the whole item is held (root/resource-granularity only).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub complete: Option<bool>,
    /// Providers that hold the item — present on a miss when holders were
    /// located (enriched answer).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub providers: Option<Vec<Provider>>,
}

/// Result for [`dig.getAvailability`](crate::method::Method::GetAvailability) —
/// one answer per query item, in order.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct AvailabilityBatch {
    /// The per-item answers (index-aligned to the query items served).
    pub items: Vec<AvailabilityAnswer>,
}

// ===========================================================================
// dig.listInventory  (PEER)
// ===========================================================================

/// Params for [`dig.listInventory`](crate::method::Method::ListInventory).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct ListInventoryParams {
    /// The store to list roots for (64-hex). Absent ⇒ list all stores served.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub store_id: Option<HexId>,
    /// The maximum number of entries to return.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub limit: Option<u64>,
}

/// Result for [`dig.listInventory`](crate::method::Method::ListInventory).
///
/// With a `store_id` the node returns the roots it holds for that store; without
/// one it returns the stores it serves. `#[serde(untagged)]` keeps the wire flat
/// (`{"roots": …}` or `{"stores": …}`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub enum Inventory {
    /// The roots held for a specific store.
    ForStore {
        /// The store launcher id (echoed, 64-hex).
        store_id: HexId,
        /// The roots this node holds for the store.
        roots: Vec<HexId>,
    },
    /// The stores this node serves (no `store_id` given).
    AllStores {
        /// The store launcher ids served.
        stores: Vec<HexId>,
    },
}

// ===========================================================================
// dig.fetchRange  (PEER)
// ===========================================================================

/// Params for [`dig.fetchRange`](crate::method::Method::FetchRange) — a single
/// range frame of a resource this node holds.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct FetchRangeParams {
    /// The store launcher id (64-hex, required).
    pub store_id: HexId,
    /// The generation root (64-hex, required for a resource fetch).
    pub root: HexId,
    /// `SHA-256(urn)` (64-hex, required for a resource fetch).
    pub retrieval_key: HexId,
    /// The range start (default 0).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub offset: Option<u64>,
    /// The range length in bytes (> 0; clamped to the window cap).
    pub length: u64,
    /// Whole-capsule mode (default false). Capsule range fetch is not yet
    /// served; a `true` here yields `-32004`.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub capsule: Option<bool>,
    /// The redirect budget already consumed (echoed from a `-32008` redirect).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub redirect_depth: Option<u64>,
}

/// One range frame of a resource. The first frame (`offset == 0`) carries the
/// per-resource verification metadata; later frames carry only the window.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct RangeFrame {
    /// The window start offset (echoed).
    pub offset: u64,
    /// This window's byte length.
    pub length: u64,
    /// This window's ciphertext, base64.
    pub bytes: String,
    /// Whether this frame ends the resource.
    pub complete: bool,
    /// The full resource ciphertext length. First frame only.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub total_length: Option<u64>,
    /// Per-chunk ciphertext lengths of the full resource. First frame only.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub chunk_lens: Option<Vec<u64>>,
    /// This frame's chunk index. First frame only (`0`).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub chunk_index: Option<u64>,
    /// Whole-resource merkle proof, base64. First frame only.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub inclusion_proof: Option<String>,
    /// The chain-anchored root (64-hex). First frame only.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub root: Option<HexId>,
}

// ===========================================================================
// dig.stage  (CONTROL — loopback / in-process only)
// ===========================================================================

/// Params for [`dig.stage`](crate::method::Method::Stage) — compile a local
/// folder into a capsule `.dig` module in-process.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct StageParams {
    /// The absolute path to the folder to compile.
    pub dir: String,
    /// The target store launcher id (64-hex). Absent ⇒ an ephemeral,
    /// content-derived id (a preview).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub store_id: Option<HexId>,
    /// The store salt (64-hex). Present ⇒ a private store.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub salt: Option<HexId>,
    /// Optional DIGHub-style manifest metadata to embed.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub metadata: Option<serde_json::Value>,
}

/// Result for [`dig.stage`](crate::method::Method::Stage) — the compiled capsule.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct StageResult {
    /// The canonical capsule identity, `storeId:rootHash`.
    pub capsule: String,
    /// The store launcher id (64-hex).
    pub store_id: HexId,
    /// The compiled generation root (64-hex).
    pub root: HexId,
    /// The filesystem path to the compiled `.dig` module.
    pub module_path: String,
    /// The module size in bytes.
    pub size: u64,
    /// The `chia://storeId:rootHash/` content address.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub content_address: Option<String>,
    /// The relative paths compiled into the capsule.
    #[serde(default)]
    pub files: Vec<String>,
    /// Whether this is an ephemeral preview (not advancing a real store).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub ephemeral: Option<bool>,
}

// ===========================================================================
// cache.*  (CONTROL — loopback / in-process only)
// ===========================================================================

/// Result for [`cache.getConfig`](crate::method::Method::CacheGetConfig).
///
/// The canonical field name for the cache path is `cache_dir` everywhere (the
/// shell's historical `dir` is unified onto this name).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct CacheConfig {
    /// The on-disk cache size cap in bytes (floored at 64 MiB).
    pub cap_bytes: u64,
    /// The bytes currently used.
    pub used_bytes: u64,
    /// The effective resolved cache directory.
    pub cache_dir: String,
    /// Whether that directory is the canonical shared location (vs a
    /// process-private fallback).
    pub shared: bool,
}

/// Params for [`cache.setCapBytes`](crate::method::Method::CacheSetCapBytes).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct SetCapBytesParams {
    /// The requested cap in bytes (floored at 64 MiB by the node).
    pub cap_bytes: u64,
}

/// Result for [`cache.setCapBytes`](crate::method::Method::CacheSetCapBytes).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct SetCapBytesResult {
    /// The effective cap after flooring.
    pub cap_bytes: u64,
}

/// One durable cached-module entry.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct CachedCapsule {
    /// The canonical capsule identity, `storeId:rootHash`.
    pub capsule: String,
    /// The store launcher id (64-hex).
    pub store_id: HexId,
    /// The generation root (64-hex).
    pub root: HexId,
    /// The module size in bytes.
    pub size_bytes: u64,
    /// When the module was last used (unix ms).
    pub last_used_unix_ms: u64,
}

/// Result for [`cache.listCached`](crate::method::Method::CacheListCached).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct CachedList {
    /// The cached capsules.
    pub cached: Vec<CachedCapsule>,
}

/// Params for a capsule-keyed cache op
/// ([`cache.removeCached`](crate::method::Method::CacheRemoveCached),
/// [`cache.fetchAndCache`](crate::method::Method::CacheFetchAndCache)).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct CapsuleKey {
    /// The store launcher id (64-hex).
    pub store_id: HexId,
    /// The generation root (64-hex).
    pub root: HexId,
}

/// Result for [`cache.removeCached`](crate::method::Method::CacheRemoveCached).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct RemoveCachedResult {
    /// Whether an entry was removed.
    pub removed: bool,
}

/// Result for [`cache.fetchAndCache`](crate::method::Method::CacheFetchAndCache).
///
/// A failed fetch is reported in-band (`status = "failed"` + `message`) so the
/// caller can show it without treating it as a transport error.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct FetchAndCacheResult {
    /// `"cached"`, `"already_cached"`, or `"failed"`.
    pub status: String,
    /// The fetched module size in bytes (on success).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub size_bytes: Option<u64>,
    /// The served generation root (64-hex, on success).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub served_root: Option<HexId>,
    /// The failure message (on `status = "failed"`).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub message: Option<String>,
}

// ===========================================================================
// control.peerStatus  (CONTROL — loopback / in-process only)
// ===========================================================================

/// Result for [`control.peerStatus`](crate::method::Method::ControlPeerStatus) —
/// a snapshot of the node's L7 peer network. Always safe to call; reports
/// `running: false` on the FFI path.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct PeerStatusSnapshot {
    /// Whether a peer network is currently active.
    pub running: bool,
    /// This node's `peer_id` (64-hex), if a peer network is running.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub peer_id: Option<HexId>,
    /// The DIG network id.
    pub network_id: String,
    /// The relay reservation posture.
    pub relay: RelayStatus,
    /// The number of currently connected peers.
    pub connected_peers: u64,
    /// The last peer-network error, if any.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub last_error: Option<String>,
}

// ===========================================================================
// dig.health / dig.methods / rpc.discover  (discovery)
// ===========================================================================

/// Result for [`dig.health`](crate::method::Method::Health) — liveness + a
/// capability summary.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct Health {
    /// Liveness — `"ok"` when the node can serve.
    pub status: String,
    /// The node's software version.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub version: Option<String>,
    /// The DIG network id the node serves.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub network_id: Option<String>,
    /// The method names this node implements (its profile).
    #[serde(default)]
    pub methods: Vec<String>,
}

/// Result for [`dig.methods`](crate::method::Method::Methods) — the method names
/// this node implements (agent self-describe).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct Methods {
    /// The implemented method names.
    pub methods: Vec<String>,
}

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

    /// **Proves:** `ContentChunk` round-trips a node-profile window (no
    /// network-profile fields) without inventing keys.
    /// **Catches:** a missing `skip_serializing_if` that would leak `null`
    /// network-profile fields onto the node profile.
    #[test]
    fn content_chunk_node_profile_is_lean() {
        let c = ContentChunk {
            ciphertext: "AAA=".into(),
            root: "ab".repeat(32),
            complete: false,
            next_offset: Some(3_145_728),
            inclusion_proof: Some("cHJvb2Y=".into()),
            chunk_lens: Some(vec![10, 20]),
            source: Some("local".into()),
            total_length: None,
            length: None,
            offset: None,
            program_hash: None,
        };
        let v = serde_json::to_value(&c).unwrap();
        assert_eq!(v["source"], "local");
        assert!(
            v.get("total_length").is_none(),
            "node profile must omit total_length"
        );
        assert!(v.get("program_hash").is_none());
        assert_eq!(serde_json::from_value::<ContentChunk>(v).unwrap(), c);
    }

    /// **Proves:** the network-profile fields serialize when present.
    #[test]
    fn content_chunk_network_profile_carries_extras() {
        let c = ContentChunk {
            ciphertext: "AAA=".into(),
            root: "cd".repeat(32),
            complete: true,
            next_offset: None,
            inclusion_proof: None,
            chunk_lens: None,
            source: None,
            total_length: Some(100),
            length: Some(100),
            offset: Some(0),
            program_hash: Some("ef".repeat(32)),
        };
        let v = serde_json::to_value(&c).unwrap();
        assert_eq!(v["total_length"], 100);
        assert_eq!(v["length"], 100);
        assert!(v.get("source").is_none());
    }

    /// **Proves:** the untagged `Inventory` picks `ForStore` vs `AllStores` by
    /// shape.
    /// **Catches:** a lost `#[serde(untagged)]` that would tag the variant.
    #[test]
    fn inventory_untagged_by_shape() {
        let for_store = Inventory::ForStore {
            store_id: "ab".repeat(32),
            roots: vec!["cd".repeat(32)],
        };
        let s = serde_json::to_string(&for_store).unwrap();
        assert!(s.contains("\"roots\""));
        assert!(!s.contains("ForStore"));
        assert_eq!(serde_json::from_str::<Inventory>(&s).unwrap(), for_store);

        let all = Inventory::AllStores {
            stores: vec!["ef".repeat(32)],
        };
        let s = serde_json::to_string(&all).unwrap();
        assert!(s.contains("\"stores\""));
        assert_eq!(serde_json::from_str::<Inventory>(&s).unwrap(), all);
    }

    /// **Proves:** `RedirectInfo` serializes the full redirect payload the
    /// `-32008` envelope carries.
    #[test]
    fn redirect_info_shape() {
        let r = RedirectInfo {
            content: ContentRef {
                store_id: "ab".repeat(32),
                root: Some("cd".repeat(32)),
                retrieval_key: Some("ef".repeat(32)),
            },
            providers: vec![Provider {
                peer_id: "12".repeat(32),
                addresses: vec![PeerAddress {
                    host: "::1".into(),
                    port: 9444,
                    kind: "direct".into(),
                }],
            }],
            redirect_depth: 1,
            max_redirects: 4,
        };
        let v = serde_json::to_value(&r).unwrap();
        assert_eq!(v["redirect_depth"], 1);
        assert_eq!(v["max_redirects"], 4);
        assert_eq!(v["providers"][0]["addresses"][0]["host"], "::1");
        assert_eq!(serde_json::from_value::<RedirectInfo>(v).unwrap(), r);
    }

    /// **Proves:** `cache.getConfig` uses the canonical `cache_dir` field name.
    /// **Catches:** a regression to the shell's historical `dir` name.
    #[test]
    fn cache_config_field_name_is_cache_dir() {
        let c = CacheConfig {
            cap_bytes: 1 << 30,
            used_bytes: 0,
            cache_dir: "/var/cache/dig".into(),
            shared: true,
        };
        let v = serde_json::to_value(&c).unwrap();
        assert!(v.get("cache_dir").is_some());
        assert!(v.get("dir").is_none(), "must not use the legacy `dir` name");
    }
}