Skip to main content

world_id_primitives/
config.rs

1use serde::{Deserialize, Serialize};
2
3use alloy_primitives::Address;
4use url::Url;
5
6use crate::PrimitiveError;
7
8const fn default_nullifier_oracle_threshold() -> usize {
9    2
10}
11
12/// Configuration for a protocol service endpoint (indexer or gateway).
13///
14/// The target URL is required in both variants. The OHTTP variant additionally
15/// carries the relay configuration needed to encrypt and route requests through
16/// an Oblivious HTTP relay.
17#[derive(Clone, Debug, Serialize, Deserialize)]
18#[serde(tag = "type", rename_all = "snake_case")]
19pub enum ServiceEndpoint {
20    /// Direct HTTP(S) connection to `url`.
21    Direct {
22        /// Target service URL.
23        url: String,
24    },
25    /// OHTTP-routed connection: encrypted requests pass through `relay_url` to `url`.
26    Ohttp {
27        /// Target service URL (placed inside the encrypted BHTTP envelope).
28        url: String,
29        /// URL of the OHTTP relay that receives encrypted requests.
30        relay_url: String,
31        /// Base64-encoded `application/ohttp-keys` payload listing the gateway HPKE configs.
32        key_config_base64: String,
33    },
34}
35
36impl ServiceEndpoint {
37    /// Convenience constructor for a direct (non-OHTTP) endpoint.
38    #[must_use]
39    pub const fn direct(url: String) -> Self {
40        Self::Direct { url }
41    }
42
43    /// Convenience constructor for an OHTTP-routed endpoint.
44    #[must_use]
45    pub const fn ohttp(url: String, relay_url: String, key_config_base64: String) -> Self {
46        Self::Ohttp {
47            url,
48            relay_url,
49            key_config_base64,
50        }
51    }
52
53    /// Target service URL (works for both variants).
54    #[must_use]
55    pub fn url(&self) -> &str {
56        match self {
57            Self::Direct { url } | Self::Ohttp { url, .. } => url,
58        }
59    }
60}
61
62/// Global configuration to interact with the different components of the Protocol.
63///
64/// Used by Authenticators and RPs.
65#[derive(Clone, Debug, Serialize, Deserialize)]
66pub struct Config {
67    /// A fully qualified RPC domain to perform on-chain call functions.
68    ///
69    /// When not available, other services will be used (e.g. the indexer to fetch packed account index).
70    rpc_url: Option<Url>,
71    /// The chain ID of the network where the `WorldIDRegistry` contract is deployed.
72    chain_id: u64,
73    /// The address of the `WorldIDRegistry` contract
74    registry_address: Address,
75    /// Indexer endpoint (`world-id-indexer`). Used to fetch inclusion proofs from the `WorldIDRegistry`.
76    indexer: ServiceEndpoint,
77    /// Gateway endpoint (`world-id-gateway`). Used to submit management operations on authenticators.
78    gateway: ServiceEndpoint,
79    /// The Base URLs of all Nullifier Oracles to use
80    nullifier_oracle_urls: Vec<String>,
81    /// Minimum number of Nullifier Oracle responses required to build a nullifier.
82    #[serde(default = "default_nullifier_oracle_threshold")]
83    nullifier_oracle_threshold: usize,
84}
85
86impl Config {
87    /// Instantiates a new configuration.
88    ///
89    /// # Errors
90    ///
91    /// Returns an error if the `rpc_url` is invalid.
92    pub fn new(
93        rpc_url: Option<String>,
94        chain_id: u64,
95        registry_address: Address,
96        indexer: ServiceEndpoint,
97        gateway: ServiceEndpoint,
98        nullifier_oracle_urls: Vec<String>,
99        nullifier_oracle_threshold: usize,
100    ) -> Result<Self, PrimitiveError> {
101        let rpc_url = rpc_url
102            .map(|url| {
103                Url::parse(&url).map_err(|e| PrimitiveError::InvalidInput {
104                    reason: e.to_string(),
105                    attribute: "rpc_url".to_string(),
106                })
107            })
108            .transpose()?;
109
110        Ok(Self {
111            rpc_url,
112            chain_id,
113            registry_address,
114            indexer,
115            gateway,
116            nullifier_oracle_urls,
117            nullifier_oracle_threshold,
118        })
119    }
120
121    /// Loads a configuration from JSON.
122    ///
123    /// # Errors
124    /// Will error if the JSON is not valid.
125    pub fn from_json(json_str: &str) -> Result<Self, PrimitiveError> {
126        serde_json::from_str(json_str)
127            .map_err(|e| PrimitiveError::Serialization(format!("failed to parse config: {e}")))
128    }
129
130    /// The RPC endpoint to perform RPC calls.
131    #[must_use]
132    pub const fn rpc_url(&self) -> Option<&Url> {
133        self.rpc_url.as_ref()
134    }
135
136    /// The chain ID of the network where the `WorldIDRegistry` contract is deployed.
137    #[must_use]
138    pub const fn chain_id(&self) -> u64 {
139        self.chain_id
140    }
141
142    /// The address of the `WorldIDRegistry` contract.
143    #[must_use]
144    pub const fn registry_address(&self) -> &Address {
145        &self.registry_address
146    }
147
148    /// The indexer endpoint configuration. The indexer is used to fetch inclusion
149    /// proofs from the `WorldIDRegistry` contract.
150    #[must_use]
151    pub const fn indexer(&self) -> &ServiceEndpoint {
152        &self.indexer
153    }
154
155    /// The gateway endpoint configuration. The gateway is used to perform operations
156    /// on the `WorldIDRegistry` contract without leaking a wallet address.
157    #[must_use]
158    pub const fn gateway(&self) -> &ServiceEndpoint {
159        &self.gateway
160    }
161
162    /// Convenience accessor for the indexer's target URL.
163    #[must_use]
164    pub fn indexer_url(&self) -> &str {
165        self.indexer.url()
166    }
167
168    /// Convenience accessor for the gateway's target URL.
169    #[must_use]
170    pub fn gateway_url(&self) -> &str {
171        self.gateway.url()
172    }
173
174    /// The list of URLs of all and each node of the Nullifier Oracle.
175    #[must_use]
176    pub const fn nullifier_oracle_urls(&self) -> &Vec<String> {
177        &self.nullifier_oracle_urls
178    }
179
180    /// The minimum number of Nullifier Oracle responses required to build a nullifier.
181    #[must_use]
182    pub const fn nullifier_oracle_threshold(&self) -> usize {
183        self.nullifier_oracle_threshold
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn from_json_direct_endpoints() {
193        let json = serde_json::json!({
194            "chain_id": 480,
195            "registry_address": "0x0000000000000000000000000000000000000001",
196            "indexer": { "type": "direct", "url": "http://indexer.example.com" },
197            "gateway": { "type": "direct", "url": "http://gateway.example.com" },
198            "nullifier_oracle_urls": [],
199            "nullifier_oracle_threshold": 2
200        });
201
202        let config = Config::from_json(&json.to_string()).unwrap();
203        assert!(matches!(config.indexer(), ServiceEndpoint::Direct { .. }));
204        assert!(matches!(config.gateway(), ServiceEndpoint::Direct { .. }));
205        assert_eq!(config.indexer_url(), "http://indexer.example.com");
206        assert_eq!(config.gateway_url(), "http://gateway.example.com");
207    }
208
209    #[test]
210    fn from_json_ohttp_endpoints() {
211        let json = serde_json::json!({
212            "chain_id": 480,
213            "registry_address": "0x0000000000000000000000000000000000000001",
214            "indexer": {
215                "type": "ohttp",
216                "url": "http://indexer.example.com",
217                "relay_url": "https://relay.example.com/gateway",
218                "key_config_base64": "dGVzdC1rZXk="
219            },
220            "gateway": {
221                "type": "ohttp",
222                "url": "http://gateway.example.com",
223                "relay_url": "https://relay.example.com/gateway",
224                "key_config_base64": "dGVzdC1rZXk="
225            },
226            "nullifier_oracle_urls": [],
227            "nullifier_oracle_threshold": 2
228        });
229
230        let config = Config::from_json(&json.to_string()).unwrap();
231        match config.indexer() {
232            ServiceEndpoint::Ohttp {
233                url,
234                relay_url,
235                key_config_base64,
236            } => {
237                assert_eq!(url, "http://indexer.example.com");
238                assert_eq!(relay_url, "https://relay.example.com/gateway");
239                assert_eq!(key_config_base64, "dGVzdC1rZXk=");
240            }
241            other => panic!("expected Ohttp variant, got: {other:?}"),
242        }
243        match config.gateway() {
244            ServiceEndpoint::Ohttp {
245                url,
246                relay_url,
247                key_config_base64,
248            } => {
249                assert_eq!(url, "http://gateway.example.com");
250                assert_eq!(relay_url, "https://relay.example.com/gateway");
251                assert_eq!(key_config_base64, "dGVzdC1rZXk=");
252            }
253            other => panic!("expected Ohttp variant, got: {other:?}"),
254        }
255    }
256}