use eyre::eyre;
use std::ops::RangeInclusive;
use abscissa_core::clap::Parser;
use abscissa_core::config::Override;
use abscissa_core::{Command, FrameworkErrorKind, Runnable};
use ibc_relayer::chain::handle::{BaseChainHandle, ChainHandle};
use ibc_relayer::chain::requests::{IncludeProof, QueryChannelRequest, QueryHeight};
use ibc_relayer::config::Config;
use ibc_relayer::link::error::LinkError;
use ibc_relayer::link::{Link, LinkParameters};
use ibc_relayer::util::seq_range::parse_seq_range;
use ibc_relayer_types::core::ics04_channel::packet::Sequence;
use ibc_relayer_types::core::ics24_host::identifier::{ChainId, ChannelId, PortId};
use ibc_relayer_types::events::IbcEvent;
use crate::application::app_config;
use crate::cli_utils::spawn_chain_counterparty;
use crate::conclude::Output;
#[derive(Command, Debug, Parser, Runnable)]
pub enum ClearCmds {
Packets(ClearPacketsCmd),
}
#[derive(Debug, Parser, Command, PartialEq, Eq)]
pub struct ClearPacketsCmd {
#[clap(
long = "chain",
required = true,
value_name = "CHAIN_ID",
help_heading = "REQUIRED",
help = "Identifier of the chain"
)]
chain_id: ChainId,
#[clap(
long = "port",
required = true,
value_name = "PORT_ID",
help_heading = "REQUIRED",
help = "Identifier of the port"
)]
port_id: PortId,
#[clap(
long = "channel",
alias = "chan",
required = true,
value_name = "CHANNEL_ID",
help_heading = "REQUIRED",
help = "Identifier of the channel"
)]
channel_id: ChannelId,
#[clap(
long = "packet-sequences",
help = "Sequences of packets to be cleared on the specified chain. \
Either a single sequence or a range of sequences can be specified. \
If not provided, all pending packets will be cleared on both chains. \
Each element of the comma-separated list must be either a single \
sequence or a range of sequences. \
Example: `1,10..20` will clear packets with sequences 1, 10, 11, ..., 20",
value_delimiter = ',',
value_parser = parse_seq_range
)]
packet_sequences: Vec<RangeInclusive<Sequence>>,
#[clap(
long = "key-name",
help = "Use the given signing key for the specified chain (default: `key_name` config)"
)]
key_name: Option<String>,
#[clap(
long = "counterparty-key-name",
help = "Use the given signing key for the counterparty chain (default: `counterparty_key_name` config)"
)]
counterparty_key_name: Option<String>,
#[clap(
long = "query-packets-chunk-size",
help = "Number of packets to fetch at once from the chain (default: `query_packets_chunk_size` config)"
)]
query_packets_chunk_size: Option<usize>,
}
impl Override<Config> for ClearPacketsCmd {
fn override_config(&self, mut config: Config) -> Result<Config, abscissa_core::FrameworkError> {
let chain_config = config.find_chain_mut(&self.chain_id).ok_or_else(|| {
FrameworkErrorKind::ComponentError.context(format!(
"missing configuration for chain '{}'",
self.chain_id
))
})?;
if let Some(ref key_name) = self.key_name {
chain_config.set_key_name(key_name.to_string());
}
Ok(config)
}
}
impl Runnable for ClearPacketsCmd {
fn run(&self) {
let config = app_config();
let chains = match spawn_chain_counterparty::<BaseChainHandle>(
&config,
&self.chain_id,
&self.port_id,
&self.channel_id,
) {
Ok((chains, _)) => chains,
Err(e) => Output::error(e).exit(),
};
if let Some(ref counterparty_key_name) = self.counterparty_key_name {
match chains.dst.config() {
Ok(mut dst_chain_cfg) => {
dst_chain_cfg.set_key_name(counterparty_key_name.to_string());
}
Err(e) => Output::error(e).exit(),
}
}
if let Some(chunk_size) = self.query_packets_chunk_size {
match chains.src.config() {
Ok(mut src_chain_cfg) => {
src_chain_cfg.set_query_packets_chunk_size(chunk_size);
}
Err(e) => Output::error(e).exit(),
}
}
let (channel, _) = match chains.src.query_channel(
QueryChannelRequest {
port_id: self.port_id.clone(),
channel_id: self.channel_id.clone(),
height: QueryHeight::Latest,
},
IncludeProof::No,
) {
Ok(channel) => channel,
Err(e) => Output::error(e).exit(),
};
let exclude_src_sequences = config
.find_chain(&chains.src.id())
.map(|chain_config| chain_config.excluded_sequences(&self.channel_id).to_vec())
.unwrap_or_default();
let exclude_dst_sequences =
if let Some(counterparty_channel_id) = channel.counterparty().channel_id() {
config
.find_chain(&chains.dst.id())
.map(|chain_config| {
chain_config
.excluded_sequences(counterparty_channel_id)
.to_vec()
})
.unwrap_or_default()
} else {
Vec::new()
};
let fwd_opts = LinkParameters {
src_port_id: self.port_id.clone(),
src_channel_id: self.channel_id.clone(),
max_memo_size: config.mode.packets.ics20_max_memo_size,
max_receiver_size: config.mode.packets.ics20_max_receiver_size,
exclude_src_sequences,
};
let counterparty_channel_id = match channel.counterparty().channel_id() {
Some(channel_id) => channel_id.clone(),
None => Output::error(eyre!(
"Channel `{}` and port `{}` does not have a counterparty channel id",
self.channel_id,
self.port_id
))
.exit(),
};
let reverse_opts = LinkParameters {
src_port_id: channel.counterparty().port_id().clone(),
src_channel_id: counterparty_channel_id,
max_memo_size: config.mode.packets.ics20_max_memo_size,
max_receiver_size: config.mode.packets.ics20_max_receiver_size,
exclude_src_sequences: exclude_dst_sequences,
};
let fwd_link = match Link::new_from_opts(
chains.src.clone(),
chains.dst.clone(),
fwd_opts,
false,
false,
) {
Ok(link) => link,
Err(e) => Output::error(e).exit(),
};
let rev_link = match Link::new_from_opts(chains.dst, chains.src, reverse_opts, false, false)
{
Ok(link) => link,
Err(e) => Output::error(e).exit(),
};
let mut ev_list = vec![];
run_and_collect_events("forward recv and timeout", &mut ev_list, || {
fwd_link.relay_recv_packet_and_timeout_messages(self.packet_sequences.clone())
});
if self.packet_sequences.is_empty() {
run_and_collect_events("reverse recv and timeout", &mut ev_list, || {
rev_link.relay_recv_packet_and_timeout_messages(vec![])
});
}
run_and_collect_events("reverse ack", &mut ev_list, || {
rev_link.relay_ack_packet_messages(self.packet_sequences.clone())
});
if self.packet_sequences.is_empty() {
run_and_collect_events("forward ack", &mut ev_list, || {
fwd_link.relay_ack_packet_messages(vec![])
});
}
Output::success(ev_list).exit()
}
}
fn run_and_collect_events<F>(desc: &str, ev_list: &mut Vec<IbcEvent>, f: F)
where
F: FnOnce() -> Result<Vec<IbcEvent>, LinkError>,
{
match f() {
Ok(mut ev) => ev_list.append(&mut ev),
Err(e) => tracing::error!("Failed to relay {desc} packets: {e:?}"),
};
}
#[cfg(test)]
mod tests {
use super::ClearPacketsCmd;
use std::str::FromStr;
use abscissa_core::clap::Parser;
use ibc_relayer_types::core::ics04_channel::packet::Sequence;
use ibc_relayer_types::core::ics24_host::identifier::{ChainId, ChannelId, PortId};
#[test]
fn test_clear_packets_required_only() {
assert_eq!(
ClearPacketsCmd {
chain_id: ChainId::from_string("chain_id"),
port_id: PortId::from_str("port_id").unwrap(),
channel_id: ChannelId::from_str("channel-07").unwrap(),
packet_sequences: vec![],
key_name: None,
counterparty_key_name: None,
query_packets_chunk_size: None
},
ClearPacketsCmd::parse_from([
"test",
"--chain",
"chain_id",
"--port",
"port_id",
"--channel",
"channel-07"
])
)
}
#[test]
fn test_clear_packets_chan_alias() {
assert_eq!(
ClearPacketsCmd {
chain_id: ChainId::from_string("chain_id"),
port_id: PortId::from_str("port_id").unwrap(),
channel_id: ChannelId::from_str("channel-07").unwrap(),
packet_sequences: vec![],
key_name: None,
counterparty_key_name: None,
query_packets_chunk_size: None
},
ClearPacketsCmd::parse_from([
"test",
"--chain",
"chain_id",
"--port",
"port_id",
"--chan",
"channel-07"
])
)
}
#[test]
fn test_clear_packets_sequences() {
assert_eq!(
ClearPacketsCmd {
chain_id: ChainId::from_string("chain_id"),
port_id: PortId::from_str("port_id").unwrap(),
channel_id: ChannelId::from_str("channel-07").unwrap(),
packet_sequences: vec![
Sequence::from(1)..=Sequence::from(1),
Sequence::from(10)..=Sequence::from(20)
],
key_name: Some("key_name".to_owned()),
counterparty_key_name: None,
query_packets_chunk_size: None
},
ClearPacketsCmd::parse_from([
"test",
"--chain",
"chain_id",
"--port",
"port_id",
"--channel",
"channel-07",
"--packet-sequences",
"1,10..20",
"--key-name",
"key_name"
])
)
}
#[test]
fn test_clear_packets_key_name() {
assert_eq!(
ClearPacketsCmd {
chain_id: ChainId::from_string("chain_id"),
port_id: PortId::from_str("port_id").unwrap(),
channel_id: ChannelId::from_str("channel-07").unwrap(),
packet_sequences: vec![],
key_name: Some("key_name".to_owned()),
counterparty_key_name: None,
query_packets_chunk_size: None
},
ClearPacketsCmd::parse_from([
"test",
"--chain",
"chain_id",
"--port",
"port_id",
"--channel",
"channel-07",
"--key-name",
"key_name"
])
)
}
#[test]
fn test_clear_packets_counterparty_key_name() {
assert_eq!(
ClearPacketsCmd {
chain_id: ChainId::from_string("chain_id"),
port_id: PortId::from_str("port_id").unwrap(),
channel_id: ChannelId::from_str("channel-07").unwrap(),
packet_sequences: vec![],
key_name: None,
counterparty_key_name: Some("counterparty_key_name".to_owned()),
query_packets_chunk_size: None
},
ClearPacketsCmd::parse_from([
"test",
"--chain",
"chain_id",
"--port",
"port_id",
"--channel",
"channel-07",
"--counterparty-key-name",
"counterparty_key_name"
])
)
}
#[test]
fn test_clear_packets_query_packets_chunk_size() {
assert_eq!(
ClearPacketsCmd {
chain_id: ChainId::from_string("chain_id"),
port_id: PortId::from_str("port_id").unwrap(),
channel_id: ChannelId::from_str("channel-07").unwrap(),
packet_sequences: vec![],
key_name: None,
counterparty_key_name: Some("counterparty_key_name".to_owned()),
query_packets_chunk_size: Some(100),
},
ClearPacketsCmd::parse_from([
"test",
"--chain",
"chain_id",
"--port",
"port_id",
"--channel",
"channel-07",
"--counterparty-key-name",
"counterparty_key_name",
"--query-packets-chunk-size",
"100"
])
)
}
#[test]
fn test_clear_packets_no_chan() {
assert!(ClearPacketsCmd::try_parse_from([
"test", "--chain", "chain_id", "--port", "port_id"
])
.is_err())
}
#[test]
fn test_clear_packets_no_port() {
assert!(ClearPacketsCmd::try_parse_from([
"test",
"--chain",
"chain_id",
"--channel",
"channel-07"
])
.is_err())
}
#[test]
fn test_clear_packets_no_chain() {
assert!(ClearPacketsCmd::try_parse_from([
"test",
"--port",
"port_id",
"--channel",
"channel-07"
])
.is_err())
}
}