sui_graphql_rpc/
functional_group.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::BTreeMap;
5
6use async_graphql::*;
7use once_cell::sync::Lazy;
8use serde::{Deserialize, Serialize};
9use serde_json as json;
10
11/// Groups of features served by the RPC service.  The GraphQL Service can be configured to enable
12/// or disable these features.
13#[derive(Enum, Copy, Clone, Serialize, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd)]
14#[serde(rename_all = "kebab-case")]
15#[graphql(name = "Feature")]
16pub enum FunctionalGroup {
17    /// Statistics about how the network was running (TPS, top packages, APY, etc)
18    Analytics,
19
20    /// Coin metadata, per-address coin and balance information.
21    Coins,
22
23    /// Querying an object's dynamic fields.
24    DynamicFields,
25
26    /// SuiNS name and reverse name look-up.
27    NameService,
28
29    /// Transaction and Event subscriptions.
30    Subscriptions,
31
32    /// Aspects that affect the running of the system that are managed by the
33    /// validators either directly, or through system transactions.
34    SystemState,
35
36    /// Named packages service (utilizing dotmove package registry).
37    MoveRegistry,
38}
39
40impl FunctionalGroup {
41    /// Name that the group is referred to by in configuration and responses on the GraphQL API.
42    /// Not a suitable `Display` implementation because it enquotes the representation.
43    pub(crate) fn name(&self) -> String {
44        json::ser::to_string(self).expect("Serializing `FunctionalGroup` cannot fail.")
45    }
46
47    /// List of all functional groups
48    pub(crate) fn all() -> &'static [FunctionalGroup] {
49        use FunctionalGroup as G;
50        static ALL: &[FunctionalGroup] = &[
51            G::Analytics,
52            G::Coins,
53            G::DynamicFields,
54            G::NameService,
55            G::Subscriptions,
56            G::SystemState,
57            G::MoveRegistry,
58        ];
59        ALL
60    }
61}
62
63/// Mapping from type and field name in the schema to the functional group it belongs to.
64fn functional_groups() -> &'static BTreeMap<(&'static str, &'static str), FunctionalGroup> {
65    // TODO: Introduce a macro to declare the functional group for a field and/or type on the
66    // appropriate type, field, or function, instead of here.  This may also be able to set the
67    // graphql `visible` attribute to control schema visibility by functional groups.
68
69    use FunctionalGroup as G;
70    static GROUPS: Lazy<BTreeMap<(&str, &str), FunctionalGroup>> = Lazy::new(|| {
71        BTreeMap::from_iter([
72            (("Address", "balance"), G::Coins),
73            (("Address", "balances"), G::Coins),
74            (("Address", "coins"), G::Coins),
75            (("Address", "defaultSuinsName"), G::NameService),
76            (("Address", "suinsRegistrations"), G::NameService),
77            (("Checkpoint", "addressMetrics"), G::Analytics),
78            (("Checkpoint", "networkTotalTransactions"), G::Analytics),
79            (("Epoch", "protocolConfigs"), G::SystemState),
80            (("Epoch", "referenceGasPrice"), G::SystemState),
81            (("Epoch", "validatorSet"), G::SystemState),
82            (("Object", "balance"), G::Coins),
83            (("Object", "balances"), G::Coins),
84            (("Object", "coins"), G::Coins),
85            (("Object", "defaultSuinsName"), G::NameService),
86            (("Object", "dynamicField"), G::DynamicFields),
87            (("Object", "dynamicObjectField"), G::DynamicFields),
88            (("Object", "dynamicFields"), G::DynamicFields),
89            (("Object", "suinsRegistrations"), G::NameService),
90            (("Owner", "balance"), G::Coins),
91            (("Owner", "balances"), G::Coins),
92            (("Owner", "coins"), G::Coins),
93            (("Owner", "defaultSuinsName"), G::NameService),
94            (("Owner", "dynamicField"), G::DynamicFields),
95            (("Owner", "dynamicObjectField"), G::DynamicFields),
96            (("Owner", "dynamicFields"), G::DynamicFields),
97            (("Owner", "suinsRegistrations"), G::NameService),
98            (("Query", "coinMetadata"), G::Coins),
99            (("Query", "moveCallMetrics"), G::Analytics),
100            (("Query", "networkMetrics"), G::Analytics),
101            (("Query", "protocolConfig"), G::SystemState),
102            (("Query", "resolveSuinsAddress"), G::NameService),
103            (("Query", "packageByName"), G::MoveRegistry),
104            (("Query", "typeByName"), G::MoveRegistry),
105            (("Subscription", "events"), G::Subscriptions),
106            (("Subscription", "transactions"), G::Subscriptions),
107            (("SystemStateSummary", "safeMode"), G::SystemState),
108            (("SystemStateSummary", "storageFund"), G::SystemState),
109            (("SystemStateSummary", "systemParameters"), G::SystemState),
110            (("SystemStateSummary", "systemStateVersion"), G::SystemState),
111        ])
112    });
113
114    Lazy::force(&GROUPS)
115}
116
117/// Map a type and field name to a functional group.  If an explicit group does not exist for the
118/// field, then it is assumed to be a "core" feature.
119pub(crate) fn functional_group(type_: &str, field: &str) -> Option<FunctionalGroup> {
120    functional_groups().get(&(type_, field)).copied()
121}
122
123#[cfg(test)]
124mod tests {
125    use std::collections::BTreeSet;
126
127    use async_graphql::registry::Registry;
128    use async_graphql::OutputType;
129
130    use crate::types::query::Query;
131
132    use super::*;
133
134    #[test]
135    /// Makes sure all the functional groups correspond to real elements of the schema unless they
136    /// are explicitly recorded as unimplemented.  Complementarily, makes sure that fields marked as
137    /// unimplemented don't appear in the set of unimplemented fields.
138    fn test_groups_match_schema() {
139        let mut registry = Registry::default();
140        Query::create_type_info(&mut registry);
141
142        let unimplemented = BTreeSet::from_iter([
143            ("Checkpoint", "addressMetrics"),
144            ("Epoch", "protocolConfig"),
145            ("Query", "moveCallMetrics"),
146            ("Query", "networkMetrics"),
147            ("Subscription", "events"),
148            ("Subscription", "transactions"),
149        ]);
150
151        for (type_, field) in &unimplemented {
152            let Some(meta_type) = registry.concrete_type_by_name(type_) else {
153                continue;
154            };
155
156            let Some(_) = meta_type.field_by_name(field) else {
157                continue;
158            };
159
160            panic!(
161                "Field '{type_}.{field}' is marked as unimplemented in this test, but it's in the \
162                 schema.  Fix this by removing it from the `unimplemented` set."
163            );
164        }
165
166        for (type_, field) in functional_groups().keys() {
167            if unimplemented.contains(&(type_, field)) {
168                continue;
169            }
170
171            let Some(meta_type) = registry.concrete_type_by_name(type_) else {
172                panic!("Type '{type_}' from functional group configs does not appear in schema.");
173            };
174
175            let Some(_) = meta_type.field_by_name(field) else {
176                panic!(
177                    "Field '{type_}.{field}' from functional group configs does not appear in \
178                     schema."
179                );
180            };
181        }
182    }
183}