1use std::time::{Duration, Instant};
8
9use hotshot_builder_api::v0_1::{
10 block_info::AvailableBlockInfo,
11 builder::{BuildError, Error as BuilderApiError},
12};
13use hotshot_types::{
14 constants::LEGACY_BUILDER_MODULE,
15 data::VidCommitment,
16 traits::{node_implementation::NodeType, signature_key::SignatureKey},
17};
18use serde::{Deserialize, Serialize};
19use surf_disco::{client::HealthStatus, Client, Url};
20use tagged_base64::TaggedBase64;
21use thiserror::Error;
22use tokio::time::sleep;
23use vbs::version::StaticVersionType;
24
25#[derive(Debug, Error, Serialize, Deserialize)]
26pub enum BuilderClientError {
28 #[error("Requested block not found")]
30 BlockNotFound,
31
32 #[error("Requested block was missing")]
34 BlockMissing,
35
36 #[error("Builder API error: {0}")]
38 Api(String),
39}
40
41impl From<BuilderApiError> for BuilderClientError {
42 fn from(value: BuilderApiError) -> Self {
43 match value {
44 BuilderApiError::Request(source) | BuilderApiError::TxnUnpack(source) => {
45 Self::Api(source.to_string())
46 },
47 BuilderApiError::TxnSubmit(source) | BuilderApiError::BuilderAddress(source) => {
48 Self::Api(source.to_string())
49 },
50 BuilderApiError::Custom { message, .. } => Self::Api(message),
51 BuilderApiError::BlockAvailable { source, .. }
52 | BuilderApiError::BlockClaim { source, .. } => match source {
53 BuildError::NotFound => Self::BlockNotFound,
54 BuildError::Missing => Self::BlockMissing,
55 BuildError::Error(message) => Self::Api(message),
56 },
57 BuilderApiError::TxnStat(source) => Self::Api(source.to_string()),
58 }
59 }
60}
61
62pub struct BuilderClient<TYPES: NodeType, Ver: StaticVersionType> {
64 client: Client<BuilderApiError, Ver>,
66 _marker: std::marker::PhantomData<TYPES>,
68}
69
70impl<TYPES: NodeType, Ver: StaticVersionType> BuilderClient<TYPES, Ver> {
71 pub fn new(base_url: impl Into<Url>) -> Self {
77 let url = base_url.into();
78
79 Self {
80 client: Client::builder(url.clone())
81 .set_timeout(Some(Duration::from_secs(2)))
82 .build(),
83 _marker: std::marker::PhantomData,
84 }
85 }
86
87 pub async fn connect(&self, timeout: Duration) -> bool {
91 let timeout = Instant::now() + timeout;
92 let mut backoff = Duration::from_millis(50);
93 while Instant::now() < timeout {
94 if matches!(
95 self.client.healthcheck::<HealthStatus>().await,
96 Ok(HealthStatus::Available)
97 ) {
98 return true;
99 }
100 sleep(backoff).await;
101 backoff *= 2;
102 }
103 false
104 }
105
106 pub async fn available_blocks(
112 &self,
113 parent: VidCommitment,
114 view_number: u64,
115 sender: TYPES::SignatureKey,
116 signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
117 ) -> Result<Vec<AvailableBlockInfo<TYPES>>, BuilderClientError> {
118 let encoded_signature: TaggedBase64 = signature.clone().into();
119 self.client
120 .get(&format!(
121 "{LEGACY_BUILDER_MODULE}/availableblocks/{parent}/{view_number}/{sender}/\
122 {encoded_signature}"
123 ))
124 .send()
125 .await
126 .map_err(Into::into)
127 }
128}
129
130pub mod v0_1 {
132 use hotshot_builder_api::v0_1::block_info::{
133 AvailableBlockData, AvailableBlockHeaderInputV2, AvailableBlockHeaderInputV2Either,
134 AvailableBlockHeaderInputV2Legacy,
135 };
136 pub use hotshot_builder_api::v0_1::Version;
137 use hotshot_types::{
138 constants::LEGACY_BUILDER_MODULE,
139 traits::{node_implementation::NodeType, signature_key::SignatureKey},
140 utils::BuilderCommitment,
141 };
142 use tagged_base64::TaggedBase64;
143 use vbs::BinarySerializer;
144
145 use super::BuilderClientError;
146
147 pub type BuilderClient<TYPES> = super::BuilderClient<TYPES, Version>;
149
150 impl<TYPES: NodeType> BuilderClient<TYPES> {
151 pub async fn claim_block_header_input(
157 &self,
158 block_hash: BuilderCommitment,
159 view_number: u64,
160 sender: TYPES::SignatureKey,
161 signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
162 ) -> Result<AvailableBlockHeaderInputV2<TYPES>, BuilderClientError> {
163 let encoded_signature: TaggedBase64 = signature.clone().into();
164 self.client
165 .get(&format!(
166 "{LEGACY_BUILDER_MODULE}/claimheaderinput/v2/{block_hash}/{view_number}/\
167 {sender}/{encoded_signature}"
168 ))
169 .send()
170 .await
171 .map_err(Into::into)
172 }
173
174 pub async fn claim_legacy_block_header_input(
180 &self,
181 block_hash: BuilderCommitment,
182 view_number: u64,
183 sender: TYPES::SignatureKey,
184 signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
185 ) -> Result<AvailableBlockHeaderInputV2Legacy<TYPES>, BuilderClientError> {
186 let encoded_signature: TaggedBase64 = signature.clone().into();
187 self.client
188 .get(&format!(
189 "{LEGACY_BUILDER_MODULE}/claimheaderinput/v2/{block_hash}/{view_number}/\
190 {sender}/{encoded_signature}"
191 ))
192 .send()
193 .await
194 .map_err(Into::into)
195 }
196
197 pub async fn claim_either_block_header_input(
204 &self,
205 block_hash: BuilderCommitment,
206 view_number: u64,
207 sender: TYPES::SignatureKey,
208 signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
209 ) -> Result<AvailableBlockHeaderInputV2Either<TYPES>, BuilderClientError> {
210 let encoded_signature: TaggedBase64 = signature.clone().into();
211 let result = self
212 .client
213 .get::<Vec<u8>>(&format!(
214 "{LEGACY_BUILDER_MODULE}/claimheaderinput/v2/{block_hash}/{view_number}/\
215 {sender}/{encoded_signature}"
216 ))
217 .bytes()
218 .await
219 .map_err(Into::<BuilderClientError>::into)?;
220
221 if let Ok(available_block_header_input_v2) = vbs::Serializer::<Version>::deserialize::<
225 AvailableBlockHeaderInputV2<TYPES>,
226 >(&result)
227 {
228 Ok(AvailableBlockHeaderInputV2Either::Current(
229 available_block_header_input_v2,
230 ))
231 } else {
232 vbs::Serializer::<Version>::deserialize::<AvailableBlockHeaderInputV2Legacy<TYPES>>(
233 &result,
234 )
235 .map_err(|e| BuilderClientError::Api(format!("Failed to deserialize: {e:?}")))
236 .map(AvailableBlockHeaderInputV2Either::Legacy)
237 }
238 }
239
240 pub async fn claim_block(
246 &self,
247 block_hash: BuilderCommitment,
248 view_number: u64,
249 sender: TYPES::SignatureKey,
250 signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
251 ) -> Result<AvailableBlockData<TYPES>, BuilderClientError> {
252 let encoded_signature: TaggedBase64 = signature.clone().into();
253 self.client
254 .get(&format!(
255 "{LEGACY_BUILDER_MODULE}/claimblock/{block_hash}/{view_number}/{sender}/\
256 {encoded_signature}"
257 ))
258 .send()
259 .await
260 .map_err(Into::into)
261 }
262
263 pub async fn claim_block_with_num_nodes(
270 &self,
271 block_hash: BuilderCommitment,
272 view_number: u64,
273 sender: TYPES::SignatureKey,
274 signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
275 num_nodes: usize,
276 ) -> Result<AvailableBlockData<TYPES>, BuilderClientError> {
277 let encoded_signature: TaggedBase64 = signature.clone().into();
278 self.client
279 .get(&format!(
280 "{LEGACY_BUILDER_MODULE}/claimblockwithnumnodes/{block_hash}/{view_number}/\
281 {sender}/{encoded_signature}/{num_nodes}"
282 ))
283 .send()
284 .await
285 .map_err(Into::into)
286 }
287 }
288}
289
290pub mod v0_2 {
292 use vbs::version::StaticVersion;
293
294 pub use super::v0_1::*;
295
296 pub type Version = StaticVersion<0, 2>;
298}