Skip to main content

dig_node_control_interface/
traits.rs

1//! The two contract traits: the client-facing call builder/parser and the node-facing handler.
2//!
3//! * [`ControlCall`] binds a typed params struct to its [`ControlMethod`] and its typed result — so
4//!   a caller writes `client.request(&SetCapParams { cap_bytes })` and gets back a `SetCapResult`,
5//!   never a stringly-typed `Value`.
6//! * [`ControlClient`] is what a CLIENT depends on: build a JSON-RPC request from a typed call, and
7//!   parse a response back into the typed result (or a [`ControlError`]). Pure — no transport; the
8//!   consumer carries the bytes over dig-ipc / loopback-mTLS itself.
9//! * [`ControlHandler`] is what a NODE implements to SERVE the surface: one typed method per control
10//!   method, plus a provided [`dispatch`](ControlHandler::dispatch) that routes a raw request to the
11//!   right method — the single anti-drift seam the conformance KATs exercise.
12
13use async_trait::async_trait;
14use serde::de::DeserializeOwned;
15use serde::Serialize;
16use serde_json::Value;
17
18use crate::envelope::{JsonRpcRequest, JsonRpcResponse, RequestId};
19use crate::error::{ControlError, ControlErrorCode};
20use crate::method::ControlMethod;
21use crate::params;
22use crate::results;
23
24/// A typed control call: a params struct that knows its [`ControlMethod`] and its result type.
25///
26/// Implemented by every struct in [`crate::params`]; this is what makes
27/// [`ControlClient::parse_response`] return the right typed result for each method at compile time.
28pub trait ControlCall: Serialize {
29    /// The wire method this call invokes.
30    const METHOD: ControlMethod;
31    /// The typed result this call returns on success.
32    type Output: DeserializeOwned;
33}
34
35/// Serialize a typed call's params into a JSON object (`{}` for a no-param call, never `null`).
36fn params_value<C: ControlCall>(call: &C) -> Value {
37    match serde_json::to_value(call) {
38        Ok(Value::Null) => Value::Object(Default::default()),
39        Ok(v) => v,
40        Err(_) => Value::Object(Default::default()),
41    }
42}
43
44/// Build the JSON-RPC request envelope for a typed control call. Pure.
45pub fn build_request<C: ControlCall>(id: RequestId, call: &C) -> JsonRpcRequest {
46    JsonRpcRequest::new(id, C::METHOD.name(), params_value(call))
47}
48
49/// Parse a JSON-RPC response into a typed result, or the [`ControlError`] it carried. Pure.
50pub fn parse_response<C: ControlCall>(
51    response: JsonRpcResponse,
52) -> Result<C::Output, ControlError> {
53    let value = response.into_result()?;
54    serde_json::from_value(value).map_err(|e| {
55        ControlError::of(
56            ControlErrorCode::ControlError,
57            format!("failed to parse {} result: {e}", C::METHOD.name()),
58        )
59    })
60}
61
62/// The client-facing half of the contract: turn typed calls into requests and responses back into
63/// typed results.
64///
65/// The default implementations cover every client; a consumer implements this trait only to
66/// customise request construction (e.g. attaching the control token in a bespoke way). The blanket
67/// [`DefaultControlClient`] gives callers the standard behaviour for free.
68pub trait ControlClient {
69    /// Build the request envelope for a typed call with the given request `id`.
70    fn build_request<C: ControlCall>(&self, id: RequestId, call: &C) -> JsonRpcRequest {
71        build_request(id, call)
72    }
73
74    /// Parse a response envelope into the typed result for call type `C`.
75    fn parse_response<C: ControlCall>(
76        &self,
77        response: JsonRpcResponse,
78    ) -> Result<C::Output, ControlError> {
79        parse_response::<C>(response)
80    }
81}
82
83/// The standard, zero-configuration [`ControlClient`] using the default request/response behaviour.
84#[derive(Debug, Clone, Copy, Default)]
85pub struct DefaultControlClient;
86
87impl ControlClient for DefaultControlClient {}
88
89/// The node-facing half of the contract: a running node implements this to SERVE the control
90/// surface. Each method is typed to the catalog's params/results; the provided
91/// [`dispatch`](ControlHandler::dispatch) routes a raw [`JsonRpcRequest`] to the right method so a
92/// server needs only one entry point and can never mis-route.
93///
94/// Open/proxied shapes (the updater beacon status, the pairing list, the peer-pool snapshot) return
95/// [`Value`] rather than a frozen struct, matching the catalog's [`ControlCall::Output`] for those
96/// methods.
97#[async_trait]
98pub trait ControlHandler: Sync {
99    /// `control.status`
100    async fn status(&self) -> Result<results::StatusResult, ControlError>;
101    /// `control.config.get`
102    async fn config_get(&self) -> Result<results::ConfigResult, ControlError>;
103    /// `control.config.setUpstream`
104    async fn config_set_upstream(
105        &self,
106        params: params::SetUpstreamParams,
107    ) -> Result<results::SetUpstreamResult, ControlError>;
108    /// `control.log.setLevel`
109    async fn log_set_level(
110        &self,
111        params: params::SetLevelParams,
112    ) -> Result<results::SetLevelResult, ControlError>;
113    /// `control.cache.get`
114    async fn cache_get(&self) -> Result<results::CacheView, ControlError>;
115    /// `control.cache.setCap`
116    async fn cache_set_cap(
117        &self,
118        params: params::SetCapParams,
119    ) -> Result<results::SetCapResult, ControlError>;
120    /// `control.cache.clear`
121    async fn cache_clear(&self) -> Result<results::CacheClearResult, ControlError>;
122    /// `control.hostedStores.list`
123    async fn hosted_stores_list(&self) -> Result<results::HostedStoresListResult, ControlError>;
124    /// `control.hostedStores.pin`
125    async fn hosted_stores_pin(
126        &self,
127        params: params::PinParams,
128    ) -> Result<results::PinResult, ControlError>;
129    /// `control.hostedStores.unpin`
130    async fn hosted_stores_unpin(
131        &self,
132        params: params::UnpinParams,
133    ) -> Result<results::UnpinResult, ControlError>;
134    /// `control.hostedStores.status`
135    async fn hosted_stores_status(
136        &self,
137        params: params::HostedStoreStatusParams,
138    ) -> Result<results::HostedStoreStatusResult, ControlError>;
139    /// `control.sync.status`
140    async fn sync_status(&self) -> Result<results::SyncStatusResult, ControlError>;
141    /// `control.sync.trigger`
142    async fn sync_trigger(
143        &self,
144        params: params::SyncTriggerParams,
145    ) -> Result<results::SyncTriggerResult, ControlError>;
146    /// `control.updater.status`
147    async fn updater_status(&self) -> Result<Value, ControlError>;
148    /// `control.updater.setChannel`
149    async fn updater_set_channel(
150        &self,
151        params: params::SetChannelParams,
152    ) -> Result<Value, ControlError>;
153    /// `control.updater.pause`
154    async fn updater_pause(&self, params: params::PauseParams) -> Result<Value, ControlError>;
155    /// `control.updater.resume`
156    async fn updater_resume(&self) -> Result<Value, ControlError>;
157    /// `control.updater.checkNow`
158    async fn updater_check_now(&self) -> Result<Value, ControlError>;
159    /// `control.pairing.list`
160    async fn pairing_list(&self) -> Result<Value, ControlError>;
161    /// `control.pairing.approve`
162    async fn pairing_approve(
163        &self,
164        params: params::ApproveParams,
165    ) -> Result<results::PairingApproveResult, ControlError>;
166    /// `control.pairing.revoke`
167    async fn pairing_revoke(
168        &self,
169        params: params::RevokeParams,
170    ) -> Result<results::PairingRevokeResult, ControlError>;
171    /// `control.peerStatus`
172    async fn peer_status(&self) -> Result<Value, ControlError>;
173    /// `control.peers.connect`
174    async fn peers_connect(
175        &self,
176        params: params::PeersConnectParams,
177    ) -> Result<results::PeersConnectResult, ControlError>;
178    /// `control.peers.disconnect`
179    async fn peers_disconnect(
180        &self,
181        params: params::PeersDisconnectParams,
182    ) -> Result<results::PeersDisconnectResult, ControlError>;
183    /// `control.chiaPeers.add` — start trusting a Chia full node the operator RUNS.
184    ///
185    /// An implementation MUST write through to the ONE peer store its wallet replica reads; a
186    /// second peer list is a drift bug waiting to happen.
187    ///
188    /// Three obligations the shell cannot infer from the types:
189    ///
190    /// - it requires the MASTER token ([`ControlMethod::requires_master_token`]). The entry it
191    ///   writes carries authority that outlives the calling token, and `control.pairing.revoke`
192    ///   does not remove it;
193    /// - `params.ip` is canonicalised with [`crate::params::canonical_peer_ip`] and STORED in that
194    ///   form, so `remove` and `list` can match what `add` wrote;
195    /// - `corroboration_bypassed` reports the RESULTING trust state and `notice` carries the
196    ///   node's own warning verbatim. Reporting a bypass that did not happen tells an operator
197    ///   they configured a node they did not.
198    async fn chia_peers_add(
199        &self,
200        params: params::ChiaPeersAddParams,
201    ) -> Result<results::ChiaPeersAddResult, ControlError>;
202    /// `control.chiaPeers.list` — the tracked Chia full-node peers, banned ones included.
203    ///
204    /// Ordinary token tier: a read that confers nothing, and a paired client that cannot show the
205    /// operator this list cannot show them the trust state they are subject to.
206    async fn chia_peers_list(&self) -> Result<results::ChiaPeersListResult, ControlError>;
207    /// `control.chiaPeers.remove` — stop trusting a Chia full node.
208    ///
209    /// MASTER token, like `add`. This is the ONLY un-trust remedy, so an implementation MUST
210    /// return [`results::ChiaPeerRemovalOutcome::NoSuchPeer`] when nothing matched rather than
211    /// reporting a removal it did not perform.
212    async fn chia_peers_remove(
213        &self,
214        params: params::ChiaPeersRemoveParams,
215    ) -> Result<results::ChiaPeersRemoveResult, ControlError>;
216    /// `control.subscribe`
217    ///
218    /// `params.kind` is OPTIONAL on the wire and absent means
219    /// [`SubscriptionKind::Capsule`](params::SubscriptionKind::Capsule). An implementation MUST
220    /// persist untagged rows it already holds as capsules rather than discarding them: a node that
221    /// refuses to read its own pre-existing `subscriptions.json` starts with an empty one, and the
222    /// upgrade silently unsubscribes the user from everything.
223    async fn subscribe(
224        &self,
225        params: params::SubscribeParams,
226    ) -> Result<results::SubscribeResult, ControlError>;
227    /// `control.unsubscribe`
228    async fn unsubscribe(
229        &self,
230        params: params::UnsubscribeParams,
231    ) -> Result<results::UnsubscribeResult, ControlError>;
232    /// `control.listSubscriptions`
233    async fn list_subscriptions(&self) -> Result<results::ListSubscriptionsResult, ControlError>;
234    /// `control.wallet.balance` (READ-only)
235    async fn wallet_balance(
236        &self,
237        params: params::WalletBalanceParams,
238    ) -> Result<results::WalletBalanceResult, ControlError>;
239    /// `control.wallet.coins` (READ-only, OPEN)
240    ///
241    /// An empty `coins` list MUST mean "a chain was consulted and this address holds nothing".
242    /// A read that could not consult a chain MUST return the matching catalogued error instead.
243    async fn wallet_coins(
244        &self,
245        params: params::WalletCoinsParams,
246    ) -> Result<results::WalletCoinsResult, ControlError>;
247    /// `control.wallet.coinById` (READ-only, OPEN)
248    ///
249    /// `Ok(coin: None)` MUST mean "a chain was consulted and holds no such coin". A read that could
250    /// not consult a chain MUST return the matching catalogued error instead — a caller that cannot
251    /// tell those apart reports a spent mint as pending forever.
252    ///
253    /// The params are validated at DESERIALIZATION (lowercase 64-hex, `0x` stripped), so any path
254    /// that decodes `WalletCoinByIdParams` refuses malformed ids as `INVALID_PARAMS` before this
255    /// method is called.
256    async fn wallet_coin_by_id(
257        &self,
258        params: params::WalletCoinByIdParams,
259    ) -> Result<results::WalletCoinByIdResult, ControlError>;
260    /// `control.wallet.coinSpend` (READ-only, OPEN)
261    ///
262    /// `Ok(spend: None)` MUST mean "a chain was consulted and holds no spend of that coin" — the
263    /// coin is unspent, or unknown. A read that could not consult a chain MUST return the matching
264    /// catalogued error instead: a caller following a singleton forward reads "no spend" as *this is
265    /// the tip* and stops walking, so a failure disguised as absence produces a spend built against
266    /// a superseded singleton.
267    ///
268    /// A returned spend's `puzzle_reveal` MUST tree-hash to the spent coin's own `puzzle_hash`, and
269    /// the implementation MUST fail closed — an error, never an unverified reveal — when it does not
270    /// or when the reveal will not parse. The reveal comes from a peer, and a peer can lie.
271    ///
272    /// The params are validated at DESERIALIZATION (lowercase 64-hex, `0x` stripped), so any path
273    /// that decodes `WalletCoinSpendParams` refuses malformed ids as `INVALID_PARAMS` before this
274    /// method is called.
275    async fn wallet_coin_spend(
276        &self,
277        params: params::WalletCoinSpendParams,
278    ) -> Result<results::WalletCoinSpendResult, ControlError>;
279    /// `control.wallet.coinsByParent` (READ-only, OPEN)
280    ///
281    /// Returns the parent's DIRECT children and nothing further. An implementation MUST NOT recurse:
282    /// a transitive walk over caller-supplied input is unbounded work the caller cannot bound, and a
283    /// partial walk returned as a complete one is a lineage with a silent hole in it.
284    ///
285    /// An empty list MUST mean "a chain was consulted and this parent created no known children".
286    /// A read that could not consult a chain MUST return the matching catalogued error instead.
287    ///
288    /// The answer is ONE PAGE. An implementation MUST return at most
289    /// `params.effective_limit()` records, in ASCENDING `coin_id` order, starting strictly after
290    /// `params.after_coin_id` when one is given; it MUST set `complete` to whether the page carries
291    /// the last child; and it MUST set `cursor` to the last record it actually returned (`None` for
292    /// an empty page). It MUST NOT report `complete: true` on a page it truncated — a caller reads
293    /// that as the end of a lineage branch. The params are validated at DESERIALIZATION, so an
294    /// out-of-range page size is refused as `INVALID_PARAMS` before this method is called.
295    ///
296    /// Every record MUST report `asset: None`: naming a coin by its parent classifies nothing, and
297    /// asserting a class this read never verified is a claim a caller would then spend against.
298    async fn wallet_coins_by_parent(
299        &self,
300        params: params::WalletCoinsByParentParams,
301    ) -> Result<results::WalletCoinsByParentResult, ControlError>;
302    /// `control.wallet.arrivals` (READ-only, TOKEN-GATED)
303    ///
304    /// Gated although it is a read: the caller supplies only a cursor, so the answer names this
305    /// node's OWN watched puzzle hashes and the receive history behind them.
306    ///
307    /// Every returned row MUST be a CONFIRMED arrival that the node itself judged: above its arrival
308    /// baseline, not previously reported, and not the wallet's own change. An implementation MUST NOT
309    /// emit a mempool sighting here, and MUST answer an empty page rather than an error when it has
310    /// no baseline — "nothing arrived" is the honest answer from a wallet that cannot yet tell
311    /// history from news.
312    ///
313    /// `cursor` MUST be the position of the last row actually returned (or the caller's `after_seq`
314    /// for an empty page) and MUST NOT be `latest`; see
315    /// [`WalletArrivalsResult::latest`](results::WalletArrivalsResult::latest).
316    async fn wallet_arrivals(
317        &self,
318        params: params::WalletArrivalsParams,
319    ) -> Result<results::WalletArrivalsResult, ControlError>;
320    /// `control.wallet.peak` (READ-only, OPEN)
321    async fn wallet_peak(&self) -> Result<results::WalletPeakResult, ControlError>;
322    /// `control.peerCounts` (READ-only, OPEN)
323    ///
324    /// `dig_peer_count` MUST be dig-node-core's `connected_peers` — the same figure
325    /// `control.peerStatus` reports — and `chia_peer_count` MUST be the SAME observation
326    /// `wallet_sync_status` reports, served from ONE source so the two answers agree. `None` means
327    /// the count cannot be observed; a network that is not running is UNKNOWN, never `Some(0)`.
328    async fn peer_counts(&self) -> Result<results::PeerCountsResult, ControlError>;
329    /// `control.wallet.syncStatus` (READ-only, OPEN)
330    ///
331    /// `WalletSyncPhase::Synced` MUST require BOTH that the initial catch-up completed and that at
332    /// least one Chia peer connection is live now, which makes it strictly stronger than
333    /// `WalletPeakResult::synced`. `peak_height` MUST be the node's OWN replica's height or `None`,
334    /// never an oracle's, and `chia_peer_count` counts CHIA full-node peers -- never DIG peers.
335    async fn wallet_sync_status(&self) -> Result<results::WalletSyncStatusResult, ControlError>;
336    /// `control.wallet.broadcast` (TOKEN-GATED)
337    ///
338    /// Pushes an ALREADY-SIGNED bundle: the implementation never signs, and never receives anything
339    /// it could sign with (§908). A mempool refusal is `Ok` with `accepted: false`; failing to
340    /// reach a mempool is `Err`.
341    async fn wallet_broadcast(
342        &self,
343        params: params::WalletBroadcastParams,
344    ) -> Result<results::WalletBroadcastResult, ControlError>;
345    /// `control.wallet.watch` (TOKEN-GATED)
346    ///
347    /// Enrols PUBLIC keys for the node's chain replica to follow. The implementation MUST derive the
348    /// addresses itself, from the SAME derivation it applies to the keys already in its own custody
349    /// — a second derivation is a second opinion about which addresses a key covers, and the client
350    /// would read the difference as missing money.
351    ///
352    /// It MUST be IDEMPOTENT: keys already enrolled are reported as `added: 0` and the call
353    /// succeeds. It MUST persist the enrolment across restarts — a set that evaporates on restart
354    /// makes a node that syncs today and reports a zero balance tomorrow.
355    ///
356    /// The params are validated at DESERIALIZATION (lowercase 96-hex, `0x` stripped), so any path
357    /// that decodes `WalletWatchParams` refuses a malformed key as `INVALID_PARAMS` — for the WHOLE
358    /// request — before this method is called.
359    async fn wallet_watch(
360        &self,
361        params: params::WalletWatchParams,
362    ) -> Result<results::WalletWatchResult, ControlError>;
363    /// `control.wallet.unwatch` (TOKEN-GATED)
364    ///
365    /// Deregisters keys, and the following MUST actually stop: the addresses leave the replica's
366    /// watched set, not merely the list this node reports. A registry that keeps syncing what it
367    /// says it forgot is the failure this method exists to make impossible.
368    ///
369    /// Deregistering a key that was never enrolled is a success reporting `removed: 0`.
370    async fn wallet_unwatch(
371        &self,
372        params: params::WalletUnwatchParams,
373    ) -> Result<results::WalletUnwatchResult, ControlError>;
374    /// `control.wallet.watched` (READ-only, TOKEN-GATED)
375    ///
376    /// Gated although it is a read: the caller supplies nothing, so the answer names this node's OWN
377    /// enrolled keys.
378    ///
379    /// MUST return exactly the keys enrolment added, in the wire form they were accepted in, so a
380    /// client can compare its own set against the node's by value. MUST NOT include the node's own
381    /// custody keys: this method reports what was ENROLLED through it, and a caller reconciling
382    /// against a superset would unwatch keys it never watched.
383    async fn wallet_watched(&self) -> Result<results::WalletWatchedResult, ControlError>;
384    /// `control.profile.putBody` (TOKEN-GATED)
385    ///
386    /// An implementation MUST independently resolve the profile's root ON CHAIN, recompute the root
387    /// of the supplied body, and REFUSE the call unless the two agree and that root is confirmed.
388    /// The caller's `root` is a claim to be checked — never a fact to be trusted — and dig-app gets
389    /// no exemption: it holds the key and signs the root (§908), but the bytes reach the node the
390    /// same way a peer's bytes do, and the same check binds both. An implementation that stores
391    /// what it is handed makes this node serve arbitrary bytes under someone else's profile id.
392    ///
393    /// A body whose DECODED length exceeds [`MAX_BODY_BYTES`](params::MAX_BODY_BYTES) (4 MiB) MUST
394    /// be refused as `INVALID_PARAMS`, before it is persisted: a body larger than that cannot be
395    /// served to a peer inside dig-gossip's frame ceiling, so accepting it would store something
396    /// permanently unsyncable.
397    ///
398    /// Returning `Ok` therefore asserts BOTH that the root was confirmed on chain and that the body
399    /// is persisted and servable. A refusal is an error, never an `Ok` carrying `stored: false`.
400    async fn profile_put_body(
401        &self,
402        params: params::ProfilePutBodyParams,
403    ) -> Result<results::ProfilePutBodyResult, ControlError>;
404    /// `control.profile.getBody` (READ-only, TOKEN-GATED)
405    ///
406    /// `Ok(body_b64: None)` MUST mean "this node was consulted and holds no body at that root". A
407    /// read that FAILED MUST return a catalogued error instead — a caller that reads a failure as
408    /// absence renders an existing profile as an empty one.
409    ///
410    /// The returned `root` MUST be the root the caller asked for; a node MUST NOT substitute a
411    /// newer body it happens to hold.
412    async fn profile_get_body(
413        &self,
414        params: params::ProfileGetBodyParams,
415    ) -> Result<results::ProfileGetBodyResult, ControlError>;
416    /// `pairing.request` (OPEN)
417    async fn pairing_request(
418        &self,
419        params: params::RequestParams,
420    ) -> Result<results::PairingRequestResult, ControlError>;
421    /// `pairing.poll` (OPEN)
422    async fn pairing_poll(
423        &self,
424        params: params::PollParams,
425    ) -> Result<results::PairingPollResult, ControlError>;
426
427    /// Route a raw JSON-RPC request to the right typed method and build the response envelope.
428    ///
429    /// Deserializes the params for methods that take them, calls the handler, and serializes the
430    /// typed result. An unknown method → `METHOD_NOT_FOUND`; malformed params → `INVALID_PARAMS`.
431    /// This is the single seam a server dispatches through — the KATs exercise it end-to-end.
432    async fn dispatch(&self, request: JsonRpcRequest) -> JsonRpcResponse {
433        let id = request.id.clone();
434        let Some(method) = ControlMethod::from_name(&request.method) else {
435            return JsonRpcResponse::error(
436                id,
437                ControlError::of(
438                    ControlErrorCode::MethodNotFound,
439                    format!("unknown control method: {}", request.method),
440                ),
441            );
442        };
443        match self.dispatch_method(method, request.params).await {
444            Ok(result) => JsonRpcResponse::success(id, result),
445            Err(err) => JsonRpcResponse::error(id, err),
446        }
447    }
448
449    /// Route to the typed method by [`ControlMethod`], returning the result as a [`Value`]. Split
450    /// from [`dispatch`](ControlHandler::dispatch) so the envelope wrapping stays in one place.
451    #[doc(hidden)]
452    async fn dispatch_method(
453        &self,
454        method: ControlMethod,
455        params: Value,
456    ) -> Result<Value, ControlError> {
457        /// Deserialize a method's params, mapping a shape error to `INVALID_PARAMS`.
458        fn decode<T: DeserializeOwned>(params: Value) -> Result<T, ControlError> {
459            serde_json::from_value(params)
460                .map_err(|e| ControlError::of(ControlErrorCode::InvalidParams, e.to_string()))
461        }
462        /// Serialize a typed result to a `Value` (infallible for our derive-Serialize results).
463        fn encode<T: Serialize>(value: T) -> Result<Value, ControlError> {
464            serde_json::to_value(value)
465                .map_err(|e| ControlError::of(ControlErrorCode::ControlError, e.to_string()))
466        }
467        match method {
468            ControlMethod::Status => encode(self.status().await?),
469            ControlMethod::ConfigGet => encode(self.config_get().await?),
470            ControlMethod::ConfigSetUpstream => {
471                encode(self.config_set_upstream(decode(params)?).await?)
472            }
473            ControlMethod::LogSetLevel => encode(self.log_set_level(decode(params)?).await?),
474            ControlMethod::CacheGet => encode(self.cache_get().await?),
475            ControlMethod::CacheSetCap => encode(self.cache_set_cap(decode(params)?).await?),
476            ControlMethod::CacheClear => encode(self.cache_clear().await?),
477            ControlMethod::HostedStoresList => encode(self.hosted_stores_list().await?),
478            ControlMethod::HostedStoresPin => {
479                encode(self.hosted_stores_pin(decode(params)?).await?)
480            }
481            ControlMethod::HostedStoresUnpin => {
482                encode(self.hosted_stores_unpin(decode(params)?).await?)
483            }
484            ControlMethod::HostedStoresStatus => {
485                encode(self.hosted_stores_status(decode(params)?).await?)
486            }
487            ControlMethod::SyncStatus => encode(self.sync_status().await?),
488            ControlMethod::SyncTrigger => encode(self.sync_trigger(decode(params)?).await?),
489            ControlMethod::UpdaterStatus => self.updater_status().await,
490            ControlMethod::UpdaterSetChannel => self.updater_set_channel(decode(params)?).await,
491            ControlMethod::UpdaterPause => self.updater_pause(decode(params)?).await,
492            ControlMethod::UpdaterResume => self.updater_resume().await,
493            ControlMethod::UpdaterCheckNow => self.updater_check_now().await,
494            ControlMethod::PairingList => self.pairing_list().await,
495            ControlMethod::PairingApprove => encode(self.pairing_approve(decode(params)?).await?),
496            ControlMethod::PairingRevoke => encode(self.pairing_revoke(decode(params)?).await?),
497            ControlMethod::PeerStatus => self.peer_status().await,
498            ControlMethod::PeerCounts => encode(self.peer_counts().await?),
499            ControlMethod::PeersConnect => encode(self.peers_connect(decode(params)?).await?),
500            ControlMethod::PeersDisconnect => encode(self.peers_disconnect(decode(params)?).await?),
501            ControlMethod::ChiaPeersAdd => encode(self.chia_peers_add(decode(params)?).await?),
502            ControlMethod::ChiaPeersList => encode(self.chia_peers_list().await?),
503            ControlMethod::ChiaPeersRemove => {
504                encode(self.chia_peers_remove(decode(params)?).await?)
505            }
506            ControlMethod::Subscribe => encode(self.subscribe(decode(params)?).await?),
507            ControlMethod::Unsubscribe => encode(self.unsubscribe(decode(params)?).await?),
508            ControlMethod::ListSubscriptions => encode(self.list_subscriptions().await?),
509            ControlMethod::WalletBalance => encode(self.wallet_balance(decode(params)?).await?),
510            ControlMethod::WalletCoins => encode(self.wallet_coins(decode(params)?).await?),
511            // Re-validated here idempotently; deserialization already enforced the same rule.
512            ControlMethod::WalletCoinById => {
513                let params: params::WalletCoinByIdParams = decode(params)?;
514                encode(self.wallet_coin_by_id(params.validated()?).await?)
515            }
516            // Re-validated here idempotently; deserialization already enforced the same rule.
517            ControlMethod::WalletCoinSpend => {
518                let params: params::WalletCoinSpendParams = decode(params)?;
519                encode(self.wallet_coin_spend(params.validated()?).await?)
520            }
521            ControlMethod::WalletCoinsByParent => {
522                let params: params::WalletCoinsByParentParams = decode(params)?;
523                encode(self.wallet_coins_by_parent(params.validated()?).await?)
524            }
525            ControlMethod::WalletArrivals => encode(self.wallet_arrivals(decode(params)?).await?),
526            ControlMethod::WalletPeak => encode(self.wallet_peak().await?),
527            ControlMethod::WalletSyncStatus => encode(self.wallet_sync_status().await?),
528            ControlMethod::WalletBroadcast => encode(self.wallet_broadcast(decode(params)?).await?),
529            // Re-validated here idempotently; deserialization already enforced the same rule.
530            ControlMethod::WalletWatch => {
531                let params: params::WalletWatchParams = decode(params)?;
532                encode(self.wallet_watch(params.validated()?).await?)
533            }
534            // Re-validated here idempotently; deserialization already enforced the same rule.
535            ControlMethod::WalletUnwatch => {
536                let params: params::WalletUnwatchParams = decode(params)?;
537                encode(self.wallet_unwatch(params.validated()?).await?)
538            }
539            ControlMethod::WalletWatched => encode(self.wallet_watched().await?),
540            ControlMethod::ProfilePutBody => encode(self.profile_put_body(decode(params)?).await?),
541            ControlMethod::ProfileGetBody => encode(self.profile_get_body(decode(params)?).await?),
542            ControlMethod::PairingRequest => encode(self.pairing_request(decode(params)?).await?),
543            ControlMethod::PairingPoll => encode(self.pairing_poll(decode(params)?).await?),
544        }
545    }
546}