ordinals-parser 0.1.0

A lightweight parser for Bitcoin Ordinals inscriptions
Documentation
use ordinals_parser::{parse_inscriptions_from_transaction, Inscription};
use bitcoin::{consensus::deserialize, Transaction};
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
use std::env;
use reqwest::blocking::Client;
use serde_json::Value;

const MEMPOOL_API_BASE: &str = "https://mempool.space/api";

/// Extract transaction ID from inscription ID
fn extract_txid_from_inscription_id(inscription_id: &str) -> Option<String> {
    // The format is typically <txid>i<index>
    inscription_id.split('i').next().map(|s| s.to_string())
}

/// Fetch raw transaction hex from mempool.space API
fn fetch_transaction_hex(txid: &str) -> Result<String, Box<dyn std::error::Error>> {
    let client = Client::new();
    let url = format!("{}/tx/{}/hex", MEMPOOL_API_BASE, txid);
    
    println!("Fetching transaction from: {}", url);
    let response = client.get(url).send()?;
    
    if response.status().is_success() {
        Ok(response.text()?)
    } else {
        Err(format!("Failed to fetch transaction: HTTP {}", response.status()).into())
    }
}

/// Fetch information about an inscription from the ordinals.com API
fn fetch_inscription_info(inscription_id: &str) -> Result<Value, Box<dyn std::error::Error>> {
    let client = Client::new();
    let url = format!("https://ordinals.com/api/inscription/{}", inscription_id);
    
    println!("Fetching inscription info from: {}", url);
    let response = client.get(url).send()?;
    
    if response.status().is_success() {
        Ok(response.json()?)
    } else {
        Err(format!("Failed to fetch inscription info: HTTP {}", response.status()).into())
    }
}

/// Manually inspect transaction inputs for any potential inscription patterns
fn inspect_transaction(tx: &Transaction) {
    println!("\n[DEBUG] Transaction inspection:");
    println!("[DEBUG] Transaction has {} inputs", tx.input.len());
    println!("[DEBUG] Transaction has {} outputs", tx.output.len());

    for (i, input) in tx.input.iter().enumerate() {
        println!("\n[DEBUG] Inspecting input #{}", i);
        println!("[DEBUG] Previous output: {:?}", input.previous_output);
        println!("[DEBUG] Witness stack size: {}", input.witness.len());
        
        for j in 0..input.witness.len() {
            if let Some(witness_item) = input.witness.nth(j) {
                println!("[DEBUG] Witness item #{} - size: {} bytes", j, witness_item.len());
                
                // Look for potential ordinals protocol markers in the witness item
                if witness_item.len() > 10 {
                    // Check for the ord marker bytes
                    let ord_marker = b"ord\x01";
                    let has_ord_marker = witness_item.windows(ord_marker.len()).any(|window| window == ord_marker);
                    
                    if has_ord_marker {
                        println!("[DEBUG] Witness item #{} CONTAINS potential ord marker bytes!", j);
                    } else {
                        println!("[DEBUG] Witness item #{} does NOT contain ord marker bytes", j);
                    }
                    
                    // For text witness data, print a preview
                    if let Ok(text) = std::str::from_utf8(witness_item) {
                        let preview = if text.len() > 40 {
                            format!("{}...", &text[..40])
                        } else {
                            text.to_string()
                        };
                        println!("[DEBUG] Witness item #{} text preview: \"{}\"", j, preview);
                    }
                }
            } else {
                println!("[DEBUG] Witness item #{} is None", j);
            }
        }
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args: Vec<String> = env::args().collect();
    
    let tx_hex = if args.len() > 1 {
        // Read transaction from file if provided
        let path = PathBuf::from(&args[1]);
        let mut file = File::open(path)?;
        let mut hex = String::new();
        file.read_to_string(&mut hex)?;
        hex.trim().to_string()
    } else if args.len() > 2 && args[1] == "--txid" {
        // Directly use transaction ID
        let txid = &args[2];
        println!("Using transaction ID: {}", txid);
        fetch_transaction_hex(txid)?
    } else if args.len() > 2 && args[1] == "--inscription" {
        // Use provided inscription ID
        let inscription_id = &args[2];
        println!("Using inscription ID: {}", inscription_id);
        
        // Try to get some information about the inscription
        match fetch_inscription_info(inscription_id) {
            Ok(info) => {
                println!("Inscription info:");
                println!("  Content type: {}", info.get("content_type").and_then(Value::as_str).unwrap_or("unknown"));
                println!("  Content length: {}", info.get("content_length").and_then(Value::as_u64).unwrap_or(0));
                println!("  Genesis timestamp: {}", info.get("timestamp").and_then(Value::as_u64).unwrap_or(0));
                if let Some(content_type) = info.get("content_type").and_then(Value::as_str) {
                    if content_type.starts_with("text/") {
                        if let Some(content) = info.get("content").and_then(Value::as_str) {
                            let preview = if content.len() > 100 {
                                format!("{}...", &content[..100])
                            } else {
                                content.to_string()
                            };
                            println!("  Content preview: \"{}\"", preview);
                        }
                    }
                }
            },
            Err(e) => println!("Could not fetch inscription info: {}", e),
        }
        
        // Extract transaction ID from the inscription ID
        let txid = extract_txid_from_inscription_id(inscription_id)
            .ok_or("Could not extract transaction ID from inscription ID")?;
        
        println!("Extracted transaction ID: {}", txid);
        
        // Fetch the raw transaction hex
        fetch_transaction_hex(&txid)?
    } else {
        // Use the text/plain inscription example that may contain JSON
        let inscription_id = "787e0c399c13190ef00e4c8e85ddc2761bda18b31f9629919c8fe3c57e9b0d4fi0";
        println!("Using text/plain inscription ID: {}", inscription_id);
        
        // Try to get some information about the inscription
        match fetch_inscription_info(inscription_id) {
            Ok(info) => {
                println!("Inscription info:");
                println!("  Content type: {}", info.get("content_type").and_then(Value::as_str).unwrap_or("unknown"));
                println!("  Content length: {}", info.get("content_length").and_then(Value::as_u64).unwrap_or(0));
                println!("  Genesis timestamp: {}", info.get("timestamp").and_then(Value::as_u64).unwrap_or(0));
                if let Some(content_type) = info.get("content_type").and_then(Value::as_str) {
                    if content_type.starts_with("text/") {
                        if let Some(content) = info.get("content").and_then(Value::as_str) {
                            let preview = if content.len() > 100 {
                                format!("{}...", &content[..100])
                            } else {
                                content.to_string()
                            };
                            println!("  Content preview: \"{}\"", preview);
                        }
                    }
                }
            },
            Err(e) => println!("Could not fetch inscription info: {}", e),
        }
        
        // Extract transaction ID from the inscription ID
        let txid = extract_txid_from_inscription_id(inscription_id)
            .ok_or("Could not extract transaction ID from inscription ID")?;
        
        println!("Extracted transaction ID: {}", txid);
        
        // Fetch the raw transaction hex
        fetch_transaction_hex(&txid)?
    };
    
    // Parse the transaction
    println!("\n[DEBUG] Decoding transaction hex of size: {} characters", tx_hex.len());
    let tx_bytes = hex::decode(tx_hex.trim())?;
    println!("[DEBUG] Transaction binary size: {} bytes", tx_bytes.len());
    
    let tx: Transaction = deserialize(&tx_bytes)?;
    println!("\nTransaction ID: {}", tx.txid());
    
    // Inspect the transaction before parsing inscriptions
    inspect_transaction(&tx);
    
    // Parse all inscriptions in the transaction
    println!("\n[DEBUG] Calling parse_inscriptions_from_transaction...");
    let inscriptions = parse_inscriptions_from_transaction(&tx);
    
    println!("[DEBUG] Parsing complete. Found {} inscriptions", inscriptions.len());
    
    if inscriptions.is_empty() {
        println!("No inscriptions found in this transaction.");
        return Ok(());
    }
    
    println!("Found {} inscription(s):", inscriptions.len());
    
    for (idx, inscription) in inscriptions.iter().enumerate() {
        println!("\nInscription #{}", idx + 1);
        print_inscription_details(inscription);
    }
    
    Ok(())
}

fn print_inscription_details(inscription: &Inscription) {
    println!("  Content Type: {:?}", inscription.content_type());
    
    if let Some(content_encoding) = inscription.content_encoding() {
        println!("  Content Encoding: {}", content_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);
    }
    
    // Print content based on content type
    if let Some(content_type) = inscription.content_type() {
        if let Some(body) = inscription.body() {
            if let Ok(text) = std::str::from_utf8(body) {
                // Check if application/json or text/plain that might contain JSON
                if content_type == "application/json" || 
                   (content_type.starts_with("text/plain") && text.trim().starts_with('{') && text.trim().ends_with('}')) {
                    // For JSON content, try to pretty print it
                    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]");
                }
            }
        }
    }
    
    // Print some flags about the inscription
    println!("  Flags:");
    println!("    Duplicate Fields: {}", inscription.duplicate_field);
    println!("    Incomplete Fields: {}", inscription.incomplete_field);
    println!("    Unrecognized Even Fields: {}", inscription.unrecognized_even_field);
    println!("    Hidden: {}", inscription.hidden());
}