use alloy_primitives::B256;
use clap::Parser;
use serde::{Deserialize, Serialize};
use std::fs;
use std::net::IpAddr;
use std::ops::{Deref, DerefMut};
use std::path::PathBuf;
use std::str::FromStr;
use url::Url;
use crate::chain::eip155;
use crate::chain::solana;
use crate::chain::{ChainId, ChainIdPattern};
#[derive(Parser, Debug)]
#[command(name = "x402-rs")]
#[command(about = "x402 Facilitator HTTP server")]
struct CliArgs {
#[arg(long = "config", short = 'c')]
config: Option<PathBuf>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
#[serde(default = "config_defaults::default_port")]
port: u16,
#[serde(default = "config_defaults::default_host")]
host: IpAddr,
#[serde(default, with = "chains_serde")]
chains: Vec<ChainConfig>,
#[serde(default)]
schemes: Vec<SchemeConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemeConfig {
#[serde(default = "scheme_config_defaults::default_enabled")]
pub enabled: bool,
pub id: String,
pub chains: ChainIdPattern,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<serde_json::Value>,
}
mod scheme_config_defaults {
pub fn default_enabled() -> bool {
true
}
}
#[derive(Debug, Clone)]
pub enum ChainConfig {
Eip155(Eip155ChainConfig),
Solana(Box<SolanaChainConfig>),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RpcConfig {
pub http: Url,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rate_limit: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LiteralOrEnv<T>(T);
impl<T> LiteralOrEnv<T> {
#[allow(dead_code)]
pub fn inner(&self) -> &T {
&self.0
}
#[allow(dead_code)]
pub fn into_inner(self) -> T {
self.0
}
fn parse_env_var_syntax(s: &str) -> Option<String> {
if s.starts_with("${") && s.ends_with('}') {
Some(s[2..s.len() - 1].to_string())
} else if s.starts_with('$') && s.len() > 1 {
let var_name = &s[1..];
if var_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
Some(var_name.to_string())
} else {
None
}
} else {
None
}
}
}
impl<T> Deref for LiteralOrEnv<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for LiteralOrEnv<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'de, T> Deserialize<'de> for LiteralOrEnv<T>
where
T: FromStr,
T::Err: std::fmt::Display,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let value = if let Some(var_name) = Self::parse_env_var_syntax(&s) {
std::env::var(&var_name).map_err(|_| {
serde::de::Error::custom(format!(
"Environment variable '{}' not found (referenced as '{}')",
var_name, s
))
})?
} else {
s
};
let parsed = value
.parse::<T>()
.map_err(|e| serde::de::Error::custom(format!("Failed to parse value: {}", e)))?;
Ok(LiteralOrEnv(parsed))
}
}
impl<T> serde::Serialize for LiteralOrEnv<T>
where
T: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.serialize(serializer)
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct EvmPrivateKey(B256);
impl EvmPrivateKey {
pub fn as_bytes(&self) -> &[u8; 32] {
self.0.as_ref()
}
}
impl PartialEq for EvmPrivateKey {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl FromStr for EvmPrivateKey {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
B256::from_str(s)
.map(Self)
.map_err(|e| format!("Invalid evm private key: {}", e))
}
}
pub type Eip155SignersConfig = Vec<LiteralOrEnv<EvmPrivateKey>>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SolanaPrivateKey([u8; 64]);
impl SolanaPrivateKey {
pub fn from_base58(s: &str) -> Result<Self, String> {
let bytes = bs58::decode(s)
.into_vec()
.map_err(|e| format!("Invalid base58: {}", e))?;
if bytes.len() != 64 {
return Err(format!(
"Private key must be 64 bytes (standard Solana format), got {} bytes",
bytes.len()
));
}
let mut arr = [0u8; 64];
arr.copy_from_slice(&bytes);
Ok(Self(arr))
}
pub fn to_base58(&self) -> String {
bs58::encode(&self.0).into_string()
}
}
impl Serialize for SolanaPrivateKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_base58())
}
}
impl FromStr for SolanaPrivateKey {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_base58(s)
}
}
impl std::fmt::Display for SolanaPrivateKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_base58())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SolanaSignerConfig(LiteralOrEnv<SolanaPrivateKey>);
impl Deref for SolanaSignerConfig {
type Target = SolanaPrivateKey;
fn deref(&self) -> &Self::Target {
self.0.inner()
}
}
#[derive(Debug, Clone)]
pub struct Eip155ChainConfig {
chain_reference: eip155::Eip155ChainReference,
inner: Eip155ChainConfigInner,
}
impl Eip155ChainConfig {
pub fn chain_id(&self) -> ChainId {
self.chain_reference.into()
}
pub fn eip1559(&self) -> bool {
self.inner.eip1559
}
pub fn flashblocks(&self) -> bool {
self.inner.flashblocks
}
pub fn receipt_timeout_secs(&self) -> u64 {
self.inner.receipt_timeout_secs
}
pub fn signers(&self) -> &Eip155SignersConfig {
&self.inner.signers
}
pub fn rpc(&self) -> &Vec<RpcConfig> {
&self.inner.rpc
}
pub fn chain_reference(&self) -> eip155::Eip155ChainReference {
self.chain_reference
}
}
#[derive(Debug, Clone)]
pub struct SolanaChainConfig {
chain_reference: solana::SolanaChainReference,
inner: SolanaChainConfigInner,
}
impl SolanaChainConfig {
pub fn signer(&self) -> &SolanaSignerConfig {
&self.inner.signer
}
pub fn rpc(&self) -> &Url {
&self.inner.rpc
}
pub fn max_compute_unit_limit(&self) -> u32 {
self.inner.max_compute_unit_limit
}
pub fn max_compute_unit_price(&self) -> u64 {
self.inner.max_compute_unit_price
}
pub fn chain_reference(&self) -> solana::SolanaChainReference {
self.chain_reference
}
pub fn pubsub(&self) -> &Option<Url> {
&self.inner.pubsub
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Eip155ChainConfigInner {
#[serde(default = "eip155_chain_config::default_eip1559")]
pub eip1559: bool,
#[serde(default = "eip155_chain_config::default_flashblocks")]
pub flashblocks: bool,
pub signers: Eip155SignersConfig,
pub rpc: Vec<RpcConfig>,
#[serde(default = "eip155_chain_config::default_receipt_timeout_secs")]
pub receipt_timeout_secs: u64,
}
mod eip155_chain_config {
pub fn default_eip1559() -> bool {
true
}
pub fn default_flashblocks() -> bool {
false
}
pub fn default_receipt_timeout_secs() -> u64 {
30
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SolanaChainConfigInner {
pub signer: SolanaSignerConfig,
pub rpc: Url,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pubsub: Option<Url>,
#[serde(default = "solana_chain_config::default_max_compute_unit_limit")]
pub max_compute_unit_limit: u32,
#[serde(default = "solana_chain_config::default_max_compute_unit_price")]
pub max_compute_unit_price: u64,
}
mod solana_chain_config {
pub fn default_max_compute_unit_limit() -> u32 {
400_000
}
pub fn default_max_compute_unit_price() -> u64 {
1_000_000
}
}
mod chains_serde {
use super::*;
use serde::de::{MapAccess, Visitor};
use serde::ser::SerializeMap;
use serde::{Deserializer, Serializer};
use std::fmt;
#[allow(dead_code)]
pub fn serialize<S>(chains: &Vec<ChainConfig>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(chains.len()))?;
for chain_config in chains {
match chain_config {
ChainConfig::Eip155(config) => {
let chain_id: ChainId = config.chain_reference.into();
let inner = &config.inner;
map.serialize_entry(&chain_id, inner)?;
}
ChainConfig::Solana(config) => {
let chain_id: ChainId = config.chain_reference.into();
let inner = &config.inner;
map.serialize_entry(&chain_id, inner)?;
}
}
}
map.end()
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<ChainConfig>, D::Error>
where
D: Deserializer<'de>,
{
struct ChainsVisitor;
impl<'de> Visitor<'de> for ChainsVisitor {
type Value = Vec<ChainConfig>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a map of chain identifiers to chain configurations")
}
fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut chains = Vec::with_capacity(access.size_hint().unwrap_or(0));
while let Some(chain_id) = access.next_key::<ChainId>()? {
let namespace = chain_id.namespace();
let config = match namespace {
eip155::EIP155_NAMESPACE => {
let inner: Eip155ChainConfigInner = access.next_value()?;
let config = Eip155ChainConfig {
chain_reference: chain_id
.try_into()
.map_err(|e| serde::de::Error::custom(format!("{}", e)))?,
inner,
};
ChainConfig::Eip155(config)
}
solana::SOLANA_NAMESPACE => {
let inner: SolanaChainConfigInner = access.next_value()?;
let config = SolanaChainConfig {
chain_reference: chain_id
.try_into()
.map_err(|e| serde::de::Error::custom(format!("{}", e)))?,
inner,
};
ChainConfig::Solana(Box::new(config))
}
_ => {
return Err(serde::de::Error::custom(format!(
"Unexpected namespace: {}",
namespace
)));
}
};
chains.push(config)
}
Ok(chains)
}
}
deserializer.deserialize_map(ChainsVisitor)
}
}
impl Default for Config {
fn default() -> Self {
Config {
port: config_defaults::default_port(),
host: config_defaults::default_host(),
chains: Vec::new(),
schemes: Vec::new(),
}
}
}
mod config_defaults {
use std::env;
use std::net::IpAddr;
pub const DEFAULT_PORT: u16 = 8080;
pub const DEFAULT_HOST: &str = "0.0.0.0";
pub fn default_port() -> u16 {
env::var("PORT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(DEFAULT_PORT)
}
pub fn default_host() -> IpAddr {
env::var("HOST")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(IpAddr::V4(DEFAULT_HOST.parse().unwrap()))
}
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("Failed to read config file: {0}")]
FileRead(#[from] std::io::Error),
#[error("Failed to parse config file: {0}")]
JsonParse(#[from] serde_json::Error),
}
impl Config {
pub fn load() -> Result<Self, ConfigError> {
let cli_args = CliArgs::parse();
let config_path = Self::get_config_path(cli_args.config);
Self::load_from_path(config_path)
}
fn load_from_path(path: Option<PathBuf>) -> Result<Self, ConfigError> {
match path {
Some(p) => {
let content = fs::read_to_string(&p)?;
let config: Config = serde_json::from_str(&content)?;
Ok(config)
}
None => Ok(Config::default()),
}
}
fn get_config_path(cli_config: Option<PathBuf>) -> Option<PathBuf> {
if let Some(path) = cli_config {
return Some(path);
}
let default_path = PathBuf::from("config.json");
if default_path.exists() {
Some(default_path)
} else {
None
}
}
pub fn port(&self) -> u16 {
self.port
}
pub fn host(&self) -> IpAddr {
self.host
}
pub fn chains(&self) -> &Vec<ChainConfig> {
&self.chains
}
pub fn schemes(&self) -> &Vec<SchemeConfig> {
&self.schemes
}
}