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}