#[macro_use]
extern crate clap;
#[macro_use]
extern crate amplify;
extern crate serde_crate as serde;
use std::fmt::{Debug, Display};
use std::fs;
use std::io::{self, Read};
use std::path::PathBuf;
use std::str::FromStr;
use amplify::hex::{FromHex, ToHex};
use bitcoin::psbt::serialize::{Deserialize, Serialize};
use bitcoin::OutPoint;
use bitcoin_scripts::taproot::{DfsOrder, DfsPath};
use bp::seals::txout::CloseMethod;
use clap::Parser;
use commit_verify::ConsensusCommit;
use electrum_client::Client as ElectrumClient;
use rgb::psbt::RgbExt;
use rgb::{Disclosure, Extension, Schema, StateTransfer, Transition};
use rgb_core::{seal, Node, Validator};
use strict_encoding::{StrictDecode, StrictEncode};
use wallet::psbt::Psbt;
#[derive(Parser, Clone, Debug)]
#[clap(
name = "rgb",
bin_name = "rgb",
author,
version,
about = "Command-line tool for working with RGB smart contracts"
)]
pub struct Opts {
#[clap(subcommand)]
pub command: Command,
}
#[derive(Subcommand, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum Command {
Blind {
#[clap(short, long, default_value = "tapret1st")]
method: CloseMethod,
utxo: OutPoint,
},
Consignment {
#[clap(subcommand)]
subcommand: ConsignmentCommand,
},
Disclosure {
#[clap(subcommand)]
subcommand: DisclosureCommand,
},
Schema {
#[clap(subcommand)]
subcommand: SchemaCommand,
},
Extension {
#[clap(subcommand)]
subcommand: ExtensionCommand,
},
Transition {
#[clap(subcommand)]
subcommand: TransitionCommand,
},
Psbt {
#[clap(subcommand)]
subcommand: PsbtCommand,
},
}
#[derive(Subcommand, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum ConsignmentCommand {
Inspect {
#[clap(short, long, default_value = "yaml")]
format: Format,
consignment: PathBuf,
},
Validate {
consignment: String,
#[clap(default_value = "pandora.network:60001")]
electrum: String,
},
}
#[derive(Subcommand, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum DisclosureCommand {
Convert {
disclosure: Option<String>,
#[clap(short, long, default_value = "bech32")]
input: Format,
#[clap(short, long, default_value = "yaml")]
output: Format,
},
}
#[derive(Subcommand, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum SchemaCommand {
Convert {
schema: Option<String>,
#[clap(short, long, default_value = "bech32")]
input: Format,
#[clap(short, long, default_value = "yaml")]
output: Format,
},
}
#[derive(Subcommand, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum ExtensionCommand {
Convert {
extension: Option<String>,
#[clap(short, long, default_value = "bech32")]
input: Format,
#[clap(short, long, default_value = "yaml")]
output: Format,
},
}
#[derive(Subcommand, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum TransitionCommand {
Convert {
transition: Option<String>,
#[clap(short, long, default_value = "bech32")]
input: Format,
#[clap(short, long, default_value = "yaml")]
output: Format,
},
}
#[derive(Subcommand, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum PsbtCommand {
Bundle {
psbt_in: PathBuf,
psbt_out: Option<PathBuf>,
#[clap(short, long, default_value = "tapret1st")]
method: CloseMethod,
},
Analyze {
psbt: PathBuf,
},
}
#[derive(ArgEnum, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display)]
pub enum Format {
#[display("debug")]
Debug,
#[display("bech32")]
Bech32,
#[display("yaml")]
Yaml,
#[display("json")]
Json,
#[display("hex")]
Hexadecimal,
#[display("rust")]
Rust,
#[display("raw")]
Binary,
#[display("commitment")]
Commitment,
}
impl FromStr for Format {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s.trim().to_lowercase().as_str() {
"debug" => Format::Debug,
"bech32" => Format::Bech32,
"yaml" => Format::Yaml,
"json" => Format::Json,
"hex" => Format::Hexadecimal,
"raw" | "bin" | "binary" => Format::Binary,
"rust" => Format::Rust,
"commitment" => Format::Commitment,
other => Err(format!("Unknown format: {}", other))?,
})
}
}
fn input_read<T>(data: Option<String>, format: Format) -> Result<T, Error>
where T: StrictDecode + for<'de> serde::Deserialize<'de> {
let data = data
.map(|d| d.as_bytes().to_vec())
.ok_or_else(String::new)
.or_else(|_| -> Result<Vec<u8>, Error> {
let mut buf = Vec::new();
io::stdin().read_to_end(&mut buf)?;
Ok(buf)
})?;
Ok(match format {
Format::Yaml => serde_yaml::from_str(&String::from_utf8_lossy(&data))?,
Format::Json => serde_json::from_str(&String::from_utf8_lossy(&data))?,
Format::Hexadecimal => {
T::strict_deserialize(Vec::<u8>::from_hex(&String::from_utf8_lossy(&data))?)?
}
Format::Binary => T::strict_deserialize(&data)?,
_ => panic!("Can't read data from {} format", format),
})
}
fn output_print<T>(data: T, format: Format) -> Result<(), Error>
where
T: Debug + serde::Serialize + StrictEncode + ConsensusCommit,
<T as ConsensusCommit>::Commitment: Display,
{
match format {
Format::Debug => println!("{:#?}", data),
Format::Yaml => println!("{}", serde_yaml::to_string(&data)?),
Format::Json => println!("{}", serde_json::to_string(&data)?),
Format::Hexadecimal => {
println!("{}", data.strict_serialize()?.to_hex())
}
Format::Rust => println!("{:#04X?}", data.strict_serialize()?),
Format::Binary => {
data.strict_encode(io::stdout())?;
}
Format::Commitment => {
println!("{}", data.consensus_commit())
}
format => panic!("Can't read data in {} format", format),
}
Ok(())
}
#[derive(Debug, Display)]
#[display(inner)]
pub struct Error(Box<dyn std::error::Error>);
impl<E> From<E> for Error
where E: std::error::Error + 'static
{
fn from(e: E) -> Self { Error(Box::new(e)) }
}
fn main() -> Result<(), Error> {
let opts = Opts::parse();
match opts.command {
Command::Blind { utxo, method } => {
let seal = seal::Revealed::new(method, utxo);
println!("{}", seal.to_concealed_seal());
println!("Blinding factor: {}", seal.blinding);
}
Command::Consignment { subcommand } => match subcommand {
ConsignmentCommand::Inspect {
format,
consignment,
} => {
let transfer = StateTransfer::strict_file_load(consignment)?;
output_print(transfer, format)?;
}
ConsignmentCommand::Validate {
consignment,
electrum,
} => {
let transfer = StateTransfer::strict_file_load(consignment)?;
let electrum = ElectrumClient::new(&electrum)?;
let status = Validator::validate(&transfer, &electrum);
println!("{}", serde_yaml::to_string(&status)?);
}
},
Command::Disclosure { subcommand } => match subcommand {
DisclosureCommand::Convert {
disclosure,
input,
output,
} => {
let disclosure: Disclosure = input_read(disclosure, input)?;
output_print(disclosure, output)?;
}
},
Command::Schema { subcommand } => match subcommand {
SchemaCommand::Convert {
schema,
input,
output,
} => {
let schema: Schema = input_read(schema, input)?;
output_print(schema, output)?;
}
},
Command::Extension { subcommand } => match subcommand {
ExtensionCommand::Convert {
extension,
input,
output,
} => {
let extension: Extension = input_read(extension, input)?;
output_print(extension, output)?;
}
},
Command::Transition { subcommand } => match subcommand {
TransitionCommand::Convert {
transition,
input,
output,
} => {
let transition: Transition = input_read(transition, input)?;
output_print(transition, output)?;
}
},
Command::Psbt { subcommand } => match subcommand {
PsbtCommand::Bundle {
psbt_in,
psbt_out,
method,
} => {
let psbt_bytes = fs::read(&psbt_in)?;
let mut psbt = Psbt::deserialize(&psbt_bytes)?;
let mut count: usize = 0;
match method {
CloseMethod::TapretFirst => {
if let Some(output) =
psbt.outputs.iter_mut().find(|o| o.script.is_v1_p2tr())
{
if output.tapret_dfs_path().is_none() {
output.set_tapret_dfs_path(&DfsPath::with([&DfsOrder::Last]))?;
}
}
count = psbt.rgb_bundle_to_lnpbp4()?;
}
CloseMethod::OpretFirst => {
count = psbt.rgb_bundle_to_lnpbp4()?;
if let Some(output) =
psbt.outputs.iter_mut().find(|o| o.script.is_op_return())
{
if !output.is_opret_host() {
output.set_opret_host()?;
}
}
}
_ => {}
};
println!("Total {} bundles converted", count);
let psbt_bytes = psbt.serialize();
fs::write(psbt_out.unwrap_or(psbt_in), psbt_bytes)?;
}
PsbtCommand::Analyze { psbt } => {
let psbt_bytes = fs::read(psbt)?;
let psbt = Psbt::deserialize(&psbt_bytes)?;
println!("contracts:");
for contract_id in psbt.rgb_contract_ids() {
println!("- contract_id: {}", contract_id);
if let Some(contract) = psbt.rgb_contract(contract_id)? {
println!(" - source: {}", contract);
} else {
println!(" - warning: contract source is absent");
}
println!(" - transitions:");
for node_id in psbt.rgb_node_ids(contract_id) {
if let Some(transition) = psbt.rgb_transition(node_id)? {
println!(" - {}", transition.strict_serialize()?.to_hex());
} else {
println!(" - warning: transition is absent");
}
}
println!(" - used in:");
for (node_id, vin) in psbt.rgb_contract_consumers(contract_id)? {
println!(" - input: {}", vin);
println!(" node_id: {}", node_id);
}
}
println!("bundles:");
for (contract_id, bundle) in psbt.rgb_bundles()? {
println!("- contract_id: {}", contract_id);
println!(" bundle_id: {}", bundle.bundle_id());
println!(" - revealed: # nodes");
for transition in bundle.known_transitions() {
println!(
" - {}: {}",
transition.node_id(),
transition.strict_serialize()?.to_hex()
);
}
println!(" - concealed: # nodes and inputs");
for (node_id, vins) in bundle.concealed_iter() {
println!(" - {}: {:?}", node_id, vins);
}
}
println!("proprietary: # all proprietary keys");
println!("- global:");
for (key, value) in psbt.proprietary {
let prefix = String::from_utf8(key.prefix.clone())
.unwrap_or_else(|_| key.prefix.to_hex());
println!(
" - {}/{:#04x}/{}: {}",
prefix,
key.subtype,
key.key.to_hex(),
value.to_hex()
);
}
println!("- inputs:");
for (no, input) in psbt.inputs.iter().enumerate() {
println!(" - {}:", no);
for (key, value) in &input.proprietary {
let prefix = String::from_utf8(key.prefix.clone())
.unwrap_or_else(|_| key.prefix.to_hex());
println!(
" - {}/{:#04x}/{}: {}",
prefix,
key.subtype,
key.key.to_hex(),
value.to_hex()
);
}
}
println!("- outputs:");
for (no, output) in psbt.outputs.iter().enumerate() {
println!(" - {}:", no);
for (key, value) in &output.proprietary {
let prefix = String::from_utf8(key.prefix.clone())
.unwrap_or_else(|_| key.prefix.to_hex());
println!(
" - {}/{:#04x}/{}: {}",
prefix,
key.subtype,
key.key.to_hex(),
value.to_hex()
);
}
}
}
},
}
Ok(())
}