use std::time::{Duration, Instant};
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 surf_disco::{client::HealthStatus, Client, Url};
use tagged_base64::TaggedBase64;
use thiserror::Error;
use tokio::time::sleep;
use vbs::version::StaticVersionType;
#[derive(Debug, Error, Serialize, Deserialize)]
pub enum BuilderClientError {
#[error("Requested block not found")]
BlockNotFound,
#[error("Requested block was missing")]
BlockMissing,
#[error("Builder API error: {0}")]
Api(String),
}
impl From<BuilderApiError> for BuilderClientError {
fn from(value: BuilderApiError) -> Self {
match value {
BuilderApiError::Request(source) | BuilderApiError::TxnUnpack(source) => {
Self::Api(source.to_string())
}
BuilderApiError::TxnSubmit(source) | BuilderApiError::BuilderAddress(source) => {
Self::Api(source.to_string())
}
BuilderApiError::Custom { message, .. } => Self::Api(message),
BuilderApiError::BlockAvailable { source, .. }
| BuilderApiError::BlockClaim { source, .. } => match source {
BuildError::NotFound => Self::BlockNotFound,
BuildError::Missing => Self::BlockMissing,
BuildError::Error(message) => Self::Api(message),
},
BuilderApiError::TxnStat(source) => Self::Api(source.to_string()),
}
}
}
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;
}
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 async fn claim_block_with_num_nodes(
&self,
block_hash: BuilderCommitment,
view_number: u64,
sender: TYPES::SignatureKey,
signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
num_nodes: usize,
) -> Result<AvailableBlockData<TYPES>, BuilderClientError> {
let encoded_signature: TaggedBase64 = signature.clone().into();
self.client
.get(&format!(
"{LEGACY_BUILDER_MODULE}/claimblockwithnumnodes/{block_hash}/{view_number}/{sender}/{encoded_signature}/{num_nodes}"
))
.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_99 {
pub use hotshot_builder_api::v0_99::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, 99>>;
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)
}
}
}