Skip to content

Commit 720bcdd

Browse files
committed
feat: example code for Fillers
1 parent 69a7ef3 commit 720bcdd

File tree

2 files changed

+292
-0
lines changed

2 files changed

+292
-0
lines changed

crates/rpc/examples/filler.rs

+272
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
use alloy::{
2+
eips::Encodable2718,
3+
network::{Ethereum, EthereumWallet, TransactionBuilder},
4+
primitives::{Address, Bytes},
5+
providers::{
6+
fillers::{
7+
BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller,
8+
WalletFiller,
9+
},
10+
Identity, Provider as _, RootProvider, SendableTx,
11+
},
12+
rpc::types::{mev::EthSendBundle, TransactionRequest},
13+
signers::Signer,
14+
};
15+
use eyre::{eyre, Error};
16+
use signet_bundle::SignetEthBundle;
17+
use signet_rpc::TxCache;
18+
use signet_types::{AggregateOrders, SignedFill, SignedOrder, UnsignedFill};
19+
use std::collections::HashMap;
20+
21+
/// Multiplier for converting gwei to wei.
22+
const GWEI_TO_WEI: u64 = 1_000_000_000;
23+
24+
/// Type alias for the provider used to build and submit transactions to the rollup and host.
25+
type Provider = FillProvider<
26+
JoinFill<
27+
JoinFill<
28+
Identity,
29+
JoinFill<GasFiller, JoinFill<BlobGasFiller, JoinFill<NonceFiller, ChainIdFiller>>>,
30+
>,
31+
WalletFiller<EthereumWallet>,
32+
>,
33+
RootProvider,
34+
Ethereum,
35+
>;
36+
37+
/// Example code demonstrating API usage and patterns for Signet Fillers.
38+
#[derive(Debug)]
39+
pub struct Filler<S: Signer> {
40+
/// The signer to use for signing transactions.
41+
signer: S,
42+
/// The provider to use for building transactions on the Rollup.
43+
ru_provider: Provider,
44+
/// The transaction cache endpoint.
45+
tx_cache: TxCache,
46+
/// A HashMap of the Order contract addresses for each chain.
47+
/// MUST contain an address for both Host and Rollup.
48+
order_contracts: HashMap<u64, Address>,
49+
/// The chain id of the rollup.
50+
ru_chain_id: u64,
51+
/// The chain id of the host.
52+
host_chain_id: u64,
53+
}
54+
55+
impl<S> Filler<S>
56+
where
57+
S: Signer,
58+
{
59+
/// Create a new Filler with the given signer, provider, and transaction cache endpoint.
60+
pub const fn new(
61+
signer: S,
62+
ru_provider: Provider,
63+
tx_cache: TxCache,
64+
order_contracts: HashMap<u64, Address>,
65+
ru_chain_id: u64,
66+
host_chain_id: u64,
67+
) -> Self {
68+
Self { signer, ru_provider, tx_cache, order_contracts, ru_chain_id, host_chain_id }
69+
}
70+
71+
/// Fills Orders by aggregating them into a single, atomic Bundle.
72+
///
73+
/// Filling orders in aggregate means that Fills are batched and more gas efficient;
74+
/// however, if a single Order cannot be filled, then the entire Bundle will not mine.
75+
///
76+
/// For example, using this strategy, if one Order is filled by another Filler first, then all other Orders will also not be filled.
77+
///
78+
/// It may be a preferred strategy to fill each Order in a separate Bundle, as in `fill_individual`.
79+
pub async fn fill_aggregate(&self) -> Result<(), Error> {
80+
let fillable_orders = self.get_fillable_orders().await?;
81+
82+
// submit one bundle that fills the entire set of orders
83+
self.fill(fillable_orders).await
84+
}
85+
86+
/// Fills Orders individually, by submitting a separate Bundle for each Order.
87+
///
88+
/// Filling Orders individually ensures that even if some Orders are not fillable, others may still mine;
89+
/// however, it is less gas efficient.
90+
///
91+
/// It may be a preferred strategy to fill Orders within a single Bundle, as in `fill_aggregate`.
92+
pub async fn fill_individual(&self) -> Result<(), Error> {
93+
let fillable_orders = self.get_fillable_orders().await?;
94+
95+
// submit one bundle per individual order
96+
for order in fillable_orders {
97+
self.fill(vec![order]).await?;
98+
}
99+
100+
Ok(())
101+
}
102+
103+
/// Query the transaction cache to get all possible orders.
104+
pub async fn get_orders(&self) -> Result<Vec<SignedOrder>, Error> {
105+
self.tx_cache.get_orders().await
106+
}
107+
108+
/// Query the transaction cache to get all possible orders and filter them down based on the provided logic.
109+
///
110+
/// This is a simple, naive way of filtering the orders down to those to attempt to fill.
111+
/// Fillers may implement more complex business logic that creates bespoke groupings of Orders.
112+
pub async fn get_fillable_orders(&self) -> Result<Vec<SignedOrder>, Error> {
113+
let all_orders = self.get_orders().await?;
114+
115+
// filter the SignedOrders based on the provided function
116+
self.filter_orders(all_orders).await
117+
}
118+
119+
/// Fillers should implement bespoke business logic to filter orders
120+
/// down to those they are capable of filling & desire to fill.
121+
async fn filter_orders(&self, _orders: Vec<SignedOrder>) -> Result<Vec<SignedOrder>, Error> {
122+
todo!()
123+
}
124+
125+
/// Construct a Bundle to fill the selected set of orders.
126+
pub async fn fill(&self, orders: Vec<SignedOrder>) -> Result<(), Error> {
127+
// if orders is empty, exit the function without doing anything
128+
if orders.is_empty() {
129+
println!("No orders to fill");
130+
return Ok(());
131+
}
132+
133+
// sign a SignedFill for the orders
134+
let signed_fills = self.sign_fills(orders.clone()).await?;
135+
136+
// get the transaction requests for the rollup
137+
let tx_requests = self.rollup_txn_requests(&signed_fills, &orders).await?;
138+
139+
// sign & encode the transactions for the Bundle
140+
let txs = self.sign_and_encode_txns(tx_requests).await?;
141+
142+
// get the aggregated host fill for the Bundle, if any
143+
let host_fills = signed_fills.get(&self.host_chain_id).cloned();
144+
145+
// set the Bundle to only be valid if mined in the next rollup block
146+
let block_number = self.ru_provider.get_block_number().await? + 1;
147+
148+
// construct a Bundle containing the Rollup transactions and the Host fill (if any)
149+
let bundle = SignetEthBundle {
150+
host_fills,
151+
bundle: EthSendBundle {
152+
txs,
153+
reverting_tx_hashes: vec![], // generally, if the Order initiations revert, then fills should not be submitted
154+
block_number,
155+
min_timestamp: None, // sufficiently covered by pinning to next block number
156+
max_timestamp: None, // sufficiently covered by pinning to next block number
157+
replacement_uuid: None, // optional if implementing strategies that replace or cancel bundles
158+
},
159+
};
160+
161+
// submit the Bundle to the transaction cache
162+
self.tx_cache.forward_bundle(bundle).await
163+
}
164+
165+
/// Aggregate the given orders into a SignedFill, sign it, and
166+
/// return a HashMap of SignedFills for each destination chain.
167+
///
168+
/// This is the simplest, minimally viable way to turn a set of SignedOrders into a single Aggregated Fill on each chain;
169+
/// Fillers may wish to implement more complex setups.
170+
///
171+
/// For example, if utilizing different signers for each chain, they may use `UnsignedFill.sign_for(chain_id)` instead of `sign()`.
172+
///
173+
/// If filling multiple Orders, they may wish to utilize one Order's Outputs to provide another Order's rollup Inputs.
174+
/// In this case, the Filler would wish to split up the Fills for each Order,
175+
/// rather than signing a single, aggregate a Fill for each chain, as is done here.
176+
pub async fn sign_fills(
177+
&self,
178+
orders: Vec<SignedOrder>,
179+
) -> Result<HashMap<u64, SignedFill>, Error> {
180+
// create an AggregateOrder from the SignedOrders they want to fill
181+
let agg = AggregateOrders::from(orders);
182+
// produce an UnsignedFill from the AggregateOrder
183+
let mut unsigned_fill = UnsignedFill::from(&agg);
184+
// populate the Order contract addresses for each chain
185+
for chain_id in agg.destination_chain_ids() {
186+
unsigned_fill =
187+
unsigned_fill.with_chain(chain_id, self.order_contract_address_for(chain_id)?);
188+
}
189+
// sign the UnsignedFill, producing a SignedFill for each target chain
190+
Ok(unsigned_fill.sign(&self.signer).await?)
191+
}
192+
193+
/// Construct a set of transaction requests to be submitted on the rollup.
194+
///
195+
/// Perform a single, aggregate Fill upfront, then Initiate each Order.
196+
/// Transaction requests look like [`fill_aggregate`, `initiate_1`, `initiate_2`].
197+
///
198+
/// This is the simplest, minimally viable way to get a set of Orders mined;
199+
/// Fillers may wish to implement more complex strategies.
200+
///
201+
/// For example, Fillers might utilize one Order's Inputs to fill subsequent Orders' Outputs.
202+
/// In this case, the rollup transactions should look like [`fill_1`, `inititate_1`, `fill_2`, `initiate_2`].
203+
async fn rollup_txn_requests(
204+
&self,
205+
signed_fills: &HashMap<u64, SignedFill>,
206+
orders: &Vec<SignedOrder>,
207+
) -> Result<Vec<TransactionRequest>, Error> {
208+
// construct the transactions to be submitted to the Rollup
209+
let mut tx_requests = Vec::new();
210+
211+
// first, if there is a SignedFill for the Rollup, add a transaction to submit the fill
212+
// Note that `fill` transactions MUST be mined *before* the corresponding Order(s) `initiate` transactions in order to cound
213+
// Host `fill` transactions are always considered to be mined "before" the rollup block is processed,
214+
// but Rollup `fill` transactions MUST take care to be ordered before the Orders are `initiate`d
215+
if let Some(rollup_fill) = signed_fills.get(&self.ru_chain_id) {
216+
// add the fill tx to the rollup txns
217+
let ru_fill_tx = rollup_fill.to_fill_tx(self.ru_order_contract()?);
218+
tx_requests.push(ru_fill_tx);
219+
}
220+
221+
// next, add a transaction to initiate each SignedOrder
222+
for signed_order in orders {
223+
// add the initiate tx to the rollup txns
224+
let ru_initiate_tx =
225+
signed_order.to_initiate_tx(self.signer.address(), self.ru_order_contract()?);
226+
tx_requests.push(ru_initiate_tx);
227+
}
228+
229+
Ok(tx_requests)
230+
}
231+
232+
/// Given an ordered set of Transaction Requests,
233+
/// Sign them and encode them for inclusion in a Bundle.
234+
pub async fn sign_and_encode_txns(
235+
&self,
236+
tx_requests: Vec<TransactionRequest>,
237+
) -> Result<Vec<Bytes>, Error> {
238+
let mut encoded_txs: Vec<Bytes> = Vec::new();
239+
for mut tx in tx_requests {
240+
// fill out the transaction fields
241+
tx = tx
242+
.with_from(self.signer.address())
243+
.with_gas_limit(1_000_000)
244+
.with_max_priority_fee_per_gas((GWEI_TO_WEI * 16) as u128);
245+
246+
// sign the transaction
247+
let SendableTx::Envelope(filled) = self.ru_provider.fill(tx).await? else {
248+
return Err(eyre!("Failed to fill rollup transaction"));
249+
};
250+
251+
// encode it
252+
let encoded = filled.encoded_2718();
253+
254+
// add to array
255+
encoded_txs.push(Bytes::from(encoded));
256+
}
257+
Ok(encoded_txs)
258+
}
259+
260+
/// Get the Order contract address for the given chain id.
261+
fn order_contract_address_for(&self, chain_id: u64) -> Result<Address, Error> {
262+
self.order_contracts
263+
.get(&chain_id)
264+
.cloned()
265+
.ok_or(eyre!("No Order contract address configured for chain id {}", chain_id))
266+
}
267+
268+
/// Get the Order contract address for the rollup.
269+
fn ru_order_contract(&self) -> Result<Address, Error> {
270+
self.order_contract_address_for(self.ru_chain_id)
271+
}
272+
}

crates/types/src/agg/order.rs

+20
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,26 @@ impl<'a> FromIterator<&'a RollupOrders::Order> for AggregateOrders {
131131
}
132132
}
133133

134+
impl From<Vec<RollupOrders::Order>> for AggregateOrders {
135+
fn from(orders: Vec<RollupOrders::Order>) -> Self {
136+
let mut agg = AggregateOrders::new();
137+
for order in orders {
138+
agg.ingest(&order);
139+
}
140+
agg
141+
}
142+
}
143+
144+
impl From<Vec<SignedOrder>> for AggregateOrders {
145+
fn from(orders: Vec<SignedOrder>) -> Self {
146+
let mut agg = AggregateOrders::new();
147+
for order in orders {
148+
agg.ingest_signed(&order);
149+
}
150+
agg
151+
}
152+
}
153+
134154
impl<'a> FromIterator<&'a SignedOrder> for AggregateOrders {
135155
fn from_iter<T: IntoIterator<Item = &'a SignedOrder>>(iter: T) -> Self {
136156
let mut orders = AggregateOrders::new();

0 commit comments

Comments
 (0)