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