Skip to main content

dig_node_control_interface/
results.rs

1//! Typed result payloads for the control methods.
2//!
3//! Each struct is field-for-field identical to what dig-node emits (snake_case wire fields), so a
4//! client deserializes the node's real response and re-serializes the same bytes — the property the
5//! conformance KATs pin. Genuinely open/proxied shapes (the updater beacon's status, the peer-pool
6//! snapshot, the pairing list) stay [`serde_json::Value`] on the call's `Output` rather than being
7//! frozen into a struct that would drift from the proxied source.
8
9use serde::{Deserialize, Serialize};
10
11/// The on-disk content-cache view (`control.cache.get`, and embedded in [`StatusResult`]).
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct CacheView {
14    /// The configured cache size cap, in bytes.
15    pub cap_bytes: u64,
16    /// Bytes currently used on disk.
17    pub used_bytes: u64,
18    /// The cache directory.
19    pub dir: String,
20    /// Whether the cache directory is the machine-wide shared cache.
21    pub shared: bool,
22}
23
24/// The §21 sync availability flag embedded in [`StatusResult`].
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct SyncAvailability {
27    /// Whether authenticated §21 whole-store sync is available on this node.
28    pub available: bool,
29}
30
31/// `control.status` — a rich node status snapshot.
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub struct StatusResult {
34    /// Always `true` for a responding node.
35    pub running: bool,
36    /// The service name (`"dig-node"`).
37    pub service: String,
38    /// The node binary's semantic version.
39    pub version: String,
40    /// The git commit the binary was built from (or `"unknown"`).
41    pub commit: String,
42    /// The DIG read protocol version the node speaks.
43    pub protocol: String,
44    /// Process uptime in seconds.
45    pub uptime_secs: u64,
46    /// The loopback `host:port` the node is bound to.
47    pub addr: String,
48    /// The upstream DIG RPC the node proxies/syncs to.
49    pub upstream: String,
50    /// The on-disk cache view.
51    pub cache: CacheView,
52    /// Distinct stores held (from the cache).
53    pub hosted_store_count: u64,
54    /// Cached capsule count.
55    pub cached_capsule_count: u64,
56    /// Pinned-store count.
57    pub pinned_store_count: u64,
58    /// §21 sync availability.
59    pub sync: SyncAvailability,
60}
61
62/// `control.config.get` — the node's effective configuration.
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct ConfigResult {
65    /// The bound `host:port`.
66    pub addr: String,
67    /// The bound port, as a string.
68    pub port: String,
69    /// The effective upstream DIG RPC.
70    pub upstream: String,
71    /// The persisted upstream override, or `null` when unset.
72    pub upstream_override: Option<String>,
73    /// The cache directory.
74    pub cache_dir: String,
75    /// Whether the cache is the machine-wide shared cache.
76    pub cache_shared: bool,
77    /// The node's config.json path.
78    pub config_path: String,
79    /// Whether authenticated §21 sync is available.
80    pub sync_available: bool,
81}
82
83/// `control.config.setUpstream` — the persisted override + a restart hint.
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct SetUpstreamResult {
86    /// The normalized upstream that was persisted.
87    pub upstream: String,
88    /// Always `true` — the change takes effect on next node start.
89    pub requires_restart: bool,
90}
91
92/// `control.log.setLevel` — the applied filter directive.
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct SetLevelResult {
95    /// The EnvFilter directive now in effect.
96    pub filter: String,
97}
98
99/// `control.cache.setCap` — the applied cap (after the 64 MiB floor).
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101pub struct SetCapResult {
102    /// The cache cap now in effect, in bytes.
103    pub cap_bytes: u64,
104}
105
106/// `control.cache.clear` — the clear acknowledgement.
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108pub struct CacheClearResult {
109    /// Always `true`.
110    pub cleared: bool,
111}
112
113/// One cached capsule of a store, as listed by the hosted-stores methods.
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115pub struct CapsuleEntry {
116    /// The capsule reference (`storeId:rootHash`).
117    pub capsule: String,
118    /// The capsule root hash.
119    pub root: String,
120    /// The capsule size on disk, in bytes.
121    pub size_bytes: u64,
122    /// When the capsule was last served, in unix milliseconds.
123    pub last_used_unix_ms: u64,
124}
125
126/// One hosted/pinned store (`control.hostedStores.list`).
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128pub struct HostedStore {
129    /// The canonical lowercase 64-hex store id.
130    pub store_id: String,
131    /// Whether the operator has pinned this store.
132    pub pinned: bool,
133    /// The number of cached capsules of this store.
134    pub capsule_count: u64,
135    /// The total cached bytes across this store's capsules.
136    pub total_bytes: u64,
137    /// The cached capsules of this store.
138    pub capsules: Vec<CapsuleEntry>,
139}
140
141/// `control.hostedStores.list` — every held/pinned store.
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143pub struct HostedStoresListResult {
144    /// The stores, one entry per distinct store id.
145    pub stores: Vec<HostedStore>,
146}
147
148/// `control.hostedStores.pin` — the pin acknowledgement + the pre-fetch outcome.
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150pub struct PinResult {
151    /// The store id that was pinned.
152    pub store_id: String,
153    /// The pinned root, or `null` when pinned at store level.
154    pub root: Option<String>,
155    /// Always `true`.
156    pub pinned: bool,
157    /// The in-band pre-fetch outcome (`{status, …}`) — its shape varies with the fetch path.
158    pub fetch: serde_json::Value,
159}
160
161/// `control.hostedStores.unpin` — the unpin acknowledgement + eviction count.
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163pub struct UnpinResult {
164    /// The store id that was unpinned.
165    pub store_id: String,
166    /// Whether a pin registry entry was actually removed.
167    pub unpinned: bool,
168    /// How many cached capsules of the store were evicted.
169    pub evicted_capsules: u64,
170}
171
172/// `control.hostedStores.status` — per-store pinned flag + cached capsules.
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
174pub struct HostedStoreStatusResult {
175    /// The store id queried.
176    pub store_id: String,
177    /// Whether the store is pinned.
178    pub pinned: bool,
179    /// The number of cached capsules.
180    pub capsule_count: u64,
181    /// The total cached bytes.
182    pub total_bytes: u64,
183    /// The cached capsules.
184    pub capsules: Vec<CapsuleEntry>,
185}
186
187/// `control.sync.status` — §21 sync availability + pin coverage.
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
189pub struct SyncStatusResult {
190    /// Whether authenticated §21 whole-store sync is available.
191    pub available: bool,
192    /// The sync method name.
193    pub method: String,
194    /// The number of pinned stores.
195    pub pinned_total: u64,
196    /// How many pinned stores currently have a cached capsule.
197    pub pinned_synced: u64,
198    /// Whether whole-store (root-less) sync is supported by this build.
199    pub whole_store_trigger_supported: bool,
200}
201
202/// `control.sync.trigger` — the synced-capsule outcome.
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204pub struct SyncTriggerResult {
205    /// The store id synced.
206    pub store_id: String,
207    /// The capsule root synced.
208    pub root: String,
209    /// The outcome status (`"synced"`).
210    pub status: String,
211    /// The synced capsule size, in bytes.
212    pub size_bytes: u64,
213    /// The served root the node verified against.
214    pub served_root: String,
215}
216
217/// `control.pairing.approve` — the mint acknowledgement + the new token's id.
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219pub struct PairingApproveResult {
220    /// Always `true`.
221    pub approved: bool,
222    /// The requesting client's declared name.
223    pub client_name: String,
224    /// The short id of the minted paired token (used to revoke it).
225    pub token_id: String,
226}
227
228/// `control.pairing.revoke` — the revoke acknowledgement.
229#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
230pub struct PairingRevokeResult {
231    /// Whether a token was actually removed.
232    pub revoked: bool,
233    /// The token id that was targeted.
234    pub token_id: String,
235}
236
237/// `control.peers.connect` — the connected peer's id.
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
239pub struct PeersConnectResult {
240    /// Always `true` on success.
241    pub connected: bool,
242    /// The connected peer's id.
243    pub peer_id: String,
244}
245
246/// `control.peers.disconnect` — the dropped peer's id.
247#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
248pub struct PeersDisconnectResult {
249    /// Always `true` (idempotent — dropping an absent peer still succeeds).
250    pub disconnected: bool,
251    /// The peer id that was targeted (trimmed + lower-cased).
252    pub peer_id: String,
253}
254
255/// `control.subscribe` — the subscription acknowledgement.
256#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
257pub struct SubscribeResult {
258    /// Always `true`.
259    pub subscribed: bool,
260    /// Whether the store was newly added (vs already subscribed).
261    pub added: bool,
262    /// The canonical persisted store id (trimmed + lower-cased).
263    pub store_id: String,
264}
265
266/// `control.unsubscribe` — the unsubscription acknowledgement.
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
268pub struct UnsubscribeResult {
269    /// Always `false`.
270    pub subscribed: bool,
271    /// Whether the store was actually removed.
272    pub removed: bool,
273    /// The canonical store id.
274    pub store_id: String,
275}
276
277/// `control.listSubscriptions` — the node's persisted subscription set.
278#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
279pub struct ListSubscriptionsResult {
280    /// The subscribed store ids.
281    pub subscriptions: Vec<String>,
282    /// The subscription count.
283    pub count: u64,
284}
285
286/// `control.wallet.balance` — an address's balance for one asset, as the node's chain read saw it.
287///
288/// A READ-only result: this reports chain state, it never moves funds. It is a strict SUPERSET of
289/// dig-app's frozen `BalanceResponse { balance }` — the node emits the richer shape, and because
290/// dig-app's struct does not deny unknown fields it reads [`balance`](Self::balance) losslessly and
291/// ignores the rest. That superset relationship is the "no dig-app code change" guarantee, pinned by
292/// the conformance KAT.
293#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
294pub struct WalletBalanceResult {
295    /// The CONFIRMED, spendable balance in the asset's base unit (mojos for XCH, base units for DIG).
296    /// The only field dig-app 3.x reads.
297    pub balance: u64,
298    /// Incoming funds seen but not yet confirmed (asset base units); not yet spendable.
299    pub pending: u64,
300    /// Whether the node's chain view is caught up. When `false`, the figures are STALE.
301    pub synced: bool,
302    /// The peak block height the reported figures reflect, or `null` when the node has no height yet.
303    pub peak_height: Option<u32>,
304}
305
306/// `pairing.request` — the pairing handshake bootstrap (OPEN, no token).
307#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
308pub struct PairingRequestResult {
309    /// The opaque pairing id to poll with.
310    pub pairing_id: String,
311    /// A short numeric code the operator compares before approving.
312    pub pairing_code: String,
313    /// When the pending pairing expires, in unix milliseconds.
314    pub expires_ms: u64,
315}
316
317/// `pairing.poll` — the pairing poll outcome (OPEN, no token).
318#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
319pub struct PairingPollResult {
320    /// The pairing status (`"pending"` / `"approved"` / …).
321    pub status: String,
322    /// The minted scoped token, present exactly once after approval.
323    #[serde(skip_serializing_if = "Option::is_none", default)]
324    pub token: Option<String>,
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330    use serde_json::json;
331
332    #[test]
333    fn status_result_round_trips_the_node_shape() {
334        let v = json!({
335            "running": true, "service": "dig-node", "version": "0.30.0", "commit": "abc",
336            "protocol": "21", "uptime_secs": 5, "addr": "127.0.0.1:9256", "upstream": "https://rpc.dig.net",
337            "cache": {"cap_bytes": 1024, "used_bytes": 10, "dir": "/c", "shared": false},
338            "hosted_store_count": 2, "cached_capsule_count": 3, "pinned_store_count": 1,
339            "sync": {"available": true}
340        });
341        let parsed: StatusResult = serde_json::from_value(v.clone()).unwrap();
342        assert_eq!(serde_json::to_value(&parsed).unwrap(), v);
343    }
344
345    #[test]
346    fn config_result_keeps_upstream_override_null_when_unset() {
347        let parsed = ConfigResult {
348            addr: "127.0.0.1:9256".into(),
349            port: "9256".into(),
350            upstream: "https://rpc.dig.net".into(),
351            upstream_override: None,
352            cache_dir: "/c".into(),
353            cache_shared: false,
354            config_path: "/c/config.json".into(),
355            sync_available: true,
356        };
357        let v = serde_json::to_value(&parsed).unwrap();
358        assert_eq!(v["upstream_override"], json!(null));
359        assert!(v.as_object().unwrap().contains_key("upstream_override"));
360    }
361
362    #[test]
363    fn pairing_poll_omits_token_until_approved() {
364        let pending = PairingPollResult {
365            status: "pending".into(),
366            token: None,
367        };
368        let v = serde_json::to_value(&pending).unwrap();
369        assert_eq!(v, json!({"status": "pending"}));
370        let approved = PairingPollResult {
371            status: "approved".into(),
372            token: Some("deadbeef".into()),
373        };
374        assert_eq!(
375            serde_json::to_value(&approved).unwrap(),
376            json!({"status": "approved", "token": "deadbeef"})
377        );
378    }
379}