1use alloy_primitives::B256;
4use clap::Parser;
5use serde::{Deserialize, Serialize};
6use std::fs;
7use std::net::IpAddr;
8use std::ops::{Deref, DerefMut};
9use std::path::{Path, PathBuf};
10use std::str::FromStr;
11use url::Url;
12
13use crate::chain::eip155;
14use crate::chain::solana;
15use crate::chain::{ChainId, ChainIdPattern};
16
17#[derive(Parser, Debug)]
19#[command(name = "x402-rs")]
20#[command(about = "x402 Facilitator HTTP server")]
21struct CliArgs {
22 #[arg(long, short, env = "CONFIG", default_value = "config.json")]
24 config: PathBuf,
25}
26
27#[derive(Debug, Clone, Deserialize)]
32pub struct Config<TChainsConfig = ChainsConfig> {
33 #[serde(default = "config_defaults::default_port")]
34 port: u16,
35 #[serde(default = "config_defaults::default_host")]
36 host: IpAddr,
37 #[serde(default)]
38 chains: TChainsConfig,
39 #[serde(default)]
40 schemes: Vec<SchemeConfig>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct SchemeConfig {
48 #[serde(default = "scheme_config_defaults::default_enabled")]
50 pub enabled: bool,
51 pub id: String,
53 pub chains: ChainIdPattern,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub config: Option<serde_json::Value>,
58}
59
60mod scheme_config_defaults {
61 pub fn default_enabled() -> bool {
62 true
63 }
64}
65
66#[derive(Debug, Clone)]
72pub enum ChainConfig {
73 Eip155(Box<Eip155ChainConfig>),
75 Solana(Box<SolanaChainConfig>),
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81pub struct RpcConfig {
82 pub http: Url,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub rate_limit: Option<u32>,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct LiteralOrEnv<T>(T);
103
104impl<T> LiteralOrEnv<T> {
105 #[allow(dead_code)]
107 pub fn inner(&self) -> &T {
108 &self.0
109 }
110
111 #[allow(dead_code)]
113 pub fn into_inner(self) -> T {
114 self.0
115 }
116
117 fn parse_env_var_syntax(s: &str) -> Option<String> {
120 if s.starts_with("${") && s.ends_with('}') {
121 Some(s[2..s.len() - 1].to_string())
123 } else if s.starts_with('$') && s.len() > 1 {
124 let var_name = &s[1..];
126 if var_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
127 Some(var_name.to_string())
128 } else {
129 None
130 }
131 } else {
132 None
133 }
134 }
135}
136
137impl<T> Deref for LiteralOrEnv<T> {
138 type Target = T;
139
140 fn deref(&self) -> &Self::Target {
141 &self.0
142 }
143}
144
145impl<T> DerefMut for LiteralOrEnv<T> {
146 fn deref_mut(&mut self) -> &mut Self::Target {
147 &mut self.0
148 }
149}
150
151impl<'de, T> Deserialize<'de> for LiteralOrEnv<T>
152where
153 T: FromStr,
154 T::Err: std::fmt::Display,
155{
156 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
157 where
158 D: serde::Deserializer<'de>,
159 {
160 let s = String::deserialize(deserializer)?;
161
162 let value = if let Some(var_name) = Self::parse_env_var_syntax(&s) {
164 std::env::var(&var_name).map_err(|_| {
165 serde::de::Error::custom(format!(
166 "Environment variable '{}' not found (referenced as '{}')",
167 var_name, s
168 ))
169 })?
170 } else {
171 s
172 };
173
174 let parsed = value
176 .parse::<T>()
177 .map_err(|e| serde::de::Error::custom(format!("Failed to parse value: {}", e)))?;
178
179 Ok(LiteralOrEnv(parsed))
180 }
181}
182
183impl<T> serde::Serialize for LiteralOrEnv<T>
184where
185 T: Serialize,
186{
187 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
188 where
189 S: serde::Serializer,
190 {
191 self.0.serialize(serializer)
192 }
193}
194
195#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
210pub struct EvmPrivateKey(B256);
211
212impl EvmPrivateKey {
213 pub fn as_bytes(&self) -> &[u8; 32] {
215 self.0.as_ref()
216 }
217}
218
219impl PartialEq for EvmPrivateKey {
220 fn eq(&self, other: &Self) -> bool {
221 self.0 == other.0
222 }
223}
224
225impl FromStr for EvmPrivateKey {
226 type Err = String;
227
228 fn from_str(s: &str) -> Result<Self, Self::Err> {
229 B256::from_str(s)
230 .map(Self)
231 .map_err(|e| format!("Invalid evm private key: {}", e))
232 }
233}
234
235pub type Eip155SignersConfig = Vec<LiteralOrEnv<EvmPrivateKey>>;
259
260#[derive(Clone, Debug, PartialEq, Eq)]
273pub struct SolanaPrivateKey([u8; 64]);
274
275impl SolanaPrivateKey {
276 pub fn from_base58(s: &str) -> Result<Self, String> {
282 let bytes = bs58::decode(s)
283 .into_vec()
284 .map_err(|e| format!("Invalid base58: {}", e))?;
285
286 if bytes.len() != 64 {
287 return Err(format!(
288 "Private key must be 64 bytes (standard Solana format), got {} bytes",
289 bytes.len()
290 ));
291 }
292
293 let mut arr = [0u8; 64];
294 arr.copy_from_slice(&bytes);
295 Ok(Self(arr))
296 }
297
298 pub fn to_base58(&self) -> String {
300 bs58::encode(&self.0).into_string()
301 }
302}
303
304impl Serialize for SolanaPrivateKey {
305 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
306 where
307 S: serde::Serializer,
308 {
309 serializer.serialize_str(&self.to_base58())
310 }
311}
312
313impl FromStr for SolanaPrivateKey {
314 type Err = String;
315
316 fn from_str(s: &str) -> Result<Self, Self::Err> {
317 Self::from_base58(s)
318 }
319}
320
321impl std::fmt::Display for SolanaPrivateKey {
322 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323 write!(f, "{}", self.to_base58())
324 }
325}
326
327#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
338pub struct SolanaSignerConfig(LiteralOrEnv<SolanaPrivateKey>);
339
340impl Deref for SolanaSignerConfig {
341 type Target = SolanaPrivateKey;
342
343 fn deref(&self) -> &Self::Target {
344 self.0.inner()
345 }
346}
347
348#[derive(Debug, Clone)]
353pub struct Eip155ChainConfig {
354 pub chain_reference: eip155::Eip155ChainReference,
355 pub inner: Eip155ChainConfigInner,
356}
357
358impl Eip155ChainConfig {
359 pub fn chain_id(&self) -> ChainId {
360 self.chain_reference.into()
361 }
362 pub fn eip1559(&self) -> bool {
363 self.inner.eip1559
364 }
365 pub fn flashblocks(&self) -> bool {
366 self.inner.flashblocks
367 }
368 pub fn receipt_timeout_secs(&self) -> u64 {
369 self.inner.receipt_timeout_secs
370 }
371 pub fn signers(&self) -> &Eip155SignersConfig {
372 &self.inner.signers
373 }
374 pub fn rpc(&self) -> &Vec<RpcConfig> {
375 &self.inner.rpc
376 }
377 pub fn chain_reference(&self) -> eip155::Eip155ChainReference {
378 self.chain_reference
379 }
380}
381
382#[derive(Debug, Clone)]
383pub struct SolanaChainConfig {
384 pub chain_reference: solana::SolanaChainReference,
385 pub inner: SolanaChainConfigInner,
386}
387
388impl SolanaChainConfig {
389 pub fn signer(&self) -> &SolanaSignerConfig {
390 &self.inner.signer
391 }
392 pub fn rpc(&self) -> &Url {
393 &self.inner.rpc
394 }
395 pub fn max_compute_unit_limit(&self) -> u32 {
396 self.inner.max_compute_unit_limit
397 }
398 pub fn max_compute_unit_price(&self) -> u64 {
399 self.inner.max_compute_unit_price
400 }
401 pub fn chain_reference(&self) -> solana::SolanaChainReference {
402 self.chain_reference
403 }
404 pub fn chain_id(&self) -> ChainId {
405 self.chain_reference.into()
406 }
407 pub fn pubsub(&self) -> &Option<Url> {
408 &self.inner.pubsub
409 }
410}
411
412#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct Eip155ChainConfigInner {
415 #[serde(default = "eip155_chain_config::default_eip1559")]
417 pub eip1559: bool,
418 #[serde(default = "eip155_chain_config::default_flashblocks")]
420 pub flashblocks: bool,
421 pub signers: Eip155SignersConfig,
424 pub rpc: Vec<RpcConfig>,
426 #[serde(default = "eip155_chain_config::default_receipt_timeout_secs")]
428 pub receipt_timeout_secs: u64,
429}
430
431mod eip155_chain_config {
432 pub fn default_eip1559() -> bool {
433 true
434 }
435 pub fn default_flashblocks() -> bool {
436 false
437 }
438 pub fn default_receipt_timeout_secs() -> u64 {
439 30
440 }
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize)]
445pub struct SolanaChainConfigInner {
446 pub signer: SolanaSignerConfig,
449 pub rpc: Url,
451 #[serde(default, skip_serializing_if = "Option::is_none")]
453 pub pubsub: Option<Url>,
454 #[serde(default = "solana_chain_config::default_max_compute_unit_limit")]
456 pub max_compute_unit_limit: u32,
457 #[serde(default = "solana_chain_config::default_max_compute_unit_price")]
459 pub max_compute_unit_price: u64,
460}
461
462mod solana_chain_config {
463 pub fn default_max_compute_unit_limit() -> u32 {
464 400_000
465 }
466 pub fn default_max_compute_unit_price() -> u64 {
467 1_000_000
468 }
469}
470
471#[derive(Debug, Clone, Default)]
476pub struct ChainsConfig(pub Vec<ChainConfig>);
477
478impl Deref for ChainsConfig {
479 type Target = Vec<ChainConfig>;
480
481 fn deref(&self) -> &Self::Target {
482 &self.0
483 }
484}
485
486impl Serialize for ChainsConfig {
487 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
488 where
489 S: serde::Serializer,
490 {
491 use serde::ser::SerializeMap;
492
493 let chains = &self.0;
494 let mut map = serializer.serialize_map(Some(chains.len()))?;
495 for chain_config in chains {
496 match chain_config {
497 ChainConfig::Eip155(config) => {
498 let chain_id = config.chain_id();
499 let inner = &config.inner;
500 map.serialize_entry(&chain_id, inner)?;
501 }
502 ChainConfig::Solana(config) => {
503 let chain_id = config.chain_id();
504 let inner = &config.inner;
505 map.serialize_entry(&chain_id, inner)?;
506 }
507 }
508 }
509 map.end()
510 }
511}
512
513impl<'de> Deserialize<'de> for ChainsConfig {
514 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
515 where
516 D: serde::Deserializer<'de>,
517 {
518 use serde::de::{MapAccess, Visitor};
519 use std::fmt;
520
521 struct ChainsVisitor;
522
523 impl<'de> Visitor<'de> for ChainsVisitor {
524 type Value = ChainsConfig;
525
526 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
527 formatter.write_str("a map of chain identifiers to chain configurations")
528 }
529
530 fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
531 where
532 M: MapAccess<'de>,
533 {
534 let mut chains = Vec::with_capacity(access.size_hint().unwrap_or(0));
535
536 while let Some(chain_id) = access.next_key::<ChainId>()? {
537 let namespace = chain_id.namespace();
538 let config = match namespace {
539 eip155::EIP155_NAMESPACE => {
540 let inner: Eip155ChainConfigInner = access.next_value()?;
541 let config = Eip155ChainConfig {
542 chain_reference: chain_id
543 .try_into()
544 .map_err(|e| serde::de::Error::custom(format!("{}", e)))?,
545 inner,
546 };
547 ChainConfig::Eip155(Box::new(config))
548 }
549 solana::SOLANA_NAMESPACE => {
550 let inner: SolanaChainConfigInner = access.next_value()?;
551 let config = SolanaChainConfig {
552 chain_reference: chain_id
553 .try_into()
554 .map_err(|e| serde::de::Error::custom(format!("{}", e)))?,
555 inner,
556 };
557 ChainConfig::Solana(Box::new(config))
558 }
559 _ => {
560 return Err(serde::de::Error::custom(format!(
561 "Unexpected namespace: {}",
562 namespace
563 )));
564 }
565 };
566 chains.push(config)
567 }
568
569 Ok(ChainsConfig(chains))
570 }
571 }
572
573 deserializer.deserialize_map(ChainsVisitor)
574 }
575}
576
577impl<TChainsConfig> Default for Config<TChainsConfig>
578where
579 TChainsConfig: Default,
580{
581 fn default() -> Self {
582 Config {
583 port: config_defaults::default_port(),
584 host: config_defaults::default_host(),
585 chains: TChainsConfig::default(),
586 schemes: Vec::new(),
587 }
588 }
589}
590
591pub mod config_defaults {
592 use std::env;
593 use std::net::IpAddr;
594
595 pub const DEFAULT_PORT: u16 = 8080;
596 pub const DEFAULT_HOST: &str = "0.0.0.0";
597
598 pub fn default_port() -> u16 {
600 env::var("PORT")
601 .ok()
602 .and_then(|s| s.parse().ok())
603 .unwrap_or(DEFAULT_PORT)
604 }
605
606 pub fn default_host() -> IpAddr {
608 env::var("HOST")
609 .ok()
610 .and_then(|s| s.parse().ok())
611 .unwrap_or(IpAddr::V4(DEFAULT_HOST.parse().unwrap()))
612 }
613}
614
615#[derive(Debug, thiserror::Error)]
617pub enum ConfigError {
618 #[error("Failed to read config file at {0}: {1}")]
619 FileRead(PathBuf, std::io::Error),
620 #[error("Failed to parse config file: {0}")]
621 JsonParse(#[from] serde_json::Error),
622}
623
624impl<TChainsConfig> Config<TChainsConfig> {
625 pub fn port(&self) -> u16 {
627 self.port
628 }
629
630 pub fn host(&self) -> IpAddr {
634 self.host
635 }
636
637 pub fn schemes(&self) -> &Vec<SchemeConfig> {
641 &self.schemes
642 }
643
644 pub fn chains(&self) -> &TChainsConfig {
648 &self.chains
649 }
650}
651
652impl<TChainsConfig> Config<TChainsConfig>
653where
654 TChainsConfig: Default + for<'de> Deserialize<'de>,
655{
656 pub fn load() -> Result<Self, ConfigError> {
665 let cli_args = CliArgs::parse();
666 let config_path = Path::new(&cli_args.config)
667 .canonicalize()
668 .map_err(|e| ConfigError::FileRead(cli_args.config, e))?;
669 Self::load_from_path(config_path)
670 }
671
672 fn load_from_path(path: PathBuf) -> Result<Self, ConfigError> {
674 let content = fs::read_to_string(&path).map_err(|e| ConfigError::FileRead(path, e))?;
675 let config: Config<TChainsConfig> = serde_json::from_str(&content)?;
676 Ok(config)
677 }
678}