use std::{fs, path::PathBuf, process, str};
use bitcoin::{consensus::deserialize, Transaction};
use clap::{Parser, Subcommand};
use ordinals_parser::{
parse_inscriptions_from_transaction,
Inscription,
InscriptionBuilder,
InscriptionId,
};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Parse {
tx_hex: String,
#[arg(short, long, default_value = "text")]
format: String,
},
Create {
#[arg(short, long, default_value = "text/plain;charset=utf-8")]
content_type: String,
#[arg(short, long)]
content: PathBuf,
#[arg(short, long)]
parent: Option<String>,
#[arg(short, long)]
delegate: Option<String>,
#[arg(short, long)]
metaprotocol: Option<String>,
},
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
match &cli.command {
Commands::Parse { tx_hex, format } => {
let tx_data = if PathBuf::from(tx_hex).exists() {
fs::read_to_string(tx_hex)?
} else {
tx_hex.clone()
};
let tx_bytes = hex::decode(tx_data.trim())?;
let tx: Transaction = deserialize(&tx_bytes)?;
let inscriptions = parse_inscriptions_from_transaction(&tx);
match format.as_str() {
"text" => print_inscriptions_text(&inscriptions),
"json" => print_inscriptions_json(&inscriptions)?,
_ => {
eprintln!("Error: Unknown output format '{}'. Use 'text' or 'json'.", format);
process::exit(1);
}
}
},
Commands::Create { content_type, content, parent, delegate, metaprotocol } => {
let content_data = fs::read(content)?;
let mut builder = InscriptionBuilder::new()
.content_type(content_type)
.body(&content_data);
if let Some(parent_id) = parent {
let parent_inscription_id = parent_id.parse::<InscriptionId>()?;
builder = builder.parent(parent_inscription_id);
}
if let Some(delegate_id) = delegate {
let delegate_inscription_id = delegate_id.parse::<InscriptionId>()?;
builder = builder.delegate(delegate_inscription_id);
}
if let Some(proto) = metaprotocol {
builder = builder.metaprotocol(&proto);
}
let inscription = builder.build();
let script = inscription.to_script();
println!("{}", hex::encode(script.as_bytes()));
}
}
Ok(())
}
fn print_inscriptions_text(inscriptions: &[Inscription]) {
if inscriptions.is_empty() {
println!("No inscriptions found.");
return;
}
println!("Found {} inscription(s):", inscriptions.len());
for (i, inscription) in inscriptions.iter().enumerate() {
println!("\nInscription #{}", i + 1);
println!(" Content Type: {:?}", inscription.content_type());
if let Some(encoding) = inscription.content_encoding() {
println!(" Content Encoding: {}", encoding);
}
println!(" Content Length: {:?} bytes", inscription.content_length());
if let Some(metaprotocol) = inscription.metaprotocol() {
println!(" Metaprotocol: {}", metaprotocol);
}
if !inscription.parents().is_empty() {
println!(" Parents:");
for parent in inscription.parents() {
println!(" {}", parent);
}
}
if let Some(delegate) = inscription.delegate() {
println!(" Delegate: {}", delegate);
}
if let Some(pointer) = inscription.pointer() {
println!(" Pointer: {}", pointer);
}
if let Some(content_type) = inscription.content_type() {
if let Some(body) = inscription.body() {
if let Ok(text) = str::from_utf8(body) {
if content_type == "application/json" ||
(content_type.starts_with("text/plain") && is_json_text(text)) {
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(text) {
if let Ok(pretty_json) = serde_json::to_string_pretty(&json_value) {
println!(" JSON Content:\n{}", pretty_json);
} else {
println!(" JSON Content: {}", text);
}
} else {
if content_type.starts_with("text/plain") {
let preview = if text.len() > 100 {
format!("{}...", &text[..100])
} else {
text.to_string()
};
println!(" Content Preview: \"{}\"", preview);
} else {
println!(" Content: [INVALID JSON]");
}
}
} else if content_type.starts_with("text/") {
let preview = if text.len() > 100 {
format!("{}...", &text[..100])
} else {
text.to_string()
};
println!(" Content Preview: \"{}\"", preview);
} else if content_type.starts_with("image/") {
println!(" Content: [IMAGE DATA]");
} else {
println!(" Content: [BINARY DATA]");
}
} else {
if content_type.starts_with("image/") {
println!(" Content: [IMAGE DATA]");
} else {
println!(" Content: [BINARY DATA]");
}
}
}
}
}
}
fn is_json_text(text: &str) -> bool {
let trimmed = text.trim();
(trimmed.starts_with('{') && trimmed.ends_with('}')) ||
(trimmed.starts_with('[') && trimmed.ends_with(']'))
}
fn print_inscriptions_json(inscriptions: &[Inscription]) -> Result<(), Box<dyn std::error::Error>> {
#[derive(serde::Serialize)]
struct InscriptionJson<'a> {
content_type: Option<&'a str>,
content_encoding: Option<&'a str>,
content_length: Option<usize>,
metaprotocol: Option<&'a str>,
parents: Vec<String>,
delegate: Option<String>,
pointer: Option<u64>,
body_preview: Option<String>,
json_content: Option<serde_json::Value>,
duplicate_field: bool,
incomplete_field: bool,
unrecognized_even_field: bool,
hidden: bool,
}
let json_inscriptions: Vec<InscriptionJson> = inscriptions
.iter()
.map(|insc| {
let (body_preview, json_content) = if let (Some(content_type), Some(body)) = (insc.content_type(), insc.body()) {
if let Ok(text) = str::from_utf8(body) {
let parsed_json = if content_type == "application/json" ||
(content_type.starts_with("text/plain") && is_json_text(text)) {
serde_json::from_str(text).ok()
} else {
None
};
let preview = if content_type.starts_with("text/") && parsed_json.is_none() {
if text.len() > 100 {
Some(format!("{}...", &text[..100]))
} else {
Some(text.to_string())
}
} else {
None
};
(preview, parsed_json)
} else {
(None, None)
}
} else {
(None, None)
};
InscriptionJson {
content_type: insc.content_type(),
content_encoding: insc.content_encoding(),
content_length: insc.content_length(),
metaprotocol: insc.metaprotocol(),
parents: insc.parents().iter().map(|p| p.to_string()).collect(),
delegate: insc.delegate().map(|d| d.to_string()),
pointer: insc.pointer(),
body_preview,
json_content,
duplicate_field: insc.duplicate_field,
incomplete_field: insc.incomplete_field,
unrecognized_even_field: insc.unrecognized_even_field,
hidden: insc.hidden(),
}
})
.collect();
let json_output = serde_json::to_string_pretty(&json_inscriptions)?;
println!("{}", json_output);
Ok(())
}