Skip to main content

charms/cli/
mod.rs

1pub mod app;
2pub mod server;
3pub mod spell;
4pub mod tx;
5pub mod util;
6pub mod wallet;
7
8use crate::{
9    cli::{
10        server::Server,
11        spell::{Check, Prove, SpellCli},
12        wallet::{List, WalletCli},
13    },
14    spell::{CharmsFee, MockProver, ProveSpellTx, ProveSpellTxImpl},
15    utils,
16    utils::BoxedSP1Prover,
17};
18#[cfg(feature = "prover")]
19use crate::{
20    spell::Prover,
21    utils::{Shared, sp1::cuda::SP1CudaProver},
22};
23use bitcoin::{Address, Network};
24use charms_app_runner::AppRunner;
25use charms_client::tx::Chain;
26use charms_data::{App, check};
27use clap::{Args, CommandFactory, Parser, Subcommand};
28use clap_complete::{CompleteEnv, Shell, generate};
29use serde::Serialize;
30use sp1_sdk::{CpuProver, NetworkProver, ProverClient, install::try_install_circuit_artifacts};
31use std::{io, net::IpAddr, path::PathBuf, str::FromStr, sync::Arc};
32
33#[derive(Parser)]
34#[command(
35    author,
36    version,
37    about,
38    long_about = "Charms CLI: create, prove, and manage programmable assets (charms) on Bitcoin and Cardano using zero-knowledge proofs."
39)]
40pub struct Cli {
41    #[command(subcommand)]
42    pub command: Commands,
43}
44
45#[derive(Args)]
46pub struct ServerConfig {
47    /// IP address to listen on.
48    #[arg(long, default_value = "0.0.0.0")]
49    ip: IpAddr,
50
51    /// Port to listen on.
52    #[arg(long, default_value = "17784")]
53    port: u16,
54}
55
56#[derive(Subcommand)]
57pub enum Commands {
58    /// Start the Charms REST API server (POST /spells/prove, GET /ready).
59    Server(#[command(flatten)] ServerConfig),
60
61    /// Create, verify, and prove spells (transaction metadata that defines charm operations).
62    Spell {
63        #[command(subcommand)]
64        command: SpellCommands,
65    },
66
67    /// Inspect blockchain transactions for embedded spells.
68    Tx {
69        #[command(subcommand)]
70        command: TxCommands,
71    },
72
73    /// Create, build, and inspect Charms apps (WebAssembly programs).
74    App {
75        #[command(subcommand)]
76        command: AppCommands,
77    },
78
79    /// List UTXOs with charms in the connected wallet.
80    Wallet {
81        #[command(subcommand)]
82        command: WalletCommands,
83    },
84
85    /// Generate shell completion scripts (static).
86    ///
87    /// For dynamic completions (recommended), run `charms completions --help`.
88    #[command(after_long_help = "\
89DYNAMIC COMPLETIONS (RECOMMENDED):
90
91  Dynamic completions stay up-to-date automatically when charms is upgraded.
92
93  Bash (add to ~/.bashrc):
94    source <(COMPLETE=bash charms)
95
96  Zsh (add to ~/.zshrc):
97    source <(COMPLETE=zsh charms)
98
99  Fish (add to ~/.config/fish/completions/charms.fish):
100    COMPLETE=fish charms | source
101
102  Elvish (add to ~/.elvish/rc.elv):
103    eval (E:COMPLETE=elvish charms | slurp)
104
105STATIC COMPLETIONS:
106
107  Use this if you prefer pre-generated scripts or if dynamic completions
108  don't work in your environment.
109
110  Bash (add to ~/.bashrc):
111    source <(charms completions bash)
112
113  Zsh (add to ~/.zshrc):
114    source <(charms completions zsh)
115
116  Fish (run once):
117    charms completions fish > ~/.config/fish/completions/charms.fish
118
119  PowerShell (add to $PROFILE):
120    charms completions powershell | Out-String | Invoke-Expression
121
122  Elvish (add to ~/.elvish/rc.elv):
123    eval (charms completions elvish | slurp)
124
125After setup, restart your shell or source the config file.
126Then type `charms <TAB>` to see available commands and options.")]
127    Completions {
128        /// Shell to generate completions for
129        #[arg(value_enum)]
130        shell: Shell,
131    },
132
133    /// Utility commands
134    Util {
135        #[command(subcommand)]
136        command: UtilCommands,
137    },
138}
139
140#[derive(Args)]
141pub struct SpellProveParams {
142    /// Path to the spell file (YAML or JSON).
143    #[arg(long, default_value = "/dev/stdin")]
144    spell: PathBuf,
145
146    /// Path to the private inputs file (YAML or JSON).
147    #[arg(long)]
148    private_inputs: Option<PathBuf>,
149
150    /// Beamed-from mapping as a YAML/JSON string (e.g. '{0: "txid:vout"}').
151    #[arg(long)]
152    beamed_from: Option<String>,
153
154    /// Hex-encoded pre-requisite transactions.
155    /// Must include the transactions that create the UTXOs spent by the spell.
156    /// If the spell has reference UTXOs, transactions creating them must also be included.
157    #[arg(long)]
158    prev_txs: Vec<String>,
159
160    /// Paths to app Wasm binaries (.wasm files).
161    #[arg(long)]
162    app_bins: Vec<PathBuf>,
163
164    /// Bitcoin or Cardano address to send the change to.
165    #[arg(long)]
166    change_address: String,
167
168    /// Fee rate in sats/vB (Bitcoin only).
169    #[arg(long, default_value = "2.0")]
170    fee_rate: f64,
171
172    /// Target chain.
173    #[arg(long, default_value = "bitcoin")]
174    chain: Chain,
175
176    /// Use mock mode (skip proof generation).
177    #[arg(long, default_value = "false", hide_env = true)]
178    mock: bool,
179
180    /// Collateral UTXO for Cardano transactions, as txid:vout (required for --chain cardano).
181    #[arg(long, alias = "collateral")]
182    collateral_utxo: Option<String>,
183}
184
185#[derive(Args)]
186pub struct SpellCheckParams {
187    /// Path to the spell file (YAML or JSON).
188    #[arg(long, default_value = "/dev/stdin")]
189    spell: PathBuf,
190
191    /// Path to the private inputs file (YAML or JSON).
192    #[arg(long)]
193    private_inputs: Option<PathBuf>,
194
195    /// Beamed-from mapping as a YAML/JSON string (e.g. '{0: "txid:vout"}').
196    #[arg(long)]
197    beamed_from: Option<String>,
198
199    /// Paths to app Wasm binaries (.wasm files).
200    #[arg(long)]
201    app_bins: Vec<PathBuf>,
202
203    /// Hex-encoded pre-requisite transactions.
204    /// Must include the transactions that create the UTXOs spent by the spell.
205    /// If the spell has reference UTXOs, transactions creating them must also be included.
206    #[arg(long)]
207    prev_txs: Option<Vec<String>>,
208
209    /// Target chain.
210    #[arg(long, default_value = "bitcoin")]
211    chain: Chain,
212
213    /// Use mock mode (skip proof generation).
214    #[arg(long, default_value = "false", hide_env = true)]
215    mock: bool,
216}
217
218#[derive(Args)]
219pub struct SpellVkParams {
220    /// Use mock mode (show mock verification key).
221    #[arg(long, default_value = "false", hide_env = true)]
222    mock: bool,
223}
224
225const SPELL_DATA_HELP: &str = "\
226DATA STRUCTURES:
227
228  Spell file (--spell):
229    version: 11                     # protocol version
230    tx:
231      ins:                          # input UTXOs (txid:vout)
232        - deadbeef...:0
233      outs:                         # output charms (app_index: value)
234        - 0: ~                      # output 0: app 0 has no data
235        - 1: 4000000                # output 1: app 1 = 4000000
236          2: 10000000               #            app 2 = 10000000
237      beamed_outs:                  # (optional) beamed output index -> dest hash
238        1: 009fb489...
239      coins:                        # native coin outputs
240        - amount: 4000000           #   amount in lovelace (Cardano) or sats (Bitcoin)
241          dest: 716fc738...         #   hex-encoded destination (use `charms util dest`)
242          content:                  #   (optional, Cardano) native tokens
243            multiasset:
244              <policy_id_hex>:
245                <asset_name_hex>: <quantity>
246    app_public_inputs:              # map of app -> public input data
247      t/<identity_hex>/<vk_hex>:    #   token app (t), NFT (n), or contract (c)
248      c/0000...0000/<vk_hex>:       #   value can be null or app-specific data
249
250  Private inputs file (--private-inputs):
251    t/<identity_hex>/<vk_hex>: <app-specific data>
252    c/0000...0000/<vk_hex>: <app-specific data>
253
254  Previous transactions (--prev-txs):
255    Each value is one of:
256      - raw hex (auto-detected as Bitcoin or Cardano)
257      - YAML-tagged: '!bitcoin <hex>' or '!cardano <hex>'
258      - YAML-tagged with finality proof:
259          '!cardano {tx: <hex>, signature: <hex>}'
260      - JSON: '{\"bitcoin\": \"<hex>\"}' or '{\"cardano\": \"<hex>\"}'
261
262  Beamed-from mapping (--beamed-from):
263    YAML/JSON mapping: input_index -> [source_utxo, nonce]
264    Example: '{0: [712fcb00...f66c:1, 4538918914141394474]}'
265";
266
267#[derive(Subcommand)]
268pub enum SpellCommands {
269    /// Check spell correctness by running app contracts locally (no proof generation).
270    #[command(after_long_help = SPELL_DATA_HELP)]
271    Check(#[command(flatten)] SpellCheckParams),
272    /// Prove spell correctness and build a ready-to-broadcast transaction.
273    ///
274    /// Outputs a JSON array of hex-encoded transactions (Bitcoin)
275    /// or a Ledger CDDL JSON envelope (Cardano).
276    #[command(after_long_help = SPELL_DATA_HELP)]
277    Prove(#[command(flatten)] SpellProveParams),
278    /// Print the current protocol version and spell verification key (VK) as JSON to stdout.
279    Vk(#[command(flatten)] SpellVkParams),
280}
281
282#[derive(Args)]
283pub struct ShowSpellParams {
284    /// Target chain.
285    #[arg(long, default_value = "bitcoin")]
286    chain: Chain,
287
288    /// Hex-encoded transaction.
289    #[arg(long)]
290    tx: String,
291
292    /// Output in JSON format (default is YAML).
293    #[arg(long)]
294    json: bool,
295
296    /// Use mock mode (accept mock proofs).
297    #[arg(long, default_value = "false", hide_env = true)]
298    mock: bool,
299}
300
301#[derive(Subcommand)]
302pub enum TxCommands {
303    /// Extract and display the spell from a transaction.
304    ///
305    /// Prints the spell as YAML (default) or JSON if the transaction contains a valid proof.
306    ShowSpell(#[command(flatten)] ShowSpellParams),
307}
308
309#[derive(Subcommand)]
310pub enum AppCommands {
311    /// Create a new app from template.
312    New {
313        /// Name of the app. A directory with this name will be created.
314        name: String,
315    },
316
317    /// Build the app to WebAssembly (wasm32-wasip1).
318    ///
319    /// Prints the path to the built .wasm binary to stdout.
320    Build,
321
322    /// Show verification key (SHA-256 of Wasm binary) for an app.
323    ///
324    /// Prints the hex-encoded VK to stdout.
325    Vk {
326        /// Path to app Wasm binary (builds the app if omitted).
327        path: Option<PathBuf>,
328    },
329}
330
331#[derive(Subcommand)]
332pub enum WalletCommands {
333    /// List outputs with charms in the user's wallet.
334    ///
335    /// Outputs YAML (default) or JSON. Requires `bitcoin-cli` to be available.
336    List(#[command(flatten)] WalletListParams),
337}
338
339#[derive(Args)]
340pub struct WalletListParams {
341    /// Output in JSON format (default is YAML).
342    #[arg(long)]
343    json: bool,
344
345    /// Use mock mode (accept mock proofs).
346    #[arg(long, default_value = "false", hide_env = true)]
347    mock: bool,
348}
349
350#[derive(Args)]
351pub struct DestParams {
352    /// Bitcoin or Cardano address to convert.
353    #[arg(long)]
354    addr: Option<String>,
355
356    /// Charms apps for proxy script address (format: tag/identity_hex/vk_hex).
357    #[arg(long)]
358    apps: Vec<App>,
359
360    /// Target chain (auto-detected from address if omitted).
361    #[arg(long)]
362    chain: Option<Chain>,
363}
364
365#[derive(Subcommand)]
366pub enum UtilCommands {
367    /// Install circuit files.
368    #[clap(hide = true)]
369    InstallCircuitFiles,
370
371    /// Print hex-encoded `dest` value for use in spell YAML.
372    ///
373    /// Accepts either --addr (Bitcoin/Cardano address) or --apps (Cardano only).
374    /// Prints the hex-encoded destination bytes to stdout.
375    Dest(#[command(flatten)] DestParams),
376}
377
378pub async fn run() -> anyhow::Result<()> {
379    utils::logger::setup_logger();
380
381    CompleteEnv::with_factory(Cli::command).complete();
382    let cli = Cli::parse();
383
384    match cli.command {
385        Commands::Server(server_config) => {
386            let server = server(server_config);
387            server.serve().await
388        }
389        Commands::Spell { command } => {
390            let spell_cli = spell_cli();
391            match command {
392                SpellCommands::Check(params) => spell_cli.check(params),
393                SpellCommands::Prove(params) => spell_cli.prove(params).await,
394                SpellCommands::Vk(params) => spell_cli.print_vk(params.mock),
395            }
396        }
397        Commands::Tx { command } => match command {
398            TxCommands::ShowSpell(params) => tx::tx_show_spell(params),
399        },
400        Commands::App { command } => match command {
401            AppCommands::New { name } => app::new(&name),
402            AppCommands::Vk { path } => app::vk(path),
403            AppCommands::Build => app::build(),
404        },
405        Commands::Wallet { command } => {
406            let wallet_cli = wallet_cli();
407            match command {
408                WalletCommands::List(params) => wallet_cli.list(params),
409            }
410        }
411        Commands::Completions { shell } => generate_completions(shell),
412        Commands::Util { command } => match command {
413            UtilCommands::InstallCircuitFiles => {
414                let _ = try_install_circuit_artifacts("groth16");
415                Ok(())
416            }
417            UtilCommands::Dest(params) => util::dest(params),
418        },
419    }
420}
421
422fn server(server_config: ServerConfig) -> Server {
423    let prover = ProveSpellTxImpl::new(false);
424    Server::new(server_config, prover)
425}
426
427pub fn prove_impl(mock: bool) -> Box<dyn crate::spell::Prove> {
428    tracing::debug!(mock);
429    #[cfg(feature = "prover")]
430    match mock {
431        false => {
432            let app_prover = Arc::new(crate::app::Prover {
433                sp1_client: Arc::new(Shared::new(crate::cli::app_sp1_client)),
434                runner: AppRunner::new(false),
435            });
436            let spell_sp1_client = crate::cli::spell_sp1_client(&app_prover.sp1_client);
437            Box::new(Prover::new(app_prover, spell_sp1_client))
438        }
439        true => Box::new(MockProver {
440            spell_prover_client: Arc::new(utils::Shared::new(|| Box::new(sp1_cpu_prover()))),
441        }),
442    }
443    #[cfg(not(feature = "prover"))]
444    {
445        Box::new(MockProver {
446            spell_prover_client: Arc::new(utils::Shared::new(|| Box::new(sp1_cpu_prover()))),
447        })
448    }
449}
450
451pub(crate) fn charms_fee_settings() -> Option<CharmsFee> {
452    let fee_settings_file = std::env::var("CHARMS_FEE_SETTINGS").ok()?;
453    let fee_settings: CharmsFee = serde_yaml::from_reader(
454        &std::fs::File::open(fee_settings_file)
455            .expect("should be able to open the fee settings file"),
456    )
457    .expect("should be able to parse the fee settings file");
458
459    assert!(
460        fee_settings.fee_addresses[&Chain::Bitcoin]
461            .iter()
462            .all(|(network, address)| {
463                let network = Network::from_core_arg(network)
464                    .expect("network should be a valid `bitcoind -chain` argument");
465                check!(
466                    Address::from_str(address)
467                        .is_ok_and(|address| address.is_valid_for_network(network))
468                );
469                true
470            }),
471        "a fee address is not valid for the specified network"
472    );
473
474    Some(fee_settings)
475}
476
477fn spell_cli() -> SpellCli {
478    let spell_cli = SpellCli {
479        app_runner: AppRunner::new(true),
480    };
481    spell_cli
482}
483
484#[cfg(feature = "prover")]
485fn app_sp1_client() -> BoxedSP1Prover {
486    let name = std::env::var("APP_SP1_PROVER").unwrap_or_default();
487    sp1_named_env_client(name.as_str())
488}
489
490#[cfg(feature = "prover")]
491fn spell_sp1_client(app_sp1_client: &Arc<Shared<BoxedSP1Prover>>) -> Arc<Shared<BoxedSP1Prover>> {
492    let name = std::env::var("SPELL_SP1_PROVER").unwrap_or_default();
493    match name.as_str() {
494        "app" => app_sp1_client.clone(),
495        "network" => Arc::new(Shared::new(sp1_network_client)),
496        _ => unreachable!("Only 'app' or 'network' are supported as SPELL_SP1_PROVER values"),
497    }
498}
499
500#[tracing::instrument(level = "info")]
501#[cfg(feature = "prover")]
502fn charms_sp1_cuda_prover() -> utils::sp1::CudaProver {
503    utils::sp1::CudaProver::new(
504        sp1_prover::SP1Prover::new(),
505        SP1CudaProver::new(gpu_service_url()).unwrap(),
506    )
507}
508
509#[cfg(feature = "prover")]
510fn gpu_service_url() -> String {
511    std::env::var("SP1_GPU_SERVICE_URL").unwrap_or("http://localhost:3000/twirp/".to_string())
512}
513
514#[tracing::instrument(level = "info")]
515pub fn sp1_cpu_prover() -> CpuProver {
516    ProverClient::builder().cpu().build()
517}
518
519#[tracing::instrument(level = "info")]
520pub fn sp1_network_prover() -> NetworkProver {
521    ProverClient::builder().network().build()
522}
523
524#[tracing::instrument(level = "info")]
525pub fn sp1_network_client() -> BoxedSP1Prover {
526    sp1_named_env_client("network")
527}
528
529#[tracing::instrument(level = "debug")]
530fn sp1_named_env_client(name: &str) -> BoxedSP1Prover {
531    let sp1_prover_env_var = std::env::var("SP1_PROVER").unwrap_or_default();
532    let name = match name {
533        "env" => sp1_prover_env_var.as_str(),
534        _ => name,
535    };
536    match name {
537        #[cfg(feature = "prover")]
538        "cuda" => Box::new(charms_sp1_cuda_prover()),
539        "cpu" => Box::new(sp1_cpu_prover()),
540        "network" => Box::new(sp1_network_prover()),
541        _ => unimplemented!("only 'cuda', 'cpu' and 'network' are supported as prover values"),
542    }
543}
544
545fn wallet_cli() -> WalletCli {
546    let wallet_cli = WalletCli {};
547    wallet_cli
548}
549
550fn generate_completions(shell: Shell) -> anyhow::Result<()> {
551    let cmd = &mut Cli::command();
552    generate(shell, cmd, cmd.get_name().to_string(), &mut io::stdout());
553    Ok(())
554}
555
556fn print_output<T: Serialize>(output: &T, json: bool) -> anyhow::Result<()> {
557    match json {
558        true => serde_json::to_writer_pretty(io::stdout(), &output)?,
559        false => serde_yaml::to_writer(io::stdout(), &output)?,
560    };
561    Ok(())
562}
563
564#[cfg(test)]
565mod test {
566    #[test]
567    fn dummy() {}
568}