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}