Skip to main content

forest/cli/subcommands/
mpool_cmd.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use 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    /// Get pending messages
27    Pending {
28        /// Print pending messages for addresses in local wallet only
29        #[arg(long)]
30        local: bool,
31        /// Only print `CIDs` of messages in output
32        #[arg(long)]
33        cids: bool,
34        /// Return messages to a given address
35        #[arg(long)]
36        to: Option<StrictAddress>,
37        /// Return messages from a given address
38        #[arg(long)]
39        from: Option<StrictAddress>,
40    },
41    /// Get the current nonce for an address
42    Nonce {
43        /// Address to check nonce for
44        address: StrictAddress,
45    },
46    /// Print mempool stats
47    Stat {
48        /// Number of blocks to look back for minimum `basefee`
49        #[arg(long, default_value = "60")]
50        basefee_lookback: u32,
51        /// Print stats for addresses in local wallet only
52        #[arg(long)]
53        local: bool,
54    },
55    /// Fill an on-chain nonce gap by pushing signed self-transfer messages.
56    NonceFix {
57        /// Address to fill nonce gaps (must be signable by the node's wallet).
58        #[arg(long)]
59        addr: StrictAddress,
60        /// Derive the fill range from chain state and the mempool (ignores `--start` / `--end`).
61        #[arg(long, conflicts_with_all = ["start", "end"])]
62        auto: bool,
63        /// First sequence to fill (inclusive); required unless `--auto`.
64        #[arg(long, required_unless_present = "auto")]
65        start: Option<u64>,
66        /// End of range (exclusive); required unless `--auto`.
67        #[arg(long, required_unless_present = "auto")]
68        end: Option<u64>,
69        /// Gas fee cap for filler messages. Default: twice the parent base fee from chain head.
70        #[arg(long, value_parser = humantoken::parse)]
71        gas_fee_cap: Option<TokenAmount>,
72    },
73    /// Replace a pending message in the mempool with updated gas parameters (replace-by-fee).
74    Replace {
75        /// Address that sent the message (required unless `--cid` is used).
76        #[arg(long, required_unless_present = "cid")]
77        from: Option<StrictAddress>,
78        /// Nonce of the message to replace (required unless `--cid` is used).
79        #[arg(long, required_unless_present = "cid")]
80        nonce: Option<u64>,
81        /// CID of the message to replace (alternative to `--from`/`--nonce`).
82        #[arg(long, conflicts_with_all = ["from", "nonce"])]
83        cid: Option<Cid>,
84        /// Automatically re-estimate gas, ensuring the RBF minimum premium is met.
85        #[arg(long, conflicts_with_all = ["gas_premium", "gas_feecap", "gas_limit"])]
86        auto: bool,
87        /// Maximum total fee; only used with `--auto`.
88        #[arg(long, value_parser = humantoken::parse, alias = "fee-limit", requires = "auto")]
89        max_fee: Option<TokenAmount>,
90        /// Gas premium (required unless `--auto` is used).
91        #[arg(long, value_parser = humantoken::parse, required_unless_present = "auto")]
92        gas_premium: Option<TokenAmount>,
93        /// Gas fee cap (required unless `--auto` is used).
94        #[arg(long, value_parser = humantoken::parse, required_unless_present = "auto")]
95        gas_feecap: Option<TokenAmount>,
96        /// Gas limit (Optional; keeps original value if unset).
97        #[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                    // Keep the original gas limit when replacing a pending message.
496                    // Re-estimating it would simulate against the message being replaced.
497                    // See <https://github.com/filecoin-project/lotus/blob/797feebc63bfbd4fdfb742b674c97bfb7846cccb/cli/mpool.go#L482>
498                    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        // No filtering is set up
568        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        // Create a message with addresses from an external wallet
590        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        // Filter local addresses
609        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        // Create a message from a second sender
634        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        // Filtering messages from sender2
642        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        // Create a message to a second target
667        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        // Filtering messages to target2
675        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        // Above RBF floor: estimated premium kept.
908        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        // Below RBF floor: premium bumped, fee cap >= premium.
921        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        // Exactly at floor: unchanged.
935        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        // Fee cap raised when below bumped premium.
950        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        // cap_gas_fee after RBF bump.
961        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}