Skip to main content

paralock_cli/
output.rs

1//! Human and `--json` output.
2//!
3//! In human mode, progress and results go to stdout and warnings and errors to stderr. With `--json`, stdout carries
4//! exactly one JSON document (the result, or `{ "ok": false, "error": … }`), so scripts can parse it; warnings and
5//! logs still go to stderr.
6
7use std::collections::HashMap;
8use std::io::Write as _;
9use std::sync::{Arc, Mutex};
10
11use paralock::paralock_core::protocol::SwapEvent;
12use paralock::{Swap, SwapStatus};
13use serde_json::{Value, json};
14
15use crate::error::CliError;
16
17/// Prints a warning to stderr (in both modes).
18pub fn warn(message: &str) {
19    let _ = writeln!(std::io::stderr(), "warning: {message}");
20}
21
22/// The wire name of a status (`zcash_locked`, …).
23pub const fn status_str(status: SwapStatus) -> &'static str {
24    match status {
25        SwapStatus::Quoted => "quoted",
26        SwapStatus::ZcashLocked => "zcash_locked",
27        SwapStatus::EvmLocked => "evm_locked",
28        SwapStatus::EvmClaimed => "evm_claimed",
29        SwapStatus::Completed => "completed",
30        SwapStatus::RefundedZcash => "refunded_zcash",
31        SwapStatus::RefundedEvm => "refunded_evm",
32        SwapStatus::Failed => "failed",
33    }
34}
35
36/// One timeline line: `  [zcash   ] zcash_locked    Taker funded the Zcash HTLC  <txid>`.
37pub fn event_line(event: &SwapEvent) -> String {
38    let detail = event.detail.as_deref().unwrap_or("");
39    let txid = event.txid.as_deref().map(|t| format!("  {t}")).unwrap_or_default();
40    format!("  [{:<8}] {:<15} {detail}{txid}", event.chain.as_str(), status_str(event.status))
41}
42
43/// Where output goes.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct Output {
46    /// `--json`.
47    pub json: bool,
48}
49
50/// A callback that receives swaps as they progress.
51pub type Progress = Arc<dyn Fn(&Swap) + Send + Sync>;
52
53impl Output {
54    /// A line of human output (nothing with `--json`).
55    pub fn say(&self, line: impl AsRef<str>) {
56        if !self.json {
57            let _ = writeln!(std::io::stdout(), "{}", line.as_ref());
58        }
59    }
60
61    /// Prints each swap event once, as it happens (human mode only). `prefix` goes before the swap's lines (e.g. a
62    /// short swap id for a maker serving many swaps).
63    pub fn progress(&self, with_id: bool) -> Progress {
64        let json = self.json;
65        let printed: Mutex<HashMap<String, usize>> = Mutex::new(HashMap::new());
66        Arc::new(move |swap: &Swap| {
67            if json {
68                return;
69            }
70            let mut printed = printed.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
71            let done = printed.entry(swap.id.clone()).or_insert(0);
72            let mut out = std::io::stdout().lock();
73            if with_id && *done == 0 && swap.events.is_empty() {
74                let _ = writeln!(
75                    out,
76                    "[{}] new {} swap: {} ZEC → {} {} on {}",
77                    short(&swap.id),
78                    swap.mode.as_str(),
79                    crate::amount::zec(swap.zcash.amount_zat),
80                    crate::amount::usd(swap.evm.amount, 6),
81                    swap.quote.token_symbol,
82                    swap.evm.chain
83                );
84            }
85            for event in swap.events.iter().skip(*done) {
86                let line = event_line(event);
87                if with_id {
88                    let _ = writeln!(out, "[{}]{line}", short(&swap.id));
89                } else {
90                    let _ = writeln!(out, "{line}");
91                }
92            }
93            *done = swap.events.len();
94        })
95    }
96
97    /// The result document (`--json` only): `{ "ok": true, …fields }`.
98    pub fn result(&self, fields: Value) {
99        if !self.json {
100            return;
101        }
102        let mut doc = json!({ "ok": true });
103        if let (Some(doc), Value::Object(fields)) = (doc.as_object_mut(), fields) {
104            doc.extend(fields);
105        }
106        print_json(&doc);
107    }
108
109    /// Reports an error: `{ "ok": false, "error": { code, message, hint }, "swap"? }` with `--json`, otherwise
110    /// `error: …` and `hint: …` on stderr.
111    pub fn error(&self, error: &CliError) {
112        if self.json {
113            let mut doc = json!({
114                "ok": false,
115                "error": {
116                    "code": error.code.map(|c| c.as_str()),
117                    "message": error.message,
118                    "hint": error.hint,
119                    "exitCode": error.exit.code(),
120                },
121            });
122            if let (Some(swap), Some(map)) = (&error.swap, doc.as_object_mut()) {
123                map.insert("swap".into(), serde_json::to_value(swap).unwrap_or(Value::Null));
124            }
125            print_json(&doc);
126            return;
127        }
128        let mut err = std::io::stderr().lock();
129        let code = error.code.map(|c| format!(" ({})", c.as_str())).unwrap_or_default();
130        let _ = writeln!(err, "error{code}: {}", error.message);
131        if let Some(hint) = &error.hint {
132            let _ = writeln!(err, "  hint: {hint}");
133        }
134    }
135}
136
137/// Prints a JSON document on stdout as is (for results that are not `ok`).
138pub fn print_document(value: &Value) {
139    print_json(value);
140}
141
142/// One line of newline-delimited JSON on stdout (the maker's `--json` records).
143pub fn print_record(value: &Value) {
144    let _ = writeln!(std::io::stdout(), "{value}");
145}
146
147fn print_json(value: &Value) {
148    let text = serde_json::to_string_pretty(value).unwrap_or_else(|_| "{\"ok\":false}".into());
149    let _ = writeln!(std::io::stdout(), "{text}");
150}
151
152/// The first 10 characters of a swap id.
153pub fn short(id: &str) -> &str {
154    id.get(..10).unwrap_or(id)
155}