1use std::env;
13use std::fmt::{self, Debug};
14use std::sync::LazyLock;
15use std::time::Duration;
16
17use anyhow::bail;
18use futures::future::Either;
19use http::{HeaderMap, HeaderValue, header};
20use jsonrpsee::core::ClientError;
21use jsonrpsee::core::client::ClientT as _;
22use jsonrpsee::core::params::{ArrayParams, ObjectParams};
23use jsonrpsee::core::traits::ToRpcParams;
24use serde::de::DeserializeOwned;
25use tracing::{Instrument, Level, debug};
26use url::Url;
27
28use super::{ApiPaths, MAX_REQUEST_BODY_SIZE, MAX_RESPONSE_BODY_SIZE, Request};
29
30static USER_AGENT: LazyLock<HeaderValue> = LazyLock::new(|| {
35 HeaderValue::from_str(&format!(
36 "forest/{}",
37 crate::utils::version::FOREST_VERSION_STRING.as_str()
38 ))
39 .expect("Forest version string is a valid header value")
40});
41
42pub struct Client {
44 base_url: Url,
46 token: Option<String>,
47 v0: tokio::sync::OnceCell<UrlClient>,
49 v1: tokio::sync::OnceCell<UrlClient>,
50 v2: tokio::sync::OnceCell<UrlClient>,
51}
52
53impl Client {
54 pub fn default_or_from_env(token: Option<&str>) -> anyhow::Result<Self> {
58 static DEFAULT: LazyLock<Url> = LazyLock::new(|| "http://127.0.0.1:2345/".parse().unwrap());
59
60 let mut base_url = match env::var("FULLNODE_API_INFO") {
61 Ok(it) => {
62 let crate::utils::UrlFromMultiAddr(url) = it.parse()?;
63 url
64 }
65 Err(env::VarError::NotPresent) => DEFAULT.clone(),
66 Err(e @ env::VarError::NotUnicode(_)) => bail!(e),
67 };
68 if token.is_some() && base_url.set_password(token).is_err() {
69 bail!("couldn't set override password")
70 }
71 if token.is_none() && base_url.password().is_none() {
73 let default_token_path = crate::cli_shared::default_token_path();
76 if default_token_path.is_file() {
77 if let Ok(token) = std::fs::read_to_string(&default_token_path) {
78 if base_url.set_password(Some(token.trim())).is_ok() {
79 tracing::debug!("Loaded the default RPC token");
80 } else {
81 tracing::warn!("Failed to set the default RPC token");
82 }
83 } else {
84 tracing::warn!("Failed to load the default token file");
85 }
86 }
87 }
88 Ok(Self::from_url(base_url))
89 }
90 pub fn from_url(mut base_url: Url) -> Self {
91 let token = base_url.password().map(Into::into);
92 let _defer = base_url.set_password(None);
93 Self {
94 token,
95 base_url,
96 v0: Default::default(),
97 v1: Default::default(),
98 v2: Default::default(),
99 }
100 }
101 pub fn base_url(&self) -> &Url {
102 &self.base_url
103 }
104 pub async fn call<T: crate::lotus_json::HasLotusJson + std::fmt::Debug>(
105 &self,
106 req: Request<T>,
107 ) -> Result<T, ClientError> {
108 let api_path = req.api_path;
109 let Request {
110 method_name,
111 params,
112 timeout,
113 ..
114 } = req;
115 let method_name = method_name.as_ref();
116 let client = self.get_or_init_client(api_path).await?;
117 let span = tracing::debug_span!("request", method = %method_name, url = %client.url);
118 let work = async {
119 let result_or_timeout = tokio::time::timeout(
123 timeout,
124 match params {
125 serde_json::Value::Null => Either::Left(Either::Left(
126 client.request::<T::LotusJson, _>(method_name, ArrayParams::new()),
127 )),
128 serde_json::Value::Array(it) => {
129 let mut params = ArrayParams::new();
130 for param in it {
131 params.insert(param)?
132 }
133 trace_params(params.clone());
134 Either::Left(Either::Right(client.request(method_name, params)))
135 }
136 serde_json::Value::Object(it) => {
137 let mut params = ObjectParams::new();
138 for (name, param) in it {
139 params.insert(&name, param)?
140 }
141 trace_params(params.clone());
142 Either::Right(client.request(method_name, params))
143 }
144 prim @ (serde_json::Value::Bool(_)
145 | serde_json::Value::Number(_)
146 | serde_json::Value::String(_)) => {
147 return Err(ClientError::Custom(format!(
148 "invalid parameter type: `{prim}`"
149 )));
150 }
151 },
152 )
153 .await;
154 let result = match result_or_timeout {
155 Ok(Ok(it)) => Ok(T::from_lotus_json(it)),
156 Ok(Err(e)) => Err(e),
157 Err(_) => Err(ClientError::RequestTimeout),
158 };
159 debug!(?result);
160 result
161 };
162 work.instrument(span.or_current()).await
163 }
164 async fn get_or_init_client(&self, path: ApiPaths) -> Result<&UrlClient, ClientError> {
165 match path {
166 ApiPaths::V0 => &self.v0,
167 ApiPaths::V1 => &self.v1,
168 ApiPaths::V2 => &self.v2,
169 }
170 .get_or_try_init(|| async {
171 let url = self.base_url.join(path.path()).map_err(|it| {
172 ClientError::Custom(format!("creating url for endpoint failed: {it}"))
173 })?;
174 UrlClient::new(url, self.token.clone()).await
175 })
176 .await
177 }
178}
179
180fn trace_params(params: impl jsonrpsee::core::traits::ToRpcParams) {
181 if tracing::enabled!(Level::TRACE) {
182 match params.to_rpc_params() {
183 Ok(Some(it)) => tracing::trace!(params = %it),
184 Ok(None) => tracing::trace!("no params"),
185 Err(error) => tracing::trace!(%error, "couldn't decode params"),
186 }
187 }
188}
189
190pub struct UrlClient {
193 url: Url,
194 inner: UrlClientInner,
195}
196
197impl Debug for UrlClient {
198 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199 f.debug_struct("OneClient")
200 .field("url", &self.url)
201 .finish_non_exhaustive()
202 }
203}
204
205impl UrlClient {
206 pub async fn new(url: Url, token: impl Into<Option<String>>) -> Result<Self, ClientError> {
207 const ONE_DAY: Duration = Duration::from_secs(24 * 3600); let mut headers = HeaderMap::from_iter([(header::USER_AGENT, USER_AGENT.clone())]);
209 if let Some(token) = token.into() {
210 let value = HeaderValue::try_from(format!("Bearer {token}"))
211 .map_err(|e| ClientError::Custom(format!("Invalid authorization token: {e}")))?;
212 headers.insert(header::AUTHORIZATION, value);
213 }
214 let inner = match url.scheme() {
215 "ws" | "wss" => UrlClientInner::Ws(
216 jsonrpsee::ws_client::WsClientBuilder::new()
217 .set_headers(headers)
218 .max_request_size(MAX_REQUEST_BODY_SIZE)
219 .max_response_size(*MAX_RESPONSE_BODY_SIZE)
220 .request_timeout(ONE_DAY)
221 .build(&url)
222 .await?,
223 ),
224 "http" | "https" => UrlClientInner::Https(
225 jsonrpsee::http_client::HttpClientBuilder::new()
226 .set_headers(headers)
227 .max_request_size(MAX_REQUEST_BODY_SIZE)
228 .max_response_size(*MAX_RESPONSE_BODY_SIZE)
229 .request_timeout(ONE_DAY)
230 .build(&url)?,
231 ),
232 it => {
233 return Err(ClientError::Custom(format!("Unsupported URL scheme: {it}")));
234 }
235 };
236 Ok(Self { url, inner })
237 }
238}
239
240#[allow(clippy::large_enum_variant)]
241enum UrlClientInner {
242 Ws(jsonrpsee::ws_client::WsClient),
243 Https(jsonrpsee::http_client::HttpClient),
244}
245
246impl jsonrpsee::core::client::ClientT for UrlClient {
247 fn notification<Params>(
248 &self,
249 method: &str,
250 params: Params,
251 ) -> impl Future<Output = Result<(), jsonrpsee::core::client::Error>> + Send
252 where
253 Params: ToRpcParams + Send,
254 {
255 match &self.inner {
256 UrlClientInner::Ws(it) => Either::Left(it.notification(method, params)),
257 UrlClientInner::Https(it) => Either::Right(it.notification(method, params)),
258 }
259 }
260
261 fn request<R, Params>(
262 &self,
263 method: &str,
264 params: Params,
265 ) -> impl Future<Output = Result<R, jsonrpsee::core::client::Error>> + Send
266 where
267 R: DeserializeOwned,
268 Params: ToRpcParams + Send,
269 {
270 match &self.inner {
271 UrlClientInner::Ws(it) => Either::Left(it.request(method, params)),
272 UrlClientInner::Https(it) => Either::Right(it.request(method, params)),
273 }
274 }
275
276 fn batch_request<'a, R>(
277 &self,
278 batch: jsonrpsee::core::params::BatchRequestBuilder<'a>,
279 ) -> impl Future<
280 Output = Result<
281 jsonrpsee::core::client::BatchResponse<'a, R>,
282 jsonrpsee::core::client::Error,
283 >,
284 > + Send
285 where
286 R: DeserializeOwned + fmt::Debug + 'a,
287 {
288 match &self.inner {
289 UrlClientInner::Ws(it) => Either::Left(it.batch_request(batch)),
290 UrlClientInner::Https(it) => Either::Right(it.batch_request(batch)),
291 }
292 }
293}
294
295impl jsonrpsee::core::client::SubscriptionClientT for UrlClient {
296 fn subscribe<'a, N, Params>(
297 &self,
298 subscribe_method: &'a str,
299 params: Params,
300 unsubscribe_method: &'a str,
301 ) -> impl Future<
302 Output = Result<jsonrpsee::core::client::Subscription<N>, jsonrpsee::core::client::Error>,
303 >
304 where
305 Params: ToRpcParams + Send,
306 N: DeserializeOwned,
307 {
308 match &self.inner {
309 UrlClientInner::Ws(it) => {
310 Either::Left(it.subscribe(subscribe_method, params, unsubscribe_method))
311 }
312 UrlClientInner::Https(it) => {
313 Either::Right(it.subscribe(subscribe_method, params, unsubscribe_method))
314 }
315 }
316 }
317
318 fn subscribe_to_method<N>(
319 &self,
320 method: &str,
321 ) -> impl Future<
322 Output = Result<jsonrpsee::core::client::Subscription<N>, jsonrpsee::core::client::Error>,
323 >
324 where
325 N: DeserializeOwned,
326 {
327 match &self.inner {
328 UrlClientInner::Ws(it) => Either::Left(it.subscribe_to_method(method)),
329 UrlClientInner::Https(it) => Either::Right(it.subscribe_to_method(method)),
330 }
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use crate::cli_shared::FOREST_DATA_DIR_ENV;
338
339 #[test]
343 #[serial_test::serial]
344 fn default_token_is_loaded_from_forest_path_data_dir() {
345 let tmp_dir = tempfile::tempdir().unwrap();
346 std::fs::write(tmp_dir.path().join("token"), "secret-token").unwrap();
347
348 unsafe {
349 env::remove_var("FULLNODE_API_INFO");
350 env::set_var(FOREST_DATA_DIR_ENV, tmp_dir.path());
351 }
352 let client = Client::default_or_from_env(None).unwrap();
353 unsafe { env::remove_var(FOREST_DATA_DIR_ENV) };
354
355 assert_eq!(client.token.as_deref(), Some("secret-token"));
356 }
357}