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 #[arg(long, default_value = "0.0.0.0")]
49 ip: IpAddr,
50
51 #[arg(long, default_value = "17784")]
53 port: u16,
54}
55
56#[derive(Subcommand)]
57pub enum Commands {
58 Server(#[command(flatten)] ServerConfig),
60
61 Spell {
63 #[command(subcommand)]
64 command: SpellCommands,
65 },
66
67 Tx {
69 #[command(subcommand)]
70 command: TxCommands,
71 },
72
73 App {
75 #[command(subcommand)]
76 command: AppCommands,
77 },
78
79 Wallet {
81 #[command(subcommand)]
82 command: WalletCommands,
83 },
84
85 #[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 #[arg(value_enum)]
130 shell: Shell,
131 },
132
133 Util {
135 #[command(subcommand)]
136 command: UtilCommands,
137 },
138}
139
140#[derive(Args)]
141pub struct SpellProveParams {
142 #[arg(long, default_value = "/dev/stdin")]
144 spell: PathBuf,
145
146 #[arg(long)]
148 private_inputs: Option<PathBuf>,
149
150 #[arg(long)]
152 beamed_from: Option<String>,
153
154 #[arg(long)]
158 prev_txs: Vec<String>,
159
160 #[arg(long)]
162 app_bins: Vec<PathBuf>,
163
164 #[arg(long)]
166 change_address: String,
167
168 #[arg(long, default_value = "2.0")]
170 fee_rate: f64,
171
172 #[arg(long, default_value = "bitcoin")]
174 chain: Chain,
175
176 #[arg(long, default_value = "false", hide_env = true)]
178 mock: bool,
179
180 #[arg(long, alias = "collateral")]
182 collateral_utxo: Option<String>,
183}
184
185#[derive(Args)]
186pub struct SpellCheckParams {
187 #[arg(long, default_value = "/dev/stdin")]
189 spell: PathBuf,
190
191 #[arg(long)]
193 private_inputs: Option<PathBuf>,
194
195 #[arg(long)]
197 beamed_from: Option<String>,
198
199 #[arg(long)]
201 app_bins: Vec<PathBuf>,
202
203 #[arg(long)]
207 prev_txs: Option<Vec<String>>,
208
209 #[arg(long, default_value = "bitcoin")]
211 chain: Chain,
212
213 #[arg(long, default_value = "false", hide_env = true)]
215 mock: bool,
216}
217
218#[derive(Args)]
219pub struct SpellVkParams {
220 #[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 #[command(after_long_help = SPELL_DATA_HELP)]
271 Check(#[command(flatten)] SpellCheckParams),
272 #[command(after_long_help = SPELL_DATA_HELP)]
277 Prove(#[command(flatten)] SpellProveParams),
278 Vk(#[command(flatten)] SpellVkParams),
280}
281
282#[derive(Args)]
283pub struct ShowSpellParams {
284 #[arg(long, default_value = "bitcoin")]
286 chain: Chain,
287
288 #[arg(long)]
290 tx: String,
291
292 #[arg(long)]
294 json: bool,
295
296 #[arg(long, default_value = "false", hide_env = true)]
298 mock: bool,
299}
300
301#[derive(Subcommand)]
302pub enum TxCommands {
303 ShowSpell(#[command(flatten)] ShowSpellParams),
307}
308
309#[derive(Subcommand)]
310pub enum AppCommands {
311 New {
313 name: String,
315 },
316
317 Build,
321
322 Vk {
326 path: Option<PathBuf>,
328 },
329}
330
331#[derive(Subcommand)]
332pub enum WalletCommands {
333 List(#[command(flatten)] WalletListParams),
337}
338
339#[derive(Args)]
340pub struct WalletListParams {
341 #[arg(long)]
343 json: bool,
344
345 #[arg(long, default_value = "false", hide_env = true)]
347 mock: bool,
348}
349
350#[derive(Args)]
351pub struct DestParams {
352 #[arg(long)]
354 addr: Option<String>,
355
356 #[arg(long)]
358 apps: Vec<App>,
359
360 #[arg(long)]
362 chain: Option<Chain>,
363}
364
365#[derive(Subcommand)]
366pub enum UtilCommands {
367 #[clap(hide = true)]
369 InstallCircuitFiles,
370
371 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}