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 #[serde(default)]
17 pub afpay_rpc: std::collections::HashMap<String, AfpayRpcConfig>,
18 #[serde(default)]
20 pub providers: std::collections::HashMap<String, String>,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub storage_backend: Option<String>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub postgres_url_secret: Option<String>,
27 #[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 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#[derive(Debug, Serialize, Deserialize, Clone)]
157pub struct RateLimitConfig {
158 #[serde(default = "default_rate_limit_rps")]
160 pub requests_per_second: u32,
161 #[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}