Skip to main content

uts_sdk/
lib.rs

1//! Rust SDK for the Universal Timestamps protocol.
2
3// MIT License
4//
5// Copyright (c) 2025 UTS Contributors
6//
7// Permission is hereby granted, free of charge, to any person obtaining a copy
8// of this software and associated documentation files (the "Software"), to deal
9// in the Software without restriction, including without limitation the rights
10// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11// copies of the Software, and to permit persons to whom the Software is
12// furnished to do so, subject to the following conditions:
13//
14// The above copyright notice and this permission notice shall be included in all
15// copies or substantial portions of the Software.
16//
17// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23// SOFTWARE.
24//
25// Apache License, Version 2.0
26//
27// Copyright (c) 2025 UTS Contributors
28//
29// Licensed under the Apache License, Version 2.0 (the "License");
30// you may not use this file except in compliance with the License.
31// You may obtain a copy of the License at
32//
33//     http://www.apache.org/licenses/LICENSE-2.0
34//
35// Unless required by applicable law or agreed to in writing, software
36// distributed under the License is distributed on an "AS IS" BASIS,
37// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
38// See the License for the specific language governing permissions and
39// limitations under the License.
40
41use backon::{ExponentialBuilder, Retryable};
42use bytes::Bytes;
43use http::StatusCode;
44use reqwest::{Client, RequestBuilder};
45use std::{collections::HashSet, sync::Arc, time::Duration};
46use tracing::trace;
47use url::Url;
48#[cfg(feature = "eas-verifier")]
49use {alloy_primitives::ChainId, alloy_provider::DynProvider, std::collections::BTreeMap};
50
51mod builder;
52mod error;
53mod stamp;
54mod upgrade;
55mod verify;
56
57pub use error::Error;
58pub use upgrade::UpgradeResult;
59
60/// Alias `Result` to use the crate's error type by default.
61pub type Result<T, E = Error> = std::result::Result<T, E>;
62
63/// SDK for interacting with Universal Timestamping protocol.
64#[derive(Debug, Clone)]
65pub struct Sdk {
66    inner: Arc<SdkInner>,
67}
68
69#[derive(Debug)]
70struct SdkInner {
71    http_client: Client,
72
73    // Stamp
74    calendars: HashSet<Url>,
75    quorum: usize,
76    timeout_seconds: u64,
77    retry: ExponentialBuilder,
78
79    // Privacy
80    nonce_size: usize,
81
82    // Upgrade
83    keep_pending: bool,
84
85    // Verify
86    #[cfg(feature = "eas-verifier")]
87    eth_providers: BTreeMap<ChainId, DynProvider>,
88    #[cfg(feature = "bitcoin-verifier")]
89    bitcoin_rpc: Url,
90}
91
92impl Default for Sdk {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98impl Sdk {
99    /// Create a new SDK with default settings.
100    pub fn new() -> Self {
101        Self::builder()
102            .build()
103            .expect("Default SDK should be valid")
104    }
105
106    /// Create a new SDK builder with default settings.
107    pub fn builder() -> builder::SdkBuilder {
108        builder::SdkBuilder::default()
109    }
110
111    /// Create a new SDK builder with given calendars and default settings.
112    pub fn try_builder_from_calendars(
113        calendars: impl IntoIterator<Item = Url>,
114    ) -> Result<builder::SdkBuilder, builder::BuilderError> {
115        builder::SdkBuilder::try_default_from_calendars(calendars)
116    }
117
118    async fn http_request_with_retry<Builder>(
119        &self,
120        method: http::Method,
121        url: Url,
122        response_size_limit: usize,
123        builder_fn: Builder,
124    ) -> Result<(http::response::Parts, Bytes)>
125    where
126        Builder: Fn(RequestBuilder) -> RequestBuilder + Send + Sync + 'static,
127    {
128        let client = self.inner.http_client.clone();
129        let timeout_seconds = self.inner.timeout_seconds;
130        let res = {
131            move || {
132                let client = client.clone();
133                let method = method.clone();
134                let url = url.clone();
135                let req = client
136                    .request(method, url)
137                    .timeout(Duration::from_secs(timeout_seconds));
138                let req = builder_fn(req);
139
140                async move {
141                    let res = req.send().await?;
142                    if res.status().is_server_error()
143                        || (
144                            // specially treat 404 as non-error
145                            res.status().is_client_error() && res.status() != StatusCode::NOT_FOUND
146                        )
147                    {
148                        res.error_for_status()
149                    } else {
150                        Ok::<_, reqwest::Error>(res)
151                    }
152                }
153            }
154        }
155        .retry(self.inner.retry)
156        .when(|e| {
157            if e.is_connect() || e.is_timeout() {
158                return true;
159            }
160            if let Some(status) = e.status() {
161                return status.is_server_error() || status == StatusCode::TOO_MANY_REQUESTS;
162            }
163            false
164        })
165        .notify(|e, duration| {
166            trace!("retrying error {e:?} after sleeping {duration:?}");
167        })
168        .await?;
169
170        let res: http::Response<reqwest::Body> = res.into();
171        let (parts, body) = res.into_parts();
172        let body = http_body_util::Limited::new(body, response_size_limit);
173        let bytes = http_body_util::BodyExt::collect(body)
174            .await
175            .map_err(Error::Http)?
176            .to_bytes();
177        Ok((parts, bytes))
178    }
179}