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}/{encoded_signature}"
122 ))
123 .send()
124 .await
125 .map_err(Into::into)
126 }
127}
128
129pub mod v0_1 {
131 use hotshot_builder_api::v0_1::block_info::{
132 AvailableBlockData, AvailableBlockHeaderInputV2, AvailableBlockHeaderInputV2Either,
133 AvailableBlockHeaderInputV2Legacy,
134 };
135 pub use hotshot_builder_api::v0_1::Version;
136 use hotshot_types::{
137 constants::LEGACY_BUILDER_MODULE,
138 traits::{node_implementation::NodeType, signature_key::SignatureKey},
139 utils::BuilderCommitment,
140 };
141 use tagged_base64::TaggedBase64;
142 use vbs::BinarySerializer;
143
144 use super::BuilderClientError;
145
146 pub type BuilderClient<TYPES> = super::BuilderClient<TYPES, Version>;
148
149 impl<TYPES: NodeType> BuilderClient<TYPES> {
150 pub async fn claim_block_header_input(
156 &self,
157 block_hash: BuilderCommitment,
158 view_number: u64,
159 sender: TYPES::SignatureKey,
160 signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
161 ) -> Result<AvailableBlockHeaderInputV2<TYPES>, BuilderClientError> {
162 let encoded_signature: TaggedBase64 = signature.clone().into();
163 self.client
164 .get(&format!(
165 "{LEGACY_BUILDER_MODULE}/claimheaderinput/v2/{block_hash}/{view_number}/{sender}/{encoded_signature}"
166 ))
167 .send()
168 .await
169 .map_err(Into::into)
170 }
171
172 pub async fn claim_legacy_block_header_input(
178 &self,
179 block_hash: BuilderCommitment,
180 view_number: u64,
181 sender: TYPES::SignatureKey,
182 signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
183 ) -> Result<AvailableBlockHeaderInputV2Legacy<TYPES>, BuilderClientError> {
184 let encoded_signature: TaggedBase64 = signature.clone().into();
185 self.client
186 .get(&format!(
187 "{LEGACY_BUILDER_MODULE}/claimheaderinput/v2/{block_hash}/{view_number}/{sender}/{encoded_signature}"
188 ))
189 .send()
190 .await
191 .map_err(Into::into)
192 }
193
194 pub async fn claim_either_block_header_input(
201 &self,
202 block_hash: BuilderCommitment,
203 view_number: u64,
204 sender: TYPES::SignatureKey,
205 signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
206 ) -> Result<AvailableBlockHeaderInputV2Either<TYPES>, BuilderClientError> {
207 let encoded_signature: TaggedBase64 = signature.clone().into();
208 let result = self.client
209 .get::<Vec<u8>>(&format!(
210 "{LEGACY_BUILDER_MODULE}/claimheaderinput/v2/{block_hash}/{view_number}/{sender}/{encoded_signature}"
211 ))
212 .bytes()
213 .await
214 .map_err(Into::<BuilderClientError>::into)?;
215
216 if let Ok(available_block_header_input_v2) = vbs::Serializer::<Version>::deserialize::<
220 AvailableBlockHeaderInputV2<TYPES>,
221 >(&result)
222 {
223 Ok(AvailableBlockHeaderInputV2Either::Current(
224 available_block_header_input_v2,
225 ))
226 } else {
227 vbs::Serializer::<Version>::deserialize::<AvailableBlockHeaderInputV2Legacy<TYPES>>(
228 &result,
229 )
230 .map_err(|e| BuilderClientError::Api(format!("Failed to deserialize: {e:?}")))
231 .map(AvailableBlockHeaderInputV2Either::Legacy)
232 }
233 }
234
235 pub async fn claim_block(
241 &self,
242 block_hash: BuilderCommitment,
243 view_number: u64,
244 sender: TYPES::SignatureKey,
245 signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
246 ) -> Result<AvailableBlockData<TYPES>, BuilderClientError> {
247 let encoded_signature: TaggedBase64 = signature.clone().into();
248 self.client
249 .get(&format!(
250 "{LEGACY_BUILDER_MODULE}/claimblock/{block_hash}/{view_number}/{sender}/{encoded_signature}"
251 ))
252 .send()
253 .await
254 .map_err(Into::into)
255 }
256
257 pub async fn claim_block_with_num_nodes(
264 &self,
265 block_hash: BuilderCommitment,
266 view_number: u64,
267 sender: TYPES::SignatureKey,
268 signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
269 num_nodes: usize,
270 ) -> Result<AvailableBlockData<TYPES>, BuilderClientError> {
271 let encoded_signature: TaggedBase64 = signature.clone().into();
272 self.client
273 .get(&format!(
274 "{LEGACY_BUILDER_MODULE}/claimblockwithnumnodes/{block_hash}/{view_number}/{sender}/{encoded_signature}/{num_nodes}"
275 ))
276 .send()
277 .await
278 .map_err(Into::into)
279 }
280 }
281}
282
283pub mod v0_2 {
285 use vbs::version::StaticVersion;
286
287 pub use super::v0_1::*;
288
289 pub type Version = StaticVersion<0, 2>;
291}