1use crate::eth::EVMMethod;
4use crate::message::SignedMessage;
5use crate::networks::calibnet::ETH_CHAIN_ID;
6use crate::rpc::eth::EthUint64;
7use crate::rpc::eth::pubsub_trait::SubscriptionKind;
8use crate::rpc::eth::types::*;
9use crate::rpc::types::ApiTipsetKey;
10use crate::rpc::{self, RpcMethod, prelude::*};
11use crate::shim::{address::Address, message::Message};
12
13use anyhow::{Context, ensure};
14use cbor4ii::core::Value;
15use cid::Cid;
16use ethereum_types::H256;
17use futures::{SinkExt, StreamExt};
18use serde_json::json;
19use tokio::time::Duration;
20use tokio_tungstenite::{connect_async, tungstenite::Message as WsMessage};
21use tokio_util::sync::CancellationToken;
22
23use std::io::{self, Write};
24use std::pin::Pin;
25use std::sync::Arc;
26
27type TestRunner = Arc<
28 dyn Fn(Arc<rpc::Client>) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + Send>>
29 + Send
30 + Sync,
31>;
32
33#[derive(Clone)]
34pub struct TestTransaction {
35 pub to: Address,
36 pub from: Address,
37 pub payload: Vec<u8>,
38 pub topic: EthHash,
39}
40
41#[derive(Clone)]
42pub struct RpcTestScenario {
43 pub run: TestRunner,
44 pub name: Option<&'static str>,
45 pub should_fail_with: Option<&'static str>,
46 pub used_methods: Vec<&'static str>,
47 pub ignore: Option<&'static str>,
48}
49
50impl RpcTestScenario {
51 pub fn basic<F, Fut>(run_fn: F) -> Self
53 where
54 F: Fn(Arc<rpc::Client>) -> Fut + Send + Sync + 'static,
55 Fut: Future<Output = anyhow::Result<()>> + Send + 'static,
56 {
57 let run = Arc::new(move |client: Arc<rpc::Client>| {
58 Box::pin(run_fn(client)) as Pin<Box<dyn Future<Output = anyhow::Result<()>> + Send>>
59 });
60 Self {
61 run,
62 name: Default::default(),
63 should_fail_with: Default::default(),
64 used_methods: Default::default(),
65 ignore: None,
66 }
67 }
68
69 fn name(mut self, name: &'static str) -> Self {
70 self.name = Some(name);
71 self
72 }
73
74 pub fn should_fail_with(mut self, msg: &'static str) -> Self {
75 self.should_fail_with = Some(msg);
76 self
77 }
78
79 fn using<const ARITY: usize, M>(mut self) -> Self
80 where
81 M: RpcMethod<ARITY>,
82 {
83 self.used_methods.push(M::NAME);
84 if let Some(alias) = M::NAME_ALIAS {
85 self.used_methods.push(alias);
86 }
87 self
88 }
89
90 fn _ignore(mut self, msg: &'static str) -> Self {
91 self.ignore = Some(msg);
92 self
93 }
94}
95
96pub(super) async fn run_tests(
97 tests: impl IntoIterator<Item = RpcTestScenario> + Clone,
98 client: impl Into<Arc<rpc::Client>>,
99 filter: String,
100) -> anyhow::Result<()> {
101 let client: Arc<rpc::Client> = client.into();
102 let mut passed = 0;
103 let mut failed = 0;
104 let mut ignored = 0;
105 let mut filtered = 0;
106
107 println!("running {} tests", tests.clone().into_iter().count());
108
109 for (i, test) in tests.into_iter().enumerate() {
110 if !filter.is_empty() && !test.used_methods.iter().any(|m| m.starts_with(&filter)) {
111 filtered += 1;
112 continue;
113 }
114 if test.ignore.is_some() {
115 ignored += 1;
116 println!(
117 "test {} ... ignored",
118 if let Some(name) = test.name {
119 name.to_string()
120 } else {
121 format!("#{i}")
122 },
123 );
124 continue;
125 }
126
127 print!(
128 "test {} ... ",
129 if let Some(name) = test.name {
130 name.to_string()
131 } else {
132 format!("#{i}")
133 }
134 );
135
136 io::stdout().flush()?;
137
138 let result = (test.run)(client.clone()).await;
139
140 match result {
141 Ok(_) => {
142 if let Some(expected_msg) = test.should_fail_with {
143 println!("FAILED (expected failure containing '{expected_msg}')");
144 failed += 1;
145 } else {
146 println!("ok");
147 passed += 1;
148 }
149 }
150 Err(e) => {
151 if let Some(expected_msg) = test.should_fail_with {
152 let err_str = format!("{e:#}");
153 if err_str
154 .to_lowercase()
155 .contains(&expected_msg.to_lowercase())
156 {
157 println!("ok");
158 passed += 1;
159 } else {
160 println!("FAILED ({e:#})");
161 failed += 1;
162 }
163 } else {
164 println!("FAILED {e:#}");
165 failed += 1;
166 }
167 }
168 }
169 }
170 let status = if failed == 0 { "ok" } else { "FAILED" };
171 println!(
172 "test result: {status}. {passed} passed; {failed} failed; {ignored} ignored; {filtered} filtered out"
173 );
174 ensure!(failed == 0, "{failed} test(s) failed");
175 Ok(())
176}
177
178type EthSubStream =
180 tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>;
181
182async fn connect_ws(client: &rpc::Client) -> anyhow::Result<EthSubStream> {
185 let mut url = client.base_url().clone();
186 let ws_scheme = match url.scheme() {
187 "http" => "ws",
188 "https" => "wss",
189 scheme => anyhow::bail!("unsupported RPC URL scheme: {scheme}"),
190 };
191 url.set_scheme(ws_scheme)
192 .map_err(|_| anyhow::anyhow!("failed to set scheme"))?;
193 url.set_path("/rpc/v1");
194 let (ws_stream, _) = connect_async(url.as_str()).await?;
195 Ok(ws_stream)
196}
197
198async fn wait_next_epoch(client: &rpc::Client) -> anyhow::Result<()> {
199 let base = client.call(ChainHead::request(())?).await?.epoch();
200 tokio::time::timeout(Duration::from_secs(180), async {
201 loop {
202 tokio::time::sleep(Duration::from_millis(100)).await;
203 if client.call(ChainHead::request(())?).await?.epoch() > base {
204 break Ok(());
205 }
206 }
207 })
208 .await
209 .context("timeout waiting for the next epoch")?
210}
211
212async fn wait_in_mempool(client: &rpc::Client, message_cid: Cid) -> anyhow::Result<()> {
215 let mut retries = 100;
216 loop {
217 let pending = client
218 .call(MpoolPending::request((ApiTipsetKey(None),))?)
219 .await?;
220 if pending.0.iter().any(|msg| msg.cid() == message_cid) {
221 break Ok(());
222 }
223 ensure!(retries != 0, "Message not found in mpool");
224 retries -= 1;
225 tokio::time::sleep(Duration::from_millis(10)).await;
226 }
227}
228
229async fn wait_pending_message(client: &rpc::Client, message_cid: Cid) -> anyhow::Result<()> {
231 let tipset = client.call(ChainHead::request(())?).await?;
232 wait_in_mempool(client, message_cid).await?;
233 client
234 .call(
235 StateWaitMsg::request((message_cid, 1, tipset.epoch(), true))?
236 .with_timeout(Duration::from_secs(300)),
237 )
238 .await?;
239 Ok(())
240}
241
242async fn poll_pending_filter_until(
249 client: &rpc::Client,
250 filter_id: &FilterID,
251 want: &EthHash,
252) -> anyhow::Result<Vec<EthHash>> {
253 let mut seen: Vec<EthHash> = Vec::new();
254 let mut retries = 100;
255 loop {
256 let result = client
257 .call(EthGetFilterChanges::request((filter_id.clone(),))?)
258 .await?;
259 let EthFilterResult::Hashes(hashes) = result else {
260 anyhow::bail!("expected hashes, got {result:?}");
261 };
262 seen.extend(hashes);
263 if seen.contains(want) {
264 break Ok(seen);
265 }
266 ensure!(
267 retries != 0,
268 "filter did not return {want:?} in time; saw {seen:?}"
269 );
270 retries -= 1;
271 tokio::time::sleep(Duration::from_millis(10)).await;
272 }
273}
274
275async fn invoke_contract(client: &rpc::Client, tx: &TestTransaction) -> anyhow::Result<Cid> {
276 let encoded_params = cbor4ii::serde::to_vec(
277 Vec::with_capacity(tx.payload.len()),
278 &Value::Bytes(tx.payload.clone()),
279 )
280 .context("failed to encode params")?;
281 let nonce = client.call(MpoolGetNonce::request((tx.from,))?).await?;
282 let message = Message {
283 to: tx.to,
284 from: tx.from,
285 sequence: nonce,
286 method_num: EVMMethod::InvokeContract as u64,
287 params: encoded_params.into(),
288 ..Default::default()
289 };
290 let unsigned_msg = client
291 .call(GasEstimateMessageGas::request((
292 message,
293 None,
294 ApiTipsetKey(None),
295 ))?)
296 .await?;
297
298 let eth_tx_args = crate::eth::EthEip1559TxArgsBuilder::default()
299 .chain_id(ETH_CHAIN_ID)
300 .unsigned_message(&unsigned_msg)?
301 .build()
302 .map_err(|e| anyhow::anyhow!("Failed to build EIP-1559 transaction: {}", e))?;
303 let eth_tx = crate::eth::EthTx::from(eth_tx_args);
304 let data = eth_tx.rlp_unsigned_message(ETH_CHAIN_ID)?;
305
306 let sig = client.call(WalletSign::request((tx.from, data))?).await?;
307 let smsg = SignedMessage::new_unchecked(unsigned_msg, sig);
308 let cid = smsg.cid();
309
310 client.call(MpoolPush::request((smsg,))?).await?;
311
312 Ok(cid)
313}
314
315async fn open_eth_subscription(
319 client: &rpc::Client,
320 kind: SubscriptionKind,
321 filter: Option<serde_json::Value>,
322) -> anyhow::Result<(EthSubStream, serde_json::Value)> {
323 let mut ws_stream = connect_ws(client).await?;
324
325 let mut params = vec![serde_json::to_value(kind)?];
326 params.extend(filter);
327 let request = json!({
328 "jsonrpc": "2.0",
329 "id": 1,
330 "method": "eth_subscribe",
331 "params": params,
332 });
333 ws_stream
334 .send(WsMessage::Text(request.to_string().into()))
335 .await
336 .context("failed to send eth_subscribe request")?;
337
338 let subscription_id = loop {
340 let msg = match tokio::time::timeout(Duration::from_secs(30), ws_stream.next()).await {
341 Ok(Some(msg)) => msg,
342 Ok(None) => anyhow::bail!("WebSocket stream closed before eth_subscribe ack"),
343 Err(_) => anyhow::bail!("timeout waiting for eth_subscribe ack"),
344 };
345 match msg {
346 Ok(WsMessage::Text(text)) => {
347 let json: serde_json::Value = serde_json::from_str(&text)?;
348 if let Some(error) = json.get("error") {
349 anyhow::bail!("eth_subscribe failed: {error}");
350 }
351 if let Some(result) = json.get("result") {
352 break result.clone();
353 }
354 }
355 Err(..) | Ok(WsMessage::Close(..)) => {
356 anyhow::bail!("WebSocket closed before eth_subscribe ack")
357 }
358 _ => {}
359 }
360 };
361
362 Ok((ws_stream, subscription_id))
363}
364
365async fn next_subscription_payload(
368 ws_stream: &mut EthSubStream,
369 subscription_id: &serde_json::Value,
370 timeout: Duration,
371) -> anyhow::Result<serde_json::Value> {
372 loop {
373 let msg = match tokio::time::timeout(timeout, ws_stream.next()).await {
374 Ok(Some(msg)) => msg,
375 Ok(None) => anyhow::bail!("WebSocket stream closed"),
376 Err(_) => anyhow::bail!("timeout waiting for subscription notification"),
377 };
378 match msg {
379 Ok(WsMessage::Text(text)) => {
380 let json: serde_json::Value = serde_json::from_str(&text)?;
381 if json.get("method").and_then(|m| m.as_str()) != Some("eth_subscription") {
382 continue;
383 }
384 let params = json
385 .get("params")
386 .context("subscription notification missing params")?;
387 anyhow::ensure!(
388 params.get("subscription") == Some(subscription_id),
389 "subscription id mismatch in notification"
390 );
391 return params
392 .get("result")
393 .cloned()
394 .context("subscription notification missing result");
395 }
396 Err(..) | Ok(WsMessage::Close(..)) => anyhow::bail!("WebSocket closed unexpectedly"),
397 _ => {}
398 }
399 }
400}
401
402async fn close_eth_subscription(
405 ws_stream: &mut EthSubStream,
406 subscription_id: &serde_json::Value,
407) -> anyhow::Result<()> {
408 let request = json!({
409 "jsonrpc": "2.0",
410 "id": 2,
411 "method": "eth_unsubscribe",
412 "params": [subscription_id],
413 });
414 ws_stream
415 .send(WsMessage::Text(request.to_string().into()))
416 .await
417 .context("failed to send eth_unsubscribe request")?;
418 ws_stream.close(None).await?;
419 Ok(())
420}
421
422fn eth_subscribe_new_heads() -> RpcTestScenario {
423 RpcTestScenario::basic(|client| async move {
424 let start = client.call(EthBlockNumber::request(())?).await?;
427
428 let (mut ws_stream, subscription_id) =
429 open_eth_subscription(&client, SubscriptionKind::NewHeads, None).await?;
430
431 let payload =
433 next_subscription_payload(&mut ws_stream, &subscription_id, Duration::from_secs(180))
434 .await;
435
436 let _ = close_eth_subscription(&mut ws_stream, &subscription_id).await;
437 let payload = payload?;
438
439 anyhow::ensure!(
441 payload.is_object(),
442 "newHeads must yield a block-header object, got: {payload}"
443 );
444 let header: ApiHeaders = serde_json::from_value(payload)
445 .context("newHeads payload is not a valid Eth block header")?;
446
447 anyhow::ensure!(
450 header.0.number.0 >= start.0 as i64,
451 "newHeads number {} precedes the head {} seen at subscription time",
452 header.0.number.0,
453 start.0
454 );
455 Ok(())
456 })
457}
458
459fn eth_subscribe_pending_transactions(tx: TestTransaction) -> RpcTestScenario {
460 RpcTestScenario::basic(move |client| {
461 let tx = tx.clone();
462 async move {
463 let (mut ws_stream, subscription_id) =
464 open_eth_subscription(&client, SubscriptionKind::PendingTransactions, None).await?;
465
466 let cid = invoke_contract(&client, &tx).await?;
468 let tx_hash = client
469 .call(EthGetTransactionHashByCid::request((cid,))?)
470 .await?
471 .context("no Eth transaction hash for CID")?;
472
473 let watch = async {
475 loop {
476 let payload = next_subscription_payload(
477 &mut ws_stream,
478 &subscription_id,
479 Duration::from_secs(120),
480 )
481 .await?;
482 anyhow::ensure!(
484 payload.is_string(),
485 "pendingTransactions must yield a tx-hash string, got: {payload}"
486 );
487 let hash: EthHash = serde_json::from_value(payload)
488 .context("pendingTransactions payload is not an Eth hash")?;
489 if hash.eq(&tx_hash) {
491 break;
492 }
493 }
494 anyhow::Ok(())
495 };
496 let outcome = tokio::time::timeout(Duration::from_secs(120), watch)
497 .await
498 .unwrap_or_else(|_| {
499 Err(anyhow::anyhow!(
500 "timed out waiting for our pendingTransactions notification"
501 ))
502 });
503
504 let _ = close_eth_subscription(&mut ws_stream, &subscription_id).await;
505 outcome
506 }
507 })
508}
509
510#[derive(serde::Deserialize)]
512#[serde(rename_all = "camelCase")]
513struct LogView {
514 topics: Vec<EthHash>,
515 transaction_hash: EthHash,
516}
517
518struct CaseSub {
520 label: &'static str,
521 ws: EthSubStream,
522 sub_id: serde_json::Value,
523 expected: Vec<EthHash>,
525}
526
527async fn next_our_log(
530 ws: &mut EthSubStream,
531 sub_id: &serde_json::Value,
532 our_tx: &EthHash,
533 timeout: Duration,
534) -> anyhow::Result<Option<LogView>> {
535 while let Ok(payload) = next_subscription_payload(ws, sub_id, timeout).await {
536 ensure!(
537 payload.is_object(),
538 "logs must yield a single log object, got: {payload}"
539 );
540 let log: LogView =
541 serde_json::from_value(payload).context("logs payload is not an Eth log")?;
542 if &log.transaction_hash == our_tx {
543 return Ok(Some(log));
544 }
545 }
546 Ok(None)
547}
548
549const LOGS_DELIVERY_TIMEOUT: Duration = Duration::from_secs(100);
552
553async fn verify_case(
557 label: &str,
558 ws: &mut EthSubStream,
559 sub_id: &serde_json::Value,
560 our_tx: &EthHash,
561 expected: &[EthHash],
562 stop: &CancellationToken,
563) -> anyhow::Result<()> {
564 let mut got = Vec::new();
565 loop {
566 tokio::select! {
567 _ = stop.cancelled() => break,
568 log = next_our_log(ws, sub_id, our_tx, LOGS_DELIVERY_TIMEOUT) => {
569 if let Some(log) = log? {
570 got.push(*log.topics.first().context("log is missing topic[0]")?);
571 }
572 }
573 }
574 }
575 got.sort();
578 got.dedup();
579 let mut want = expected.to_vec();
580 want.sort();
581 ensure!(got == want, "{label}: expected {want:?}, got {got:?}");
582 Ok(())
583}
584
585fn eth_subscribe_logs(tx: TestTransaction) -> RpcTestScenario {
586 RpcTestScenario::basic(move |client| {
587 const SETTLE: Duration = Duration::from_secs(10);
589
590 let tx = tx.clone();
591 async move {
592 const WRONG_ADDRESS: &str = "0x000000000000000000000000000000000000dead";
594 let mint_topic = tx.topic;
597 let transfer = EthHash(H256::from(crate::utils::encoding::keccak_256(
598 b"Transfer(address,address,uint256)",
599 )));
600 let contract = EthAddress::from_filecoin_address(&tx.to)?;
601
602 let v_to = EthHash(H256::from_slice(
606 tx.payload
607 .get(4..36)
608 .context("mint calldata missing `to`")?,
609 ));
610 let v_amount = EthHash(H256::from_slice(
611 tx.payload
612 .get(36..68)
613 .context("mint calldata missing `amount`")?,
614 ));
615 let v_zero = EthHash::default(); let c = format!("{:#x}", contract.0);
619
620 let by_topics =
622 |topics: serde_json::Value| json!({ "address": [c.as_str()], "topics": topics });
623
624 let both_topics = vec![mint_topic, transfer];
626 let only_mint_topic = vec![mint_topic];
627 let only_transfer_topic = vec![transfer];
628 let none: Vec<EthHash> = vec![];
629
630 let specs: Vec<(&'static str, serde_json::Value, Vec<EthHash>)> = vec![
632 (
634 "address empty list (wildcard)",
635 json!({ "address": [], "topics": null }),
636 both_topics.clone(),
637 ),
638 (
639 "address [contract, other]",
640 json!({ "address": [c.as_str(), WRONG_ADDRESS], "topics": null }),
641 both_topics.clone(),
642 ),
643 (
644 "address non-matching",
645 json!({ "address": [WRONG_ADDRESS], "topics": null }),
646 none.clone(),
647 ),
648 (
650 "topic0 = Mint",
651 by_topics(json!([mint_topic.to_string()])),
652 only_mint_topic.clone(),
653 ),
654 (
655 "topic0 = Transfer",
656 by_topics(json!([transfer.to_string()])),
657 only_transfer_topic.clone(),
658 ),
659 (
660 "topic0 OR [Mint, Transfer]",
661 by_topics(json!([[mint_topic.to_string(), transfer.to_string()]])),
662 both_topics.clone(),
663 ),
664 (
665 "topic0 empty-list wildcard",
666 by_topics(json!([[]])),
667 both_topics.clone(),
668 ),
669 (
670 "topic0 null wildcard",
671 by_topics(json!([null])),
672 both_topics.clone(),
673 ),
674 (
676 "Mint + trailing null",
677 by_topics(json!([mint_topic.to_string(), null])),
678 only_mint_topic.clone(),
679 ),
680 (
681 "Mint + null past topics",
682 by_topics(json!([mint_topic.to_string(), v_to.to_string(), null])),
683 only_mint_topic.clone(),
684 ),
685 (
686 "Mint + value past topics (no match)",
687 by_topics(json!([
688 mint_topic.to_string(),
689 v_to.to_string(),
690 v_to.to_string()
691 ])),
692 none.clone(),
693 ),
694 (
696 "topic1 = to (only Mint)",
697 by_topics(json!([null, v_to.to_string()])),
698 only_mint_topic.clone(),
699 ),
700 (
701 "topic1 = 0x0 (only Transfer)",
702 by_topics(json!([null, v_zero.to_string()])),
703 only_transfer_topic.clone(),
704 ),
705 (
706 "topic2 = to (only Transfer)",
707 by_topics(json!([null, null, v_to.to_string()])),
708 only_transfer_topic.clone(),
709 ),
710 (
711 "Transfer AND from=0 AND to",
712 by_topics(json!([
713 transfer.to_string(),
714 v_zero.to_string(),
715 v_to.to_string()
716 ])),
717 only_transfer_topic.clone(),
718 ),
719 (
720 "Transfer AND topic1=to (mismatch)",
721 by_topics(json!([transfer.to_string(), v_to.to_string()])),
722 none.clone(),
723 ),
724 (
725 "(Mint|Transfer) AND topic1=to",
726 by_topics(json!([
727 [mint_topic.to_string(), transfer.to_string()],
728 v_to.to_string()
729 ])),
730 only_mint_topic.clone(),
731 ),
732 (
734 "topic1 = amount (in data, no match)",
735 by_topics(json!([null, v_amount.to_string()])),
736 none.clone(),
737 ),
738 ];
739 let mut subs = Vec::with_capacity(specs.len());
740 for (label, filter, expected) in specs {
741 let (ws, sub_id) =
742 open_eth_subscription(&client, SubscriptionKind::Logs, Some(filter)).await?;
743 subs.push(CaseSub {
744 label,
745 ws,
746 sub_id,
747 expected,
748 });
749 }
750
751 let cid = invoke_contract(&client, &tx).await?;
753 let tx_hash = client
754 .call(EthGetTransactionHashByCid::request((cid,))?)
755 .await?
756 .context("no Eth transaction hash for CID")?;
757 let stop = CancellationToken::new();
761 let _cancellation_token_drop_guard = stop.drop_guard_ref();
763 let coordinator = async {
764 let executed = wait_pending_message(&client, cid).await;
765 tokio::time::sleep(SETTLE).await;
766 stop.cancel();
767 executed
768 };
769 let drains =
770 futures::future::join_all(subs.iter_mut().map(|c| {
771 verify_case(c.label, &mut c.ws, &c.sub_id, &tx_hash, &c.expected, &stop)
772 }));
773 let (executed, case_results) = tokio::join!(coordinator, drains);
774 let outcome = executed.and_then(|()| {
775 for result in case_results {
776 result?;
777 }
778 anyhow::Ok(())
779 });
780
781 for sub in &mut subs {
782 let _ = close_eth_subscription(&mut sub.ws, &sub.sub_id).await;
783 }
784 outcome
785 }
786 })
787}
788
789fn create_eth_new_filter_test() -> RpcTestScenario {
790 RpcTestScenario::basic(|client| async move {
791 const BLOCK_RANGE: u64 = 200;
792
793 let last_block = client.call(EthBlockNumber::request(())?).await?;
794
795 let filter_spec = EthFilterSpec {
796 from_block: Some(EthUint64(last_block.0.saturating_sub(BLOCK_RANGE)).to_hex_string()),
797 to_block: Some(last_block.to_hex_string()),
798 ..Default::default()
799 };
800
801 let filter_id = client.call(EthNewFilter::request((filter_spec,))?).await?;
802
803 let removed = client
804 .call(EthUninstallFilter::request((filter_id.clone(),))?)
805 .await?;
806 anyhow::ensure!(removed);
807
808 let removed = client
809 .call(EthUninstallFilter::request((filter_id,))?)
810 .await?;
811 anyhow::ensure!(!removed);
812
813 Ok(())
814 })
815}
816
817fn create_eth_new_filter_limit_test(count: usize) -> RpcTestScenario {
818 RpcTestScenario::basic(move |client| async move {
819 const BLOCK_RANGE: u64 = 200;
820
821 let last_block = client.call(EthBlockNumber::request(())?).await?;
822
823 let filter_spec = EthFilterSpec {
824 from_block: Some(format!("0x{:x}", last_block.0.saturating_sub(BLOCK_RANGE))),
825 to_block: Some(last_block.to_hex_string()),
826 ..Default::default()
827 };
828
829 let mut ids = vec![];
830
831 for _ in 0..count {
832 let result = client
833 .call(EthNewFilter::request((filter_spec.clone(),))?)
834 .await;
835
836 match result {
837 Ok(filter_id) => ids.push(filter_id),
838 Err(e) => {
839 for id in ids {
841 let removed = client.call(EthUninstallFilter::request((id,))?).await?;
842 anyhow::ensure!(removed);
843 }
844 anyhow::bail!(e)
845 }
846 }
847 }
848
849 for id in ids {
850 let removed = client.call(EthUninstallFilter::request((id,))?).await?;
851 anyhow::ensure!(removed);
852 }
853
854 Ok(())
855 })
856}
857
858fn eth_new_block_filter() -> RpcTestScenario {
859 RpcTestScenario::basic(move |client| async move {
860 async fn process_filter(client: &rpc::Client, filter_id: &FilterID) -> anyhow::Result<()> {
861 let poll = async || -> anyhow::Result<Vec<EthHash>> {
862 match client
863 .call(EthGetFilterChanges::request((filter_id.clone(),))?)
864 .await?
865 {
866 EthFilterResult::Hashes(hashes) => Ok(hashes),
867 _ => Err(anyhow::anyhow!("expecting block hashes")),
868 }
869 };
870 let verify_hashes = async |hashes: &[EthHash]| -> anyhow::Result<()> {
871 for hash in hashes {
872 let _block = client
873 .call(EthGetBlockByHash::request((*hash, false))?)
874 .await?;
875 }
876 Ok(())
877 };
878
879 let prev_hashes = poll().await?;
880 verify_hashes(&prev_hashes).await?;
881
882 let mut hashes = Vec::new();
886 for _ in 0..3 {
887 wait_next_epoch(client).await?;
888 hashes = poll().await?;
889 verify_hashes(&hashes).await?;
890 if hashes != prev_hashes {
891 break;
892 }
893 }
894 anyhow::ensure!((prev_hashes.is_empty() && hashes.is_empty()) || prev_hashes != hashes);
895
896 Ok(())
897 }
898
899 let filter_id = client.call(EthNewBlockFilter::request(())?).await?;
901
902 let result = process_filter(&client, &filter_id).await;
903
904 let cleanup: anyhow::Result<()> = async {
906 let removed = client
907 .call(EthUninstallFilter::request((filter_id,))?)
908 .await
909 .context("failed to uninstall filter")?;
910 anyhow::ensure!(removed, "filter was not removed");
911 Ok(())
912 }
913 .await;
914
915 result.and(cleanup)
917 })
918}
919
920fn eth_new_pending_transaction_filter(tx: TestTransaction) -> RpcTestScenario {
921 RpcTestScenario::basic(move |client| {
922 let tx = tx.clone();
923 async move {
924 let filter_id = client
925 .call(EthNewPendingTransactionFilter::request(())?)
926 .await?;
927
928 let filter_result = client
929 .call(EthGetFilterChanges::request((filter_id.clone(),))?)
930 .await?;
931
932 let result = if let EthFilterResult::Hashes(prev_hashes) = filter_result {
933 let cid = invoke_contract(&client, &tx).await?;
934 let tx_hash = client
935 .call(EthGetTransactionHashByCid::request((cid,))?)
936 .await?
937 .context("no Eth transaction hash for CID")?;
938
939 wait_in_mempool(&client, cid).await?;
942 let hashes = poll_pending_filter_until(&client, &filter_id, &tx_hash).await?;
943
944 anyhow::ensure!(
945 prev_hashes != hashes,
946 "prev_hashes={prev_hashes:?} hashes={hashes:?}"
947 );
948 anyhow::ensure!(
949 hashes.contains(&tx_hash),
950 "transaction hash missing from filter results: tx_hash={tx_hash:?} cid={cid:?} hashes={hashes:?}"
951 );
952 Ok(())
953 } else {
954 Err(anyhow::anyhow!("expecting transactions"))
955 };
956
957 let removed = client
958 .call(EthUninstallFilter::request((filter_id,))?)
959 .await?;
960 anyhow::ensure!(removed);
961
962 result
963 }
964 })
965}
966
967fn eth_new_pending_transaction_filter_multi_poll(tx: TestTransaction) -> RpcTestScenario {
976 RpcTestScenario::basic(move |client| {
977 let tx = tx.clone();
978 async move {
979 let filter_id = client
980 .call(EthNewPendingTransactionFilter::request(())?)
981 .await?;
982
983 let result = async {
984 let _ = client
986 .call(EthGetFilterChanges::request((filter_id.clone(),))?)
987 .await?;
988
989 let cid_a = invoke_contract(&client, &tx).await?;
991 let hash_a = client
992 .call(EthGetTransactionHashByCid::request((cid_a,))?)
993 .await?
994 .context("no Eth transaction hash for cid_a")?;
995 wait_in_mempool(&client, cid_a).await?;
996 poll_pending_filter_until(&client, &filter_id, &hash_a).await?;
997
998 let cid_b = invoke_contract(&client, &tx).await?;
1001 let hash_b = client
1002 .call(EthGetTransactionHashByCid::request((cid_b,))?)
1003 .await?
1004 .context("no Eth transaction hash for cid_b")?;
1005 wait_in_mempool(&client, cid_b).await?;
1006 let hashes_b = poll_pending_filter_until(&client, &filter_id, &hash_b).await?;
1007 anyhow::ensure!(
1008 !hashes_b.contains(&hash_a),
1009 "second poll should not return previously-consumed tx_a: \
1010 hash_a={hash_a:?} hashes={hashes_b:?}"
1011 );
1012
1013 anyhow::Ok(())
1014 }
1015 .await;
1016
1017 let removed = client
1018 .call(EthUninstallFilter::request((filter_id,))?)
1019 .await?;
1020 anyhow::ensure!(removed);
1021
1022 result
1023 }
1024 })
1025}
1026
1027fn as_logs(input: EthFilterResult) -> EthFilterResult {
1028 match input {
1029 EthFilterResult::Hashes(vec) if vec.is_empty() => EthFilterResult::Logs(Vec::new()),
1030 other => other,
1031 }
1032}
1033
1034fn eth_get_filter_logs(tx: TestTransaction) -> RpcTestScenario {
1035 RpcTestScenario::basic(move |client| {
1036 let tx = tx.clone();
1037 async move {
1038 const BLOCK_RANGE: u64 = 1;
1039
1040 let tipset = client.call(ChainHead::request(())?).await?;
1041 let cid = invoke_contract(&client, &tx).await?;
1042 let lookup = client
1043 .call(
1044 StateWaitMsg::request((cid, 1, tipset.epoch(), true))?
1045 .with_timeout(Duration::from_secs(300)),
1046 )
1047 .await?;
1048 let block_num = EthUint64(lookup.height as u64);
1049
1050 let topics = EthTopicSpec(vec![EthHashList::Single(Some(tx.topic))]);
1051 let filter_spec = EthFilterSpec {
1052 from_block: Some(format!("0x{:x}", block_num.0.saturating_sub(BLOCK_RANGE))),
1053 topics: Some(topics),
1054 ..Default::default()
1055 };
1056
1057 let filter_id = client
1058 .call(EthNewFilter::request((filter_spec.clone(),))?)
1059 .await?;
1060 let filter_result = as_logs(
1061 client
1062 .call(EthGetFilterLogs::request((filter_id.clone(),))?)
1063 .await?,
1064 );
1065 let result = if let EthFilterResult::Logs(logs) = filter_result {
1066 anyhow::ensure!(
1067 !logs.is_empty(),
1068 "Empty logs: filter_spec={filter_spec:?} cid={cid:?}",
1069 );
1070 Ok(())
1071 } else {
1072 Err(anyhow::anyhow!("expecting logs"))
1073 };
1074
1075 let removed = client
1076 .call(EthUninstallFilter::request((filter_id,))?)
1077 .await?;
1078 anyhow::ensure!(removed);
1079
1080 result
1081 }
1082 })
1083}
1084
1085const LOTUS_EVENTS_MAXFILTERS: usize = 100;
1086
1087macro_rules! with_methods {
1088 ( $builder:expr, $( $method:ty ),+ ) => {{
1089 let mut b = $builder;
1090 $(
1091 b = b.using::<{ <$method>::N_REQUIRED_PARAMS }, $method>();
1092 )+
1093 b
1094 }};
1095}
1096
1097pub(super) async fn create_tests(tx: TestTransaction) -> Vec<RpcTestScenario> {
1098 vec![
1099 with_methods!(
1100 create_eth_new_filter_test().name("eth_newFilter install/uninstall"),
1101 EthNewFilter,
1102 EthUninstallFilter
1103 ),
1104 with_methods!(
1105 create_eth_new_filter_limit_test(20).name("eth_newFilter under limit"),
1106 EthNewFilter,
1107 EthUninstallFilter
1108 ),
1109 with_methods!(
1110 create_eth_new_filter_limit_test(LOTUS_EVENTS_MAXFILTERS)
1111 .name("eth_newFilter just under limit"),
1112 EthNewFilter,
1113 EthUninstallFilter
1114 ),
1115 with_methods!(
1116 create_eth_new_filter_limit_test(LOTUS_EVENTS_MAXFILTERS + 1)
1117 .name("eth_newFilter over limit")
1118 .should_fail_with("maximum number of filters registered"),
1119 EthNewFilter,
1120 EthUninstallFilter
1121 ),
1122 with_methods!(
1123 eth_new_block_filter().name("eth_newBlockFilter works"),
1124 EthNewBlockFilter,
1125 EthGetFilterChanges,
1126 EthUninstallFilter
1127 ),
1128 with_methods!(
1129 eth_new_pending_transaction_filter(tx.clone())
1130 .name("eth_newPendingTransactionFilter works"),
1131 EthNewPendingTransactionFilter,
1132 EthGetFilterChanges,
1133 EthGetTransactionHashByCid,
1134 EthUninstallFilter
1135 ),
1136 with_methods!(
1137 eth_new_pending_transaction_filter_multi_poll(tx.clone())
1138 .name("eth_getFilterChanges returns only new pending txs per poll"),
1139 EthNewPendingTransactionFilter,
1140 EthGetFilterChanges,
1141 EthGetTransactionHashByCid,
1142 EthUninstallFilter
1143 ),
1144 with_methods!(
1145 eth_get_filter_logs(tx.clone()).name("eth_getFilterLogs works"),
1146 EthNewFilter,
1147 EthGetFilterLogs,
1148 EthUninstallFilter
1149 ),
1150 with_methods!(
1151 eth_subscribe_new_heads().name("eth_subscribe newHeads works"),
1152 EthSubscribe,
1153 EthUnsubscribe
1154 ),
1155 with_methods!(
1156 eth_subscribe_pending_transactions(tx.clone())
1157 .name("eth_subscribe pendingTransactions works"),
1158 EthSubscribe,
1159 EthUnsubscribe,
1160 EthGetTransactionHashByCid
1161 ),
1162 with_methods!(
1163 eth_subscribe_logs(tx.clone()).name("eth_subscribe logs filter matrix"),
1164 EthSubscribe,
1165 EthUnsubscribe,
1166 EthGetTransactionHashByCid
1167 ),
1168 ]
1169}