Skip to main content

signet_orders/
fee_policy.rs

1use crate::{BundleSubmitter, FillSubmitter, OrdersAndFills, TxBuilder};
2use alloy::primitives::Address;
3use alloy::{
4    eips::eip2718::Encodable2718,
5    network::{Ethereum, Network, TransactionBuilder},
6    primitives::Bytes,
7    providers::{fillers::FillerControlFlow, SendableTx},
8    rpc::types::mev::EthSendBundle,
9    transports::{RpcError, TransportErrorKind},
10};
11use futures_util::{stream, StreamExt, TryStreamExt};
12use signet_bundle::SignetEthBundle;
13use signet_constants::SignetSystemConstants;
14#[cfg(doc)]
15use signet_types::SignedFill;
16use tracing::{error, instrument};
17
18/// Errors returned by [`FeePolicySubmitter`].
19#[derive(Debug, thiserror::Error)]
20#[non_exhaustive]
21pub enum FeePolicyError {
22    /// No fills provided for submission.
23    #[error("no fills provided for submission")]
24    NoFills,
25    /// RPC call failed.
26    #[error("RPC error: {0}")]
27    Rpc(#[source] RpcError<TransportErrorKind>),
28    /// Transaction is incomplete (missing required properties).
29    #[error("transaction missing required properties: {0:?}")]
30    IncompleteTransaction(Vec<(&'static str, Vec<&'static str>)>),
31    /// Bundle submission failed.
32    #[error("failed to submit bundle: {0}")]
33    Submission(#[source] Box<dyn core::error::Error + Send + Sync>),
34}
35
36impl From<FillerControlFlow> for FeePolicyError {
37    fn from(filler_control_flow: FillerControlFlow) -> Self {
38        match filler_control_flow {
39            FillerControlFlow::Missing(missing) => Self::IncompleteTransaction(missing),
40            FillerControlFlow::Finished | FillerControlFlow::Ready => {
41                error!("fill returned Builder but status is {filler_control_flow:?}");
42                Self::IncompleteTransaction(Vec::new())
43            }
44        }
45    }
46}
47
48/// A [`FillSubmitter`] that wraps a [`BundleSubmitter`] and handles fee policy.
49///
50/// This submitter converts [`SignedFill`]s into transactions with appropriate gas pricing, builds
51/// a [`SignetEthBundle`], and submits via the wrapped submitter.
52///
53/// The providers must be configured with appropriate fillers for gas, nonce, chain ID, and wallet
54/// signing (e.g., via `ProviderBuilder::with_gas_estimation()` and `ProviderBuilder::wallet()`).
55/// Note that the provider's nonce filler must correctly increment nonces across all transactions
56/// built within a single [`FillSubmitter::submit_fills`] call.
57#[derive(Debug, Clone)]
58pub struct FeePolicySubmitter<RuP, HostP, B> {
59    ru_provider: RuP,
60    host_provider: HostP,
61    submitter: B,
62    constants: SignetSystemConstants,
63}
64
65impl<RuP, HostP, B> FeePolicySubmitter<RuP, HostP, B> {
66    /// Create a new `FeePolicySubmitter`.
67    pub const fn new(
68        ru_provider: RuP,
69        host_provider: HostP,
70        submitter: B,
71        constants: SignetSystemConstants,
72    ) -> Self {
73        Self { ru_provider, host_provider, submitter, constants }
74    }
75
76    /// Get a reference to the rollup provider.
77    pub const fn ru_provider(&self) -> &RuP {
78        &self.ru_provider
79    }
80
81    /// Get a reference to the host provider.
82    pub const fn host_provider(&self) -> &HostP {
83        &self.host_provider
84    }
85
86    /// Get a reference to the inner submitter.
87    pub const fn submitter(&self) -> &B {
88        &self.submitter
89    }
90
91    /// Get a reference to the system constants.
92    pub const fn constants(&self) -> &SignetSystemConstants {
93        &self.constants
94    }
95}
96
97impl<RuP, HostP, B> FillSubmitter for FeePolicySubmitter<RuP, HostP, B>
98where
99    RuP: TxBuilder<Ethereum>,
100    HostP: TxBuilder<Ethereum>,
101    B: BundleSubmitter + Send + Sync,
102{
103    type Response = B::Response;
104    type Error = FeePolicyError;
105
106    #[instrument(skip_all, fields(order_count = orders.len(), fill_count = fills.len()))]
107    async fn submit_fills(
108        &self,
109        OrdersAndFills { orders, fills, signer_address }: OrdersAndFills,
110    ) -> Result<Self::Response, Self::Error> {
111        if fills.is_empty() {
112            return Err(FeePolicyError::NoFills);
113        }
114
115        // Build rollup transaction requests: fill (if present, must come first) then initiates
116        let fill_iter = fills
117            .get(&self.constants.ru_chain_id())
118            .map(|fill| fill.to_fill_tx(self.constants.ru_orders()))
119            .into_iter();
120        let order_iter = orders
121            .iter()
122            .map(|order| order.to_initiate_tx(signer_address, self.constants.ru_orders()));
123        let rollup_txs: Vec<Bytes> = stream::iter(fill_iter.chain(order_iter))
124            .then(|tx_request| sign_and_encode_tx(&self.ru_provider, tx_request, signer_address))
125            .try_collect()
126            .await?;
127
128        // Build host transaction request: fill only (if present)
129        let host_txs = match fills.get(&self.constants.host_chain_id()) {
130            Some(fill) => {
131                let tx_request = fill.to_fill_tx(self.constants.host_orders());
132                vec![sign_and_encode_tx(&self.host_provider, tx_request, signer_address).await?]
133            }
134            None => vec![],
135        };
136
137        // NOTE: We could retrieve a header up front, then use number+1. We could also check that
138        // the timestamp in the orders are valid for current.timestamp + calculator.slot_duration.
139        let target_block =
140            self.ru_provider.get_block_number().await.map_err(FeePolicyError::Rpc)? + 1;
141
142        let bundle = SignetEthBundle::new(
143            EthSendBundle { txs: rollup_txs, block_number: target_block, ..Default::default() },
144            host_txs,
145        );
146
147        self.submitter
148            .submit_bundle(bundle)
149            .await
150            .map_err(|error| FeePolicyError::Submission(Box::new(error)))
151    }
152}
153
154/// Sign and encode a transaction request for inclusion in a bundle.
155#[instrument(skip_all)]
156async fn sign_and_encode_tx<N, P>(
157    provider: &P,
158    mut tx_request: N::TransactionRequest,
159    signer_address: Address,
160) -> Result<Bytes, FeePolicyError>
161where
162    N: Network,
163    P: TxBuilder<N>,
164    N::TxEnvelope: Encodable2718,
165{
166    tx_request = tx_request.with_from(signer_address);
167    let sendable = provider.fill(tx_request).await.map_err(FeePolicyError::Rpc)?;
168
169    let envelope = match sendable {
170        SendableTx::Envelope(envelope) => envelope,
171        SendableTx::Builder(tx) => {
172            return Err(FeePolicyError::from(provider.status(&tx)));
173        }
174    };
175
176    Ok(Bytes::from(envelope.encoded_2718()))
177}