use std::time::{Duration, Instant};
use async_compatibility_layer::art::async_sleep;
use hotshot_builder_api::v0_1::{
block_info::AvailableBlockInfo,
builder::{BuildError, Error as BuilderApiError},
};
use hotshot_types::{
constants::LEGACY_BUILDER_MODULE,
traits::{node_implementation::NodeType, signature_key::SignatureKey},
vid::VidCommitment,
};
use serde::{Deserialize, Serialize};
use snafu::Snafu;
use surf_disco::{client::HealthStatus, Client, Url};
use tagged_base64::TaggedBase64;
use vbs::version::StaticVersionType;
#[derive(Debug, Snafu, Serialize, Deserialize)]
pub enum BuilderClientError {
#[snafu(display("Requested block not found"))]
NotFound,
#[snafu(display("Builder API error: {message}"))]
Api {
message: String,
},
}
impl From<BuilderApiError> for BuilderClientError {
fn from(value: BuilderApiError) -> Self {
match value {
BuilderApiError::Request { source } | BuilderApiError::TxnUnpack { source } => {
Self::Api {
message: source.to_string(),
}
}
BuilderApiError::TxnSubmit { source } | BuilderApiError::BuilderAddress { source } => {
Self::Api {
message: source.to_string(),
}
}
BuilderApiError::Custom { message, .. } => Self::Api { message },
BuilderApiError::BlockAvailable { source, .. }
| BuilderApiError::BlockClaim { source, .. } => match source {
BuildError::NotFound | BuildError::Missing => Self::NotFound,
BuildError::Error { message } => Self::Api { message },
},
}
}
}
pub struct BuilderClient<TYPES: NodeType, Ver: StaticVersionType> {
client: Client<BuilderApiError, Ver>,
_marker: std::marker::PhantomData<TYPES>,
}
impl<TYPES: NodeType, Ver: StaticVersionType> BuilderClient<TYPES, Ver> {
pub fn new(base_url: impl Into<Url>) -> Self {
let url = base_url.into();
Self {
client: Client::builder(url.clone())
.set_timeout(Some(Duration::from_secs(2)))
.build(),
_marker: std::marker::PhantomData,
}
}
pub async fn connect(&self, timeout: Duration) -> bool {
let timeout = Instant::now() + timeout;
let mut backoff = Duration::from_millis(50);
while Instant::now() < timeout {
if matches!(
self.client.healthcheck::<HealthStatus>().await,
Ok(HealthStatus::Available)
) {
return true;
}
async_sleep(backoff).await;
backoff *= 2;
}
false
}
pub async fn available_blocks(
&self,
parent: VidCommitment,
view_number: u64,
sender: TYPES::SignatureKey,
signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
) -> Result<Vec<AvailableBlockInfo<TYPES>>, BuilderClientError> {
let encoded_signature: TaggedBase64 = signature.clone().into();
self.client
.get(&format!(
"{LEGACY_BUILDER_MODULE}/availableblocks/{parent}/{view_number}/{sender}/{encoded_signature}"
))
.send()
.await
.map_err(Into::into)
}
}
pub mod v0_1 {
use hotshot_builder_api::v0_1::block_info::{AvailableBlockData, AvailableBlockHeaderInput};
pub use hotshot_builder_api::v0_1::Version;
use hotshot_types::{
constants::LEGACY_BUILDER_MODULE,
traits::{node_implementation::NodeType, signature_key::SignatureKey},
utils::BuilderCommitment,
};
use tagged_base64::TaggedBase64;
use super::BuilderClientError;
pub type BuilderClient<TYPES> = super::BuilderClient<TYPES, Version>;
impl<TYPES: NodeType> BuilderClient<TYPES> {
pub async fn claim_block_header_input(
&self,
block_hash: BuilderCommitment,
view_number: u64,
sender: TYPES::SignatureKey,
signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
) -> Result<AvailableBlockHeaderInput<TYPES>, BuilderClientError> {
let encoded_signature: TaggedBase64 = signature.clone().into();
self.client
.get(&format!(
"{LEGACY_BUILDER_MODULE}/claimheaderinput/{block_hash}/{view_number}/{sender}/{encoded_signature}"
))
.send()
.await
.map_err(Into::into)
}
pub async fn claim_block(
&self,
block_hash: BuilderCommitment,
view_number: u64,
sender: TYPES::SignatureKey,
signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
) -> Result<AvailableBlockData<TYPES>, BuilderClientError> {
let encoded_signature: TaggedBase64 = signature.clone().into();
self.client
.get(&format!(
"{LEGACY_BUILDER_MODULE}/claimblock/{block_hash}/{view_number}/{sender}/{encoded_signature}"
))
.send()
.await
.map_err(Into::into)
}
}
}
pub mod v0_2 {
use vbs::version::StaticVersion;
pub use super::v0_1::*;
pub type Version = StaticVersion<0, 2>;
}
pub mod v0_3 {
pub use hotshot_builder_api::v0_3::Version;
use hotshot_types::{
bundle::Bundle, constants::MARKETPLACE_BUILDER_MODULE,
traits::node_implementation::NodeType, vid::VidCommitment,
};
use vbs::version::StaticVersion;
pub use super::BuilderClientError;
pub type BuilderClient<TYPES> = super::BuilderClient<TYPES, StaticVersion<0, 3>>;
impl<TYPES: NodeType> BuilderClient<TYPES> {
pub async fn bundle(
&self,
parent_view: u64,
parent_hash: VidCommitment,
view_number: u64,
) -> Result<Bundle<TYPES>, BuilderClientError> {
self.client
.get(&format!(
"{MARKETPLACE_BUILDER_MODULE}/bundle/{parent_view}/{parent_hash}/{view_number}"
))
.send()
.await
.map_err(Into::into)
}
}
}