Skip to main content

open_payments/client/
core.rs

1use crate::config::ClientConfig;
2use crate::error::{OpClientError, Result};
3use crate::http_signature::{jwk::Jwk, load_or_generate_key};
4use crate::types::wallet_address::{JsonWebKey, JwkAlgorithm, JwkCurve, JwkKeyType, JwkUse};
5use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
6use ed25519_dalek::SigningKey;
7use reqwest::{Client, Client as ReqwestClient};
8
9/// Base trait for HTTP clients that provides access to the underlying reqwest client.
10///
11/// This trait is implemented by both authenticated and unauthenticated clients,
12/// allowing generic code to work with either type.
13pub trait BaseClient {
14    /// Returns a reference to the underlying reqwest HTTP client.
15    fn http_client(&self) -> &ReqwestClient;
16}
17
18/// An authenticated Open Payments client that can make signed HTTP requests.
19///
20/// This client automatically handles HTTP message signature creation for all requests
21/// using the configured private key. It's used for operations that require authentication
22/// such as creating payments, quotes and managing access tokens.
23///
24/// ## Example
25///
26/// ```rust,no_run
27/// use open_payments::client::{AuthenticatedClient, ClientConfig, AuthenticatedResources, UnauthenticatedResources};
28///
29/// #[tokio::main]
30/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
31///     // In a real application, you would use actual file paths
32///     let config = ClientConfig {
33///         private_key_path: "path/to/private-key.pem".into(),
34///         key_id: "my-key-id".to_string(),
35///         jwks_path: Some("path/to/jwks.json".into()),
36///         wallet_address_url: "https://rafiki.money/alice".into(),
37///     };
38///
39///     // This would fail in a real scenario if the files don't exist
40///     // but demonstrates the API usage
41///     let _client = AuthenticatedClient::new(config)?;
42///     Ok(())
43/// }
44/// ```
45pub struct AuthenticatedOpenPaymentsClient {
46    /// The underlying HTTP client for making requests.
47    pub http_client: ReqwestClient,
48    /// Client configuration including key paths and identifiers.
49    pub config: ClientConfig,
50    /// The signing key used for HTTP message signatures.
51    pub signing_key: SigningKey,
52}
53
54impl BaseClient for AuthenticatedOpenPaymentsClient {
55    fn http_client(&self) -> &ReqwestClient {
56        &self.http_client
57    }
58}
59
60impl AuthenticatedOpenPaymentsClient {
61    /// Creates a new authenticated client with the given configuration.
62    ///
63    /// This method will:
64    /// 1. Load or generate the signing key from the specified path
65    /// 2. Generate and save JWKS if a JWKS path is provided
66    /// 3. Create an HTTP client for making requests
67    ///
68    /// # Arguments
69    ///
70    /// * `config` - The client configuration containing key paths and identifiers
71    ///
72    /// # Returns
73    ///
74    /// Returns a configured authenticated client or an error if key loading fails.
75    ///
76    /// # Errors
77    ///
78    /// Returns an `OpClientError` with:
79    /// - `description`: Human-readable error message
80    /// - `status`: HTTP status text (for HTTP errors)
81    /// - `code`: HTTP status code (for HTTP errors)
82    /// - `validation_errors`: List of validation errors (if applicable)
83    /// - `details`: Additional error details (if applicable)
84    pub fn new(config: ClientConfig) -> Result<Self> {
85        let http_client = ReqwestClient::new();
86
87        let signing_key = load_or_generate_key(&config.private_key_path).map_err(|e| {
88            OpClientError::signature(format!("Failed to load or generate signing key: {e}"))
89        })?;
90
91        if let Some(ref jwks_path) = config.jwks_path {
92            let jwks_json = Jwk::generate_jwks_json(&signing_key, &config.key_id);
93            Jwk::save_jwks(&jwks_json, jwks_path).map_err(|e| {
94                OpClientError::signature(format!("Failed to save JWK to file: {e}"))
95            })?;
96        }
97
98        Ok(Self {
99            http_client,
100            config,
101            signing_key,
102        })
103    }
104
105    /// Returns the public [`JsonWebKey`] corresponding to this client's signing key.
106    ///
107    /// Useful for directed-identity grant requests via `grant().request(..., Some(&jwk))`.
108    pub fn public_jwk(&self) -> JsonWebKey {
109        let x = URL_SAFE_NO_PAD.encode(self.signing_key.verifying_key().as_bytes());
110        JsonWebKey {
111            kid: self.config.key_id.clone(),
112            alg: JwkAlgorithm::EdDSA,
113            use_: Some(JwkUse::Signature),
114            kty: JwkKeyType::OKP,
115            crv: JwkCurve::Ed25519,
116            x,
117        }
118    }
119}
120
121/// An unauthenticated Open Payments client for making public requests.
122///
123/// This client is used for operations that don't require authentication,
124/// such as retrieving public wallet address information or accessing
125/// publicly available resources e.g. public incoming payments.
126///
127/// ## Example
128///
129/// ```rust,no_run
130/// use open_payments::client::{UnauthenticatedClient, UnauthenticatedResources};
131///
132/// #[tokio::main]
133/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
134///     let client = UnauthenticatedClient::new();
135///     // This would make an actual HTTP request in a real scenario
136///     // but demonstrates the API usage
137///     let _wallet_address = client.wallet_address().get("https://rafiki.money/alice").await?;
138///     Ok(())
139/// }
140/// ```
141pub struct UnauthenticatedOpenPaymentsClient {
142    pub http_client: ReqwestClient,
143}
144
145impl Default for UnauthenticatedOpenPaymentsClient {
146    fn default() -> Self {
147        Self::new()
148    }
149}
150
151impl UnauthenticatedOpenPaymentsClient {
152    /// Creates a new unauthenticated client.
153    ///
154    /// This creates a simple HTTP client without any authentication configuration.
155    /// It's suitable for accessing public endpoints that don't require signatures.
156    pub fn new() -> Self {
157        Self {
158            http_client: ReqwestClient::new(),
159        }
160    }
161}
162
163impl BaseClient for UnauthenticatedOpenPaymentsClient {
164    fn http_client(&self) -> &ReqwestClient {
165        &self.http_client
166    }
167}
168
169impl BaseClient for Client {
170    fn http_client(&self) -> &ReqwestClient {
171        self
172    }
173}
174
175pub use self::AuthenticatedOpenPaymentsClient as AuthenticatedClient;
176pub use self::UnauthenticatedOpenPaymentsClient as UnauthenticatedClient;