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;