1#[cfg(feature = "rust")]
2use bon::Builder;
3#[cfg(feature = "node")]
4use napi_derive::napi;
5#[cfg(feature = "python")]
6use pyo3::{pyclass, pymethods};
7#[cfg(feature = "python")]
8use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
9
10use crate::errors::SdkError;
11
12#[cfg_attr(feature = "python", gen_stub_pyclass)]
13#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
14#[cfg_attr(feature = "node", napi(object))]
15#[cfg_attr(feature = "rust", derive(Builder))]
16#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
17pub struct HttpConfig {
18 pub timeout_secs: Option<i64>,
19 pub pool_max_idle_per_host: Option<i32>,
20 pub headers: Option<std::collections::HashMap<String, String>>,
27}
28
29#[cfg(feature = "python")]
30#[gen_stub_pymethods]
31#[pymethods]
32impl HttpConfig {
33 #[new]
34 #[pyo3(signature = (timeout_secs=None, pool_max_idle_per_host=None, headers=None))]
35 pub fn new(
36 timeout_secs: Option<i64>,
37 pool_max_idle_per_host: Option<i32>,
38 headers: Option<std::collections::HashMap<String, String>>,
39 ) -> Self {
40 HttpConfig {
41 timeout_secs,
42 pool_max_idle_per_host,
43 headers,
44 }
45 }
46}
47
48#[derive(Debug, Clone)]
53pub struct ClientInfo {
54 pub language: String,
56 pub language_version: String,
58 pub sdk_version: String,
61}
62
63#[cfg_attr(feature = "python", gen_stub_pyclass)]
64#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
65#[cfg_attr(feature = "node", napi(object))]
66#[cfg_attr(feature = "rust", derive(Builder))]
67#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
68pub struct AdminConfig {
69 pub base_url: Option<String>,
70}
71
72#[cfg(feature = "python")]
73#[gen_stub_pymethods]
74#[pymethods]
75impl AdminConfig {
76 #[new]
77 #[pyo3(signature = (base_url=None))]
78 pub fn new(base_url: Option<String>) -> Self {
79 AdminConfig { base_url }
80 }
81}
82
83#[cfg_attr(feature = "python", gen_stub_pyclass)]
84#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
85#[cfg_attr(feature = "node", napi(object))]
86#[cfg_attr(feature = "rust", derive(Builder))]
87#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
88pub struct StreamsConfig {
89 pub base_url: Option<String>,
90}
91
92#[cfg(feature = "python")]
93#[gen_stub_pymethods]
94#[pymethods]
95impl StreamsConfig {
96 #[new]
97 #[pyo3(signature = (base_url=None))]
98 pub fn new(base_url: Option<String>) -> Self {
99 StreamsConfig { base_url }
100 }
101}
102
103#[cfg_attr(feature = "python", gen_stub_pyclass)]
104#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
105#[cfg_attr(feature = "node", napi(object))]
106#[cfg_attr(feature = "rust", derive(Builder))]
107#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
108pub struct WebhooksConfig {
109 pub base_url: Option<String>,
110}
111
112#[cfg(feature = "python")]
113#[gen_stub_pymethods]
114#[pymethods]
115impl WebhooksConfig {
116 #[new]
117 #[pyo3(signature = (base_url=None))]
118 pub fn new(base_url: Option<String>) -> Self {
119 WebhooksConfig { base_url }
120 }
121}
122
123#[cfg_attr(feature = "python", gen_stub_pyclass)]
124#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
125#[cfg_attr(feature = "node", napi(object))]
126#[cfg_attr(feature = "rust", derive(Builder))]
127#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
128pub struct KvStoreConfig {
129 pub base_url: Option<String>,
130}
131
132#[cfg(feature = "python")]
133#[gen_stub_pymethods]
134#[pymethods]
135impl KvStoreConfig {
136 #[new]
137 #[pyo3(signature = (base_url=None))]
138 pub fn new(base_url: Option<String>) -> Self {
139 KvStoreConfig { base_url }
140 }
141}
142
143#[cfg_attr(feature = "python", gen_stub_pyclass)]
150#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
151#[cfg_attr(feature = "node", napi(object))]
152#[cfg_attr(feature = "rust", derive(Builder))]
153#[derive(Clone, serde::Serialize, serde::Deserialize)]
154pub struct CachedToken {
155 pub endpoint_url: String,
157 pub token: String,
159 pub exp_unix: i64,
161}
162
163impl std::fmt::Debug for CachedToken {
166 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 f.debug_struct("CachedToken")
168 .field("endpoint_url", &self.endpoint_url)
169 .field("token", &"[redacted]")
170 .field("exp_unix", &self.exp_unix)
171 .finish()
172 }
173}
174
175#[cfg(feature = "python")]
176#[gen_stub_pymethods]
177#[pymethods]
178impl CachedToken {
179 #[new]
180 pub fn new(endpoint_url: String, token: String, exp_unix: i64) -> Self {
181 CachedToken {
182 endpoint_url,
183 token,
184 exp_unix,
185 }
186 }
187}
188
189#[cfg_attr(feature = "python", gen_stub_pyclass)]
190#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
191#[cfg_attr(feature = "node", napi(object))]
192#[cfg_attr(feature = "rust", derive(Builder))]
193#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
194pub struct RpcConfig {
195 pub endpoint_url: Option<String>,
202 pub seed: Option<CachedToken>,
206 pub refresh_margin_secs: Option<i64>,
210 pub networks: Option<std::collections::HashMap<String, String>>,
216 #[serde(skip)]
227 pub payment: Option<PaymentConfig>,
228}
229
230#[cfg_attr(feature = "python", gen_stub_pyclass)]
241#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
242#[cfg_attr(feature = "node", napi(object))]
243#[cfg_attr(feature = "rust", derive(Builder))]
244#[derive(Clone, serde::Serialize, serde::Deserialize)]
245pub struct PaymentConfig {
246 pub scheme: String,
248 pub key: String,
251 pub pay_network: String,
254 pub asset: String,
257 pub max_amount: String,
262 pub svm_rpc_url: Option<String>,
268 pub base_url_override: Option<String>,
270}
271
272impl std::fmt::Debug for PaymentConfig {
277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 f.debug_struct("PaymentConfig")
279 .field("scheme", &self.scheme)
280 .field("key", &"[redacted]")
281 .field("pay_network", &self.pay_network)
282 .field("asset", &self.asset)
283 .field("max_amount", &self.max_amount)
284 .field("svm_rpc_url", &self.svm_rpc_url)
285 .field("base_url_override", &self.base_url_override)
286 .finish()
287 }
288}
289
290#[cfg(feature = "python")]
291#[gen_stub_pymethods]
292#[pymethods]
293impl PaymentConfig {
294 #[new]
295 #[pyo3(signature = (scheme, key, pay_network, asset, max_amount, svm_rpc_url=None, base_url_override=None))]
296 pub fn new(
297 scheme: String,
298 key: String,
299 pay_network: String,
300 asset: String,
301 max_amount: String,
302 svm_rpc_url: Option<String>,
303 base_url_override: Option<String>,
304 ) -> Self {
305 PaymentConfig {
306 scheme,
307 key,
308 pay_network,
309 asset,
310 max_amount,
311 svm_rpc_url,
312 base_url_override,
313 }
314 }
315}
316
317#[cfg(feature = "python")]
318#[gen_stub_pymethods]
319#[pymethods]
320impl RpcConfig {
321 #[new]
322 #[pyo3(signature = (endpoint_url=None, seed=None, refresh_margin_secs=None, networks=None, payment=None))]
323 pub fn new(
324 endpoint_url: Option<String>,
325 seed: Option<CachedToken>,
326 refresh_margin_secs: Option<i64>,
327 networks: Option<std::collections::HashMap<String, String>>,
328 payment: Option<PaymentConfig>,
329 ) -> Self {
330 RpcConfig {
331 endpoint_url,
332 seed,
333 refresh_margin_secs,
334 networks,
335 payment,
336 }
337 }
338}
339
340#[cfg_attr(feature = "python", gen_stub_pyclass)]
341#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
342#[cfg_attr(feature = "node", napi(object))]
343#[cfg_attr(feature = "rust", derive(Builder))]
344#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
345pub struct SqlConfig {
346 pub base_url: Option<String>,
347}
348
349#[cfg(feature = "python")]
350#[gen_stub_pymethods]
351#[pymethods]
352impl SqlConfig {
353 #[new]
354 #[pyo3(signature = (base_url=None))]
355 pub fn new(base_url: Option<String>) -> Self {
356 SqlConfig { base_url }
357 }
358}
359
360#[cfg_attr(feature = "python", gen_stub_pyclass)]
361#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
362#[cfg_attr(feature = "node", napi(object))]
363#[cfg_attr(feature = "rust", derive(Builder))]
364#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
365pub struct SdkFullConfig {
366 #[serde(default)]
375 pub api_key: Option<String>,
376 pub http: Option<HttpConfig>,
377 pub admin: Option<AdminConfig>,
378 pub streams: Option<StreamsConfig>,
379 pub webhooks: Option<WebhooksConfig>,
380 pub kvstore: Option<KvStoreConfig>,
381 pub sql: Option<SqlConfig>,
382 pub rpc: Option<RpcConfig>,
383}
384
385impl SdkFullConfig {
386 pub fn from_api_key(api_key: String) -> Self {
387 SdkFullConfig {
388 api_key: Some(api_key),
389 http: None,
390 admin: None,
391 streams: None,
392 webhooks: None,
393 kvstore: None,
394 sql: None,
395 rpc: None,
396 }
397 }
398
399 pub fn keyless() -> Self {
403 SdkFullConfig {
404 api_key: None,
405 http: None,
406 admin: None,
407 streams: None,
408 webhooks: None,
409 kvstore: None,
410 sql: None,
411 rpc: None,
412 }
413 }
414
415 pub fn from_env() -> Result<Self, SdkError> {
416 config::Config::builder()
417 .add_source(
418 config::Environment::with_prefix("QN_SDK")
419 .separator("__")
420 .try_parsing(true),
421 )
422 .build()
423 .map_err(|e| SdkError::Config(e.to_string()))
424 .and_then(Self::from_config)
425 }
426
427 fn from_config(cfg: config::Config) -> Result<Self, SdkError> {
428 let parsed: SdkFullConfig = cfg
429 .try_deserialize::<SdkFullConfig>()
430 .map_err(|e| SdkError::Config(e.to_string()))?;
431 if parsed.api_key.as_deref().unwrap_or("").is_empty() {
436 return Err(SdkError::Config(
437 "api_key is required (set QN_SDK__API_KEY)".into(),
438 ));
439 }
440 Ok(parsed)
441 }
442}
443
444#[cfg(feature = "python")]
445#[gen_stub_pymethods]
446#[pymethods]
447impl SdkFullConfig {
448 #[new]
449 #[pyo3(signature = (api_key=None, http=None, admin=None, streams=None, webhooks=None, kvstore=None, sql=None, rpc=None))]
450 #[allow(clippy::too_many_arguments)]
451 pub fn new(
452 api_key: Option<String>,
453 http: Option<HttpConfig>,
454 admin: Option<AdminConfig>,
455 streams: Option<StreamsConfig>,
456 webhooks: Option<WebhooksConfig>,
457 kvstore: Option<KvStoreConfig>,
458 sql: Option<SqlConfig>,
459 rpc: Option<RpcConfig>,
460 ) -> Self {
461 SdkFullConfig {
462 api_key,
463 http,
464 admin,
465 streams,
466 webhooks,
467 kvstore,
468 sql,
469 rpc,
470 }
471 }
472}
473
474#[cfg(test)]
475#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
476mod tests {
477 use super::*;
478
479 fn build_config(pairs: &[(&str, &str)]) -> config::Config {
480 let mut builder = config::Config::builder();
481 for (k, v) in pairs {
482 builder = builder.set_override(*k, *v).unwrap();
483 }
484 builder.build().unwrap()
485 }
486
487 #[test]
488 fn from_env_missing_api_key_returns_error() {
489 let cfg = config::Config::builder().build().unwrap();
490 assert!(matches!(
491 SdkFullConfig::from_config(cfg),
492 Err(SdkError::Config(_))
493 ));
494 }
495
496 #[test]
497 fn from_env_only_api_key() {
498 let cfg = build_config(&[("api_key", "test-key")]);
499 let config = SdkFullConfig::from_config(cfg).unwrap();
500 assert_eq!(config.api_key.as_deref(), Some("test-key"));
501 assert!(config.http.is_none());
502 assert!(config.admin.is_none());
503 }
504
505 #[test]
506 fn from_env_all_fields() {
507 let cfg = build_config(&[
508 ("api_key", "my-api-key"),
509 ("http.timeout_secs", "30"),
510 ("http.pool_max_idle_per_host", "5"),
511 ("admin.base_url", "https://example.com/"),
512 ]);
513 let config = SdkFullConfig::from_config(cfg).unwrap();
514 assert_eq!(config.api_key.as_deref(), Some("my-api-key"));
515 let http = config.http.unwrap();
516 assert_eq!(http.timeout_secs, Some(30));
517 assert_eq!(http.pool_max_idle_per_host, Some(5));
518 let admin = config.admin.unwrap();
519 assert_eq!(admin.base_url, Some("https://example.com/".to_string()));
520 }
521
522 #[test]
523 fn from_env_invalid_timeout_secs() {
524 let cfg = build_config(&[("api_key", "test-key"), ("http.timeout_secs", "abc")]);
525 assert!(matches!(
526 SdkFullConfig::from_config(cfg),
527 Err(SdkError::Config(_))
528 ));
529 }
530
531 #[test]
532 fn from_env_headers_round_trip() {
533 let cfg = build_config(&[
534 ("api_key", "k"),
535 ("http.headers.x-correlation-id", "abc"),
536 ("http.headers.user-agent", "custom-ua/1.0"),
537 ]);
538 let config = SdkFullConfig::from_config(cfg).unwrap();
539 let headers = config.http.unwrap().headers.unwrap();
540 assert_eq!(
541 headers.get("x-correlation-id").map(String::as_str),
542 Some("abc")
543 );
544 assert_eq!(
545 headers.get("user-agent").map(String::as_str),
546 Some("custom-ua/1.0")
547 );
548 }
549}