ordinals-parser 0.1.0

A lightweight parser for Bitcoin Ordinals inscriptions
Documentation
use std::str;
use bitcoin::script::{self};
use bitcoin::opcodes::all::{OP_PUSHBYTES_0, OP_IF, OP_ENDIF};
use bitcoin::ScriptBuf;
use bitcoin::Witness;
use serde::{Deserialize, Serialize};
use lazy_static::lazy_static;
use regex::bytes::Regex;

use crate::inscription_id::InscriptionId;
use crate::tag::Tag;
use crate::envelope::PROTOCOL_ID;
use crate::envelope::BODY_TAG;

/// An ordinals inscription
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Eq, Default)]
pub struct Inscription {
    /// The content of the inscription
    pub body: Option<Vec<u8>>,
    
    /// Content encoding, e.g., "br" for Brotli
    pub content_encoding: Option<Vec<u8>>,
    
    /// MIME type of the content
    pub content_type: Option<Vec<u8>>,
    
    /// Reference to another inscription this one delegates to
    pub delegate: Option<Vec<u8>>,
    
    /// Whether the inscription has duplicate fields
    pub duplicate_field: bool,
    
    /// Whether the inscription has incomplete fields
    pub incomplete_field: bool,
    
    /// Metadata in CBOR format
    pub metadata: Option<Vec<u8>>,
    
    /// Metaprotocol identifier
    pub metaprotocol: Option<Vec<u8>>,
    
    /// Parent inscription references
    pub parents: Vec<Vec<u8>>,
    
    /// Pointer for positioning
    pub pointer: Option<Vec<u8>>,
    
    /// Properties for the inscription
    pub properties: Option<Vec<u8>>,
    
    /// Rune association
    pub rune: Option<Vec<u8>>,
    
    /// Whether there are unrecognized even-numbered fields
    pub unrecognized_even_field: bool,
}

impl Inscription {
    /// Create a new simple inscription with content type and body
    pub fn new(content_type: Option<&str>, body: Option<&[u8]>) -> Self {
        Inscription {
            content_type: content_type.map(|ct| ct.as_bytes().to_vec()),
            body: body.map(|b| b.to_vec()),
            ..Default::default()
        }
    }

    /// Get the body of the inscription
    pub fn body(&self) -> Option<&[u8]> {
        Some(self.body.as_ref()?)
    }

    /// Convert body to owned Vec<u8>
    pub fn into_body(self) -> Option<Vec<u8>> {
        self.body
    }

    /// Get the length of the inscription content
    pub fn content_length(&self) -> Option<usize> {
        Some(self.body()?.len())
    }

    /// Get the content type as a string
    pub fn content_type(&self) -> Option<&str> {
        str::from_utf8(self.content_type.as_ref()?).ok()
    }

    /// Get the content encoding
    pub fn content_encoding(&self) -> Option<&str> {
        str::from_utf8(self.content_encoding.as_ref()?).ok()
    }

    /// Get the delegate inscription ID if present
    pub fn delegate(&self) -> Option<InscriptionId> {
        InscriptionId::from_value(self.delegate.as_deref()?)
    }

    /// Get the metaprotocol identifier
    pub fn metaprotocol(&self) -> Option<&str> {
        str::from_utf8(self.metaprotocol.as_ref()?).ok()
    }

    /// Get the parent inscription IDs
    pub fn parents(&self) -> Vec<InscriptionId> {
        self
            .parents
            .iter()
            .filter_map(|parent| InscriptionId::from_value(parent))
            .collect()
    }

    /// Get the pointer value if present
    pub fn pointer(&self) -> Option<u64> {
        let value = self.pointer.as_ref()?;

        if value.iter().skip(8).copied().any(|byte| byte != 0) {
            return None;
        }

        let pointer = [
            value.first().copied().unwrap_or(0),
            value.get(1).copied().unwrap_or(0),
            value.get(2).copied().unwrap_or(0),
            value.get(3).copied().unwrap_or(0),
            value.get(4).copied().unwrap_or(0),
            value.get(5).copied().unwrap_or(0),
            value.get(6).copied().unwrap_or(0),
            value.get(7).copied().unwrap_or(0),
        ];

        Some(u64::from_le_bytes(pointer))
    }

    /// Create a Bitcoin script from this inscription
    pub fn append_reveal_script_to_builder(&self, mut builder: script::Builder) -> script::Builder {
        builder = builder
            .push_opcode(OP_PUSHBYTES_0)
            .push_opcode(OP_IF);
        
        // Push the protocol ID (without &)
        builder = builder.push_slice::<[u8; 7]>(PROTOCOL_ID);

        Tag::ContentType.append(&mut builder, &self.content_type);
        Tag::ContentEncoding.append(&mut builder, &self.content_encoding);
        Tag::Metaprotocol.append(&mut builder, &self.metaprotocol);
        Tag::Parent.append_array(&mut builder, &self.parents);
        Tag::Delegate.append(&mut builder, &self.delegate);
        Tag::Pointer.append(&mut builder, &self.pointer);
        Tag::Metadata.append(&mut builder, &self.metadata);
        Tag::Rune.append(&mut builder, &self.rune);
        Tag::Properties.append(&mut builder, &self.properties);

        if let Some(body) = &self.body {
            // Push empty BODY_TAG (without &)
            builder = builder.push_slice::<[u8; 0]>(BODY_TAG);
            
            // Push body in chunks, using the Tag's helper method
            for chunk in body.chunks(crate::tag::MAX_SCRIPT_ELEMENT_SIZE) {
                builder = Tag::push_bytes_to_builder(builder, chunk);
            }
        }

        builder.push_opcode(OP_ENDIF)
    }

    /// Create a script containing this inscription
    pub fn to_script(&self) -> ScriptBuf {
        self.append_reveal_script_to_builder(script::Builder::new()).into_script()
    }

    /// Create a witness containing this inscription
    pub fn to_witness(&self) -> Witness {
        let script = self.to_script();
        let mut witness = Witness::new();
        witness.push(script.into_bytes());
        witness.push([]);
        witness
    }

    /// Check if this inscription should be hidden in the UI
    pub fn hidden(&self) -> bool {
        const BVM_NETWORK: &[u8] = b"<body style=\"background:#F61;color:#fff;\">\
                        <h1 style=\"height:100%\">bvm.network</h1></body>";

        lazy_static! {
            static ref BRC_420: Regex = Regex::new(r"^\s*/content/[[:xdigit:]]{64}i\d+\s*$").unwrap();
        }

        self
            .body()
            .map(|body| BRC_420.is_match(body) || body.starts_with(BVM_NETWORK))
            .unwrap_or_default()
            || self.metaprotocol.is_some()
    }
}

/// Builder for creating inscriptions
pub struct InscriptionBuilder {
    content_type: Option<String>,
    body: Option<Vec<u8>>,
    delegate: Option<InscriptionId>,
    metadata: Option<Vec<u8>>,
    metaprotocol: Option<String>,
    parents: Vec<InscriptionId>,
    pointer: Option<u64>,
    rune: Option<Vec<u8>>,
}

impl InscriptionBuilder {
    /// Create a new InscriptionBuilder
    pub fn new() -> Self {
        Self {
            content_type: None,
            body: None,
            delegate: None,
            metadata: None,
            metaprotocol: None,
            parents: Vec::new(),
            pointer: None,
            rune: None,
        }
    }

    /// Set the content type
    pub fn content_type(mut self, content_type: &str) -> Self {
        self.content_type = Some(content_type.to_string());
        self
    }

    /// Set the body
    pub fn body(mut self, body: &[u8]) -> Self {
        self.body = Some(body.to_vec());
        self
    }

    /// Add a parent inscription
    pub fn parent(mut self, parent: InscriptionId) -> Self {
        self.parents.push(parent);
        self
    }

    /// Set the delegate
    pub fn delegate(mut self, delegate: InscriptionId) -> Self {
        self.delegate = Some(delegate);
        self
    }

    /// Set the metadata (in CBOR format)
    pub fn metadata(mut self, metadata: &[u8]) -> Self {
        self.metadata = Some(metadata.to_vec());
        self
    }

    /// Set the metaprotocol
    pub fn metaprotocol(mut self, metaprotocol: &str) -> Self {
        self.metaprotocol = Some(metaprotocol.to_string());
        self
    }

    /// Set the pointer
    pub fn pointer(mut self, pointer: u64) -> Self {
        self.pointer = Some(pointer);
        self
    }

    /// Set the rune
    pub fn rune(mut self, rune: &[u8]) -> Self {
        self.rune = Some(rune.to_vec());
        self
    }

    /// Build the inscription
    pub fn build(self) -> Inscription {
        Inscription {
            content_type: self.content_type.map(|ct| ct.into_bytes()),
            body: self.body,
            delegate: self.delegate.map(|d| d.value()),
            metadata: self.metadata,
            metaprotocol: self.metaprotocol.map(|m| m.into_bytes()),
            parents: self.parents.iter().map(|p| p.value()).collect(),
            pointer: self.pointer.map(|p| {
                let mut bytes = p.to_le_bytes().to_vec();
                // Trim trailing zeros
                while bytes.last().copied() == Some(0) {
                    bytes.pop();
                }
                bytes
            }),
            rune: self.rune,
            ..Default::default()
        }
    }
}