Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ default = ["alloy", "rustls"]
alloy = ["dep:alloy"]
aws = ["alloy", "alloy?/signer-aws", "dep:async-trait", "dep:aws-config", "dep:aws-sdk-kms"]
perms = ["dep:oauth2", "dep:tokio", "dep:reqwest", "dep:signet-tx-cache"]
pylon = ["perms", "alloy/kzg"]
block_watcher = ["dep:tokio"]
rustls = ["dep:rustls", "rustls/aws-lc-rs"]

Expand Down
6 changes: 6 additions & 0 deletions src/perms/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ pub mod middleware;
///
/// [`BuilderTxCache`]: tx_cache::BuilderTxCache
pub mod tx_cache;

/// Contains [`PylonClient`] for interacting with the Pylon blob server API.
///
/// [`PylonClient`]: pylon::PylonClient
#[cfg(feature = "pylon")]
pub mod pylon;
143 changes: 143 additions & 0 deletions src/perms/pylon.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
use crate::perms::oauth::SharedToken;
use alloy::{consensus::BlobTransactionSidecarEip7594, primitives::B256};
use thiserror::Error;
use tracing::instrument;

/// Errors that can occur when interacting with the Pylon API.
#[derive(Debug, Error)]
pub enum PylonError {
/// Invalid sidecar format (400).
#[error("invalid sidecar: {0}")]
InvalidSidecar(String),

/// Sidecar already exists for this transaction hash (409).
#[error("sidecar already exists")]
SidecarAlreadyExists,

/// Internal server error (500).
#[error("internal server error: {0}")]
InternalError(String),

/// Request error.
#[error("request error: {0}")]
Request(#[from] reqwest::Error),

/// URL parse error.
#[error("URL parse error: {0}")]
UrlParse(#[from] url::ParseError),

/// Missing auth token.
#[error("missing auth token")]
MissingAuthToken(tokio::sync::watch::error::RecvError),
}

/// A client for interacting with the Pylon blob server API.
#[derive(Debug, Clone)]
pub struct PylonClient {
/// The reqwest client.
client: reqwest::Client,
/// The base URL of the Pylon server.
url: reqwest::Url,
/// The shared token for authentication.
token: SharedToken,
}

impl PylonClient {
/// Instantiate with the given URL and shared token.
pub fn new(url: reqwest::Url, token: SharedToken) -> Self {
Self {
client: reqwest::Client::new(),
url,
token,
}
}

/// Instantiate from a string URL and shared token.
pub fn new_from_string(url: &str, token: SharedToken) -> Result<Self, PylonError> {
let url = url.parse()?;
Ok(Self::new(url, token))
}

/// Instantiate with a custom reqwest client.
pub const fn new_with_client(
url: reqwest::Url,
client: reqwest::Client,
token: SharedToken,
) -> Self {
Self { client, url, token }
}

/// Get a reference to the base URL.
pub const fn url(&self) -> &reqwest::Url {
&self.url
}

/// Get a reference to the reqwest client.
pub const fn client(&self) -> &reqwest::Client {
&self.client
}

/// Get a reference to the shared token.
pub const fn token(&self) -> &SharedToken {
&self.token
}

/// Post a blob transaction sidecar to the Pylon server.
///
/// If the sidecar is in EIP-4844 format, it will be converted to EIP-7594
/// format before posting.
///
/// # Arguments
///
/// * `tx_hash` - The transaction hash ([`B256`]).
/// * `sidecar` - The blob transaction sidecar ([`BlobTransactionSidecarEip7594`]).
///
/// # Errors
///
/// Returns an error if:
/// - The sidecar format is invalid ([`PylonError::InvalidSidecar`])
/// - A sidecar already exists for this transaction hash ([`PylonError::SidecarAlreadyExists`])
/// - An internal server error occurred ([`PylonError::InternalError`])
/// - A network error occurred ([`PylonError::Request`])
///
/// [`B256`]: <https://docs.rs/alloy/latest/alloy/primitives/aliases/type.B256.html>
/// [`BlobTransactionSidecarEip7594`]: <https://docs.rs/alloy/latest/alloy/consensus/struct.BlobTransactionSidecarEip7594.html>
#[instrument(skip_all)]
pub async fn post_sidecar(
&self,
tx_hash: B256,
sidecar: BlobTransactionSidecarEip7594,
) -> Result<(), PylonError> {
let url = self.url.join(&format!("v2/sidecar/{tx_hash}"))?;
let secret = self
.token
.secret()
.await
.map_err(PylonError::MissingAuthToken)?;

let response = self
.client
.post(url)
.json(&sidecar)
.bearer_auth(secret)
.send()
.await?;

match response.status() {
reqwest::StatusCode::OK => Ok(()),
reqwest::StatusCode::BAD_REQUEST => {
let text = response.text().await.unwrap_or_default();
Err(PylonError::InvalidSidecar(text))
}
reqwest::StatusCode::CONFLICT => Err(PylonError::SidecarAlreadyExists),
reqwest::StatusCode::INTERNAL_SERVER_ERROR => {
let text = response.text().await.unwrap_or_default();
Err(PylonError::InternalError(text))
}
_ => {
response.error_for_status()?;
Ok(())
}
}
}
}