Skip to main content

agent_first_pay/types/
config.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Serialize, Deserialize, Clone)]
4pub struct RuntimeConfig {
5    #[serde(default)]
6    pub data_dir: String,
7    #[serde(default, skip_serializing_if = "Option::is_none")]
8    pub rpc_endpoint: Option<String>,
9    #[serde(default, skip_serializing_if = "Option::is_none")]
10    pub rpc_secret: Option<String>,
11    #[serde(default)]
12    pub log: Vec<String>,
13    #[serde(default, skip_serializing_if = "Option::is_none")]
14    pub exchange_rate: Option<ExchangeRateConfig>,
15    /// Named afpay RPC nodes (e.g. `[afpay_rpc.wallet-server]`).
16    #[serde(default)]
17    pub afpay_rpc: std::collections::HashMap<String, AfpayRpcConfig>,
18    /// Network → afpay_rpc node name (omit = local provider).
19    #[serde(default)]
20    pub providers: std::collections::HashMap<String, String>,
21    /// Storage backend: "redb" (default) or "postgres".
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub storage_backend: Option<String>,
24    /// PostgreSQL connection URL (used when storage_backend = "postgres").
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub postgres_url_secret: Option<String>,
27    /// Rate limiting for REST/RPC endpoints.
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub rate_limit: Option<RateLimitConfig>,
30}
31
32impl std::fmt::Debug for RuntimeConfig {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        f.debug_struct("RuntimeConfig")
35            .field("data_dir", &self.data_dir)
36            .field("rpc_endpoint", &self.rpc_endpoint)
37            .field("rpc_secret", &self.rpc_secret.as_ref().map(|_| "***"))
38            .field("log", &self.log)
39            .field("exchange_rate", &self.exchange_rate)
40            .field("afpay_rpc", &self.afpay_rpc)
41            .field("providers", &self.providers)
42            .field("storage_backend", &self.storage_backend)
43            .field(
44                "postgres_url_secret",
45                &self.postgres_url_secret.as_ref().map(|_| "***"),
46            )
47            .field("rate_limit", &self.rate_limit)
48            .finish()
49    }
50}
51
52impl Default for RuntimeConfig {
53    fn default() -> Self {
54        Self {
55            data_dir: default_data_dir(),
56            rpc_endpoint: None,
57            rpc_secret: None,
58            log: vec![],
59            exchange_rate: None,
60            afpay_rpc: std::collections::HashMap::new(),
61            providers: std::collections::HashMap::new(),
62            storage_backend: None,
63            postgres_url_secret: None,
64            rate_limit: None,
65        }
66    }
67}
68
69fn default_data_dir() -> String {
70    // AFPAY_HOME takes priority, then ~/.afpay
71    if let Some(val) = std::env::var_os("AFPAY_HOME") {
72        return std::path::PathBuf::from(val).to_string_lossy().into_owned();
73    }
74    if let Some(home) = std::env::var_os("HOME") {
75        let mut p = std::path::PathBuf::from(home);
76        p.push(".afpay");
77        p.to_string_lossy().into_owned()
78    } else {
79        ".afpay".to_string()
80    }
81}
82
83#[derive(Serialize, Deserialize, Clone)]
84pub struct AfpayRpcConfig {
85    pub endpoint: String,
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub endpoint_secret: Option<String>,
88}
89
90impl std::fmt::Debug for AfpayRpcConfig {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        f.debug_struct("AfpayRpcConfig")
93            .field("endpoint", &self.endpoint)
94            .field(
95                "endpoint_secret",
96                &self.endpoint_secret.as_ref().map(|_| "***"),
97            )
98            .finish()
99    }
100}
101
102#[derive(Debug, Serialize, Deserialize, Clone)]
103pub struct ExchangeRateConfig {
104    #[serde(default = "default_exchange_rate_ttl_s")]
105    pub ttl_s: u64,
106    #[serde(default = "default_exchange_rate_sources")]
107    pub sources: Vec<ExchangeRateSource>,
108}
109
110impl Default for ExchangeRateConfig {
111    fn default() -> Self {
112        Self {
113            ttl_s: default_exchange_rate_ttl_s(),
114            sources: default_exchange_rate_sources(),
115        }
116    }
117}
118
119#[derive(Serialize, Deserialize, Clone)]
120pub struct ExchangeRateSource {
121    #[serde(rename = "type")]
122    pub source_type: ExchangeRateSourceType,
123    pub endpoint: String,
124    #[serde(default, alias = "api_key", skip_serializing_if = "Option::is_none")]
125    pub api_key_secret: Option<String>,
126}
127
128impl std::fmt::Debug for ExchangeRateSource {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        f.debug_struct("ExchangeRateSource")
131            .field("source_type", &self.source_type)
132            .field("endpoint", &self.endpoint)
133            .field(
134                "api_key_secret",
135                &self.api_key_secret.as_ref().map(|_| "***"),
136            )
137            .finish()
138    }
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
142#[serde(rename_all = "snake_case")]
143pub enum ExchangeRateSourceType {
144    Generic,
145    CoinGecko,
146    Kraken,
147}
148
149/// Rate limiting configuration for REST/RPC endpoints.
150///
151/// ```toml
152/// [rate_limit]
153/// requests_per_second = 20
154/// max_concurrent = 50
155/// ```
156#[derive(Debug, Serialize, Deserialize, Clone)]
157pub struct RateLimitConfig {
158    /// Maximum requests per second (token-bucket refill rate). 0 = unlimited.
159    #[serde(default = "default_rate_limit_rps")]
160    pub requests_per_second: u32,
161    /// Maximum concurrent in-flight requests. 0 = unlimited.
162    #[serde(default = "default_rate_limit_concurrent")]
163    pub max_concurrent: u32,
164}
165
166impl Default for RateLimitConfig {
167    fn default() -> Self {
168        Self {
169            requests_per_second: default_rate_limit_rps(),
170            max_concurrent: default_rate_limit_concurrent(),
171        }
172    }
173}
174
175fn default_rate_limit_rps() -> u32 {
176    20
177}
178
179fn default_rate_limit_concurrent() -> u32 {
180    50
181}
182
183fn default_exchange_rate_ttl_s() -> u64 {
184    300
185}
186
187fn default_exchange_rate_sources() -> Vec<ExchangeRateSource> {
188    vec![
189        ExchangeRateSource {
190            source_type: ExchangeRateSourceType::Kraken,
191            endpoint: "https://api.kraken.com".to_string(),
192            api_key_secret: None,
193        },
194        ExchangeRateSource {
195            source_type: ExchangeRateSourceType::CoinGecko,
196            endpoint: "https://api.coingecko.com/api/v3".to_string(),
197            api_key_secret: None,
198        },
199    ]
200}
201
202#[derive(Debug, Serialize, Deserialize, Default)]
203pub struct ConfigPatch {
204    #[serde(default)]
205    pub data_dir: Option<String>,
206    #[serde(default)]
207    pub log: Option<Vec<String>>,
208    #[serde(default)]
209    pub exchange_rate: Option<ExchangeRateConfig>,
210    #[serde(default)]
211    pub afpay_rpc: Option<std::collections::HashMap<String, AfpayRpcConfig>>,
212    #[serde(default)]
213    pub providers: Option<std::collections::HashMap<String, String>>,
214}