Skip to main content

blokli_client/api/v1/graphql/
services.rs

1use super::{CountResult, MissingFilterError, QueryFailedError, Uint64, schema};
2use crate::{
3    api::v1::{ChainAddress, ServiceSelector, ServiceTypeId},
4    errors::{BlokliClientError, ErrorKind},
5};
6
7/// Renders a service type id in the `0x`-prefixed hexadecimal form accepted by the API.
8///
9/// The API also accepts the ASCII name of a type, but the client holds the raw 32-byte id and the
10/// hexadecimal form is accepted for every id, including the ones that do not follow the ASCII
11/// convention.
12pub(crate) fn service_type_to_filter(service_type: ServiceTypeId) -> String {
13    format!("0x{}", hex::encode(service_type))
14}
15
16/// Decodes a service type id into the ASCII name Blokli renders it as, when the id follows the
17/// right-padded printable-ASCII convention of the registry.
18///
19/// Returns `None` for any id that does not follow it, which Blokli renders as `0x`-prefixed hex
20/// instead. `hopr_types::internal::service::ServiceType` is the source of truth for this
21/// convention; it is reimplemented here because the client deliberately models a service type as a
22/// plain [`ServiceTypeId`] and does not enable the `internal` feature of `hopr-types`.
23///
24/// Only the in-memory test client needs to reverse the rendering, so this is not compiled into a
25/// plain library build.
26#[cfg(any(test, feature = "testing"))]
27pub(crate) fn service_type_name(service_type: &ServiceTypeId) -> Option<&str> {
28    let len = service_type.iter().rposition(|byte| *byte != 0)? + 1;
29    let name = std::str::from_utf8(&service_type[..len]).ok()?;
30
31    name.bytes().all(|byte| byte.is_ascii_graphic()).then_some(name)
32}
33
34/// Renders a node address for an API filter.
35///
36/// Unprefixed, matching every other address filter this client sends. The service type filter keeps its `0x`
37/// prefix instead, because that is the form the API documents for an id that is not a printable ASCII name.
38fn node_to_filter(node: ChainAddress) -> String {
39    hex::encode(node)
40}
41
42#[derive(cynic::QueryVariables, Debug, Default)]
43pub struct ServiceVariables {
44    pub service_type: Option<String>,
45    pub node: Option<String>,
46}
47
48#[derive(cynic::QueryVariables, Debug)]
49pub struct ServicePageVariables {
50    pub service_type: Option<String>,
51    pub node: Option<String>,
52    pub first: i32,
53    pub after: Option<Uint64>,
54    pub watermark: Option<Uint64>,
55    pub live_only: bool,
56}
57
58impl ServicePageVariables {
59    pub fn new(selector: ServiceSelector, after: Option<Uint64>, watermark: Option<Uint64>, live_only: bool) -> Self {
60        let filters = ServiceVariables::from(selector);
61        Self {
62            service_type: filters.service_type,
63            node: filters.node,
64            first: 1000,
65            after,
66            watermark,
67            live_only,
68        }
69    }
70}
71
72impl From<ServiceSelector> for ServiceVariables {
73    fn from(value: ServiceSelector) -> Self {
74        match value {
75            ServiceSelector::ServiceType(service_type) => ServiceVariables {
76                service_type: Some(service_type_to_filter(service_type)),
77                node: None,
78            },
79            ServiceSelector::Node(node) => ServiceVariables {
80                service_type: None,
81                node: Some(node_to_filter(node)),
82            },
83            ServiceSelector::ServiceTypeAndNode { service_type, node } => ServiceVariables {
84                service_type: Some(service_type_to_filter(service_type)),
85                node: Some(node_to_filter(node)),
86            },
87            ServiceSelector::Any => ServiceVariables::default(),
88        }
89    }
90}
91
92#[derive(cynic::QueryVariables, Debug, Default)]
93pub struct ServiceTypeVariables {
94    pub service_type: Option<String>,
95}
96
97impl From<Option<ServiceTypeId>> for ServiceTypeVariables {
98    fn from(value: Option<ServiceTypeId>) -> Self {
99        ServiceTypeVariables {
100            service_type: value.map(service_type_to_filter),
101        }
102    }
103}
104
105#[derive(cynic::QueryFragment, Debug)]
106#[cynic(graphql_type = "QueryRoot", variables = "ServicePageVariables")]
107pub struct QueryServices {
108    #[arguments(serviceType: $service_type, node: $node, first: $first, after: $after, watermark: $watermark, liveOnly: $live_only)]
109    pub services: ServicesResult,
110}
111
112#[derive(cynic::QueryFragment, Debug)]
113#[cynic(graphql_type = "QueryRoot", variables = "ServiceVariables")]
114pub struct QueryServiceCount {
115    #[arguments(serviceType: $service_type, node: $node)]
116    pub service_count: CountResult,
117}
118
119#[derive(cynic::QueryFragment, Debug)]
120#[cynic(graphql_type = "QueryRoot", variables = "ServiceTypeVariables")]
121pub struct QueryServiceTypes {
122    #[arguments(serviceType: $service_type)]
123    pub service_types: ServiceTypesResult,
124}
125
126#[derive(cynic::QueryFragment, Debug)]
127#[cynic(graphql_type = "QueryRoot")]
128pub struct QueryServiceRegistryConfig {
129    pub service_registry_config: ServiceRegistryConfigResult,
130}
131
132#[derive(cynic::QueryFragment, Debug)]
133#[cynic(graphql_type = "SubscriptionRoot", variables = "ServiceVariables")]
134pub struct SubscribeServices {
135    #[arguments(serviceType: $service_type, node: $node)]
136    pub service_updated: ServiceUpdate,
137}
138
139#[derive(cynic::QueryFragment, Debug)]
140#[cynic(graphql_type = "SubscriptionRoot", variables = "ServiceTypeVariables")]
141pub struct SubscribeServiceTypes {
142    #[arguments(serviceType: $service_type)]
143    pub service_type_updated: ServiceTypeUpdate,
144}
145
146#[derive(cynic::QueryFragment, Debug)]
147#[cynic(graphql_type = "SubscriptionRoot")]
148pub struct SubscribeServiceRegistryConfig {
149    pub service_registry_config_updated: ServiceRegistryConfig,
150}
151
152/// Single entry of the on-chain service registry: one node offering one service type.
153#[derive(cynic::QueryFragment, Debug, Clone, PartialEq, Eq)]
154#[cfg_attr(feature = "serde", derive(serde::Serialize))]
155pub struct ServiceEntry {
156    /// Service type identifier: the ASCII name, or `0x`-prefixed hex when the id is not printable
157    /// ASCII.
158    pub service_type: String,
159    /// Chain address of the node offering the service, encoded as a hex string.
160    pub node: String,
161    /// Safe that performed the last write to this entry, encoded as a hex string.
162    pub safe: String,
163    /// Opaque metadata as `0x`-prefixed hex; the schema belongs to the service type.
164    pub metadata: String,
165    /// Unix timestamp in seconds at which the entry was registered.
166    pub registered_at: Uint64,
167    /// Unix timestamp in seconds at which the entry was last updated.
168    pub updated_at: Uint64,
169}
170
171/// Configuration of a single service type.
172#[derive(cynic::QueryFragment, Debug, Clone, PartialEq, Eq)]
173#[cfg_attr(feature = "serde", derive(serde::Serialize))]
174pub struct ServiceTypeInfo {
175    /// Service type identifier: the ASCII name, or `0x`-prefixed hex.
176    pub service_type: String,
177    /// Owner of the type; `None` once the type has been abandoned, which is one-way.
178    pub owner: Option<String>,
179    /// Requirement contract gating registration; `None` for an open type.
180    pub requirement: Option<String>,
181    /// wxHOPR burned on self-registration, as a decimal string in wei.
182    pub registration_burn: String,
183    /// wxHOPR burned on self-update, as a decimal string in wei.
184    pub update_burn: String,
185}
186
187/// Registry-wide configuration, shared by every service type.
188#[derive(cynic::QueryFragment, Debug, Clone, PartialEq, Eq)]
189#[cfg_attr(feature = "serde", derive(serde::Serialize))]
190pub struct ServiceRegistryConfig {
191    /// wxHOPR burned to register a new service type, as a decimal string in wei.
192    pub type_registration_fee: String,
193    /// Node-safe registry the service registry resolves node bindings against, as a hex string.
194    pub node_safe_registry: String,
195}
196
197/// Kind of change reported for a single registry entry.
198#[derive(cynic::Enum, Clone, Copy, Debug, PartialEq, Eq)]
199pub enum ServiceUpdateKind {
200    /// The entry was created.
201    #[cynic(rename = "REGISTERED")]
202    Registered,
203    /// An existing entry changed.
204    #[cynic(rename = "UPDATED")]
205    Updated,
206    /// The entry was removed.
207    #[cynic(rename = "DEREGISTERED")]
208    Deregistered,
209}
210
211/// Kind of change reported for service-type or registry-wide configuration.
212#[derive(cynic::Enum, Clone, Copy, Debug, PartialEq, Eq)]
213pub enum ServiceTypeUpdateKind {
214    /// A new service type was registered.
215    #[cynic(rename = "REGISTERED")]
216    Registered,
217    /// The owner of a service type changed, or the type was abandoned.
218    #[cynic(rename = "OWNER_CHANGED")]
219    OwnerChanged,
220    /// The requirement contract of a service type changed.
221    #[cynic(rename = "REQUIREMENT_CHANGED")]
222    RequirementChanged,
223    /// The self-registration burn of a service type changed.
224    #[cynic(rename = "REGISTRATION_BURN_CHANGED")]
225    RegistrationBurnChanged,
226    /// The self-update burn of a service type changed.
227    #[cynic(rename = "UPDATE_BURN_CHANGED")]
228    UpdateBurnChanged,
229    /// The registry-wide type registration fee changed.
230    #[cynic(rename = "REGISTRATION_FEE_CHANGED")]
231    RegistrationFeeChanged,
232    /// The node-safe registry the service registry points at changed.
233    #[cynic(rename = "REGISTRY_POINTER_CHANGED")]
234    RegistryPointerChanged,
235}
236
237/// Change to one registry entry.
238#[derive(cynic::QueryFragment, Debug, Clone, PartialEq, Eq)]
239#[cfg_attr(feature = "serde", derive(serde::Serialize))]
240pub struct ServiceUpdate {
241    /// What happened to the entry.
242    pub kind: ServiceUpdateKind,
243    /// Service type the entry belongs to.
244    pub service_type: String,
245    /// Node the entry belongs to, encoded as a hex string.
246    pub node: String,
247    /// Entry state after the change; `None` for
248    /// [`Deregistered`](ServiceUpdateKind::Deregistered), where the entry no longer exists.
249    pub entry: Option<ServiceEntry>,
250}
251
252/// Change to service-type or registry-wide configuration.
253#[derive(cynic::QueryFragment, Debug, Clone, PartialEq, Eq)]
254#[cfg_attr(feature = "serde", derive(serde::Serialize))]
255pub struct ServiceTypeUpdate {
256    /// What changed.
257    pub kind: ServiceTypeUpdateKind,
258    /// Service type affected; `None` for the two registry-wide kinds.
259    pub service_type: Option<String>,
260    /// Type configuration after the change; `None` for the two registry-wide kinds.
261    pub config: Option<ServiceTypeInfo>,
262    /// Registry-wide configuration after the change; `None` for the five per-type kinds.
263    pub registry_config: Option<ServiceRegistryConfig>,
264}
265
266/// List of registry entries returned by a service query.
267#[derive(cynic::QueryFragment, Debug, Clone, PartialEq, Eq)]
268#[cfg_attr(feature = "serde", derive(serde::Serialize))]
269pub struct ServicesList {
270    /// Matching registry entries.
271    pub services: Vec<ServiceEntry>,
272    /// Block watermark shared by every page of this enumeration.
273    pub watermark: Uint64,
274    /// Cursor for the next page.
275    pub next_cursor: Option<Uint64>,
276}
277
278#[derive(Debug)]
279pub struct ServicePage {
280    pub services: Vec<ServiceEntry>,
281    pub watermark: Uint64,
282    pub next_cursor: Option<Uint64>,
283}
284
285/// List of service types returned by a service type query.
286#[derive(cynic::QueryFragment, Debug, Clone, PartialEq, Eq)]
287#[cfg_attr(feature = "serde", derive(serde::Serialize))]
288pub struct ServiceTypesList {
289    /// Matching service types.
290    pub service_types: Vec<ServiceTypeInfo>,
291}
292
293#[derive(cynic::InlineFragments, Debug)]
294pub enum ServicesResult {
295    ServicesList(ServicesList),
296    MissingFilterError(MissingFilterError),
297    QueryFailedError(QueryFailedError),
298    #[cynic(fallback)]
299    Unknown,
300}
301
302impl From<ServicesResult> for Result<ServicePage, BlokliClientError> {
303    fn from(value: ServicesResult) -> Self {
304        match value {
305            ServicesResult::ServicesList(list) => Ok(ServicePage {
306                services: list.services,
307                watermark: list.watermark,
308                next_cursor: list.next_cursor,
309            }),
310            ServicesResult::MissingFilterError(e) => Err(e.into()),
311            ServicesResult::QueryFailedError(e) => Err(e.into()),
312            ServicesResult::Unknown => Err(ErrorKind::NoData.into()),
313        }
314    }
315}
316
317#[derive(cynic::InlineFragments, Debug)]
318pub enum ServiceTypesResult {
319    ServiceTypesList(ServiceTypesList),
320    QueryFailedError(QueryFailedError),
321    #[cynic(fallback)]
322    Unknown,
323}
324
325#[derive(cynic::InlineFragments, Debug)]
326pub enum ServiceRegistryConfigResult {
327    ServiceRegistryConfig(ServiceRegistryConfig),
328    QueryFailedError(QueryFailedError),
329    #[cynic(fallback)]
330    Unknown,
331}
332
333impl From<ServiceRegistryConfigResult> for Result<ServiceRegistryConfig, BlokliClientError> {
334    fn from(value: ServiceRegistryConfigResult) -> Self {
335        match value {
336            ServiceRegistryConfigResult::ServiceRegistryConfig(config) => Ok(config),
337            ServiceRegistryConfigResult::QueryFailedError(e) => Err(e.into()),
338            ServiceRegistryConfigResult::Unknown => Err(ErrorKind::NoData.into()),
339        }
340    }
341}
342
343impl From<ServiceTypesResult> for Result<Vec<ServiceTypeInfo>, BlokliClientError> {
344    fn from(value: ServiceTypesResult) -> Self {
345        match value {
346            ServiceTypesResult::ServiceTypesList(list) => Ok(list.service_types),
347            ServiceTypesResult::QueryFailedError(e) => Err(e.into()),
348            ServiceTypesResult::Unknown => Err(ErrorKind::NoData.into()),
349        }
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::{ServiceVariables, service_type_name, service_type_to_filter};
356    use crate::api::v1::ServiceSelector;
357
358    const GVPN_EXIT: [u8; 32] = [
359        0x67, 0x76, 0x70, 0x6e, 0x3a, 0x65, 0x78, 0x69, 0x74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
360        0, 0, 0, 0,
361    ];
362
363    #[test]
364    fn service_type_filter_is_zero_padded_prefixed_hex() {
365        assert_eq!(
366            service_type_to_filter(GVPN_EXIT),
367            "0x6776706e3a657869740000000000000000000000000000000000000000000000"
368        );
369    }
370
371    #[test]
372    fn ascii_name_is_decoded_from_a_right_padded_id() {
373        assert_eq!(service_type_name(&GVPN_EXIT), Some("gvpn:exit"));
374    }
375
376    #[test]
377    fn ascii_name_rejects_ids_outside_the_convention() {
378        // An interior NUL byte, so the trailing bytes are not padding.
379        let mut interior_nul = GVPN_EXIT;
380        interior_nul[31] = b'x';
381        assert_eq!(service_type_name(&interior_nul), None);
382
383        // Non-graphic ASCII, which `FromStr` on the foundation type also rejects.
384        let mut with_space = [0u8; 32];
385        with_space[..3].copy_from_slice(b"a b");
386        assert_eq!(service_type_name(&with_space), None);
387
388        // The all-zero id, which the registry contract itself rejects.
389        assert_eq!(service_type_name(&[0u8; 32]), None);
390    }
391
392    #[test]
393    fn any_selector_sends_no_filters() {
394        let variables = ServiceVariables::from(ServiceSelector::Any);
395
396        assert!(variables.service_type.is_none());
397        assert!(variables.node.is_none());
398    }
399
400    #[test]
401    fn combined_selector_sends_both_filters() {
402        let variables = ServiceVariables::from(ServiceSelector::ServiceTypeAndNode {
403            service_type: GVPN_EXIT,
404            node: [0x11; 20],
405        });
406
407        assert_eq!(
408            variables.node.as_deref(),
409            Some("1111111111111111111111111111111111111111")
410        );
411        assert_eq!(
412            variables.service_type.as_deref(),
413            Some("0x6776706e3a657869740000000000000000000000000000000000000000000000")
414        );
415    }
416}