Skip to main content

walletkit_core/
defaults.rs

1use alloy_core::primitives::{address, Address};
2use world_id_core::primitives::{Config, ServiceEndpoint};
3
4use crate::{error::WalletKitError, Environment, Region};
5
6/// The World ID Registry contract address on World Chain Mainnet.
7pub static WORLD_ID_REGISTRY: Address =
8    address!("0x0000000000aE079eB8a274cD51c0f44a9E4d67d4");
9
10/// The **Staging** World ID Registry contract address also on World Chain Mainnet.
11pub static STAGING_WORLD_ID_REGISTRY: Address =
12    address!("0x8556d07D75025f286fe757C7EeEceC40D54FA16D");
13
14/// The `WorldIDVerifier` proxy contract address on the staging environment
15/// (World Chain Mainnet, chain 480).
16///
17/// Source: `world-id-protocol` `contracts/deployments/core/staging.json`.
18pub static WORLD_ID_VERIFIER_STAGING: Address =
19    address!("0x703a6316c975DEabF30b637c155edD53e24657DB");
20
21/// The `WorldIDVerifier` proxy contract address on the production environment
22/// (World Chain Mainnet, chain 480).
23///
24/// Source: `world-id-protocol` `contracts/deployments/core/production.json`.
25pub static WORLD_ID_VERIFIER_PRODUCTION: Address =
26    address!("0x00000000009E00F9FE82CfeeBB4556686da094d7");
27
28/// Returns the `WorldIDVerifier` proxy contract address for the given environment.
29#[must_use]
30pub fn world_id_verifier_address(environment: &Environment) -> Address {
31    match environment {
32        Environment::Staging => WORLD_ID_VERIFIER_STAGING,
33        Environment::Production => WORLD_ID_VERIFIER_PRODUCTION,
34    }
35}
36
37/// The `PoH` Recovery Agent contract address on the staging environment.
38pub static POH_RECOVERY_AGENT_ADDRESS_STAGING: Address =
39    address!("0x8df366ed8ef894f0d1d25dc21b7e36e2d97a7140");
40
41/// The `PoH` Recovery Agent contract address on the production environment.
42pub static POH_RECOVERY_AGENT_ADDRESS_PRODUCTION: Address =
43    address!("0x00000000CBBA8Cb46C8CD414B62213F1B334fC59");
44
45pub(crate) fn poh_recovery_agent_address(environment: &Environment) -> Address {
46    match environment {
47        Environment::Staging => POH_RECOVERY_AGENT_ADDRESS_STAGING,
48        Environment::Production => POH_RECOVERY_AGENT_ADDRESS_PRODUCTION,
49    }
50}
51
52const OPRF_NODE_COUNT: usize = 5;
53
54/// Generates the list of OPRF node URLs for a given region and environment.
55fn oprf_node_urls(region: Region, environment: &Environment) -> Vec<String> {
56    let env_segment = match environment {
57        Environment::Staging => ".staging",
58        Environment::Production => "",
59    };
60
61    (0..OPRF_NODE_COUNT)
62        .map(|i| {
63            format!("https://node{i}.{region}{env_segment}.world.oprf.taceo.network")
64        })
65        .collect()
66}
67
68fn indexer_url(region: Region, environment: &Environment) -> String {
69    let domain = match environment {
70        Environment::Staging => "worldcoin.dev",
71        Environment::Production => "world.org",
72    };
73    format!("https://indexer.{region}.id-infra.{domain}")
74}
75
76fn gateway_url(environment: &Environment) -> String {
77    match environment {
78        Environment::Staging => "https://gateway.id-infra.worldcoin.dev".to_string(),
79        Environment::Production => "https://gateway.id-infra.world.org".to_string(),
80    }
81}
82
83fn ohttp_relay_url(region: Region, environment: &Environment) -> String {
84    let path = match environment {
85        Environment::Staging => format!("{region}-world-id-stage"),
86        Environment::Production => format!("{region}-world-id"),
87    };
88    let host = match environment {
89        Environment::Staging => "staging.privacy-relay.cloudflare.com",
90        Environment::Production => "privacy-relay.cloudflare.com",
91    };
92    format!("https://{host}/{path}")
93}
94
95// Base64-encoded `application/ohttp-keys` payloads fetched from /ohttp-keys endpoints.
96// Each region has an independent HPKE key derived from its own seed secret.
97// To refresh, run the `refresh-ohttp-keys` GitHub Action workflow.
98const OHTTP_KEY_CONFIG_STAGING_US: &str = include_str!("ohttp_keys/staging_us.b64");
99const OHTTP_KEY_CONFIG_STAGING_EU: &str = include_str!("ohttp_keys/staging_eu.b64");
100const OHTTP_KEY_CONFIG_STAGING_AP: &str = include_str!("ohttp_keys/staging_ap.b64");
101const OHTTP_KEY_CONFIG_PRODUCTION_US: &str =
102    include_str!("ohttp_keys/production_us.b64");
103const OHTTP_KEY_CONFIG_PRODUCTION_EU: &str =
104    include_str!("ohttp_keys/production_eu.b64");
105const OHTTP_KEY_CONFIG_PRODUCTION_AP: &str =
106    include_str!("ohttp_keys/production_ap.b64");
107
108const fn ohttp_key_config(region: Region, environment: &Environment) -> &'static str {
109    match (environment, region) {
110        (Environment::Staging, Region::Us) => OHTTP_KEY_CONFIG_STAGING_US,
111        (Environment::Staging, Region::Eu) => OHTTP_KEY_CONFIG_STAGING_EU,
112        (Environment::Staging, Region::Ap) => OHTTP_KEY_CONFIG_STAGING_AP,
113        (Environment::Production, Region::Us) => OHTTP_KEY_CONFIG_PRODUCTION_US,
114        (Environment::Production, Region::Eu) => OHTTP_KEY_CONFIG_PRODUCTION_EU,
115        (Environment::Production, Region::Ap) => OHTTP_KEY_CONFIG_PRODUCTION_AP,
116    }
117}
118
119fn ohttp_endpoint(
120    url: String,
121    region: Region,
122    environment: &Environment,
123) -> ServiceEndpoint {
124    ServiceEndpoint::ohttp(
125        url,
126        ohttp_relay_url(region, environment),
127        ohttp_key_config(region, environment).to_string(),
128    )
129}
130
131fn build_config(
132    environment: &Environment,
133    rpc_url: Option<String>,
134    region: Region,
135    indexer: ServiceEndpoint,
136    gateway: ServiceEndpoint,
137) -> Result<Config, WalletKitError> {
138    let (chain_id, registry_address) = match environment {
139        // Staging also runs on World Chain Mainnet.
140        Environment::Staging => (480, STAGING_WORLD_ID_REGISTRY),
141        Environment::Production => (480, WORLD_ID_REGISTRY),
142    };
143
144    Config::new(
145        rpc_url,
146        chain_id,
147        registry_address,
148        indexer,
149        gateway,
150        oprf_node_urls(region, environment),
151        3,
152    )
153    .map_err(WalletKitError::from)
154}
155
156/// Builds a [`Config`] for the given [`Environment`] using direct (non-OHTTP)
157/// service endpoints — the default for SDK consumers.
158///
159/// # Errors
160///
161/// Returns [`WalletKitError`] if the configuration cannot be constructed
162/// (e.g. invalid RPC URL).
163pub fn default_config(
164    environment: &Environment,
165    rpc_url: Option<String>,
166    region: Option<Region>,
167) -> Result<Config, WalletKitError> {
168    let region = region.unwrap_or_default();
169    let indexer = ServiceEndpoint::direct(indexer_url(region, environment));
170    let gateway = ServiceEndpoint::direct(gateway_url(environment));
171    build_config(environment, rpc_url, region, indexer, gateway)
172}
173
174/// Builds a [`Config`] for the given [`Environment`] using OHTTP endpoints.
175///
176/// Opt-in alternative to [`default_config`] for consumers that want their
177/// indexer/gateway traffic to flow through the Cloudflare OHTTP relay.
178/// The indexer endpoint follows the caller's region; the gateway endpoint is
179/// always pinned to the US OHTTP relay because the `world-id-gateway` is
180/// centralised in the US cluster.
181///
182/// # Errors
183///
184/// Returns [`WalletKitError`] if the configuration cannot be constructed
185/// (e.g. invalid RPC URL).
186pub fn default_config_with_ohttp(
187    environment: &Environment,
188    rpc_url: Option<String>,
189    region: Option<Region>,
190) -> Result<Config, WalletKitError> {
191    let region = region.unwrap_or_default();
192    let indexer = ohttp_endpoint(indexer_url(region, environment), region, environment);
193    let gateway = ohttp_endpoint(gateway_url(environment), Region::Us, environment);
194    build_config(environment, rpc_url, region, indexer, gateway)
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    const ALL_ENVS: &[Environment] = &[Environment::Staging, Environment::Production];
202    const ALL_REGIONS: &[Region] = &[Region::Us, Region::Eu, Region::Ap];
203
204    #[test]
205    fn default_config_builds_direct_endpoints_for_every_env_and_region() {
206        for env in ALL_ENVS {
207            for region in ALL_REGIONS {
208                let config = default_config(env, None, Some(*region))
209                    .expect("default_config should succeed");
210
211                assert!(
212                    matches!(config.indexer(), ServiceEndpoint::Direct { .. }),
213                    "indexer should be Direct for env={env:?} region={region:?}"
214                );
215                assert!(
216                    matches!(config.gateway(), ServiceEndpoint::Direct { .. }),
217                    "gateway should be Direct for env={env:?} region={region:?}"
218                );
219                assert_eq!(config.indexer_url(), indexer_url(*region, env));
220                assert_eq!(config.gateway_url(), gateway_url(env));
221            }
222        }
223    }
224
225    #[test]
226    fn default_config_with_ohttp_builds_ohttp_endpoints_with_region_specific_relays() {
227        for env in ALL_ENVS {
228            for region in ALL_REGIONS {
229                let config = default_config_with_ohttp(env, None, Some(*region))
230                    .expect("default_config_with_ohttp should succeed");
231
232                match config.indexer() {
233                    ServiceEndpoint::Ohttp {
234                        url,
235                        relay_url,
236                        key_config_base64,
237                    } => {
238                        assert_eq!(url, &indexer_url(*region, env));
239                        assert_eq!(relay_url, &ohttp_relay_url(*region, env));
240                        assert_eq!(key_config_base64, ohttp_key_config(*region, env));
241                    }
242                    direct @ ServiceEndpoint::Direct { .. } => panic!(
243                        "indexer should be Ohttp for env={env:?} region={region:?}, got {direct:?}"
244                    ),
245                }
246            }
247        }
248    }
249
250    #[test]
251    fn default_config_with_ohttp_pins_gateway_to_us_relay_regardless_of_region() {
252        for env in ALL_ENVS {
253            for region in ALL_REGIONS {
254                let config = default_config_with_ohttp(env, None, Some(*region))
255                    .expect("default_config_with_ohttp should succeed");
256
257                match config.gateway() {
258                    ServiceEndpoint::Ohttp {
259                        url,
260                        relay_url,
261                        key_config_base64,
262                    } => {
263                        assert_eq!(url, &gateway_url(env));
264                        assert_eq!(relay_url, &ohttp_relay_url(Region::Us, env));
265                        assert_eq!(
266                            key_config_base64,
267                            ohttp_key_config(Region::Us, env)
268                        );
269                    }
270                    direct @ ServiceEndpoint::Direct { .. } => panic!(
271                        "gateway should be Ohttp for env={env:?} region={region:?}, got {direct:?}"
272                    ),
273                }
274            }
275        }
276    }
277
278    #[test]
279    fn default_config_defaults_region_when_not_specified() {
280        let with_default = default_config(&Environment::Staging, None, None).unwrap();
281        let with_explicit_default =
282            default_config(&Environment::Staging, None, Some(Region::default()))
283                .unwrap();
284        assert_eq!(
285            with_default.indexer_url(),
286            with_explicit_default.indexer_url(),
287        );
288    }
289
290    #[test]
291    fn default_config_with_ohttp_defaults_region_when_not_specified() {
292        let with_default =
293            default_config_with_ohttp(&Environment::Staging, None, None).unwrap();
294        let with_explicit_default = default_config_with_ohttp(
295            &Environment::Staging,
296            None,
297            Some(Region::default()),
298        )
299        .unwrap();
300        assert_eq!(
301            with_default.indexer_url(),
302            with_explicit_default.indexer_url(),
303        );
304    }
305
306    #[test]
307    fn both_builders_round_trip_through_config_json() {
308        for env in ALL_ENVS {
309            for region in ALL_REGIONS {
310                let direct = default_config(env, None, Some(*region)).unwrap();
311                let direct_json = serde_json::to_string(&direct).unwrap();
312                let direct_parsed = Config::from_json(&direct_json).unwrap();
313                assert!(matches!(
314                    direct_parsed.indexer(),
315                    ServiceEndpoint::Direct { .. }
316                ));
317                assert!(matches!(
318                    direct_parsed.gateway(),
319                    ServiceEndpoint::Direct { .. }
320                ));
321
322                let ohttp =
323                    default_config_with_ohttp(env, None, Some(*region)).unwrap();
324                let ohttp_json = serde_json::to_string(&ohttp).unwrap();
325                let ohttp_parsed = Config::from_json(&ohttp_json).unwrap();
326                assert!(matches!(
327                    ohttp_parsed.indexer(),
328                    ServiceEndpoint::Ohttp { .. }
329                ));
330                assert!(matches!(
331                    ohttp_parsed.gateway(),
332                    ServiceEndpoint::Ohttp { .. }
333                ));
334            }
335        }
336    }
337}