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
198#[allow(unreachable_code)]
199async fn next_tipset(client: &rpc::Client) -> anyhow::Result<()> {
200 async fn close_channel(
201 stream: &mut EthSubStream,
202 id: &serde_json::Value,
203 ) -> anyhow::Result<()> {
204 let request = json!({
205 "jsonrpc": "2.0",
206 "id": 1,
207 "method": "xrpc.cancel",
208 "params": [id]
209 });
210
211 stream
212 .send(WsMessage::Text(request.to_string().into()))
213 .await
214 .context("failed to send close channel request")?;
215
216 Ok(())
217 }
218
219 let mut ws_stream = connect_ws(client).await?;
220
221 let request = json!({
222 "jsonrpc": "2.0",
223 "id": 1,
224 "method": "Filecoin.ChainNotify",
225 "params": []
226 });
227 ws_stream
228 .send(WsMessage::Text(request.to_string().into()))
229 .await?;
230
231 let mut channel_id: Option<serde_json::Value> = None;
232
233 loop {
238 let msg = match tokio::time::timeout(Duration::from_secs(180), ws_stream.next()).await {
239 Ok(Some(msg)) => msg,
240 Ok(None) => anyhow::bail!("WebSocket stream closed"),
241 Err(_) => {
242 if let Some(id) = channel_id.as_ref() {
243 let _ = close_channel(&mut ws_stream, id).await;
244 }
245 let _ = ws_stream.close(None).await;
246 anyhow::bail!("timeout waiting for tipset");
247 }
248 };
249 match msg {
250 Ok(WsMessage::Text(text)) => {
251 let json: serde_json::Value = serde_json::from_str(&text)?;
252
253 if let Some(id) = json.get("result") {
254 channel_id = Some(id.clone());
255 } else {
256 let method = json!("xrpc.ch.val");
257 anyhow::ensure!(json.get("method") == Some(&method));
258
259 if let Some(params) = json.get("params").and_then(|v| v.as_array()) {
260 if let Some(id) = params.first() {
261 anyhow::ensure!(Some(id) == channel_id.as_ref());
262 } else {
263 anyhow::bail!("expecting an open channel");
264 }
265 if let Some(changes) = params.get(1).and_then(|v| v.as_array()) {
266 for change in changes {
267 if let Some(type_) = change.get("Type").and_then(|v| v.as_str()) {
268 if type_ == "apply" {
269 let id = channel_id.as_ref().ok_or_else(|| {
270 anyhow::anyhow!("subscription not opened")
271 })?;
272 close_channel(&mut ws_stream, id).await?;
273 ws_stream.close(None).await?;
274 return Ok(());
275 } else if type_ == "revert" {
276 let id = channel_id.as_ref().ok_or_else(|| {
277 anyhow::anyhow!("subscription not opened")
278 })?;
279 close_channel(&mut ws_stream, id).await?;
280 ws_stream.close(None).await?;
281 anyhow::bail!("revert");
282 }
283 }
284 }
285 }
286 } else {
287 let id = channel_id
288 .as_ref()
289 .ok_or_else(|| anyhow::anyhow!("subscription not opened"))?;
290 close_channel(&mut ws_stream, id).await?;
291 ws_stream.close(None).await?;
292 anyhow::bail!("expecting params");
293 }
294 }
295 }
296 Err(..) | Ok(WsMessage::Close(..)) => {
297 let id = channel_id
298 .as_ref()
299 .ok_or_else(|| anyhow::anyhow!("subscription not opened"))?;
300 close_channel(&mut ws_stream, id).await?;
301 ws_stream.close(None).await?;
302 anyhow::bail!("unexpected error or close message");
303 }
304 _ => {
305 }
307 }
308 }
309
310 unreachable!("loop always returns within the branches above")
311}
312
313async fn wait_in_mempool(client: &rpc::Client, message_cid: Cid) -> anyhow::Result<()> {
316 let mut retries = 100;
317 loop {
318 let pending = client
319 .call(MpoolPending::request((ApiTipsetKey(None),))?)
320 .await?;
321 if pending.0.iter().any(|msg| msg.cid() == message_cid) {
322 break Ok(());
323 }
324 ensure!(retries != 0, "Message not found in mpool");
325 retries -= 1;
326 tokio::time::sleep(Duration::from_millis(10)).await;
327 }
328}
329
330async fn wait_pending_message(client: &rpc::Client, message_cid: Cid) -> anyhow::Result<()> {
332 let tipset = client.call(ChainHead::request(())?).await?;
333 wait_in_mempool(client, message_cid).await?;
334 client
335 .call(
336 StateWaitMsg::request((message_cid, 1, tipset.epoch(), true))?
337 .with_timeout(Duration::from_secs(300)),
338 )
339 .await?;
340 Ok(())
341}
342
343async fn poll_pending_filter_until(
350 client: &rpc::Client,
351 filter_id: &FilterID,
352 want: &EthHash,
353) -> anyhow::Result<Vec<EthHash>> {
354 let mut seen: Vec<EthHash> = Vec::new();
355 let mut retries = 100;
356 loop {
357 let result = client
358 .call(EthGetFilterChanges::request((filter_id.clone(),))?)
359 .await?;
360 let EthFilterResult::Hashes(hashes) = result else {
361 anyhow::bail!("expected hashes, got {result:?}");
362 };
363 seen.extend(hashes);
364 if seen.contains(want) {
365 break Ok(seen);
366 }
367 ensure!(
368 retries != 0,
369 "filter did not return {want:?} in time; saw {seen:?}"
370 );
371 retries -= 1;
372 tokio::time::sleep(Duration::from_millis(10)).await;
373 }
374}
375
376async fn invoke_contract(client: &rpc::Client, tx: &TestTransaction) -> anyhow::Result<Cid> {
377 let encoded_params = cbor4ii::serde::to_vec(
378 Vec::with_capacity(tx.payload.len()),
379 &Value::Bytes(tx.payload.clone()),
380 )
381 .context("failed to encode params")?;
382 let nonce = client.call(MpoolGetNonce::request((tx.from,))?).await?;
383 let message = Message {
384 to: tx.to,
385 from: tx.from,
386 sequence: nonce,
387 method_num: EVMMethod::InvokeContract as u64,
388 params: encoded_params.into(),
389 ..Default::default()
390 };
391 let unsigned_msg = client
392 .call(GasEstimateMessageGas::request((
393 message,
394 None,
395 ApiTipsetKey(None),
396 ))?)
397 .await?;
398
399 let eth_tx_args = crate::eth::EthEip1559TxArgsBuilder::default()
400 .chain_id(ETH_CHAIN_ID)
401 .unsigned_message(&unsigned_msg)?
402 .build()
403 .map_err(|e| anyhow::anyhow!("Failed to build EIP-1559 transaction: {}", e))?;
404 let eth_tx = crate::eth::EthTx::from(eth_tx_args);
405 let data = eth_tx.rlp_unsigned_message(ETH_CHAIN_ID)?;
406
407 let sig = client.call(WalletSign::request((tx.from, data))?).await?;
408 let smsg = SignedMessage::new_unchecked(unsigned_msg, sig);
409 let cid = smsg.cid();
410
411 client.call(MpoolPush::request((smsg,))?).await?;
412
413 Ok(cid)
414}
415
416async fn open_eth_subscription(
420 client: &rpc::Client,
421 kind: SubscriptionKind,
422 filter: Option<serde_json::Value>,
423) -> anyhow::Result<(EthSubStream, serde_json::Value)> {
424 let mut ws_stream = connect_ws(client).await?;
425
426 let mut params = vec![serde_json::to_value(kind)?];
427 params.extend(filter);
428 let request = json!({
429 "jsonrpc": "2.0",
430 "id": 1,
431 "method": "eth_subscribe",
432 "params": params,
433 });
434 ws_stream
435 .send(WsMessage::Text(request.to_string().into()))
436 .await
437 .context("failed to send eth_subscribe request")?;
438
439 let subscription_id = loop {
441 let msg = match tokio::time::timeout(Duration::from_secs(30), ws_stream.next()).await {
442 Ok(Some(msg)) => msg,
443 Ok(None) => anyhow::bail!("WebSocket stream closed before eth_subscribe ack"),
444 Err(_) => anyhow::bail!("timeout waiting for eth_subscribe ack"),
445 };
446 match msg {
447 Ok(WsMessage::Text(text)) => {
448 let json: serde_json::Value = serde_json::from_str(&text)?;
449 if let Some(error) = json.get("error") {
450 anyhow::bail!("eth_subscribe failed: {error}");
451 }
452 if let Some(result) = json.get("result") {
453 break result.clone();
454 }
455 }
456 Err(..) | Ok(WsMessage::Close(..)) => {
457 anyhow::bail!("WebSocket closed before eth_subscribe ack")
458 }
459 _ => {}
460 }
461 };
462
463 Ok((ws_stream, subscription_id))
464}
465
466async fn next_subscription_payload(
469 ws_stream: &mut EthSubStream,
470 subscription_id: &serde_json::Value,
471 timeout: Duration,
472) -> anyhow::Result<serde_json::Value> {
473 loop {
474 let msg = match tokio::time::timeout(timeout, ws_stream.next()).await {
475 Ok(Some(msg)) => msg,
476 Ok(None) => anyhow::bail!("WebSocket stream closed"),
477 Err(_) => anyhow::bail!("timeout waiting for subscription notification"),
478 };
479 match msg {
480 Ok(WsMessage::Text(text)) => {
481 let json: serde_json::Value = serde_json::from_str(&text)?;
482 if json.get("method").and_then(|m| m.as_str()) != Some("eth_subscription") {
483 continue;
484 }
485 let params = json
486 .get("params")
487 .context("subscription notification missing params")?;
488 anyhow::ensure!(
489 params.get("subscription") == Some(subscription_id),
490 "subscription id mismatch in notification"
491 );
492 return params
493 .get("result")
494 .cloned()
495 .context("subscription notification missing result");
496 }
497 Err(..) | Ok(WsMessage::Close(..)) => anyhow::bail!("WebSocket closed unexpectedly"),
498 _ => {}
499 }
500 }
501}
502
503async fn close_eth_subscription(
506 ws_stream: &mut EthSubStream,
507 subscription_id: &serde_json::Value,
508) -> anyhow::Result<()> {
509 let request = json!({
510 "jsonrpc": "2.0",
511 "id": 2,
512 "method": "eth_unsubscribe",
513 "params": [subscription_id],
514 });
515 ws_stream
516 .send(WsMessage::Text(request.to_string().into()))
517 .await
518 .context("failed to send eth_unsubscribe request")?;
519 ws_stream.close(None).await?;
520 Ok(())
521}
522
523fn eth_subscribe_new_heads() -> RpcTestScenario {
524 RpcTestScenario::basic(|client| async move {
525 let start = client.call(EthBlockNumber::request(())?).await?;
528
529 let (mut ws_stream, subscription_id) =
530 open_eth_subscription(&client, SubscriptionKind::NewHeads, None).await?;
531
532 let payload =
534 next_subscription_payload(&mut ws_stream, &subscription_id, Duration::from_secs(180))
535 .await;
536
537 let _ = close_eth_subscription(&mut ws_stream, &subscription_id).await;
538 let payload = payload?;
539
540 anyhow::ensure!(
542 payload.is_object(),
543 "newHeads must yield a block-header object, got: {payload}"
544 );
545 let header: ApiHeaders = serde_json::from_value(payload)
546 .context("newHeads payload is not a valid Eth block header")?;
547
548 anyhow::ensure!(
551 header.0.number.0 >= start.0 as i64,
552 "newHeads number {} precedes the head {} seen at subscription time",
553 header.0.number.0,
554 start.0
555 );
556 Ok(())
557 })
558}
559
560fn eth_subscribe_pending_transactions(tx: TestTransaction) -> RpcTestScenario {
561 RpcTestScenario::basic(move |client| {
562 let tx = tx.clone();
563 async move {
564 let (mut ws_stream, subscription_id) =
565 open_eth_subscription(&client, SubscriptionKind::PendingTransactions, None).await?;
566
567 let cid = invoke_contract(&client, &tx).await?;
569 let tx_hash = client
570 .call(EthGetTransactionHashByCid::request((cid,))?)
571 .await?
572 .context("no Eth transaction hash for CID")?;
573
574 let watch = async {
576 loop {
577 let payload = next_subscription_payload(
578 &mut ws_stream,
579 &subscription_id,
580 Duration::from_secs(120),
581 )
582 .await?;
583 anyhow::ensure!(
585 payload.is_string(),
586 "pendingTransactions must yield a tx-hash string, got: {payload}"
587 );
588 let hash: EthHash = serde_json::from_value(payload)
589 .context("pendingTransactions payload is not an Eth hash")?;
590 if hash.eq(&tx_hash) {
592 break;
593 }
594 }
595 anyhow::Ok(())
596 };
597 let outcome = tokio::time::timeout(Duration::from_secs(120), watch)
598 .await
599 .unwrap_or_else(|_| {
600 Err(anyhow::anyhow!(
601 "timed out waiting for our pendingTransactions notification"
602 ))
603 });
604
605 let _ = close_eth_subscription(&mut ws_stream, &subscription_id).await;
606 outcome
607 }
608 })
609}
610
611#[derive(serde::Deserialize)]
613#[serde(rename_all = "camelCase")]
614struct LogView {
615 topics: Vec<EthHash>,
616 transaction_hash: EthHash,
617}
618
619struct CaseSub {
621 label: &'static str,
622 ws: EthSubStream,
623 sub_id: serde_json::Value,
624 expected: Vec<EthHash>,
626}
627
628async fn next_our_log(
631 ws: &mut EthSubStream,
632 sub_id: &serde_json::Value,
633 our_tx: &EthHash,
634 timeout: Duration,
635) -> anyhow::Result<Option<LogView>> {
636 while let Ok(payload) = next_subscription_payload(ws, sub_id, timeout).await {
637 ensure!(
638 payload.is_object(),
639 "logs must yield a single log object, got: {payload}"
640 );
641 let log: LogView =
642 serde_json::from_value(payload).context("logs payload is not an Eth log")?;
643 if &log.transaction_hash == our_tx {
644 return Ok(Some(log));
645 }
646 }
647 Ok(None)
648}
649
650const LOGS_DELIVERY_TIMEOUT: Duration = Duration::from_secs(100);
653
654async fn verify_case(
658 label: &str,
659 ws: &mut EthSubStream,
660 sub_id: &serde_json::Value,
661 our_tx: &EthHash,
662 expected: &[EthHash],
663 stop: &CancellationToken,
664) -> anyhow::Result<()> {
665 let mut got = Vec::new();
666 loop {
667 tokio::select! {
668 _ = stop.cancelled() => break,
669 log = next_our_log(ws, sub_id, our_tx, LOGS_DELIVERY_TIMEOUT) => {
670 if let Some(log) = log? {
671 got.push(*log.topics.first().context("log is missing topic[0]")?);
672 }
673 }
674 }
675 }
676 got.sort();
679 got.dedup();
680 let mut want = expected.to_vec();
681 want.sort();
682 ensure!(got == want, "{label}: expected {want:?}, got {got:?}");
683 Ok(())
684}
685
686fn eth_subscribe_logs(tx: TestTransaction) -> RpcTestScenario {
687 RpcTestScenario::basic(move |client| {
688 const SETTLE: Duration = Duration::from_secs(10);
690
691 let tx = tx.clone();
692 async move {
693 const WRONG_ADDRESS: &str = "0x000000000000000000000000000000000000dead";
695 let mint_topic = tx.topic;
698 let transfer = EthHash(H256::from(crate::utils::encoding::keccak_256(
699 b"Transfer(address,address,uint256)",
700 )));
701 let contract = EthAddress::from_filecoin_address(&tx.to)?;
702
703 let v_to = EthHash(H256::from_slice(
707 tx.payload
708 .get(4..36)
709 .context("mint calldata missing `to`")?,
710 ));
711 let v_amount = EthHash(H256::from_slice(
712 tx.payload
713 .get(36..68)
714 .context("mint calldata missing `amount`")?,
715 ));
716 let v_zero = EthHash::default(); let c = format!("{:#x}", contract.0);
720
721 let by_topics =
723 |topics: serde_json::Value| json!({ "address": [c.as_str()], "topics": topics });
724
725 let both_topics = vec![mint_topic, transfer];
727 let only_mint_topic = vec![mint_topic];
728 let only_transfer_topic = vec![transfer];
729 let none: Vec<EthHash> = vec![];
730
731 let specs: Vec<(&'static str, serde_json::Value, Vec<EthHash>)> = vec![
733 (
735 "address empty list (wildcard)",
736 json!({ "address": [], "topics": null }),
737 both_topics.clone(),
738 ),
739 (
740 "address [contract, other]",
741 json!({ "address": [c.as_str(), WRONG_ADDRESS], "topics": null }),
742 both_topics.clone(),
743 ),
744 (
745 "address non-matching",
746 json!({ "address": [WRONG_ADDRESS], "topics": null }),
747 none.clone(),
748 ),
749 (
751 "topic0 = Mint",
752 by_topics(json!([mint_topic.to_string()])),
753 only_mint_topic.clone(),
754 ),
755 (
756 "topic0 = Transfer",
757 by_topics(json!([transfer.to_string()])),
758 only_transfer_topic.clone(),
759 ),
760 (
761 "topic0 OR [Mint, Transfer]",
762 by_topics(json!([[mint_topic.to_string(), transfer.to_string()]])),
763 both_topics.clone(),
764 ),
765 (
766 "topic0 empty-list wildcard",
767 by_topics(json!([[]])),
768 both_topics.clone(),
769 ),
770 (
771 "topic0 null wildcard",
772 by_topics(json!([null])),
773 both_topics.clone(),
774 ),
775 (
777 "Mint + trailing null",
778 by_topics(json!([mint_topic.to_string(), null])),
779 only_mint_topic.clone(),
780 ),
781 (
782 "Mint + null past topics",
783 by_topics(json!([mint_topic.to_string(), v_to.to_string(), null])),
784 only_mint_topic.clone(),
785 ),
786 (
787 "Mint + value past topics (no match)",
788 by_topics(json!([
789 mint_topic.to_string(),
790 v_to.to_string(),
791 v_to.to_string()
792 ])),
793 none.clone(),
794 ),
795 (
797 "topic1 = to (only Mint)",
798 by_topics(json!([null, v_to.to_string()])),
799 only_mint_topic.clone(),
800 ),
801 (
802 "topic1 = 0x0 (only Transfer)",
803 by_topics(json!([null, v_zero.to_string()])),
804 only_transfer_topic.clone(),
805 ),
806 (
807 "topic2 = to (only Transfer)",
808 by_topics(json!([null, null, v_to.to_string()])),
809 only_transfer_topic.clone(),
810 ),
811 (
812 "Transfer AND from=0 AND to",
813 by_topics(json!([
814 transfer.to_string(),
815 v_zero.to_string(),
816 v_to.to_string()
817 ])),
818 only_transfer_topic.clone(),
819 ),
820 (
821 "Transfer AND topic1=to (mismatch)",
822 by_topics(json!([transfer.to_string(), v_to.to_string()])),
823 none.clone(),
824 ),
825 (
826 "(Mint|Transfer) AND topic1=to",
827 by_topics(json!([
828 [mint_topic.to_string(), transfer.to_string()],
829 v_to.to_string()
830 ])),
831 only_mint_topic.clone(),
832 ),
833 (
835 "topic1 = amount (in data, no match)",
836 by_topics(json!([null, v_amount.to_string()])),
837 none.clone(),
838 ),
839 ];
840 let mut subs = Vec::with_capacity(specs.len());
841 for (label, filter, expected) in specs {
842 let (ws, sub_id) =
843 open_eth_subscription(&client, SubscriptionKind::Logs, Some(filter)).await?;
844 subs.push(CaseSub {
845 label,
846 ws,
847 sub_id,
848 expected,
849 });
850 }
851
852 let cid = invoke_contract(&client, &tx).await?;
854 let tx_hash = client
855 .call(EthGetTransactionHashByCid::request((cid,))?)
856 .await?
857 .context("no Eth transaction hash for CID")?;
858 let stop = CancellationToken::new();
862 let _cancellation_token_drop_guard = stop.drop_guard_ref();
864 let coordinator = async {
865 let executed = wait_pending_message(&client, cid).await;
866 tokio::time::sleep(SETTLE).await;
867 stop.cancel();
868 executed
869 };
870 let drains =
871 futures::future::join_all(subs.iter_mut().map(|c| {
872 verify_case(c.label, &mut c.ws, &c.sub_id, &tx_hash, &c.expected, &stop)
873 }));
874 let (executed, case_results) = tokio::join!(coordinator, drains);
875 let outcome = executed.and_then(|()| {
876 for result in case_results {
877 result?;
878 }
879 anyhow::Ok(())
880 });
881
882 for sub in &mut subs {
883 let _ = close_eth_subscription(&mut sub.ws, &sub.sub_id).await;
884 }
885 outcome
886 }
887 })
888}
889
890fn create_eth_new_filter_test() -> RpcTestScenario {
891 RpcTestScenario::basic(|client| async move {
892 const BLOCK_RANGE: u64 = 200;
893
894 let last_block = client.call(EthBlockNumber::request(())?).await?;
895
896 let filter_spec = EthFilterSpec {
897 from_block: Some(EthUint64(last_block.0.saturating_sub(BLOCK_RANGE)).to_hex_string()),
898 to_block: Some(last_block.to_hex_string()),
899 ..Default::default()
900 };
901
902 let filter_id = client.call(EthNewFilter::request((filter_spec,))?).await?;
903
904 let removed = client
905 .call(EthUninstallFilter::request((filter_id.clone(),))?)
906 .await?;
907 anyhow::ensure!(removed);
908
909 let removed = client
910 .call(EthUninstallFilter::request((filter_id,))?)
911 .await?;
912 anyhow::ensure!(!removed);
913
914 Ok(())
915 })
916}
917
918fn create_eth_new_filter_limit_test(count: usize) -> RpcTestScenario {
919 RpcTestScenario::basic(move |client| async move {
920 const BLOCK_RANGE: u64 = 200;
921
922 let last_block = client.call(EthBlockNumber::request(())?).await?;
923
924 let filter_spec = EthFilterSpec {
925 from_block: Some(format!("0x{:x}", last_block.0.saturating_sub(BLOCK_RANGE))),
926 to_block: Some(last_block.to_hex_string()),
927 ..Default::default()
928 };
929
930 let mut ids = vec![];
931
932 for _ in 0..count {
933 let result = client
934 .call(EthNewFilter::request((filter_spec.clone(),))?)
935 .await;
936
937 match result {
938 Ok(filter_id) => ids.push(filter_id),
939 Err(e) => {
940 for id in ids {
942 let removed = client.call(EthUninstallFilter::request((id,))?).await?;
943 anyhow::ensure!(removed);
944 }
945 anyhow::bail!(e)
946 }
947 }
948 }
949
950 for id in ids {
951 let removed = client.call(EthUninstallFilter::request((id,))?).await?;
952 anyhow::ensure!(removed);
953 }
954
955 Ok(())
956 })
957}
958
959fn eth_new_block_filter() -> RpcTestScenario {
960 RpcTestScenario::basic(move |client| async move {
961 async fn process_filter(client: &rpc::Client, filter_id: &FilterID) -> anyhow::Result<()> {
962 let filter_result = client
963 .call(EthGetFilterChanges::request((filter_id.clone(),))?)
964 .await?;
965
966 if let EthFilterResult::Hashes(prev_hashes) = filter_result {
967 let verify_hashes = async |hashes: &[EthHash]| -> anyhow::Result<()> {
968 for hash in hashes {
969 let _block = client
970 .call(EthGetBlockByHash::request((*hash, false))?)
971 .await?;
972 }
973 Ok(())
974 };
975 verify_hashes(&prev_hashes).await?;
976
977 next_tipset(client).await?;
979
980 let filter_result = client
981 .call(EthGetFilterChanges::request((filter_id.clone(),))?)
982 .await?;
983
984 if let EthFilterResult::Hashes(hashes) = filter_result {
985 verify_hashes(&hashes).await?;
986 anyhow::ensure!(
987 (prev_hashes.is_empty() && hashes.is_empty()) || prev_hashes != hashes,
988 );
989
990 Ok(())
991 } else {
992 Err(anyhow::anyhow!("expecting blocks"))
993 }
994 } else {
995 Err(anyhow::anyhow!("expecting blocks"))
996 }
997 }
998
999 let mut retries = 5;
1000 loop {
1001 let filter_id = client.call(EthNewBlockFilter::request(())?).await?;
1003
1004 let result = match process_filter(&client, &filter_id).await {
1005 Ok(()) => Ok(()),
1006 Err(e) if retries != 0 && e.to_string().contains("revert") => {
1007 let removed = client
1009 .call(EthUninstallFilter::request((filter_id,))?)
1010 .await?;
1011 anyhow::ensure!(removed);
1012
1013 retries -= 1;
1014 continue;
1015 }
1016 Err(e) => Err(e),
1017 };
1018
1019 let removed = client
1021 .call(EthUninstallFilter::request((filter_id,))?)
1022 .await?;
1023 anyhow::ensure!(removed);
1024
1025 break result;
1026 }
1027 })
1028}
1029
1030fn eth_new_pending_transaction_filter(tx: TestTransaction) -> RpcTestScenario {
1031 RpcTestScenario::basic(move |client| {
1032 let tx = tx.clone();
1033 async move {
1034 let filter_id = client
1035 .call(EthNewPendingTransactionFilter::request(())?)
1036 .await?;
1037
1038 let filter_result = client
1039 .call(EthGetFilterChanges::request((filter_id.clone(),))?)
1040 .await?;
1041
1042 let result = if let EthFilterResult::Hashes(prev_hashes) = filter_result {
1043 let cid = invoke_contract(&client, &tx).await?;
1044 let tx_hash = client
1045 .call(EthGetTransactionHashByCid::request((cid,))?)
1046 .await?
1047 .context("no Eth transaction hash for CID")?;
1048
1049 wait_in_mempool(&client, cid).await?;
1052 let hashes = poll_pending_filter_until(&client, &filter_id, &tx_hash).await?;
1053
1054 anyhow::ensure!(
1055 prev_hashes != hashes,
1056 "prev_hashes={prev_hashes:?} hashes={hashes:?}"
1057 );
1058 anyhow::ensure!(
1059 hashes.contains(&tx_hash),
1060 "transaction hash missing from filter results: tx_hash={tx_hash:?} cid={cid:?} hashes={hashes:?}"
1061 );
1062 Ok(())
1063 } else {
1064 Err(anyhow::anyhow!("expecting transactions"))
1065 };
1066
1067 let removed = client
1068 .call(EthUninstallFilter::request((filter_id,))?)
1069 .await?;
1070 anyhow::ensure!(removed);
1071
1072 result
1073 }
1074 })
1075}
1076
1077fn eth_new_pending_transaction_filter_multi_poll(tx: TestTransaction) -> RpcTestScenario {
1086 RpcTestScenario::basic(move |client| {
1087 let tx = tx.clone();
1088 async move {
1089 let filter_id = client
1090 .call(EthNewPendingTransactionFilter::request(())?)
1091 .await?;
1092
1093 let result = async {
1094 let _ = client
1096 .call(EthGetFilterChanges::request((filter_id.clone(),))?)
1097 .await?;
1098
1099 let cid_a = invoke_contract(&client, &tx).await?;
1101 let hash_a = client
1102 .call(EthGetTransactionHashByCid::request((cid_a,))?)
1103 .await?
1104 .context("no Eth transaction hash for cid_a")?;
1105 wait_in_mempool(&client, cid_a).await?;
1106 poll_pending_filter_until(&client, &filter_id, &hash_a).await?;
1107
1108 let cid_b = invoke_contract(&client, &tx).await?;
1111 let hash_b = client
1112 .call(EthGetTransactionHashByCid::request((cid_b,))?)
1113 .await?
1114 .context("no Eth transaction hash for cid_b")?;
1115 wait_in_mempool(&client, cid_b).await?;
1116 let hashes_b = poll_pending_filter_until(&client, &filter_id, &hash_b).await?;
1117 anyhow::ensure!(
1118 !hashes_b.contains(&hash_a),
1119 "second poll should not return previously-consumed tx_a: \
1120 hash_a={hash_a:?} hashes={hashes_b:?}"
1121 );
1122
1123 anyhow::Ok(())
1124 }
1125 .await;
1126
1127 let removed = client
1128 .call(EthUninstallFilter::request((filter_id,))?)
1129 .await?;
1130 anyhow::ensure!(removed);
1131
1132 result
1133 }
1134 })
1135}
1136
1137fn as_logs(input: EthFilterResult) -> EthFilterResult {
1138 match input {
1139 EthFilterResult::Hashes(vec) if vec.is_empty() => EthFilterResult::Logs(Vec::new()),
1140 other => other,
1141 }
1142}
1143
1144fn eth_get_filter_logs(tx: TestTransaction) -> RpcTestScenario {
1145 RpcTestScenario::basic(move |client| {
1146 let tx = tx.clone();
1147 async move {
1148 const BLOCK_RANGE: u64 = 1;
1149
1150 let tipset = client.call(ChainHead::request(())?).await?;
1151 let cid = invoke_contract(&client, &tx).await?;
1152 let lookup = client
1153 .call(
1154 StateWaitMsg::request((cid, 1, tipset.epoch(), true))?
1155 .with_timeout(Duration::from_secs(300)),
1156 )
1157 .await?;
1158 let block_num = EthUint64(lookup.height as u64);
1159
1160 let topics = EthTopicSpec(vec![EthHashList::Single(Some(tx.topic))]);
1161 let filter_spec = EthFilterSpec {
1162 from_block: Some(format!("0x{:x}", block_num.0.saturating_sub(BLOCK_RANGE))),
1163 topics: Some(topics),
1164 ..Default::default()
1165 };
1166
1167 let filter_id = client
1168 .call(EthNewFilter::request((filter_spec.clone(),))?)
1169 .await?;
1170 let filter_result = as_logs(
1171 client
1172 .call(EthGetFilterLogs::request((filter_id.clone(),))?)
1173 .await?,
1174 );
1175 let result = if let EthFilterResult::Logs(logs) = filter_result {
1176 anyhow::ensure!(
1177 !logs.is_empty(),
1178 "Empty logs: filter_spec={filter_spec:?} cid={cid:?}",
1179 );
1180 Ok(())
1181 } else {
1182 Err(anyhow::anyhow!("expecting logs"))
1183 };
1184
1185 let removed = client
1186 .call(EthUninstallFilter::request((filter_id,))?)
1187 .await?;
1188 anyhow::ensure!(removed);
1189
1190 result
1191 }
1192 })
1193}
1194
1195const LOTUS_EVENTS_MAXFILTERS: usize = 100;
1196
1197macro_rules! with_methods {
1198 ( $builder:expr, $( $method:ty ),+ ) => {{
1199 let mut b = $builder;
1200 $(
1201 b = b.using::<{ <$method>::N_REQUIRED_PARAMS }, $method>();
1202 )+
1203 b
1204 }};
1205}
1206
1207pub(super) async fn create_tests(tx: TestTransaction) -> Vec<RpcTestScenario> {
1208 vec![
1209 with_methods!(
1210 create_eth_new_filter_test().name("eth_newFilter install/uninstall"),
1211 EthNewFilter,
1212 EthUninstallFilter
1213 ),
1214 with_methods!(
1215 create_eth_new_filter_limit_test(20).name("eth_newFilter under limit"),
1216 EthNewFilter,
1217 EthUninstallFilter
1218 ),
1219 with_methods!(
1220 create_eth_new_filter_limit_test(LOTUS_EVENTS_MAXFILTERS)
1221 .name("eth_newFilter just under limit"),
1222 EthNewFilter,
1223 EthUninstallFilter
1224 ),
1225 with_methods!(
1226 create_eth_new_filter_limit_test(LOTUS_EVENTS_MAXFILTERS + 1)
1227 .name("eth_newFilter over limit")
1228 .should_fail_with("maximum number of filters registered"),
1229 EthNewFilter,
1230 EthUninstallFilter
1231 ),
1232 with_methods!(
1233 eth_new_block_filter().name("eth_newBlockFilter works"),
1234 EthNewBlockFilter,
1235 EthGetFilterChanges,
1236 EthUninstallFilter
1237 ),
1238 with_methods!(
1239 eth_new_pending_transaction_filter(tx.clone())
1240 .name("eth_newPendingTransactionFilter works"),
1241 EthNewPendingTransactionFilter,
1242 EthGetFilterChanges,
1243 EthGetTransactionHashByCid,
1244 EthUninstallFilter
1245 ),
1246 with_methods!(
1247 eth_new_pending_transaction_filter_multi_poll(tx.clone())
1248 .name("eth_getFilterChanges returns only new pending txs per poll"),
1249 EthNewPendingTransactionFilter,
1250 EthGetFilterChanges,
1251 EthGetTransactionHashByCid,
1252 EthUninstallFilter
1253 ),
1254 with_methods!(
1255 eth_get_filter_logs(tx.clone()).name("eth_getFilterLogs works"),
1256 EthNewFilter,
1257 EthGetFilterLogs,
1258 EthUninstallFilter
1259 ),
1260 with_methods!(
1261 eth_subscribe_new_heads().name("eth_subscribe newHeads works"),
1262 EthSubscribe,
1263 EthUnsubscribe
1264 ),
1265 with_methods!(
1266 eth_subscribe_pending_transactions(tx.clone())
1267 .name("eth_subscribe pendingTransactions works"),
1268 EthSubscribe,
1269 EthUnsubscribe,
1270 EthGetTransactionHashByCid
1271 ),
1272 with_methods!(
1273 eth_subscribe_logs(tx.clone()).name("eth_subscribe logs filter matrix"),
1274 EthSubscribe,
1275 EthUnsubscribe,
1276 EthGetTransactionHashByCid
1277 ),
1278 ]
1279}