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
2 changes: 2 additions & 0 deletions crates/orders/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@ signet-types.workspace = true
signet-zenith.workspace = true

alloy.workspace = true
chrono.workspace = true
futures-util.workspace = true
thiserror.workspace = true
tracing.workspace = true
177 changes: 177 additions & 0 deletions crates/orders/src/fee_policy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
use crate::{BundleSubmitter, FillSubmitter, OrdersAndFills, TxBuilder};
use alloy::primitives::Address;
use alloy::{
eips::eip2718::Encodable2718,
network::{Ethereum, Network, TransactionBuilder},
primitives::Bytes,
providers::{fillers::FillerControlFlow, SendableTx},
rpc::types::mev::EthSendBundle,
transports::{RpcError, TransportErrorKind},
};
use futures_util::{stream, StreamExt, TryStreamExt};
use signet_bundle::SignetEthBundle;
use signet_constants::SignetSystemConstants;
#[cfg(doc)]
use signet_types::SignedFill;
use tracing::{error, instrument};

/// Errors returned by [`FeePolicySubmitter`].
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum FeePolicyError {
/// No fills provided for submission.
#[error("no fills provided for submission")]
NoFills,
/// RPC call failed.
#[error("RPC error: {0}")]
Rpc(#[source] RpcError<TransportErrorKind>),
/// Transaction is incomplete (missing required properties).
#[error("transaction missing required properties: {0:?}")]
IncompleteTransaction(Vec<(&'static str, Vec<&'static str>)>),
/// Bundle submission failed.
#[error("failed to submit bundle: {0}")]
Submission(#[source] Box<dyn core::error::Error + Send + Sync>),
}

impl From<FillerControlFlow> for FeePolicyError {
fn from(filler_control_flow: FillerControlFlow) -> Self {
match filler_control_flow {
FillerControlFlow::Missing(missing) => Self::IncompleteTransaction(missing),
FillerControlFlow::Finished | FillerControlFlow::Ready => {
error!("fill returned Builder but status is {filler_control_flow:?}");
Self::IncompleteTransaction(Vec::new())
}
}
}
}

/// A [`FillSubmitter`] that wraps a [`BundleSubmitter`] and handles fee policy.
///
/// This submitter converts [`SignedFill`]s into transactions with appropriate gas pricing, builds
/// a [`SignetEthBundle`], and submits via the wrapped submitter.
///
/// The providers must be configured with appropriate fillers for gas, nonce, chain ID, and wallet
/// signing (e.g., via `ProviderBuilder::with_gas_estimation()` and `ProviderBuilder::wallet()`).
/// Note that the provider's nonce filler must correctly increment nonces across all transactions
/// built within a single [`FillSubmitter::submit_fills`] call.
#[derive(Debug, Clone)]
pub struct FeePolicySubmitter<RuP, HostP, B> {
ru_provider: RuP,
host_provider: HostP,
submitter: B,
constants: SignetSystemConstants,
}

impl<RuP, HostP, B> FeePolicySubmitter<RuP, HostP, B> {
/// Create a new `FeePolicySubmitter`.
pub const fn new(
ru_provider: RuP,
host_provider: HostP,
submitter: B,
constants: SignetSystemConstants,
) -> Self {
Self { ru_provider, host_provider, submitter, constants }
}

/// Get a reference to the rollup provider.
pub const fn ru_provider(&self) -> &RuP {
&self.ru_provider
}

/// Get a reference to the host provider.
pub const fn host_provider(&self) -> &HostP {
&self.host_provider
}

/// Get a reference to the inner submitter.
pub const fn submitter(&self) -> &B {
&self.submitter
}

/// Get a reference to the system constants.
pub const fn constants(&self) -> &SignetSystemConstants {
&self.constants
}
}

impl<RuP, HostP, B> FillSubmitter for FeePolicySubmitter<RuP, HostP, B>
where
RuP: TxBuilder<Ethereum>,
HostP: TxBuilder<Ethereum>,
B: BundleSubmitter + Send + Sync,
{
type Response = B::Response;
type Error = FeePolicyError;

#[instrument(skip_all, fields(order_count = orders.len(), fill_count = fills.len()))]
async fn submit_fills(
&self,
OrdersAndFills { orders, fills, signer_address }: OrdersAndFills,
) -> Result<Self::Response, Self::Error> {
if fills.is_empty() {
return Err(FeePolicyError::NoFills);
}

// Build rollup transaction requests: fill (if present, must come first) then initiates
let fill_iter = fills
.get(&self.constants.ru_chain_id())
.map(|fill| fill.to_fill_tx(self.constants.ru_orders()))
.into_iter();
let order_iter = orders
.iter()
.map(|order| order.to_initiate_tx(signer_address, self.constants.ru_orders()));
let rollup_txs: Vec<Bytes> = stream::iter(fill_iter.chain(order_iter))
.then(|tx_request| sign_and_encode_tx(&self.ru_provider, tx_request, signer_address))
.try_collect()
.await?;

// Build host transaction request: fill only (if present)
let host_txs = match fills.get(&self.constants.host_chain_id()) {
Some(fill) => {
let tx_request = fill.to_fill_tx(self.constants.host_orders());
vec![sign_and_encode_tx(&self.host_provider, tx_request, signer_address).await?]
}
None => vec![],
};

// NOTE: We could retrieve a header up front, then use number+1. We could also check that
// the timestamp in the orders are valid for current.timestamp + calculator.slot_duration.
let target_block =
self.ru_provider.get_block_number().await.map_err(FeePolicyError::Rpc)? + 1;

let bundle = SignetEthBundle::new(
EthSendBundle { txs: rollup_txs, block_number: target_block, ..Default::default() },
host_txs,
);

self.submitter
.submit_bundle(bundle)
.await
.map_err(|error| FeePolicyError::Submission(Box::new(error)))
}
}

/// Sign and encode a transaction request for inclusion in a bundle.
#[instrument(skip_all)]
async fn sign_and_encode_tx<N, P>(
provider: &P,
mut tx_request: N::TransactionRequest,
signer_address: Address,
) -> Result<Bytes, FeePolicyError>
where
N: Network,
P: TxBuilder<N>,
N::TxEnvelope: Encodable2718,
{
tx_request = tx_request.with_from(signer_address);
let sendable = provider.fill(tx_request).await.map_err(FeePolicyError::Rpc)?;

let envelope = match sendable {
SendableTx::Envelope(envelope) => envelope,
SendableTx::Builder(tx) => {
return Err(FeePolicyError::from(provider.status(&tx)));
}
};

Ok(Bytes::from(envelope.encoded_2718()))
}
187 changes: 187 additions & 0 deletions crates/orders/src/filler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
use crate::{FillSubmitter, OrderSource};
use alloy::{primitives::Address, signers::Signer};
use chrono::Utc;
use futures_util::{Stream, StreamExt};
use signet_constants::SignetSystemConstants;
use signet_types::{SignedFill, SignedOrder, SigningError, UnsignedFill};
use std::collections::HashMap;
use tracing::instrument;

/// Errors returned by [`Filler`].
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum FillerError {
/// Order source error.
#[error("failed to get orders: {0}")]
Source(#[source] Box<dyn core::error::Error + Send + Sync>),
/// No orders to fill.
#[error("no orders to fill")]
NoOrders,
/// Failed to sign fills for orders.
#[error("failed to sign fills: {0}")]
Signing(#[from] SigningError),
/// Fill submission failed.
#[error("failed to submit fills: {0}")]
Submission(#[source] Box<dyn core::error::Error + Send + Sync>),
}

/// Options for configuring the [`Filler`].
#[derive(Debug, Clone, Copy, Default)]
pub struct FillerOptions {
/// Optional deadline offset in seconds for fills.
pub deadline_offset: Option<u64>,
/// Optional nonce to use for permit2 signatures.
pub nonce: Option<u64>,
}

impl FillerOptions {
/// Create a new [`FillerOptions`] with default values.
pub const fn new() -> Self {
Self { deadline_offset: None, nonce: None }
}

/// Set the deadline offset.
pub const fn with_deadline_offset(mut self, offset: u64) -> Self {
self.deadline_offset = Some(offset);
self
}

/// Set the nonce.
pub const fn with_nonce(mut self, nonce: u64) -> Self {
self.nonce = Some(nonce);
self
}
}

/// A small struct to ensure the relevant orders remain paired with the fills generated from them
/// and with the signer's address.
#[derive(Debug)]
pub struct OrdersAndFills {
pub(crate) orders: Vec<SignedOrder>,
pub(crate) fills: HashMap<u64, SignedFill>,
pub(crate) signer_address: Address,
}

/// Fills orders by fetching from a source, signing fills, and submitting them.
///
/// `Filler` is generic over:
/// - `Sign`: A [`Signer`] for signing fills
/// - `Source`: An [`OrderSource`] for fetching orders
/// - `Submit`: A [`FillSubmitter`] for submitting signed fills
#[derive(Debug, Clone)]
pub struct Filler<Sign, Source, Submit> {
signer: Sign,
order_source: Source,
submitter: Submit,
constants: SignetSystemConstants,
options: FillerOptions,
}

impl<Sign, Source, Submit> Filler<Sign, Source, Submit> {
/// Create a new filler instance.
pub const fn new(
signer: Sign,
order_source: Source,
submitter: Submit,
constants: SignetSystemConstants,
options: FillerOptions,
) -> Self {
Self { signer, order_source, submitter, constants, options }
}

/// Get a reference to the signer.
pub const fn signer(&self) -> &Sign {
&self.signer
}

/// Get a reference to the order source.
pub const fn order_source(&self) -> &Source {
&self.order_source
}

/// Get a reference to the submitter.
pub const fn submitter(&self) -> &Submit {
&self.submitter
}

/// Get a reference to the system constants.
pub const fn constants(&self) -> &SignetSystemConstants {
&self.constants
}

/// Get a reference to the filler options.
pub const fn options(&self) -> &FillerOptions {
&self.options
}
}

impl<Sign, Source, Submit> Filler<Sign, Source, Submit>
where
Source: OrderSource + Send + Sync,
{
/// Query the source for signed orders.
pub fn get_orders(
&self,
) -> impl Stream<Item = Result<SignedOrder, FillerError>> + Send + use<'_, Sign, Source, Submit>
{
self.order_source
.get_orders()
.map(|result| result.map_err(|e| FillerError::Source(Box::new(e))))
}
}

impl<Sign, Source, Submit> Filler<Sign, Source, Submit>
where
Sign: Signer + Send + Sync,
{
/// Sign fills for the given orders.
///
/// Returns a map of chain ID to signed fill for each target chain.
pub async fn sign_fills(
&self,
orders: Vec<SignedOrder>,
) -> Result<OrdersAndFills, FillerError> {
let mut unsigned_fill = UnsignedFill::new().with_chain(self.constants.clone());

if let Some(deadline_offset) = self.options.deadline_offset {
let deadline = Utc::now().timestamp() as u64 + deadline_offset;
unsigned_fill = unsigned_fill.with_deadline(deadline);
}

if let Some(nonce) = self.options.nonce {
unsigned_fill = unsigned_fill.with_nonce(nonce);
}

for order in &orders {
unsigned_fill = unsigned_fill.fill(order);
}

let fills = unsigned_fill.sign(&self.signer).await?;
let signer_address = self.signer.address();
Ok(OrdersAndFills { orders, fills, signer_address })
}
}

impl<Sign, Source, Submit> Filler<Sign, Source, Submit>
where
Sign: Signer + Send + Sync,
Submit: FillSubmitter + Send + Sync,
{
/// Fill one or more orders.
///
/// Signs fills for all orders and submits them via the [`FillSubmitter`].
///
/// Returns an error if `orders` is empty, or if signing or submission fails.
#[instrument(skip_all, fields(order_count = orders.len()))]
pub async fn fill(&self, orders: Vec<SignedOrder>) -> Result<Submit::Response, FillerError> {
if orders.is_empty() {
return Err(FillerError::NoOrders);
}

let orders_and_fills = self.sign_fills(orders).await?;
self.submitter
.submit_fills(orders_and_fills)
.await
.map_err(|error| FillerError::Submission(Box::new(error)))
}
}
24 changes: 24 additions & 0 deletions crates/orders/src/impls/fill_provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use crate::TxBuilder;
use alloy::{
network::Network,
providers::{
fillers::{FillProvider, FillerControlFlow, TxFiller},
Provider, SendableTx,
},
transports::TransportResult,
};

impl<F, P, N> TxBuilder<N> for FillProvider<F, P, N>
where
F: TxFiller<N>,
P: Provider<N>,
N: Network,
{
async fn fill(&self, tx: N::TransactionRequest) -> TransportResult<SendableTx<N>> {
FillProvider::fill(self, tx).await
}

fn status(&self, tx: &N::TransactionRequest) -> FillerControlFlow {
self.filler().status(tx)
}
}
Loading