ordinals-parser 0.1.0

A lightweight parser for Bitcoin Ordinals inscriptions
Documentation
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 inscriptions from a Bitcoin transaction
    Parse {
        /// The Bitcoin transaction in hex format or path to a file containing the hex
        tx_hex: String,
        
        /// Output format (text, json)
        #[arg(short, long, default_value = "text")]
        format: String,
    },
    
    /// Create a new inscription
    Create {
        /// Content type (MIME type) for the inscription
        #[arg(short, long, default_value = "text/plain;charset=utf-8")]
        content_type: String,
        
        /// Path to the content file
        #[arg(short, long)]
        content: PathBuf,
        
        /// Optional parent inscription ID
        #[arg(short, long)]
        parent: Option<String>,
        
        /// Optional delegate inscription ID
        #[arg(short, long)]
        delegate: Option<String>,
        
        /// Optional metaprotocol identifier
        #[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 } => {
            // Load transaction
            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)?;
            
            // Parse inscriptions
            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 } => {
            // Read content file
            let content_data = fs::read(content)?;
            
            // Create inscription builder
            let mut builder = InscriptionBuilder::new()
                .content_type(content_type)
                .body(&content_data);
            
            // Add optional fields
            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);
            }
            
            // Build the inscription
            let inscription = builder.build();
            
            // Convert to script and print as hex
            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);
        }
        
        // Content preview with JSON detection
        if let Some(content_type) = inscription.content_type() {
            if let Some(body) = inscription.body() {
                if let Ok(text) = str::from_utf8(body) {
                    // Check if it's JSON content (either application/json or text/plain with JSON)
                    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 JSON parsing fails but type is text/plain, show as text
                            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/") {
                        // Regular text content
                        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 {
                    // Not valid UTF-8 text
                    if content_type.starts_with("image/") {
                        println!("  Content: [IMAGE DATA]");
                    } else {
                        println!("  Content: [BINARY DATA]");
                    }
                }
            }
        }
    }
}

// Helper function to check if text is likely JSON
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| {
            // Parse JSON content if available
            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(())
}