Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ You can freely modify the example Order to:
```bash
export CHAIN_NAME=pecorino
export RU_RPC_URL=https://rpc.pecorino.signet.sh/
export HOST_RPC_URL=https://host-rpc.pecorino.signet.sh/
export SIGNER_KEY=[AWS KMS key ID or local private key]
export SIGNER_CHAIN_ID=14174
```

2. **Fund your key**
Expand All @@ -82,6 +82,7 @@ This key acts as **both** the Order Initiator and Filler, and must be funded wit
- Input tokens on the Rollup
- Output tokens on the Host and/or Rollup
- Gas tokens to pay for Rollup transactions
- Gas tokens to pay for Host transactions

By default, the example swaps **1 Rollup WETH Input → 1 Host WETH Output**, but you can edit the Order freely.

Expand Down
20 changes: 14 additions & 6 deletions bin/roundtrip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ async fn main() -> eyre::Result<()> {
let args = OrdersArgs::parse();

// connect signer and provider
let signer = config.signer_config.connect().await?;
let provider = connect_provider(signer.clone(), config.ru_rpc_url.clone()).await?;
let mut signer = config.signer_config.connect().await?;
// ensure signer chain ID is unset so it can be used for Host and Rollup
signer.set_chain_id(None);

let ru_provider = connect_provider(signer.clone(), config.ru_rpc_url.clone()).await?;
let host_provider = connect_provider(signer.clone(), config.host_rpc_url.clone()).await?;
info!(signer_address = %signer.address(), "Connected to Signer and Provider");

// create an example order
Expand All @@ -52,7 +56,7 @@ async fn main() -> eyre::Result<()> {
sleep(Duration::from_secs(1)).await;

// fill the order from the transaction cache
fill_orders(&signed, signer, provider, config).await?;
fill_orders(&signed, signer, ru_provider, host_provider, config).await?;
info!("Bundle sent to tx cache successfully; wait for bundle to mine.");

Ok(())
Expand Down Expand Up @@ -113,15 +117,19 @@ async fn send_order(
}

/// Fill example [`SignedOrder`]s from the transaction cache.
#[instrument(skip(target_order, signer, provider, config), level = "debug")]
#[instrument(
skip(target_order, signer, ru_provider, host_provider, config),
level = "debug"
)]
async fn fill_orders(
target_order: &SignedOrder,
signer: LocalOrAws,
provider: TxSenderProvider,
ru_provider: TxSenderProvider,
host_provider: TxSenderProvider,
config: FillerConfig,
) -> eyre::Result<()> {
info!("filling orders from transaction cache");
let filler = Filler::new(signer, provider, config.constants)?;
let filler = Filler::new(signer, ru_provider, host_provider, config.constants)?;

// get all the [`SignedOrder`]s from tx cache
let mut orders: Vec<SignedOrder> = filler.get_orders().await?;
Expand Down
24 changes: 18 additions & 6 deletions bin/submit_order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,12 @@ async fn main() -> eyre::Result<()> {
sleep_time,
} = OrdersArgs::from_env()?;

let signer = config.signer_config.connect().await?;
let provider = connect_provider(signer.clone(), config.ru_rpc_url.clone()).await?;
let mut signer = config.signer_config.connect().await?;
// ensure signer chain ID is unset so it can be used for Host and Rollup
signer.set_chain_id(None);

let ru_provider = connect_provider(signer.clone(), config.ru_rpc_url.clone()).await?;
let host_provider = connect_provider(signer.clone(), config.host_rpc_url.clone()).await?;
info!(signer_address = %signer.address(), "Connected to Signer and Provider");

loop {
Expand All @@ -54,7 +58,14 @@ async fn main() -> eyre::Result<()> {

sleep(TX_CACHE_WAIT_TIME).await;

fill_orders(&signed, signer.clone(), provider.clone(), &config).await?;
fill_orders(
&signed,
signer.clone(),
ru_provider.clone(),
host_provider.clone(),
&config,
)
.await?;

sleep(Duration::from_millis(sleep_time)).await;
}
Expand Down Expand Up @@ -119,15 +130,16 @@ async fn send_order(
}

/// Fill example [`SignedOrder`]s from the transaction cache.
#[instrument(skip(target_order, signer, provider, config), fields(target_order_signature = %target_order.permit.signature, target_order_owner = %target_order.permit.owner))]
#[instrument(skip(target_order, signer, ru_provider, host_provider, config), fields(target_order_signature = %target_order.permit.signature, target_order_owner = %target_order.permit.owner))]
async fn fill_orders(
target_order: &SignedOrder,
signer: LocalOrAws,
provider: TxSenderProvider,
ru_provider: TxSenderProvider,
host_provider: TxSenderProvider,
config: &FillerConfig,
) -> eyre::Result<()> {
info!("filling orders from transaction cache");
let filler = Filler::new(signer, provider, config.constants.clone())?;
let filler = Filler::new(signer, ru_provider, host_provider, config.constants.clone())?;

// get all the [`SignedOrder`]s from tx cache
let mut orders: Vec<SignedOrder> = filler.get_orders().await?;
Expand Down
117 changes: 88 additions & 29 deletions src/filler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use init4_bin_base::{
};
use signet_bundle::SignetEthBundle;
use signet_constants::SignetConstants;
use signet_tx_cache::{client::TxCache, types::TxCacheSendBundleResponse};
use signet_tx_cache::client::TxCache;
use signet_types::{AggregateOrders, SignedFill, SignedOrder, UnsignedFill};
use std::{collections::HashMap, slice::from_ref};

Expand All @@ -30,9 +30,12 @@ pub struct FillerConfig {
/// The Rollup RPC URL.
#[from_env(var = "RU_RPC_URL", desc = "RPC URL for the Rollup")]
pub ru_rpc_url: String,
/// The signer to use for signing transactions.
/// NOTE: For the example, this key must be funded with USDC on both the Host and Rollup, as well as gas on the Rollup.
/// .env vars: SIGNER_KEY, SIGNER_CHAIN_ID
/// The Host RPC URL.
#[from_env(var = "HOST_RPC_URL", desc = "RPC URL for the Host")]
pub host_rpc_url: String,
/// The signer to use for signing transactions on the Host and Rollup.
/// NOTE: For the example, this key must be funded with gas on both the Host and Rollup, as well as Input/Output tokens for the Orders on the Host/Rollup.
/// .env var: SIGNER_KEY
pub signer_config: LocalOrAwsConfig,
/// The Signet constants.
/// .env var: CHAIN_NAME
Expand All @@ -47,6 +50,8 @@ pub struct Filler<S: Signer> {
signer: S,
/// The provider to use for building transactions on the Rollup.
ru_provider: TxSenderProvider,
/// The provider to use for building transactions on the Host.
host_provider: TxSenderProvider,
/// The transaction cache endpoint.
tx_cache: TxCache,
/// The system constants.
Expand All @@ -61,6 +66,7 @@ where
pub fn new(
signer: S,
ru_provider: TxSenderProvider,
host_provider: TxSenderProvider,
constants: SignetConstants,
) -> Result<Self, Error> {
let tx_cache_url: reqwest::Url = constants.environment().transaction_cache().parse()?;
Expand All @@ -74,6 +80,7 @@ where
Ok(Self {
signer,
ru_provider,
host_provider,
tx_cache: TxCache::new_with_client(tx_cache_url, client),
constants,
})
Expand Down Expand Up @@ -101,8 +108,7 @@ where

// submit one bundle per individual order
for order in orders {
let response = self.fill(from_ref(order)).await?;
debug!(bundle_id = response.id.to_string(), "Bundle sent to cache");
self.fill(from_ref(order)).await?;
}

Ok(())
Expand All @@ -122,7 +128,7 @@ where
/// Filling Orders individually ensures that even if some Orders are not fillable, others may still mine;
/// however, it is less gas efficient.
#[instrument(skip(self, orders))]
pub async fn fill(&self, orders: &[SignedOrder]) -> Result<TxCacheSendBundleResponse, Error> {
pub async fn fill(&self, orders: &[SignedOrder]) -> Result<(), Error> {
info!(orders_count = orders.len(), "Filling orders in bundle");

// if orders is empty, error out
Expand All @@ -131,44 +137,69 @@ where
}

// sign a SignedFill for the orders
let mut signed_fills: HashMap<u64, SignedFill> = self.sign_fills(orders).await?;
let signed_fills: HashMap<u64, SignedFill> = self.sign_fills(orders).await?;
debug!(?signed_fills, "Signed fills for orders");
info!("Successfully signed fills");

// get the transaction requests for the rollup
let tx_requests = self.rollup_txn_requests(&signed_fills, orders).await?;
debug!(?tx_requests, "Transaction requests");
debug!(?tx_requests, "Rollup transaction requests");

// sign & encode the rollup transactions for the Bundle
let txs: Vec<Bytes> = self
.sign_and_encode_txns(&self.ru_provider, tx_requests)
.await?;
debug!(?txs, "Rollup encoded transactions");

// sign & encode the transactions for the Bundle
let txs = self.sign_and_encode_txns(tx_requests).await?;
debug!(?txs, "Encoded transactions");
// get the transaction requests for the host
let host_tx_requests = self.host_txn_requests(&signed_fills).await?;
debug!(?host_tx_requests, "Host transaction requests");

// get the aggregated host fill for the Bundle, if any
let host_fills = signed_fills.remove(&self.constants.host().chain_id());
debug!(?host_fills, "Host fills for bundle");
// sign & encode the host transactions for the Bundle
let host_txs = self
.sign_and_encode_txns(&self.host_provider, host_tx_requests)
.await?;
debug!(?host_txs, "Host encoded transactions");

// set the Bundle to only be valid if mined in the next rollup block
let block_number = self.ru_provider.get_block_number().await? + 1;
let latest_ru_block_number = self.ru_provider.get_block_number().await?;

// send the Bundle to the transaction cache
self.send_bundle(txs.clone(), host_txs.clone(), latest_ru_block_number + 1)
.await?;

Ok(())
}

async fn send_bundle(
&self,
ru_txs: Vec<Bytes>,
host_txs: Vec<Bytes>,
target_ru_block_number: u64,
) -> Result<(), Error> {
// construct a Bundle containing the Rollup transactions and the Host fill (if any)
let bundle = SignetEthBundle {
host_fills,
host_fills: None,
host_txs,
bundle: EthSendBundle {
txs,
reverting_tx_hashes: vec![], // generally, if the Order initiations revert, then fills should not be submitted
block_number,
min_timestamp: None, // sufficiently covered by pinning to next block number
max_timestamp: None, // sufficiently covered by pinning to next block number
replacement_uuid: None, // optional if implementing strategies that replace or cancel bundles
txs: ru_txs,
block_number: target_ru_block_number,
..Default::default()
},
host_txs: vec![],
};
debug!(?bundle, "bundle contents");
info!("forwarding bundle to transaction cache");
info!(
ru_tx_count = bundle.bundle.txs.len(),
host_tx_count = bundle.host_txs.len(),
target_ru_block_number,
"forwarding bundle to transaction cache"
);

// submit the Bundle to the transaction cache
self.tx_cache.forward_bundle(bundle).await
let response = self.tx_cache.forward_bundle(bundle).await?;
debug!(bundle_id = response.id.to_string(), "Bundle sent to cache");

Ok(())
}

/// Aggregate the given orders into a SignedFill, sign it, and
Expand Down Expand Up @@ -243,6 +274,7 @@ where
// Host `fill` transactions are always considered to be mined "before" the rollup block is processed,
// but Rollup `fill` transactions MUST take care to be ordered before the Orders are `initiate`d
if let Some(rollup_fill) = signed_fills.get(&self.constants.rollup().chain_id()) {
debug!(?rollup_fill, "Rollup fill");
// add the fill tx to the rollup txns
let ru_fill_tx = rollup_fill.to_fill_tx(self.constants.rollup().orders());
tx_requests.push(ru_fill_tx);
Expand All @@ -259,11 +291,37 @@ where
Ok(tx_requests)
}

/// Construct a set of transaction requests to be submitted on the host.
///
/// This example only includes one Host transaction,
/// which performs a single, aggregate Fill on the Host chain.
///
/// This is the simplest, minimally viable way to get a set of Orders mined;
/// Fillers may wish to implement more complex strategies.
///
/// For example, Fillers might wish to include swaps on Host AMMs to source liquidity as part of their filling strategy.
#[instrument(skip(self, signed_fills))]
async fn host_txn_requests(
&self,
signed_fills: &HashMap<u64, SignedFill>,
) -> Result<Vec<TransactionRequest>, Error> {
// If there is a SignedFill for the Host, add a transaction to submit the fill
if let Some(host_fill) = signed_fills.get(&self.constants.host().chain_id()) {
debug!(?host_fill, "Host fill");
// add the fill tx to the host txns
let host_fill_tx = host_fill.to_fill_tx(self.constants.host().orders());
Ok(vec![host_fill_tx])
} else {
Ok(vec![])
}
}

/// Given an ordered set of Transaction Requests,
/// Sign them and encode them for inclusion in a Bundle.
#[instrument(skip(self, tx_requests))]
#[instrument(skip(self, provider, tx_requests))]
pub async fn sign_and_encode_txns(
&self,
provider: &TxSenderProvider,
tx_requests: Vec<TransactionRequest>,
) -> Result<Vec<Bytes>, Error> {
let mut encoded_txs: Vec<Bytes> = Vec::new();
Expand All @@ -277,15 +335,16 @@ where
);

// sign the transaction
let SendableTx::Envelope(filled) = self.ru_provider.fill(tx).await? else {
let SendableTx::Envelope(filled) = provider.fill(tx).await? else {
eyre::bail!("Failed to fill transaction")
};

// encode it
let encoded = filled.encoded_2718();
info!(
tx_hash = filled.hash().to_string(),
"Rollup transaction signed and encoded"
chain_id = provider.get_chain_id().await?,
"Transaction signed and encoded"
);

// add to array
Expand Down
Loading