pub mod app;
pub mod server;
pub mod spell;
pub mod tx;
pub mod util;
pub mod wallet;
use crate::{
cli::{
server::Server,
spell::{Check, Prove, SpellCli},
wallet::{List, WalletCli},
},
spell::{CharmsFee, MockProver, ProveSpellTx, ProveSpellTxImpl},
utils,
utils::{BoxedSP1Prover, block_on},
};
#[cfg(feature = "prover")]
use crate::{spell::Prover, utils::Shared};
use bitcoin::{Address, Network};
use charms_app_runner::AppRunner;
use charms_client::tx::Chain;
use charms_data::{App, check};
use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum};
use clap_complete::{CompleteEnv, Shell, generate};
use serde::Serialize;
use sp1_sdk::{CpuProver, NetworkProver, ProverClient, install::try_install_circuit_artifacts};
use std::{io, net::IpAddr, path::PathBuf, str::FromStr, sync::Arc};
#[derive(Parser)]
#[command(
author,
version,
about,
long_about = "Charms CLI: create, prove, and manage programmable assets (charms) on Bitcoin and Cardano using zero-knowledge proofs."
)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Args)]
pub struct ServerConfig {
#[arg(long, default_value = "0.0.0.0")]
ip: IpAddr,
#[arg(long, default_value = "17784")]
port: u16,
}
#[derive(Subcommand)]
pub enum Commands {
Server(#[command(flatten)] ServerConfig),
Spell {
#[command(subcommand)]
command: SpellCommands,
},
Tx {
#[command(subcommand)]
command: TxCommands,
},
App {
#[command(subcommand)]
command: AppCommands,
},
Wallet {
#[command(subcommand)]
command: WalletCommands,
},
#[command(after_long_help = "\
DYNAMIC COMPLETIONS (RECOMMENDED):
Dynamic completions stay up-to-date automatically when charms is upgraded.
Bash (add to ~/.bashrc):
source <(COMPLETE=bash charms)
Zsh (add to ~/.zshrc):
source <(COMPLETE=zsh charms)
Fish (add to ~/.config/fish/completions/charms.fish):
COMPLETE=fish charms | source
Elvish (add to ~/.elvish/rc.elv):
eval (E:COMPLETE=elvish charms | slurp)
STATIC COMPLETIONS:
Use this if you prefer pre-generated scripts or if dynamic completions
don't work in your environment.
Bash (add to ~/.bashrc):
source <(charms completions bash)
Zsh (add to ~/.zshrc):
source <(charms completions zsh)
Fish (run once):
charms completions fish > ~/.config/fish/completions/charms.fish
PowerShell (add to $PROFILE):
charms completions powershell | Out-String | Invoke-Expression
Elvish (add to ~/.elvish/rc.elv):
eval (charms completions elvish | slurp)
After setup, restart your shell or source the config file.
Then type `charms <TAB>` to see available commands and options.")]
Completions {
#[arg(value_enum)]
shell: Shell,
},
Util {
#[command(subcommand)]
command: UtilCommands,
},
}
#[derive(Clone, Debug, ValueEnum)]
pub enum Output {
#[value(name = "cbor")]
Cbor,
#[value(name = "json")]
Json,
}
#[derive(Args)]
pub struct SpellProveParams {
#[arg(long, default_value = "/dev/stdin")]
spell: PathBuf,
#[arg(long, default_value = "false", hide_env = true)]
payload: bool,
#[arg(
long,
short = 'o',
default_value = "json",
value_enum,
requires = "payload"
)]
output: Output,
#[arg(long)]
private_inputs: Option<PathBuf>,
#[arg(long)]
beamed_from: Option<String>,
#[arg(long)]
prev_txs: Vec<String>,
#[arg(long)]
app_bins: Vec<PathBuf>,
#[arg(long)]
change_address: String,
#[arg(long, default_value = "2.0")]
fee_rate: f64,
#[arg(long, default_value = "bitcoin")]
chain: Chain,
#[arg(long, default_value = "false", hide_env = true)]
mock: bool,
#[arg(long, alias = "collateral")]
collateral_utxo: Option<String>,
}
#[derive(Args)]
pub struct SpellCheckParams {
#[arg(long, default_value = "/dev/stdin")]
spell: PathBuf,
#[arg(long)]
private_inputs: Option<PathBuf>,
#[arg(long)]
beamed_from: Option<String>,
#[arg(long)]
app_bins: Vec<PathBuf>,
#[arg(long)]
prev_txs: Option<Vec<String>>,
#[arg(long, default_value = "bitcoin")]
chain: Chain,
#[arg(long, default_value = "false", hide_env = true)]
mock: bool,
}
#[derive(Args)]
pub struct SpellVkParams {
#[arg(long, default_value = "false", hide_env = true)]
mock: bool,
}
const SPELL_DATA_HELP: &str = "\
DATA STRUCTURES:
Spell file (--spell):
version: 11 # protocol version
tx:
ins: # input UTXOs (txid:vout)
- deadbeef...:0
outs: # output charms (app_index: value)
- 0: ~ # output 0: app 0 has no data
- 1: 4000000 # output 1: app 1 = 4000000
2: 10000000 # app 2 = 10000000
beamed_outs: # (optional) beamed output index -> dest hash
1: 009fb489...
coins: # native coin outputs
- amount: 4000000 # amount in lovelace (Cardano) or sats (Bitcoin)
dest: 716fc738... # hex-encoded destination (use `charms util dest`)
content: # (optional, Cardano) native tokens
multiasset:
<policy_id_hex>:
<asset_name_hex>: <quantity>
app_public_inputs: # map of app -> public input data
t/<identity_hex>/<vk_hex>: # token app (t), NFT (n), or contract (c)
c/0000...0000/<vk_hex>: # value can be null or app-specific data
Private inputs file (--private-inputs):
t/<identity_hex>/<vk_hex>: <app-specific data>
c/0000...0000/<vk_hex>: <app-specific data>
Previous transactions (--prev-txs):
Each value is one of:
- raw hex (auto-detected as Bitcoin or Cardano)
- YAML-tagged: '!bitcoin <hex>' or '!cardano <hex>'
- YAML-tagged with finality proof:
'!cardano {tx: <hex>, signature: <hex>}'
- JSON: '{\"bitcoin\": \"<hex>\"}' or '{\"cardano\": \"<hex>\"}'
Beamed-from mapping (--beamed-from):
YAML/JSON mapping: input_index -> [source_utxo, nonce]
Example: '{0: [712fcb00...f66c:1, 4538918914141394474]}'
";
#[derive(Subcommand)]
pub enum SpellCommands {
#[command(after_long_help = SPELL_DATA_HELP)]
Check(#[command(flatten)] SpellCheckParams),
#[command(after_long_help = SPELL_DATA_HELP)]
Prove(#[command(flatten)] SpellProveParams),
Vk(#[command(flatten)] SpellVkParams),
}
#[derive(Args)]
pub struct ShowSpellParams {
#[arg(long, default_value = "bitcoin")]
chain: Chain,
#[arg(long)]
tx: String,
#[arg(long)]
json: bool,
#[arg(long, default_value = "false", hide_env = true)]
mock: bool,
}
#[derive(Subcommand)]
pub enum TxCommands {
ShowSpell(#[command(flatten)] ShowSpellParams),
}
#[derive(Subcommand)]
pub enum AppCommands {
New {
name: String,
},
Build,
Vk {
path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
pub enum WalletCommands {
List(#[command(flatten)] WalletListParams),
}
#[derive(Args)]
pub struct WalletListParams {
#[arg(long)]
json: bool,
#[arg(long, default_value = "false", hide_env = true)]
mock: bool,
}
#[derive(Args)]
pub struct DestParams {
#[arg(long)]
addr: Option<String>,
#[arg(long)]
apps: Vec<App>,
#[arg(long)]
chain: Option<Chain>,
}
#[derive(Subcommand)]
pub enum UtilCommands {
#[clap(hide = true)]
InstallCircuitFiles,
Dest(#[command(flatten)] DestParams),
}
pub async fn run() -> anyhow::Result<()> {
utils::logger::setup_logger();
CompleteEnv::with_factory(Cli::command).complete();
let cli = Cli::parse();
match cli.command {
Commands::Server(server_config) => {
let server = server(server_config);
server.serve().await
}
Commands::Spell { command } => {
let spell_cli = spell_cli();
match command {
SpellCommands::Check(params) => spell_cli.check(params),
SpellCommands::Prove(params) => spell_cli.prove(params).await,
SpellCommands::Vk(params) => spell_cli.print_vk(params.mock),
}
}
Commands::Tx { command } => match command {
TxCommands::ShowSpell(params) => tx::tx_show_spell(params),
},
Commands::App { command } => match command {
AppCommands::New { name } => app::new(&name),
AppCommands::Vk { path } => app::vk(path),
AppCommands::Build => app::build(),
},
Commands::Wallet { command } => {
let wallet_cli = wallet_cli();
match command {
WalletCommands::List(params) => wallet_cli.list(params),
}
}
Commands::Completions { shell } => generate_completions(shell),
Commands::Util { command } => match command {
UtilCommands::InstallCircuitFiles => {
let _ = try_install_circuit_artifacts("groth16");
Ok(())
}
UtilCommands::Dest(params) => util::dest(params),
},
}
}
fn server(server_config: ServerConfig) -> Server {
let prover = ProveSpellTxImpl::new(false);
Server::new(server_config, prover)
}
pub fn prove_impl(mock: bool) -> Box<dyn crate::spell::Prove> {
tracing::debug!(mock);
#[cfg(feature = "prover")]
match mock {
false => {
let app_prover = Arc::new(crate::app::Prover {
sp1_client: Arc::new(Shared::new(crate::cli::app_sp1_client)),
runner: AppRunner::new(false),
});
let spell_sp1_client = crate::cli::spell_sp1_client(&app_prover.sp1_client);
Box::new(Prover::new(app_prover, spell_sp1_client))
}
true => Box::new(MockProver {
spell_prover_client: Arc::new(utils::Shared::new(|| Box::new(sp1_cpu_prover()))),
}),
}
#[cfg(not(feature = "prover"))]
{
Box::new(MockProver {
spell_prover_client: Arc::new(utils::Shared::new(|| Box::new(sp1_cpu_prover()))),
})
}
}
pub(crate) fn charms_fee_settings() -> Option<CharmsFee> {
let fee_settings_file = std::env::var("CHARMS_FEE_SETTINGS").ok()?;
let fee_settings: CharmsFee = serde_yaml::from_reader(
&std::fs::File::open(fee_settings_file)
.expect("should be able to open the fee settings file"),
)
.expect("should be able to parse the fee settings file");
assert!(
fee_settings.fee_addresses[&Chain::Bitcoin]
.iter()
.all(|(network, address)| {
let network = Network::from_core_arg(network)
.expect("network should be a valid `bitcoind -chain` argument");
check!(
Address::from_str(address)
.is_ok_and(|address| address.is_valid_for_network(network))
);
true
}),
"a fee address is not valid for the specified network"
);
Some(fee_settings)
}
fn spell_cli() -> SpellCli {
let spell_cli = SpellCli {
app_runner: AppRunner::new(true),
};
spell_cli
}
#[cfg(feature = "prover")]
fn app_sp1_client() -> BoxedSP1Prover {
let name = std::env::var("APP_SP1_PROVER").unwrap_or_default();
sp1_named_env_client(name.as_str())
}
#[cfg(feature = "prover")]
fn spell_sp1_client(app_sp1_client: &Arc<Shared<BoxedSP1Prover>>) -> Arc<Shared<BoxedSP1Prover>> {
let name = std::env::var("SPELL_SP1_PROVER").unwrap_or_default();
match name.as_str() {
"app" => app_sp1_client.clone(),
"network" => Arc::new(Shared::new(sp1_network_client)),
_ => unreachable!("Only 'app' or 'network' are supported as SPELL_SP1_PROVER values"),
}
}
#[tracing::instrument(level = "info")]
pub fn sp1_cpu_prover() -> CpuProver {
block_on(ProverClient::builder().cpu().build())
}
#[tracing::instrument(level = "info")]
pub fn sp1_network_prover() -> NetworkProver {
block_on(ProverClient::builder().network().build())
}
#[tracing::instrument(level = "info")]
pub fn sp1_network_client() -> BoxedSP1Prover {
sp1_named_env_client("network")
}
#[tracing::instrument(level = "debug")]
fn sp1_named_env_client(name: &str) -> BoxedSP1Prover {
let sp1_prover_env_var = std::env::var("SP1_PROVER").unwrap_or_default();
let name = match name {
"env" => sp1_prover_env_var.as_str(),
_ => name,
};
match name {
#[cfg(feature = "prover")]
"cpu" => Box::new(sp1_cpu_prover()),
"network" => Box::new(sp1_network_prover()),
_ => unimplemented!("only 'cuda', 'cpu' and 'network' are supported as prover values"),
}
}
fn wallet_cli() -> WalletCli {
let wallet_cli = WalletCli {};
wallet_cli
}
fn generate_completions(shell: Shell) -> anyhow::Result<()> {
let cmd = &mut Cli::command();
generate(shell, cmd, cmd.get_name().to_string(), &mut io::stdout());
Ok(())
}
fn print_output<T: Serialize>(output: &T, json: bool) -> anyhow::Result<()> {
match json {
true => serde_json::to_writer_pretty(io::stdout(), &output)?,
false => serde_yaml::to_writer(io::stdout(), &output)?,
};
Ok(())
}
#[cfg(test)]
mod test {
#[test]
fn dummy() {}
}