#[cfg(feature = "rust")]
use bon::Builder;
#[cfg(feature = "node")]
use napi_derive::napi;
#[cfg(feature = "python")]
use pyo3::{pyclass, pymethods};
#[cfg(feature = "python")]
use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
use crate::errors::SdkError;
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[cfg_attr(feature = "rust", derive(Builder))]
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct HttpConfig {
pub timeout_secs: Option<i64>,
pub pool_max_idle_per_host: Option<i32>,
pub headers: Option<std::collections::HashMap<String, String>>,
}
#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl HttpConfig {
#[new]
#[pyo3(signature = (timeout_secs=None, pool_max_idle_per_host=None, headers=None))]
pub fn new(
timeout_secs: Option<i64>,
pool_max_idle_per_host: Option<i32>,
headers: Option<std::collections::HashMap<String, String>>,
) -> Self {
HttpConfig {
timeout_secs,
pool_max_idle_per_host,
headers,
}
}
}
#[derive(Debug, Clone)]
pub struct ClientInfo {
pub language: String,
pub language_version: String,
pub sdk_version: String,
}
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[cfg_attr(feature = "rust", derive(Builder))]
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct AdminConfig {
pub base_url: Option<String>,
}
#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl AdminConfig {
#[new]
#[pyo3(signature = (base_url=None))]
pub fn new(base_url: Option<String>) -> Self {
AdminConfig { base_url }
}
}
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[cfg_attr(feature = "rust", derive(Builder))]
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct StreamsConfig {
pub base_url: Option<String>,
}
#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl StreamsConfig {
#[new]
#[pyo3(signature = (base_url=None))]
pub fn new(base_url: Option<String>) -> Self {
StreamsConfig { base_url }
}
}
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[cfg_attr(feature = "rust", derive(Builder))]
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct WebhooksConfig {
pub base_url: Option<String>,
}
#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl WebhooksConfig {
#[new]
#[pyo3(signature = (base_url=None))]
pub fn new(base_url: Option<String>) -> Self {
WebhooksConfig { base_url }
}
}
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[cfg_attr(feature = "rust", derive(Builder))]
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct KvStoreConfig {
pub base_url: Option<String>,
}
#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl KvStoreConfig {
#[new]
#[pyo3(signature = (base_url=None))]
pub fn new(base_url: Option<String>) -> Self {
KvStoreConfig { base_url }
}
}
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[cfg_attr(feature = "rust", derive(Builder))]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct CachedToken {
pub endpoint_url: String,
pub token: String,
pub exp_unix: i64,
}
impl std::fmt::Debug for CachedToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CachedToken")
.field("endpoint_url", &self.endpoint_url)
.field("token", &"[redacted]")
.field("exp_unix", &self.exp_unix)
.finish()
}
}
#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl CachedToken {
#[new]
pub fn new(endpoint_url: String, token: String, exp_unix: i64) -> Self {
CachedToken {
endpoint_url,
token,
exp_unix,
}
}
}
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[cfg_attr(feature = "rust", derive(Builder))]
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct RpcConfig {
pub endpoint_url: Option<String>,
pub seed: Option<CachedToken>,
pub refresh_margin_secs: Option<i64>,
pub networks: Option<std::collections::HashMap<String, String>>,
#[serde(skip)]
pub payment: Option<PaymentConfig>,
}
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[cfg_attr(feature = "rust", derive(Builder))]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct PaymentConfig {
pub scheme: String,
pub key: String,
pub pay_network: String,
pub asset: String,
pub max_amount: String,
pub svm_rpc_url: Option<String>,
pub base_url_override: Option<String>,
}
impl std::fmt::Debug for PaymentConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PaymentConfig")
.field("scheme", &self.scheme)
.field("key", &"[redacted]")
.field("pay_network", &self.pay_network)
.field("asset", &self.asset)
.field("max_amount", &self.max_amount)
.field("svm_rpc_url", &self.svm_rpc_url)
.field("base_url_override", &self.base_url_override)
.finish()
}
}
#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl PaymentConfig {
#[new]
#[pyo3(signature = (scheme, key, pay_network, asset, max_amount, svm_rpc_url=None, base_url_override=None))]
pub fn new(
scheme: String,
key: String,
pay_network: String,
asset: String,
max_amount: String,
svm_rpc_url: Option<String>,
base_url_override: Option<String>,
) -> Self {
PaymentConfig {
scheme,
key,
pay_network,
asset,
max_amount,
svm_rpc_url,
base_url_override,
}
}
}
#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl RpcConfig {
#[new]
#[pyo3(signature = (endpoint_url=None, seed=None, refresh_margin_secs=None, networks=None, payment=None))]
pub fn new(
endpoint_url: Option<String>,
seed: Option<CachedToken>,
refresh_margin_secs: Option<i64>,
networks: Option<std::collections::HashMap<String, String>>,
payment: Option<PaymentConfig>,
) -> Self {
RpcConfig {
endpoint_url,
seed,
refresh_margin_secs,
networks,
payment,
}
}
}
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[cfg_attr(feature = "rust", derive(Builder))]
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct SqlConfig {
pub base_url: Option<String>,
}
#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl SqlConfig {
#[new]
#[pyo3(signature = (base_url=None))]
pub fn new(base_url: Option<String>) -> Self {
SqlConfig { base_url }
}
}
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[cfg_attr(feature = "rust", derive(Builder))]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SdkFullConfig {
#[serde(default)]
pub api_key: Option<String>,
pub http: Option<HttpConfig>,
pub admin: Option<AdminConfig>,
pub streams: Option<StreamsConfig>,
pub webhooks: Option<WebhooksConfig>,
pub kvstore: Option<KvStoreConfig>,
pub sql: Option<SqlConfig>,
pub rpc: Option<RpcConfig>,
}
impl SdkFullConfig {
pub fn from_api_key(api_key: String) -> Self {
SdkFullConfig {
api_key: Some(api_key),
http: None,
admin: None,
streams: None,
webhooks: None,
kvstore: None,
sql: None,
rpc: None,
}
}
pub fn keyless() -> Self {
SdkFullConfig {
api_key: None,
http: None,
admin: None,
streams: None,
webhooks: None,
kvstore: None,
sql: None,
rpc: None,
}
}
pub fn from_env() -> Result<Self, SdkError> {
config::Config::builder()
.add_source(
config::Environment::with_prefix("QN_SDK")
.separator("__")
.try_parsing(true),
)
.build()
.map_err(|e| SdkError::Config(e.to_string()))
.and_then(Self::from_config)
}
fn from_config(cfg: config::Config) -> Result<Self, SdkError> {
let parsed: SdkFullConfig = cfg
.try_deserialize::<SdkFullConfig>()
.map_err(|e| SdkError::Config(e.to_string()))?;
if parsed.api_key.as_deref().unwrap_or("").is_empty() {
return Err(SdkError::Config(
"api_key is required (set QN_SDK__API_KEY)".into(),
));
}
Ok(parsed)
}
}
#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl SdkFullConfig {
#[new]
#[pyo3(signature = (api_key=None, http=None, admin=None, streams=None, webhooks=None, kvstore=None, sql=None, rpc=None))]
#[allow(clippy::too_many_arguments)]
pub fn new(
api_key: Option<String>,
http: Option<HttpConfig>,
admin: Option<AdminConfig>,
streams: Option<StreamsConfig>,
webhooks: Option<WebhooksConfig>,
kvstore: Option<KvStoreConfig>,
sql: Option<SqlConfig>,
rpc: Option<RpcConfig>,
) -> Self {
SdkFullConfig {
api_key,
http,
admin,
streams,
webhooks,
kvstore,
sql,
rpc,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
fn build_config(pairs: &[(&str, &str)]) -> config::Config {
let mut builder = config::Config::builder();
for (k, v) in pairs {
builder = builder.set_override(*k, *v).unwrap();
}
builder.build().unwrap()
}
#[test]
fn from_env_missing_api_key_returns_error() {
let cfg = config::Config::builder().build().unwrap();
assert!(matches!(
SdkFullConfig::from_config(cfg),
Err(SdkError::Config(_))
));
}
#[test]
fn from_env_only_api_key() {
let cfg = build_config(&[("api_key", "test-key")]);
let config = SdkFullConfig::from_config(cfg).unwrap();
assert_eq!(config.api_key.as_deref(), Some("test-key"));
assert!(config.http.is_none());
assert!(config.admin.is_none());
}
#[test]
fn from_env_all_fields() {
let cfg = build_config(&[
("api_key", "my-api-key"),
("http.timeout_secs", "30"),
("http.pool_max_idle_per_host", "5"),
("admin.base_url", "https://example.com/"),
]);
let config = SdkFullConfig::from_config(cfg).unwrap();
assert_eq!(config.api_key.as_deref(), Some("my-api-key"));
let http = config.http.unwrap();
assert_eq!(http.timeout_secs, Some(30));
assert_eq!(http.pool_max_idle_per_host, Some(5));
let admin = config.admin.unwrap();
assert_eq!(admin.base_url, Some("https://example.com/".to_string()));
}
#[test]
fn from_env_invalid_timeout_secs() {
let cfg = build_config(&[("api_key", "test-key"), ("http.timeout_secs", "abc")]);
assert!(matches!(
SdkFullConfig::from_config(cfg),
Err(SdkError::Config(_))
));
}
#[test]
fn from_env_headers_round_trip() {
let cfg = build_config(&[
("api_key", "k"),
("http.headers.x-correlation-id", "abc"),
("http.headers.user-agent", "custom-ua/1.0"),
]);
let config = SdkFullConfig::from_config(cfg).unwrap();
let headers = config.http.unwrap().headers.unwrap();
assert_eq!(
headers.get("x-correlation-id").map(String::as_str),
Some("abc")
);
assert_eq!(
headers.get("user-agent").map(String::as_str),
Some("custom-ua/1.0")
);
}
}