Skip to main content

scope_cli/cli/watch/
mod.rs

1//! # Watchlist runs (#47)
2//!
3//! The I/O side of [`scope::watch`]: [`fetch`] reads each target's metrics
4//! through the compute functions the CLI commands use (never a printing
5//! handler), and [`run`](mod@run) repeats the ticks on the watchlist's schedule
6//! with the one repeat runner, writes the CSV trail and the report.
7//!
8//! The TUI's monitor wizard and grid drive these functions.
9
10pub mod fetch;
11pub mod monitor;
12pub mod run;
13
14use crate::cli::output::Output;
15use crate::cli::resolve::{DexCandidates, ResolveContext, VenueKind, picker_for, resolve};
16use scope::chains::{ChainClientFactory, infer_chain_from_address};
17use scope::config::Config;
18use scope::error::Result;
19use scope::metrics::MetricGroup;
20use scope::tokens::TokenAliases;
21use scope::watch::{Target, TargetKind, Watchlist};
22
23pub use fetch::{TargetReading, Tick, tick};
24pub use run::{csv_header, csv_rows, report_text, run};
25
26/// What the fetchers read with.
27#[derive(Clone, Copy)]
28pub struct Sources<'a> {
29    pub config: &'a Config,
30    pub clients: &'a dyn ChainClientFactory,
31}
32
33/// The venue IDs a watchlist may name: the built-in and the user's.
34pub fn venue_ids() -> Vec<String> {
35    scope::market::VenueRegistry::load()
36        .map(|r| r.list().into_iter().map(str::to_string).collect())
37        .unwrap_or_default()
38}
39
40/// A watchlist ready to run.
41#[derive(Debug, Clone, PartialEq)]
42pub struct Ready {
43    /// The watchlist, with `@label` targets replaced by their address.
44    pub list: Watchlist,
45    /// For each target, the token contract its token metrics read. The
46    /// target's id stays the symbol, which market metrics use on a venue.
47    pub contracts: Vec<Option<String>>,
48}
49
50/// Makes the watchlist ready to run, once before the first tick:
51/// `@label` targets become their address and chain, and a token symbol
52/// that token metrics read is resolved to its contract. A symbol on many
53/// chains asks the user (in the TUI, as a list); the answer holds for the
54/// run.
55///
56/// # Errors
57///
58/// The address book cannot be read, or a symbol cannot be resolved.
59pub async fn prepare(w: &Watchlist, src: Sources<'_>, out: &Output) -> Result<Ready> {
60    let mut list = w.clone();
61    let mut contracts = Vec::with_capacity(list.targets.len());
62    for t in &mut list.targets {
63        if let Some((address, chain)) =
64            crate::cli::address_book::resolve_address_book_input(&t.id, src.config)?
65        {
66            t.id = address;
67            t.chain.get_or_insert(chain);
68        }
69        if t.chain.is_none() && t.kind != TargetKind::Pair {
70            t.chain = infer_chain_from_address(&t.id).map(str::to_string);
71        }
72        let contract = if t.kind == TargetKind::Token && t.needs(MetricGroup::Token) {
73            Some(resolve_token(t, src, out).await?)
74        } else {
75            None
76        };
77        contracts.push(contract);
78    }
79    Ok(Ready { list, contracts })
80}
81
82/// The contract of a token target, and its chain when the target has none.
83async fn resolve_token(t: &mut Target, src: Sources<'_>, out: &Output) -> Result<String> {
84    if TokenAliases::is_address(t.id.trim()) {
85        return Ok(t.id.trim().to_string());
86    }
87    let dex = src.clients.create_dex_client();
88    let ctx = ResolveContext {
89        chain: t.chain.clone(),
90        venue: VenueKind::Dex,
91        token_flag: None,
92        yes: false,
93        save: false,
94    };
95    let source = DexCandidates {
96        dex: dex.as_ref(),
97        out,
98        fallback: Some(src.clients.http()),
99    };
100    let picker = picker_for(out, false);
101    let mut aliases = TokenAliases::load();
102    let r = resolve(&t.id, &ctx, &source, picker.as_ref(), &mut aliases, out).await?;
103    if t.chain.is_none() && !r.chain.is_empty() {
104        t.chain = Some(r.chain);
105    }
106    Ok(r.address.unwrap_or_else(|| t.id.clone()))
107}