arb_rpc/
conditional_tx.rs1use std::collections::HashMap;
15
16use alloy_primitives::{Address, Bytes, B256};
17use jsonrpsee::{
18 core::RpcResult,
19 proc_macros::rpc,
20 types::{error::INVALID_PARAMS_CODE, ErrorObject},
21};
22use serde::{Deserialize, Serialize};
23
24#[derive(Debug, Clone, Serialize, Deserialize, Default)]
28#[serde(untagged)]
29pub enum KnownAccountCondition {
30 RootHash(B256),
32 #[serde(rename_all = "camelCase")]
34 SlotValues(HashMap<B256, B256>),
35 #[default]
36 Empty,
37}
38
39#[derive(Debug, Clone, Default, Serialize, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct ConditionalOptions {
43 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
45 pub known_accounts: HashMap<Address, KnownAccountCondition>,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub block_number_min: Option<u64>,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub block_number_max: Option<u64>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub timestamp_min: Option<u64>,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub timestamp_max: Option<u64>,
58}
59
60fn condition_rejected(reason: &str) -> ErrorObject<'static> {
61 ErrorObject::owned(
62 INVALID_PARAMS_CODE,
63 format!("conditional tx rejected: {reason}"),
64 None::<()>,
65 )
66}
67
68pub fn check_simple_predicates(
74 opts: &ConditionalOptions,
75 current_l1_block: u64,
76 current_l2_timestamp: u64,
77) -> Result<(), ErrorObject<'static>> {
78 if let Some(min) = opts.block_number_min {
79 if current_l1_block < min {
80 return Err(condition_rejected("BlockNumberMin condition not met"));
81 }
82 }
83 if let Some(max) = opts.block_number_max {
84 if current_l1_block > max {
85 return Err(condition_rejected("BlockNumberMax condition not met"));
86 }
87 }
88 if let Some(min) = opts.timestamp_min {
89 if current_l2_timestamp < min {
90 return Err(condition_rejected("TimestampMin condition not met"));
91 }
92 }
93 if let Some(max) = opts.timestamp_max {
94 if current_l2_timestamp > max {
95 return Err(condition_rejected("TimestampMax condition not met"));
96 }
97 }
98 Ok(())
99}
100
101#[rpc(server, namespace = "eth")]
105pub trait ConditionalTxApi {
106 #[method(name = "sendRawTransactionConditional")]
110 async fn send_raw_transaction_conditional(
111 &self,
112 raw_tx: Bytes,
113 options: ConditionalOptions,
114 ) -> RpcResult<B256>;
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 fn some_opts() -> ConditionalOptions {
122 ConditionalOptions {
123 block_number_min: Some(100),
124 block_number_max: Some(200),
125 timestamp_min: Some(1_700_000_000),
126 timestamp_max: Some(1_800_000_000),
127 known_accounts: HashMap::new(),
128 }
129 }
130
131 #[test]
132 fn none_all_accepts() {
133 let opts = ConditionalOptions::default();
134 assert!(check_simple_predicates(&opts, 0, 0).is_ok());
135 }
136
137 #[test]
138 fn block_number_min_rejects_below() {
139 let opts = some_opts();
140 let err = check_simple_predicates(&opts, 99, 1_750_000_000).unwrap_err();
141 assert!(err.message().contains("BlockNumberMin"));
142 }
143
144 #[test]
145 fn block_number_max_rejects_above() {
146 let opts = some_opts();
147 let err = check_simple_predicates(&opts, 201, 1_750_000_000).unwrap_err();
148 assert!(err.message().contains("BlockNumberMax"));
149 }
150
151 #[test]
152 fn timestamp_min_rejects_below() {
153 let opts = some_opts();
154 let err = check_simple_predicates(&opts, 150, 1_000).unwrap_err();
155 assert!(err.message().contains("TimestampMin"));
156 }
157
158 #[test]
159 fn timestamp_max_rejects_above() {
160 let opts = some_opts();
161 let err = check_simple_predicates(&opts, 150, 2_000_000_000).unwrap_err();
162 assert!(err.message().contains("TimestampMax"));
163 }
164
165 #[test]
166 fn inside_window_accepts() {
167 let opts = some_opts();
168 assert!(check_simple_predicates(&opts, 150, 1_750_000_000).is_ok());
169 }
170
171 #[test]
172 fn boundary_inclusive() {
173 let opts = some_opts();
174 assert!(check_simple_predicates(&opts, 100, 1_700_000_000).is_ok());
175 assert!(check_simple_predicates(&opts, 200, 1_800_000_000).is_ok());
176 }
177}