nautilus_blockchain/execution/
preflight.rs1use alloy::primitives::{Address, U256};
19use nautilus_model::identifiers::InstrumentId;
20
21#[derive(Debug, Clone)]
23pub struct ContractCodeCheck {
24 pub address: Address,
26 pub has_deployed_code: bool,
28}
29
30#[derive(Debug, Clone)]
32pub struct PoolPreflightCheck {
33 pub instrument_id: InstrumentId,
35 pub address: Address,
37 pub has_deployed_code: bool,
39 pub fee: Option<u32>,
41 pub base_token: Address,
43 pub quote_token: Address,
45}
46
47#[derive(Debug, Clone)]
49pub struct TokenPreflightCheck {
50 pub address: Address,
52 pub symbol: String,
54 pub has_deployed_code: bool,
56 pub wallet_balance: U256,
58 pub router_allowances: Vec<(Address, U256)>,
60}
61
62#[derive(Debug, Clone)]
66pub struct BlockchainPreflightReport {
67 pub expected_chain_id: u64,
69 pub actual_chain_id: u64,
71 pub chain_id_matches: bool,
73 pub pool: PoolPreflightCheck,
75 pub routers: Vec<ContractCodeCheck>,
77 pub tokens: Vec<TokenPreflightCheck>,
79 pub native_balance_wei: U256,
81 pub base_fee_per_gas_wei: u128,
83 pub max_priority_fee_per_gas_wei: u128,
85 pub derived_max_fee_per_gas_wei: u128,
87 pub fee_within_ceiling: bool,
89 pub ready: bool,
91 pub issues: Vec<String>,
93}
94
95impl BlockchainPreflightReport {
96 #[must_use]
98 #[expect(clippy::too_many_arguments)]
99 pub fn new(
100 expected_chain_id: u64,
101 actual_chain_id: u64,
102 pool: PoolPreflightCheck,
103 routers: Vec<ContractCodeCheck>,
104 tokens: Vec<TokenPreflightCheck>,
105 native_balance_wei: U256,
106 base_fee_per_gas_wei: u128,
107 max_priority_fee_per_gas_wei: u128,
108 derived_max_fee_per_gas_wei: u128,
109 max_fee_per_gas_wei: u128,
110 ) -> Self {
111 let chain_id_matches = expected_chain_id == actual_chain_id;
112 let fee_within_ceiling = derived_max_fee_per_gas_wei <= max_fee_per_gas_wei;
113
114 let mut issues = Vec::new();
115
116 if !chain_id_matches {
117 issues.push(format!(
118 "Chain ID mismatch: expected {expected_chain_id}, node reported {actual_chain_id}"
119 ));
120 }
121
122 if !pool.has_deployed_code {
123 issues.push(format!(
124 "No deployed bytecode at pool address {}",
125 pool.address
126 ));
127 }
128
129 if pool.fee.is_none() {
130 issues.push("Pool fee tier is missing".to_string());
131 }
132
133 for router in &routers {
134 if !router.has_deployed_code {
135 issues.push(format!(
136 "No deployed bytecode at router address {}",
137 router.address
138 ));
139 }
140 }
141
142 for token in &tokens {
143 if !token.has_deployed_code {
144 issues.push(format!(
145 "No deployed bytecode at {} token address {}",
146 token.symbol, token.address
147 ));
148 }
149 }
150
151 if native_balance_wei.is_zero() {
152 issues.push("Native currency balance is zero; cannot pay gas".to_string());
153 }
154
155 let base_token_check = tokens.iter().find(|t| t.address == pool.base_token);
156 let input_balance_ok = base_token_check.is_some_and(|t| !t.wallet_balance.is_zero());
157
158 if !input_balance_ok {
159 issues.push(format!("Input token {} balance is zero", pool.base_token));
160 }
161
162 let execution_router = routers.first().map(|router| router.address);
163 let allowance_ok = base_token_check.is_some_and(|token| {
164 token
165 .router_allowances
166 .iter()
167 .any(|(router, amount)| Some(*router) == execution_router && !amount.is_zero())
168 });
169
170 if !allowance_ok {
171 issues.push(format!(
172 "No router allowance for input token {}",
173 pool.base_token
174 ));
175 }
176
177 if !fee_within_ceiling {
178 issues.push(format!(
179 "Derived max fee per gas {derived_max_fee_per_gas_wei} wei exceeds ceiling {max_fee_per_gas_wei} wei"
180 ));
181 }
182
183 let ready = issues.is_empty();
184
185 Self {
186 expected_chain_id,
187 actual_chain_id,
188 chain_id_matches,
189 pool,
190 routers,
191 tokens,
192 native_balance_wei,
193 base_fee_per_gas_wei,
194 max_priority_fee_per_gas_wei,
195 derived_max_fee_per_gas_wei,
196 fee_within_ceiling,
197 ready,
198 issues,
199 }
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use alloy::primitives::{U256, address};
206 use rstest::rstest;
207
208 use super::*;
209
210 #[rstest]
211 fn report_requires_allowance_on_execution_router() {
212 let base_token = address!("1111111111111111111111111111111111111111");
213 let quote_token = address!("2222222222222222222222222222222222222222");
214 let execution_router = address!("3333333333333333333333333333333333333333");
215 let alternate_router = address!("4444444444444444444444444444444444444444");
216 let report = BlockchainPreflightReport::new(
217 42161,
218 42161,
219 PoolPreflightCheck {
220 instrument_id: "0x5555555555555555555555555555555555555555.Arbitrum:UniswapV3"
221 .parse()
222 .unwrap(),
223 address: address!("5555555555555555555555555555555555555555"),
224 has_deployed_code: true,
225 fee: Some(500),
226 base_token,
227 quote_token,
228 },
229 vec![
230 ContractCodeCheck {
231 address: execution_router,
232 has_deployed_code: true,
233 },
234 ContractCodeCheck {
235 address: alternate_router,
236 has_deployed_code: true,
237 },
238 ],
239 vec![
240 TokenPreflightCheck {
241 address: base_token,
242 symbol: "BASE".to_string(),
243 has_deployed_code: true,
244 wallet_balance: U256::from(1),
245 router_allowances: vec![
246 (execution_router, U256::ZERO),
247 (alternate_router, U256::from(1)),
248 ],
249 },
250 TokenPreflightCheck {
251 address: quote_token,
252 symbol: "QUOTE".to_string(),
253 has_deployed_code: true,
254 wallet_balance: U256::ZERO,
255 router_allowances: Vec::new(),
256 },
257 ],
258 U256::from(1),
259 100_000_000,
260 10_000_000,
261 130_000_000,
262 1_000_000_000,
263 );
264
265 assert!(!report.ready);
266 assert_eq!(
267 report.issues,
268 vec![format!("No router allowance for input token {base_token}")]
269 );
270 }
271}