1use crate::blocks::Tipset;
5use crate::cli::humantoken;
6use crate::cli_shared::cli::FeeConfig;
7use crate::lotus_json::{HasLotusJson as _, NotNullVec};
8use crate::message::{MessageRead as _, SignedMessage};
9use crate::message_pool::compute_rbf;
10use crate::rpc::gas::cap_gas_fee;
11use crate::rpc::{self, prelude::*, types::ApiTipsetKey, types::MessageSendSpec};
12use crate::shim::address::StrictAddress;
13use crate::shim::message::{METHOD_SEND, Message};
14use crate::shim::{address::Address, econ::TokenAmount, percent::Percent};
15
16use ahash::{HashMap, HashSet};
17use anyhow::Context as _;
18use cid::Cid;
19use clap::Subcommand;
20use fvm_ipld_encoding::RawBytes;
21use num::BigInt;
22use std::ops::Range;
23
24#[derive(Debug, Subcommand)]
25pub enum MpoolCommands {
26 Pending {
28 #[arg(long)]
30 local: bool,
31 #[arg(long)]
33 cids: bool,
34 #[arg(long)]
36 to: Option<StrictAddress>,
37 #[arg(long)]
39 from: Option<StrictAddress>,
40 },
41 Nonce {
43 address: StrictAddress,
45 },
46 Stat {
48 #[arg(long, default_value = "60")]
50 basefee_lookback: u32,
51 #[arg(long)]
53 local: bool,
54 },
55 NonceFix {
57 #[arg(long)]
59 addr: StrictAddress,
60 #[arg(long, conflicts_with_all = ["start", "end"])]
62 auto: bool,
63 #[arg(long, required_unless_present = "auto")]
65 start: Option<u64>,
66 #[arg(long, required_unless_present = "auto")]
68 end: Option<u64>,
69 #[arg(long, value_parser = humantoken::parse)]
71 gas_fee_cap: Option<TokenAmount>,
72 },
73 Replace {
75 #[arg(long, required_unless_present = "cid")]
77 from: Option<StrictAddress>,
78 #[arg(long, required_unless_present = "cid")]
80 nonce: Option<u64>,
81 #[arg(long, conflicts_with_all = ["from", "nonce"])]
83 cid: Option<Cid>,
84 #[arg(long, conflicts_with_all = ["gas_premium", "gas_feecap", "gas_limit"])]
86 auto: bool,
87 #[arg(long, value_parser = humantoken::parse, alias = "fee-limit", requires = "auto")]
89 max_fee: Option<TokenAmount>,
90 #[arg(long, value_parser = humantoken::parse, required_unless_present = "auto")]
92 gas_premium: Option<TokenAmount>,
93 #[arg(long, value_parser = humantoken::parse, required_unless_present = "auto")]
95 gas_feecap: Option<TokenAmount>,
96 #[arg(long, conflicts_with = "auto")]
98 gas_limit: Option<u64>,
99 },
100}
101
102fn filter_messages(
103 messages: Vec<SignedMessage>,
104 local_addrs: Option<HashSet<Address>>,
105 to: Option<&StrictAddress>,
106 from: Option<&StrictAddress>,
107) -> anyhow::Result<Vec<SignedMessage>> {
108 let filtered = messages
109 .into_iter()
110 .filter(|msg| {
111 local_addrs
112 .as_ref()
113 .map(|addrs| addrs.contains(&msg.from()))
114 .unwrap_or(true)
115 && to.map(|addr| msg.to() == (*addr).into()).unwrap_or(true)
116 && from
117 .map(|addr| msg.from() == (*addr).into())
118 .unwrap_or(true)
119 })
120 .collect();
121
122 Ok(filtered)
123}
124
125fn auto_fill_range(
126 addr: Address,
127 next_on_chain_nonce: u64,
128 pending: &[SignedMessage],
129) -> Option<Range<u64>> {
130 let pending_nonce = pending
131 .iter()
132 .filter(|m| m.from() == addr)
133 .map(|m| m.sequence())
134 .filter(|&seq| seq >= next_on_chain_nonce)
135 .min()?;
136
137 if pending_nonce == next_on_chain_nonce {
138 return None;
139 }
140
141 Some(next_on_chain_nonce..pending_nonce)
142}
143
144fn manual_fill_range(start: Option<u64>, end: Option<u64>) -> anyhow::Result<Range<u64>> {
145 let start = start.context("manual mode requires --start")?;
146 let end = end.context("manual mode requires --end")?;
147 anyhow::ensure!(end > start, "--end must be greater than --start");
148 Ok(start..end)
149}
150
151fn get_gas_fee_cap(gas_fee_cap: Option<TokenAmount>, parent_base_fee: TokenAmount) -> TokenAmount {
152 gas_fee_cap.unwrap_or_else(|| parent_base_fee * 2u64)
153}
154
155fn find_pending_message(
156 from: Address,
157 nonce: u64,
158 pending: &[SignedMessage],
159) -> anyhow::Result<SignedMessage> {
160 pending
161 .iter()
162 .find(|m| m.from() == from && m.sequence() == nonce)
163 .cloned()
164 .with_context(|| format!("no pending message found from {from} with nonce {nonce}"))
165}
166
167fn auto_compute_replacement_gas(
168 mut estimated_msg: Message,
169 original_premium: TokenAmount,
170 replace_by_fee_ratio: Percent,
171) -> anyhow::Result<Message> {
172 let min_premium = compute_rbf(&original_premium, replace_by_fee_ratio);
173 if estimated_msg.gas_premium < min_premium {
174 estimated_msg.gas_premium = min_premium;
175 }
176 if estimated_msg.gas_fee_cap < estimated_msg.gas_premium {
177 estimated_msg.gas_fee_cap = estimated_msg.gas_premium.clone();
178 }
179 Ok(estimated_msg)
180}
181
182fn manual_compute_replacement_gas(
183 gas_premium: TokenAmount,
184 gas_feecap: TokenAmount,
185 gas_limit: Option<u64>,
186 mut original_msg: Message,
187) -> anyhow::Result<Message> {
188 original_msg.gas_premium = gas_premium;
189 original_msg.gas_fee_cap = gas_feecap;
190 if let Some(limit) = gas_limit {
191 original_msg.gas_limit = limit;
192 }
193 Ok(original_msg)
194}
195
196async fn get_actor_sequence(
197 message: &Message,
198 tipset: &Tipset,
199 client: &rpc::Client,
200) -> Option<u64> {
201 let address = message.from;
202 let get_actor_result = StateGetActor::call(client, (address, tipset.key().into())).await;
203 let actor_state = match get_actor_result {
204 Ok(maybe_actor) => {
205 if let Some(state) = maybe_actor {
206 state
207 } else {
208 println!("{address}, actor state not found");
209 return None;
210 }
211 }
212 Err(err) => {
213 println!("{address}, err: {err}");
214 return None;
215 }
216 };
217
218 Some(actor_state.sequence)
219}
220
221type StatBucket = HashMap<u64, Message>;
222
223#[derive(Debug, Default, Eq, PartialEq)]
224struct MpStat {
225 address: String,
226 past: u64,
227 current: u64,
228 future: u64,
229 below_current: u64,
230 below_past: u64,
231 gas_limit: BigInt,
232}
233
234fn compute_stats(
235 messages: &[Message],
236 actor_sequences: HashMap<Address, u64>,
237 curr_base_fee: TokenAmount,
238 min_base_fee: TokenAmount,
239) -> Vec<MpStat> {
240 let mut buckets = HashMap::<Address, StatBucket>::default();
241 for msg in messages {
242 buckets
243 .entry(msg.from)
244 .or_insert(StatBucket::default())
245 .insert(msg.sequence, msg.to_owned());
246 }
247
248 let mut stats: Vec<MpStat> = Vec::with_capacity(buckets.len());
249
250 for (address, bucket) in buckets {
251 let actor_sequence = *actor_sequences.get(&address).expect("get must succeed");
252
253 let mut curr_sequence = actor_sequence;
254 while bucket.contains_key(&curr_sequence) {
255 curr_sequence += 1;
256 }
257
258 let mut stat = MpStat {
259 address: address.to_string(),
260 ..Default::default()
261 };
262
263 for (_, msg) in bucket {
264 if msg.sequence < actor_sequence {
265 stat.past += 1;
266 } else if msg.sequence > curr_sequence {
267 stat.future += 1;
268 } else {
269 stat.current += 1;
270 }
271
272 if msg.gas_fee_cap < curr_base_fee {
273 stat.below_current += 1;
274 }
275 if msg.gas_fee_cap < min_base_fee {
276 stat.below_past += 1;
277 }
278
279 stat.gas_limit += msg.gas_limit;
280 }
281
282 stats.push(stat);
283 }
284
285 stats.sort_by(|m1, m2| m1.address.cmp(&m2.address));
286 stats
287}
288
289fn print_stats(stats: &[MpStat], basefee_lookback: u32) {
290 let mut total = MpStat::default();
291
292 for stat in stats {
293 total.past += stat.past;
294 total.current += stat.current;
295 total.future += stat.future;
296 total.below_current += stat.below_current;
297 total.below_past += stat.below_past;
298 total.gas_limit += &stat.gas_limit;
299
300 println!(
301 "{}: Nonce past: {}, cur: {}, future: {}; FeeCap cur: {}, min-{}: {}, gasLimit: {}",
302 stat.address,
303 stat.past,
304 stat.current,
305 stat.future,
306 stat.below_current,
307 basefee_lookback,
308 stat.below_past,
309 stat.gas_limit
310 );
311 }
312
313 println!("-----");
314 println!(
315 "total: Nonce past: {}, cur: {}, future: {}; FeeCap cur: {}, min-{}: {}, gasLimit: {}",
316 total.past,
317 total.current,
318 total.future,
319 total.below_current,
320 basefee_lookback,
321 total.below_past,
322 total.gas_limit
323 );
324}
325
326impl MpoolCommands {
327 pub async fn run(self, client: rpc::Client) -> anyhow::Result<()> {
328 match self {
329 Self::Pending {
330 local,
331 cids,
332 to,
333 from,
334 } => {
335 let NotNullVec(messages) =
336 MpoolPending::call(&client, (ApiTipsetKey(None),)).await?;
337
338 let local_addrs = if local {
339 let response = WalletList::call(&client, ()).await?;
340 Some(HashSet::from_iter(response))
341 } else {
342 None
343 };
344
345 let filtered_messages =
346 filter_messages(messages, local_addrs, to.as_ref(), from.as_ref())?;
347
348 for msg in filtered_messages {
349 if cids {
350 println!("{}", msg.cid());
351 } else {
352 println!("{}", msg.into_lotus_json_string_pretty()?);
353 }
354 }
355
356 Ok(())
357 }
358 Self::Stat {
359 basefee_lookback,
360 local,
361 } => {
362 let tipset = ChainHead::call(&client, ()).await?;
363 let curr_base_fee = tipset.block_headers().first().parent_base_fee.to_owned();
364
365 let (atto_str, NotNullVec(messages)) = tokio::try_join!(
366 ChainGetMinBaseFee::call(&client, (basefee_lookback,)),
367 MpoolPending::call(&client, (ApiTipsetKey(None),)),
368 )?;
369 let min_base_fee = TokenAmount::from_atto(atto_str.parse::<BigInt>()?);
370
371 let local_addrs = if local {
372 let response = WalletList::call(&client, ()).await?;
373 Some(HashSet::from_iter(response))
374 } else {
375 None
376 };
377
378 let messages: Vec<Message> = filter_messages(messages, local_addrs, None, None)?
379 .into_iter()
380 .map(|it| it.message)
381 .collect();
382
383 let mut actor_sequences: HashMap<Address, u64> = HashMap::default();
384 for msg in messages.iter() {
385 if let Some(sequence) = get_actor_sequence(msg, &tipset, &client).await {
386 actor_sequences.insert(msg.from, sequence);
387 }
388 }
389
390 let stats = compute_stats(&messages, actor_sequences, curr_base_fee, min_base_fee);
391
392 print_stats(&stats, basefee_lookback);
393
394 Ok(())
395 }
396 Self::Nonce { address } => {
397 let nonce = MpoolGetNonce::call(&client, (address.into(),)).await?;
398 println!("{nonce}");
399
400 Ok(())
401 }
402 Self::NonceFix {
403 addr,
404 auto,
405 start,
406 end,
407 gas_fee_cap,
408 } => {
409 let addr: Address = addr.into();
410
411 let fill_range = if auto {
412 let (actor, NotNullVec(pending)) = tokio::try_join!(
413 StateGetActor::call(&client, (addr, ApiTipsetKey(None))),
414 MpoolPending::call(&client, (ApiTipsetKey(None),)),
415 )?;
416 let next_nonce = actor
417 .with_context(|| format!("no on-chain actor found for {addr}"))?
418 .sequence;
419 auto_fill_range(addr, next_nonce, &pending)
420 } else {
421 Some(manual_fill_range(start, end)?)
422 };
423
424 let Some(fill_range) = fill_range else {
425 println!("No nonce gap found");
426 return Ok(());
427 };
428
429 let tipset = ChainHead::call(&client, ()).await?;
430 let parent_base_fee = tipset.block_headers().first().parent_base_fee.clone();
431 let fee_cap = get_gas_fee_cap(gas_fee_cap, parent_base_fee);
432 let n = fill_range.end.saturating_sub(fill_range.start);
433 println!(
434 "Creating {n} filler messages ({} ~ {})",
435 fill_range.start, fill_range.end
436 );
437
438 for sequence in fill_range {
439 let msg = Message {
440 version: 0,
441 from: addr,
442 to: addr,
443 sequence,
444 value: TokenAmount::default(),
445 method_num: METHOD_SEND,
446 params: RawBytes::new(vec![]),
447 gas_limit: 1_000_000,
448 gas_fee_cap: fee_cap.clone(),
449 gas_premium: TokenAmount::from_atto(5u64),
450 };
451 let smsg = WalletSignMessage::call(&client, (addr, msg)).await?;
452 MpoolPush::call(&client, (smsg,)).await?;
453 }
454
455 Ok(())
456 }
457 Self::Replace {
458 from,
459 nonce,
460 cid,
461 auto,
462 max_fee,
463 gas_premium,
464 gas_feecap,
465 gas_limit,
466 } => {
467 let (sender, sequence) = if let Some(msg_cid) = cid {
468 let api_msg = ChainGetMessage::call(&client, (msg_cid,)).await?;
469 (api_msg.from, api_msg.sequence)
470 } else {
471 let sender: Address = from
472 .context("--from is required when --cid is not provided")?
473 .into();
474 let seq = nonce.context("--nonce is required when --cid is not provided")?;
475 (sender, seq)
476 };
477
478 let tipset = ChainHead::call(&client, ()).await?;
479 let tsk = ApiTipsetKey(Some(tipset.key().clone()));
480
481 let NotNullVec(pending) = MpoolPending::call(&client, (tsk,)).await?;
482 let found = find_pending_message(sender, sequence, &pending)?;
483 let original_msg = found.into_message();
484
485 let msg_send_spec = Some(MessageSendSpec {
486 max_fee: max_fee.unwrap_or_default(),
487 msg_uuid: uuid::Uuid::nil(),
488 maximize_fee_cap: false,
489 });
490
491 let replacement = if auto {
492 let cfg = MpoolGetConfig::call(&client, ()).await?;
493
494 let mut msg_for_estimate = original_msg.clone();
495 msg_for_estimate.gas_fee_cap = TokenAmount::default();
499 msg_for_estimate.gas_premium = TokenAmount::default();
500
501 let estimated_msg = GasEstimateMessageGas::call(
502 &client,
503 (msg_for_estimate, msg_send_spec.clone(), ApiTipsetKey(None)),
504 )
505 .await?;
506
507 let mut replacement = auto_compute_replacement_gas(
508 estimated_msg,
509 original_msg.gas_premium,
510 cfg.replace_by_fee_ratio,
511 )?;
512 cap_gas_fee(
513 &FeeConfig::default().max_fee,
514 &mut replacement,
515 msg_send_spec,
516 )?;
517 replacement
518 } else {
519 let gas_premium =
520 gas_premium.context("--gas-premium is required unless --auto is set")?;
521 let gas_feecap =
522 gas_feecap.context("--gas-feecap is required unless --auto is set")?;
523 manual_compute_replacement_gas(
524 gas_premium,
525 gas_feecap,
526 gas_limit,
527 original_msg,
528 )?
529 };
530
531 let smsg = WalletSignMessage::call(&client, (sender, replacement)).await?;
532 let new_cid = MpoolPush::call(&client, (smsg,)).await?;
533 println!("new message cid: {new_cid}");
534
535 Ok(())
536 }
537 }
538 }
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544 use crate::key_management::{KeyStore, KeyStoreConfig, Wallet};
545 use crate::message_pool::REPLACE_BY_FEE_RATIO_DEFAULT;
546 use crate::message_pool::tests::create_smsg;
547 use crate::shim::crypto::SignatureType;
548 use itertools::Itertools as _;
549 use rstest::rstest;
550 use std::borrow::BorrowMut;
551
552 #[test]
553 fn message_filtering_none() {
554 let keystore = KeyStore::new(KeyStoreConfig::Memory).unwrap();
555 let mut wallet = Wallet::new(keystore);
556 let sender = wallet.generate_addr(SignatureType::Secp256k1).unwrap();
557 let target = wallet.generate_addr(SignatureType::Secp256k1).unwrap();
558
559 let mut smsg_vec = Vec::new();
560 for i in 0..4 {
561 let msg = create_smsg(&target, &sender, wallet.borrow_mut(), i as u64, 1000000, 1);
562 smsg_vec.push(msg);
563 }
564
565 let smsg_json_vec = smsg_vec.clone().into_iter().collect_vec();
566
567 let smsg_filtered: Vec<SignedMessage> = filter_messages(smsg_json_vec, None, None, None)
569 .unwrap()
570 .into_iter()
571 .collect();
572
573 assert_eq!(smsg_vec, smsg_filtered);
574 }
575
576 #[test]
577 fn message_filtering_local() {
578 let keystore = KeyStore::new(KeyStoreConfig::Memory).unwrap();
579 let mut wallet = Wallet::new(keystore);
580 let sender = wallet.generate_addr(SignatureType::Secp256k1).unwrap();
581 let target = wallet.generate_addr(SignatureType::Secp256k1).unwrap();
582
583 let mut smsg_vec = Vec::new();
584 for i in 0..4 {
585 let msg = create_smsg(&target, &sender, wallet.borrow_mut(), i as u64, 1000000, 1);
586 smsg_vec.push(msg);
587 }
588
589 let ext_keystore = KeyStore::new(KeyStoreConfig::Memory).unwrap();
591 let mut ext_wallet = Wallet::new(ext_keystore);
592 let ext_sender = ext_wallet.generate_addr(SignatureType::Secp256k1).unwrap();
593 let ext_target = ext_wallet.generate_addr(SignatureType::Secp256k1).unwrap();
594
595 let msg = create_smsg(
596 &ext_target,
597 &ext_sender,
598 ext_wallet.borrow_mut(),
599 4,
600 1000000,
601 1,
602 );
603 smsg_vec.push(msg);
604
605 let smsg_json_vec: Vec<SignedMessage> = smsg_vec.clone().into_iter().collect();
606 let local_addrs = HashSet::from_iter(wallet.list_addrs().unwrap());
607
608 let smsg_filtered: Vec<SignedMessage> =
610 filter_messages(smsg_json_vec, Some(local_addrs), None, None)
611 .unwrap()
612 .into_iter()
613 .collect();
614
615 for smsg in smsg_filtered.iter() {
616 assert_eq!(smsg.from(), sender);
617 }
618 }
619
620 #[test]
621 fn message_filtering_from() {
622 let keystore = KeyStore::new(KeyStoreConfig::Memory).unwrap();
623 let mut wallet = Wallet::new(keystore);
624 let sender = wallet.generate_addr(SignatureType::Secp256k1).unwrap();
625 let target = wallet.generate_addr(SignatureType::Secp256k1).unwrap();
626
627 let mut smsg_vec = Vec::new();
628 for i in 0..4 {
629 let msg = create_smsg(&target, &sender, wallet.borrow_mut(), i as u64, 1000000, 1);
630 smsg_vec.push(msg);
631 }
632
633 let sender2 = wallet.generate_addr(SignatureType::Secp256k1).unwrap();
635
636 let msg = create_smsg(&target, &sender2, wallet.borrow_mut(), 4, 1000000, 1);
637 smsg_vec.push(msg);
638
639 let smsg_json_vec: Vec<SignedMessage> = smsg_vec.clone().into_iter().collect();
640
641 let smsg_filtered: Vec<SignedMessage> =
643 filter_messages(smsg_json_vec, None, None, Some(&sender2.into()))
644 .unwrap()
645 .into_iter()
646 .collect();
647
648 for smsg in smsg_filtered.iter() {
649 assert_eq!(smsg.from(), sender2);
650 }
651 }
652
653 #[test]
654 fn message_filtering_to() {
655 let keystore = KeyStore::new(KeyStoreConfig::Memory).unwrap();
656 let mut wallet = Wallet::new(keystore);
657 let sender = wallet.generate_addr(SignatureType::Secp256k1).unwrap();
658 let target = wallet.generate_addr(SignatureType::Secp256k1).unwrap();
659
660 let mut smsg_vec = Vec::new();
661 for i in 0..4 {
662 let msg = create_smsg(&target, &sender, wallet.borrow_mut(), i as u64, 1000000, 1);
663 smsg_vec.push(msg);
664 }
665
666 let target2 = wallet.generate_addr(SignatureType::Secp256k1).unwrap();
668
669 let msg = create_smsg(&target2, &sender, wallet.borrow_mut(), 4, 1000000, 1);
670 smsg_vec.push(msg);
671
672 let smsg_json_vec: Vec<SignedMessage> = smsg_vec.clone().into_iter().collect();
673
674 let smsg_filtered: Vec<SignedMessage> =
676 filter_messages(smsg_json_vec, None, Some(&target2.into()), None)
677 .unwrap()
678 .into_iter()
679 .collect();
680
681 for smsg in smsg_filtered.iter() {
682 assert_eq!(smsg.to(), target2);
683 }
684 }
685
686 struct TestAddrs {
687 addr: Address,
688 target: Address,
689 other: Address,
690 }
691
692 fn test_wallet() -> (Wallet, TestAddrs) {
693 let keystore = KeyStore::new(KeyStoreConfig::Memory).unwrap();
694 let mut wallet = Wallet::new(keystore);
695 let addr = wallet.generate_addr(SignatureType::Secp256k1).unwrap();
696 let target = wallet.generate_addr(SignatureType::Secp256k1).unwrap();
697 let other = wallet.generate_addr(SignatureType::Secp256k1).unwrap();
698 (
699 wallet,
700 TestAddrs {
701 addr,
702 target,
703 other,
704 },
705 )
706 }
707
708 fn pending_from(
709 wallet: &mut Wallet,
710 target: &Address,
711 from: &Address,
712 nonces: &[u64],
713 ) -> Vec<SignedMessage> {
714 nonces
715 .iter()
716 .map(|&nonce| create_smsg(target, from, wallet.borrow_mut(), nonce, 1_000_000, 1))
717 .collect()
718 }
719
720 fn make_test_message(
721 from: Address,
722 to: Address,
723 nonce: u64,
724 gas_limit: u64,
725 gas_premium: u64,
726 gas_fee_cap: u64,
727 ) -> Message {
728 Message {
729 version: 0,
730 from,
731 to,
732 sequence: nonce,
733 value: TokenAmount::default(),
734 method_num: METHOD_SEND,
735 params: RawBytes::new(vec![]),
736 gas_limit,
737 gas_fee_cap: TokenAmount::from_atto(gas_fee_cap),
738 gas_premium: TokenAmount::from_atto(gas_premium),
739 }
740 }
741
742 #[rstest]
743 #[case::empty_pool(0, &[], None, None)]
744 #[case::wrong_sender(5, &[], Some(10), None)]
745 #[case::gap(5, &[7], None, Some(5..7))]
746 #[case::min_pending_nonce(5, &[10, 8], None, Some(5..8))]
747 #[case::next_nonce_in_mpool(5, &[5], None, None)]
748 #[case::ignores_stale_pending(5, &[3, 9], None, Some(5..9))]
749 #[case::only_stale_pending(5, &[3], None, None)]
750 fn nonce_fix_fill_range_auto(
751 #[case] next_on_chain: u64,
752 #[case] addr_nonces: &[u64],
753 #[case] other_sender_nonce: Option<u64>,
754 #[case] expected: Option<Range<u64>>,
755 ) {
756 let (mut wallet, addrs) = test_wallet();
757 let mut pending = pending_from(&mut wallet, &addrs.target, &addrs.addr, addr_nonces);
758 if let Some(nonce) = other_sender_nonce {
759 pending.push(create_smsg(
760 &addrs.target,
761 &addrs.other,
762 wallet.borrow_mut(),
763 nonce,
764 1_000_000,
765 1,
766 ));
767 }
768 assert_eq!(
769 auto_fill_range(addrs.addr, next_on_chain, &pending),
770 expected
771 );
772 }
773
774 #[rstest]
775 #[case::missing_start(None, Some(10), Err("manual mode requires --start"))]
776 #[case::missing_end(Some(1), None, Err("manual mode requires --end"))]
777 #[case::equal(Some(5), Some(5), Err("--end must be greater than --start"))]
778 #[case::reversed(Some(5), Some(3), Err("--end must be greater than --start"))]
779 #[case::ok(Some(2), Some(5), Ok(2..5))]
780 fn nonce_fix_fill_range_manual(
781 #[case] start: Option<u64>,
782 #[case] end: Option<u64>,
783 #[case] expected: Result<Range<u64>, &str>,
784 ) {
785 let got = manual_fill_range(start, end);
786 match expected {
787 Ok(want) => assert_eq!(got.unwrap(), want),
788 Err(want) => assert!(got.unwrap_err().to_string().contains(want)),
789 }
790 }
791
792 #[test]
793 fn nonce_fix_gas_fee_cap() {
794 let parent = TokenAmount::from_atto(100u64);
795 assert_eq!(get_gas_fee_cap(None, parent.clone()), parent.clone() * 2u64);
796 assert_eq!(
797 get_gas_fee_cap(Some(TokenAmount::from_atto(42u64)), parent),
798 TokenAmount::from_atto(42u64)
799 );
800 }
801
802 #[test]
803 fn compute_statistics() {
804 use crate::shim::message::Message;
805 use fvm_ipld_encoding::RawBytes;
806 use std::str::FromStr;
807
808 let addr0 = Address::from_str("t3urxivigpzih5f6ih3oq3lr2jlunw3m5oehbe5efts4ub5wy2oi4fbo5cw7333a4rrffo5535tjdq24wkc2aa").unwrap();
809 let addr1 = Address::from_str("t410fot3vkzzorqg4alowvghvxx4mhofhtazixbm6z2i").unwrap();
810 let messages = [
811 Message {
812 version: 0,
813 from: addr0,
814 to: Address::default(),
815 sequence: 1210,
816 value: TokenAmount::default(),
817 method_num: 5,
818 params: RawBytes::new(vec![]),
819 gas_limit: 25201703,
820 gas_fee_cap: TokenAmount::from_atto(101774),
821 gas_premium: TokenAmount::from_atto(100720),
822 },
823 Message {
824 version: 0,
825 from: addr1,
826 to: Address::default(),
827 sequence: 190,
828 value: TokenAmount::default(),
829 method_num: 5,
830 params: RawBytes::new(vec![]),
831 gas_limit: 21148671,
832 gas_fee_cap: TokenAmount::from_atto(101774),
833 gas_premium: TokenAmount::from_atto(100720),
834 },
835 Message {
836 version: 0,
837 from: addr1,
838 to: Address::default(),
839 sequence: 191,
840 value: TokenAmount::default(),
841 method_num: 5,
842 params: RawBytes::new(vec![]),
843 gas_limit: 112795625,
844 gas_fee_cap: TokenAmount::from_atto(101774),
845 gas_premium: TokenAmount::from_atto(100720),
846 },
847 ];
848 let actor_sequences = HashMap::from_iter([(addr0, 1210), (addr1, 195)]);
849 let curr_base_fee = TokenAmount::from_atto(100);
850 let min_base_fee = TokenAmount::from_atto(100);
851
852 let stats = compute_stats(&messages, actor_sequences, curr_base_fee, min_base_fee);
853
854 let expected = vec![
855 MpStat {
856 address: addr0.to_string(),
857 past: 0,
858 current: 1,
859 future: 0,
860 below_current: 0,
861 below_past: 0,
862 gas_limit: 25201703.into(),
863 },
864 MpStat {
865 address: addr1.to_string(),
866 past: 2,
867 current: 0,
868 future: 0,
869 below_current: 0,
870 below_past: 0,
871 gas_limit: 133944296.into(),
872 },
873 ];
874
875 assert_eq!(stats, expected);
876 }
877
878 #[test]
879 fn find_pending_message_lookup() {
880 let (mut wallet, addrs) = test_wallet();
881 let pending = pending_from(&mut wallet, &addrs.target, &addrs.addr, &[5]);
882
883 let found = find_pending_message(addrs.addr, 5, &pending).unwrap();
884 assert_eq!(found.cid(), pending[0].cid());
885
886 for (from, nonce) in [(addrs.addr, 99), (addrs.other, 5)] {
887 let err = find_pending_message(from, nonce, &pending).unwrap_err();
888 assert!(
889 err.to_string().contains("no pending message found"),
890 "{err}"
891 );
892 }
893
894 let err = find_pending_message(addrs.addr, 5, &[]).unwrap_err();
895 assert!(
896 err.to_string().contains("no pending message found"),
897 "{err}"
898 );
899 }
900
901 #[test]
902 fn test_auto_compute_replacement_gas() {
903 let (_wallet, addrs) = test_wallet();
904 let addr = addrs.addr;
905 let target = addrs.target;
906
907 let original_premium = TokenAmount::from_atto(100u64);
909 let floor = compute_rbf(&original_premium, REPLACE_BY_FEE_RATIO_DEFAULT);
910 let estimated = make_test_message(addr, target, 5, 2_000_000, 200, 500);
911 assert!(estimated.gas_premium > floor);
912 let result = auto_compute_replacement_gas(
913 estimated.clone(),
914 original_premium.clone(),
915 REPLACE_BY_FEE_RATIO_DEFAULT,
916 )
917 .unwrap();
918 assert_eq!(result.gas_premium, estimated.gas_premium);
919
920 let original_premium = TokenAmount::from_atto(1000u64);
922 let floor = compute_rbf(&original_premium, REPLACE_BY_FEE_RATIO_DEFAULT);
923 let estimated = make_test_message(addr, target, 5, 2_000_000, 50, 500);
924 assert!(estimated.gas_premium < floor);
925 let result = auto_compute_replacement_gas(
926 estimated,
927 original_premium.clone(),
928 REPLACE_BY_FEE_RATIO_DEFAULT,
929 )
930 .unwrap();
931 assert_eq!(result.gas_premium, floor);
932 assert!(result.gas_fee_cap >= result.gas_premium);
933
934 let original_premium = TokenAmount::from_atto(100u64);
936 let floor = compute_rbf(&original_premium, REPLACE_BY_FEE_RATIO_DEFAULT);
937 let mut estimated = make_test_message(addr, target, 5, 2_000_000, 0, 500);
938 estimated.gas_premium = floor.clone();
939 estimated.gas_fee_cap = floor.clone();
940 let result = auto_compute_replacement_gas(
941 estimated,
942 original_premium.clone(),
943 REPLACE_BY_FEE_RATIO_DEFAULT,
944 )
945 .unwrap();
946 assert_eq!(result.gas_premium, floor);
947 assert_eq!(result.gas_fee_cap, floor);
948
949 let original_premium = TokenAmount::from_atto(1000u64);
951 let floor = compute_rbf(&original_premium, REPLACE_BY_FEE_RATIO_DEFAULT);
952 let mut estimated = make_test_message(addr, target, 5, 2_000_000, 50, 10);
953 estimated.gas_premium = floor.clone();
954 let result =
955 auto_compute_replacement_gas(estimated, original_premium, REPLACE_BY_FEE_RATIO_DEFAULT)
956 .unwrap();
957 assert_eq!(result.gas_premium, floor);
958 assert_eq!(result.gas_fee_cap, floor);
959
960 let original_premium = TokenAmount::from_atto(1_000_000u64);
962 let mut estimated = make_test_message(addr, target, 5, 2_000_000, 50, 10_000_000_000);
963 estimated.gas_premium = TokenAmount::from_atto(50u64);
964 let mut replacement =
965 auto_compute_replacement_gas(estimated, original_premium, REPLACE_BY_FEE_RATIO_DEFAULT)
966 .unwrap();
967 let max_fee = TokenAmount::from_atto(1_000_000u64);
968 cap_gas_fee(&max_fee, &mut replacement, None).unwrap();
969 let total_fee = replacement.gas_fee_cap.clone() * replacement.gas_limit;
970 assert!(total_fee <= max_fee);
971 assert!(replacement.gas_premium <= replacement.gas_fee_cap);
972 }
973
974 #[test]
975 fn test_manual_compute_replacement_gas() {
976 let (_wallet, addrs) = test_wallet();
977 let addr = addrs.addr;
978 let target = addrs.target;
979
980 let original = make_test_message(addr, target, 5, 1_000_000, 100, 300);
981 let result = manual_compute_replacement_gas(
982 TokenAmount::from_atto(200u64),
983 TokenAmount::from_atto(600u64),
984 None,
985 original.clone(),
986 )
987 .unwrap();
988 assert_eq!(result.gas_premium, TokenAmount::from_atto(200u64));
989 assert_eq!(result.gas_fee_cap, TokenAmount::from_atto(600u64));
990 assert_eq!(result.gas_limit, original.gas_limit);
991
992 let original = make_test_message(addr, target, 5, 1_000_000, 100, 300);
993 let min_premium = compute_rbf(&original.gas_premium, REPLACE_BY_FEE_RATIO_DEFAULT);
994 let result = manual_compute_replacement_gas(
995 min_premium,
996 TokenAmount::from_atto(300u64),
997 Some(5_000_000),
998 original,
999 )
1000 .unwrap();
1001 assert_eq!(result.gas_limit, 5_000_000);
1002 }
1003}