Skip to main content

leo_lang/cli/commands/common/
output.rs

1// Copyright (C) 2019-2026 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17//! JSON-serializable output types and helpers for CLI commands.
18
19use serde::Serialize;
20
21use indexmap::IndexMap;
22use snarkvm::prelude::{Network, ProgramID, block::Transaction};
23use std::fmt;
24
25/// Output for `leo build`: the program checksum and each entry/view function checksum, as 32 bytes each.
26#[derive(Serialize, Default)]
27pub struct BuildOutput {
28    pub program: String,
29    pub program_checksum: Vec<u8>,
30    pub function_checksums: IndexMap<String, Vec<u8>>,
31}
32
33/// Convert microcredits to credits for display.
34pub(crate) fn microcredits_to_credits(microcredits: u64) -> f64 {
35    microcredits as f64 / 1_000_000.0
36}
37
38/// Per-function cost information for a deployed program.
39#[derive(Serialize, Clone, Default)]
40pub struct FunctionCostStats {
41    pub name: String,
42    pub finalize_cost: u64,
43    /// The storage cost of the execution transition (execution_cost - finalize_cost).
44    /// `None` when authorization sampling fails (e.g. functions requiring specific record types).
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub storage_cost: Option<u64>,
47    /// The total execution cost (finalize + proof).
48    /// `None` when authorization sampling fails (e.g. functions requiring specific record types).
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub execution_cost: Option<u64>,
51    /// Whether this function uses dynamic calls, making the finalize cost a lower bound.
52    #[serde(skip_serializing_if = "std::ops::Not::not")]
53    pub has_dynamic_calls: bool,
54}
55
56/// Statistics for a deployed program.
57#[derive(Serialize, Clone, Default)]
58pub struct DeploymentStats {
59    pub program_size_bytes: usize,
60    pub max_program_size_bytes: usize,
61    /// Total circuit variables across all functions.
62    /// `None` when `--skip-deploy-certificate` is used (placeholder verifying keys have no circuit metadata).
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub total_variables: Option<u64>,
65    /// Total circuit constraints across all functions.
66    /// `None` when `--skip-deploy-certificate` is used.
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub total_constraints: Option<u64>,
69    /// Maximum allowed circuit variables for a deployment.
70    /// `None` when `--skip-deploy-certificate` is used.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub max_variables: Option<u64>,
73    /// Maximum allowed circuit constraints for a deployment.
74    /// `None` when `--skip-deploy-certificate` is used.
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub max_constraints: Option<u64>,
77    pub storage_cost: u64,
78    pub namespace_cost: u64,
79    pub synthesis_cost: u64,
80    pub constructor_cost: u64,
81    pub priority_fee: u64,
82    pub total_cost: u64,
83    #[serde(skip_serializing_if = "Vec::is_empty")]
84    pub function_costs: Vec<FunctionCostStats>,
85}
86
87impl fmt::Display for DeploymentStats {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        use colored::*;
90        use num_format::{Locale, ToFormattedString};
91
92        writeln!(
93            f,
94            "  {:22}{:.2} KB / {:.2} KB",
95            "Program Size:".cyan(),
96            self.program_size_bytes as f64 / 1024.0,
97            self.max_program_size_bytes as f64 / 1024.0
98        )?;
99        if let Some(total_variables) = self.total_variables {
100            writeln!(
101                f,
102                "  {:22}{}",
103                "Total Variables:".cyan(),
104                total_variables.to_formatted_string(&Locale::en).yellow()
105            )?;
106        }
107        if let Some(total_constraints) = self.total_constraints {
108            writeln!(
109                f,
110                "  {:22}{}",
111                "Total Constraints:".cyan(),
112                total_constraints.to_formatted_string(&Locale::en).yellow()
113            )?;
114        }
115        if let Some(max_variables) = self.max_variables {
116            writeln!(f, "  {:22}{}", "Max Variables:".cyan(), max_variables.to_formatted_string(&Locale::en).green())?;
117        }
118        if let Some(max_constraints) = self.max_constraints {
119            writeln!(
120                f,
121                "  {:22}{}",
122                "Max Constraints:".cyan(),
123                max_constraints.to_formatted_string(&Locale::en).green()
124            )?;
125        }
126        if self.total_variables.is_some() || self.total_constraints.is_some() {
127            writeln!(f)?;
128        }
129        writeln!(f, "{}", "💰 Cost Breakdown (credits)".bold())?;
130        writeln!(f, "  {:22}{:.6}", "Transaction Storage:".cyan(), microcredits_to_credits(self.storage_cost))?;
131        writeln!(f, "  {:22}{:.6}", "Program Synthesis:".cyan(), microcredits_to_credits(self.synthesis_cost))?;
132        writeln!(f, "  {:22}{:.6}", "Namespace:".cyan(), microcredits_to_credits(self.namespace_cost))?;
133        writeln!(f, "  {:22}{:.6}", "Constructor:".cyan(), microcredits_to_credits(self.constructor_cost))?;
134        writeln!(f, "  {:22}{:.6}", "Priority Fee:".cyan(), microcredits_to_credits(self.priority_fee))?;
135        writeln!(f, "  {:22}{:.6}", "Total Fee:".cyan(), microcredits_to_credits(self.total_cost))?;
136
137        for fc in &self.function_costs {
138            writeln!(f, "{}", format!("  Function '{}'", fc.name).bold())?;
139            let bound = if fc.has_dynamic_calls { " (lower bound)" } else { "" };
140            if let Some(execution_cost) = fc.execution_cost {
141                writeln!(
142                    f,
143                    "    {:24}{:.6}{}",
144                    "Total Execution Cost:".cyan(),
145                    microcredits_to_credits(execution_cost),
146                    bound.dimmed()
147                )?;
148                if let Some(storage_cost) = fc.storage_cost {
149                    writeln!(
150                        f,
151                        "    {:24}{:.6}{}",
152                        "|- Storage Cost:".cyan(),
153                        microcredits_to_credits(storage_cost),
154                        bound.dimmed()
155                    )?;
156                }
157                writeln!(
158                    f,
159                    "    {:24}{:.6}{}",
160                    "|- Finalize Cost:".cyan(),
161                    microcredits_to_credits(fc.finalize_cost),
162                    bound.dimmed()
163                )?;
164            } else {
165                writeln!(
166                    f,
167                    "    {:24}{:.6} (lower bound)",
168                    "Total Execution Cost:".cyan(),
169                    microcredits_to_credits(fc.finalize_cost),
170                )?;
171                writeln!(f, "    {:24}{}", "|- Storage Cost:".cyan(), "N/A (dynamic call)".dimmed())?;
172                writeln!(
173                    f,
174                    "    {:24}{:.6}{}",
175                    "|- Finalize Cost:".cyan(),
176                    microcredits_to_credits(fc.finalize_cost),
177                    bound.dimmed()
178                )?;
179            }
180        }
181        Ok(())
182    }
183}
184
185/// Statistics for an execution.
186#[derive(Serialize, Clone, Default)]
187pub struct ExecutionStats {
188    pub storage_cost: u64,
189    pub execution_cost: u64,
190    pub priority_fee: u64,
191    pub total_cost: u64,
192}
193
194impl fmt::Display for ExecutionStats {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        use colored::*;
197        writeln!(f, "{}", "💰 Cost Breakdown (credits)".bold())?;
198        writeln!(f, "  {:22}{:.6}", "Transaction Storage:".cyan(), microcredits_to_credits(self.storage_cost))?;
199        writeln!(f, "  {:22}{:.6}", "On-chain Execution:".cyan(), microcredits_to_credits(self.execution_cost))?;
200        writeln!(f, "  {:22}{:.6}", "Priority Fee:".cyan(), microcredits_to_credits(self.priority_fee))?;
201        writeln!(f, "  {:22}{:.6}", "Total Fee:".cyan(), microcredits_to_credits(self.total_cost))
202    }
203}
204
205/// Broadcast result when a transaction is sent to the network.
206#[derive(Serialize, Clone, Default)]
207pub struct BroadcastStats {
208    pub fee_id: String,
209    pub fee_transaction_id: String,
210    pub confirmed: bool,
211}
212
213/// Configuration used for the command.
214#[derive(Serialize, Clone, Default)]
215pub struct Config {
216    pub address: String,
217    pub network: String,
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub endpoint: Option<String>,
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub consensus_version: Option<u8>,
222}
223
224/// Output for `leo deploy` and `leo upgrade`.
225#[derive(Serialize, Default)]
226pub struct DeployOutput {
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub config: Option<Config>,
229    pub deployments: Vec<DeployedProgram>,
230}
231
232/// A single deployed program.
233#[derive(Serialize)]
234pub struct DeployedProgram {
235    pub program_id: String,
236    pub transaction_id: String,
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub stats: Option<DeploymentStats>,
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub broadcast: Option<BroadcastStats>,
241}
242
243/// Output for `leo run`.
244#[derive(Serialize)]
245pub struct RunOutput {
246    pub program: String,
247    pub function: String,
248    pub outputs: Vec<String>,
249}
250
251/// Output for `leo execute`.
252#[derive(Serialize, Default)]
253pub struct ExecuteOutput {
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub config: Option<Config>,
256    pub program: String,
257    pub function: String,
258    pub outputs: Vec<String>,
259    pub transaction_id: String,
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub stats: Option<ExecutionStats>,
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub broadcast: Option<BroadcastStats>,
264}
265
266/// Output for `leo test`.
267#[derive(Serialize, Default)]
268pub struct TestOutput {
269    pub passed: usize,
270    pub failed: usize,
271    pub tests: Vec<TestResult>,
272}
273
274/// A single test result.
275#[derive(Serialize)]
276pub struct TestResult {
277    pub name: String,
278    pub passed: bool,
279    #[serde(skip_serializing_if = "Option::is_none")]
280    pub error: Option<String>,
281}
282
283/// Output for `leo synthesize`.
284#[derive(Serialize, Default)]
285pub struct SynthesizeOutput {
286    pub program: String,
287    pub functions: Vec<SynthesizedFunction>,
288}
289
290/// Prover/verifier key metadata.
291#[derive(Serialize, Clone, Default)]
292pub struct Metadata {
293    pub prover_checksum: String,
294    pub prover_size: usize,
295    pub verifier_checksum: String,
296    pub verifier_size: usize,
297}
298
299/// Circuit information from key synthesis.
300#[derive(Serialize, Clone, Default)]
301pub struct CircuitInfo {
302    pub num_public_inputs: u64,
303    pub num_variables: u64,
304    pub num_constraints: u64,
305    pub num_non_zero_a: u64,
306    pub num_non_zero_b: u64,
307    pub num_non_zero_c: u64,
308    pub circuit_id: String,
309}
310
311/// A single synthesized function's key metadata.
312#[derive(Serialize)]
313pub struct SynthesizedFunction {
314    pub name: String,
315    pub circuit_info: CircuitInfo,
316    #[serde(flatten)]
317    pub metadata: Metadata,
318}
319
320/// Build a `DeployOutput` from transactions, stats, and optional broadcast results.
321pub fn build_deploy_output<N: Network>(
322    config: Option<Config>,
323    transactions: &[(ProgramID<N>, Transaction<N>)],
324    stats: &[DeploymentStats],
325    broadcasts: &[BroadcastStats],
326) -> DeployOutput {
327    DeployOutput {
328        config,
329        deployments: transactions
330            .iter()
331            .zip(stats.iter().map(Some).chain(std::iter::repeat(None)))
332            .zip(broadcasts.iter().map(Some).chain(std::iter::repeat(None)))
333            .map(|(((program_id, transaction), stats), broadcast)| DeployedProgram {
334                program_id: program_id.to_string(),
335                transaction_id: transaction.id().to_string(),
336                stats: stats.cloned(),
337                broadcast: broadcast.cloned(),
338            })
339            .collect(),
340    }
341}