use clap::{arg, command, Parser};
use std::io;
use soroban_env_host::xdr::{self, ReadXdr};
use super::config::{events_file, locator, network};
use crate::commands::config::network::Network;
use crate::{rpc, toid, utils};
#[derive(Parser, Debug, Clone)]
#[group(skip)]
pub struct Cmd {
#[arg(long, conflicts_with = "cursor", required_unless_present = "cursor")]
start_ledger: Option<u32>,
#[arg(
long,
conflicts_with = "start_ledger",
required_unless_present = "start_ledger"
)]
cursor: Option<String>,
#[arg(long, value_enum, default_value = "pretty")]
output: OutputFormat,
#[arg(short, long, default_value = "10")]
count: usize,
#[arg(
long = "id",
num_args = 1..=6,
help_heading = "FILTERS"
)]
contract_ids: Vec<String>,
#[arg(
long = "topic",
num_args = 1..=5,
help_heading = "FILTERS"
)]
topic_filters: Vec<String>,
#[arg(
long = "type",
value_enum,
default_value = "all",
help_heading = "FILTERS"
)]
event_type: rpc::EventType,
#[command(flatten)]
locator: locator::Args,
#[command(flatten)]
network: network::Args,
#[command(flatten)]
events_file: events_file::Args,
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("cursor is not valid")]
InvalidCursor,
#[error("filepath does not exist: {path}")]
InvalidFile { path: String },
#[error("filepath ({path}) cannot be read: {error}")]
CannotReadFile { path: String, error: String },
#[error("cannot parse topic filter {topic} into 1-4 segments")]
InvalidTopicFilter { topic: String },
#[error("invalid segment ({segment}) in topic filter ({topic}): {error}")]
InvalidSegment {
topic: String,
segment: String,
error: xdr::Error,
},
#[error("cannot parse contract ID {contract_id}: {error}")]
InvalidContractId {
contract_id: String,
error: hex::FromHexError,
},
#[error("invalid JSON string: {error} ({debug})")]
InvalidJson {
debug: String,
error: serde_json::Error,
},
#[error("invalid timestamp in event: {ts}")]
InvalidTimestamp { ts: String },
#[error("missing start_ledger and cursor")]
MissingStartLedgerAndCursor,
#[error("missing target")]
MissingTarget,
#[error(transparent)]
Rpc(#[from] rpc::Error),
#[error(transparent)]
Generic(#[from] Box<dyn std::error::Error>),
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Xdr(#[from] xdr::Error),
#[error(transparent)]
Serde(#[from] serde_json::Error),
#[error(transparent)]
Network(#[from] network::Error),
#[error(transparent)]
EventsFile(#[from] events_file::Error),
#[error(transparent)]
Locator(#[from] locator::Error),
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum)]
pub enum OutputFormat {
Pretty,
Plain,
Json,
}
impl Cmd {
pub async fn run(&self) -> Result<(), Error> {
for topic in &self.topic_filters {
for (i, segment) in topic.split(',').enumerate() {
if i > 4 {
return Err(Error::InvalidTopicFilter {
topic: topic.to_string(),
});
}
if segment != "*" {
if let Err(e) = xdr::ScVal::from_xdr_base64(segment) {
return Err(Error::InvalidSegment {
topic: topic.to_string(),
segment: segment.to_string(),
error: e,
});
}
}
}
}
let response = if self.network.is_no_network() {
self.run_in_sandbox()
} else {
self.run_against_rpc_server().await
}?;
for event in &response.events {
match self.output {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&event).map_err(|e| {
Error::InvalidJson {
debug: format!("{event:#?}"),
error: e,
}
})?,
);
}
OutputFormat::Plain => println!("{event}"),
OutputFormat::Pretty => event.pretty_print()?,
}
}
println!("Latest Ledger: {}", response.latest_ledger);
Ok(())
}
async fn run_against_rpc_server(&self) -> Result<rpc::GetEventsResponse, Error> {
let start = self.start()?;
let Network { rpc_url, .. } = self.network.get(&self.locator)?;
for raw_contract_id in &self.contract_ids {
utils::id_from_str::<32>(raw_contract_id).map_err(|e| Error::InvalidContractId {
contract_id: raw_contract_id.clone(),
error: e,
})?;
}
let client = rpc::Client::new(&rpc_url)?;
client
.get_events(
start,
Some(self.event_type),
&self.contract_ids,
&self.topic_filters,
Some(self.count),
)
.await
.map_err(Error::Rpc)
}
pub fn run_in_sandbox(&self) -> Result<rpc::GetEventsResponse, Error> {
let start = self.start()?;
let count: usize = if self.count == 0 {
std::usize::MAX
} else {
self.count
};
let start_cursor = match start {
rpc::EventStart::Ledger(l) => (toid::Toid::new(l, 0, 0).into(), -1),
rpc::EventStart::Cursor(c) => rpc::parse_cursor(&c)?,
};
let path = self.locator.config_dir()?;
let file = self.events_file.read(&path)?;
Ok(rpc::GetEventsResponse {
events: events_file::Args::filter_events(
&file.events,
&path,
start_cursor,
&self.contract_ids,
&self.topic_filters,
count,
),
latest_ledger: file.latest_ledger,
})
}
fn start(&self) -> Result<rpc::EventStart, Error> {
let start = match (self.start_ledger, self.cursor.clone()) {
(Some(start), _) => rpc::EventStart::Ledger(start),
(_, Some(c)) => rpc::EventStart::Cursor(c),
_ => return Err(Error::MissingStartLedgerAndCursor),
};
Ok(start)
}
}
#[cfg(test)]
mod tests {
use std::path;
use assert_fs::NamedTempFile;
use soroban_env_host::events;
use super::*;
use events_file::Args;
#[test]
fn test_does_event_serialization_match() {
let temp = NamedTempFile::new("events.json").unwrap();
let events_file = Args {
events_file: Some(temp.to_path_buf()),
};
let events: Vec<events::HostEvent> = vec![
events::HostEvent {
event: events::Event::Contract(xdr::ContractEvent {
ext: xdr::ExtensionPoint::V0,
contract_id: Some(xdr::Hash([0; 32])),
type_: xdr::ContractEventType::Contract,
body: xdr::ContractEventBody::V0(xdr::ContractEventV0 {
topics: xdr::ScVec(vec![].try_into().unwrap()),
data: xdr::ScVal::U32(12345),
}),
}),
failed_call: false,
},
events::HostEvent {
event: events::Event::Contract(xdr::ContractEvent {
ext: xdr::ExtensionPoint::V0,
contract_id: Some(xdr::Hash([0x1; 32])),
type_: xdr::ContractEventType::Contract,
body: xdr::ContractEventBody::V0(xdr::ContractEventV0 {
topics: xdr::ScVec(vec![].try_into().unwrap()),
data: xdr::ScVal::I32(67890),
}),
}),
failed_call: false,
},
];
let ledger_info = soroban_ledger_snapshot::LedgerSnapshot {
protocol_version: 1,
sequence_number: 2, timestamp: 3,
network_id: [0x1; 32],
base_reserve: 5,
ledger_entries: vec![],
};
events_file.commit(&events, &ledger_info, &temp).unwrap();
let file = events_file.read(&std::env::current_dir().unwrap()).unwrap();
assert_eq!(file.events.len(), 2);
assert_eq!(file.events[0].ledger, "2");
assert_eq!(file.events[1].ledger, "2");
assert_eq!(file.events[0].contract_id, "0".repeat(64));
assert_eq!(file.events[1].contract_id, "01".repeat(32));
assert_eq!(file.latest_ledger, 2);
}
#[test]
fn test_does_event_fixture_load() {
let filename =
path::PathBuf::from("../crates/soroban-test/tests/fixtures/test-jsons/get-events.json");
let events_file = Args {
events_file: Some(filename),
};
let result = events_file.read(&std::env::current_dir().unwrap());
println!("{result:?}");
assert!(result.is_ok());
}
}