ordinals-parser 0.1.0

A lightweight parser for Bitcoin Ordinals inscriptions
Documentation
use std::fmt::{Display, Formatter};
use std::str::FromStr;

use bitcoin::Txid;
use bitcoin::hashes::Hash;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

use crate::error::{Error, Result};

/// A unique identifier for an inscription, consisting of a transaction ID and an index
#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash, Ord, PartialOrd)]
pub struct InscriptionId {
    /// The transaction ID of the transaction containing the inscription
    pub txid: Txid,
    
    /// The index of the inscription within the transaction
    pub index: u32,
}

impl Default for InscriptionId {
    fn default() -> Self {
        Self {
            txid: Txid::all_zeros(),
            index: 0,
        }
    }
}

impl InscriptionId {
    /// Create a new InscriptionId from a transaction ID and index
    pub fn new(txid: Txid, index: u32) -> Self {
        Self { txid, index }
    }
    
    /// Convert an inscription ID to its binary representation for storage
    pub fn value(self) -> Vec<u8> {
        let index = self.index.to_le_bytes();
        let mut index_slice = index.as_slice();

        // Trim trailing zeros for variable-length encoding
        while index_slice.last().copied() == Some(0) {
            index_slice = &index_slice[0..index_slice.len() - 1];
        }

        self
            .txid
            .as_byte_array()
            .iter()
            .chain(index_slice)
            .copied()
            .collect()
    }

    /// Try to parse an InscriptionId from its binary representation
    pub fn from_value(value: &[u8]) -> Option<Self> {
        if value.len() < Txid::LEN {
            return None;
        }

        if value.len() > Txid::LEN + 4 {
            return None;
        }

        let (txid, index) = value.split_at(Txid::LEN);

        if let Some(last) = index.last() {
            // Accept fixed length encoding with 4 bytes (with potential trailing zeroes)
            // or variable length (no trailing zeroes)
            if index.len() != 4 && *last == 0 {
                return None;
            }
        }

        let txid = Txid::from_raw_hash(bitcoin::hashes::Hash::from_slice(txid).ok()?);

        let index = [
            index.first().copied().unwrap_or_default(),
            index.get(1).copied().unwrap_or_default(),
            index.get(2).copied().unwrap_or_default(),
            index.get(3).copied().unwrap_or_default(),
        ];

        let index = u32::from_le_bytes(index);

        Some(Self { txid, index })
    }
}

impl Display for InscriptionId {
    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
        write!(f, "{}i{}", self.txid, self.index)
    }
}

impl FromStr for InscriptionId {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self> {
        if let Some((txid, index)) = s.split_once('i') {
            let txid = Txid::from_str(txid)
                .map_err(|_| Error::InvalidInscriptionId(format!("invalid txid: {}", txid)))?;
            
            let index = index.parse::<u32>()
                .map_err(|_| Error::InvalidInscriptionId(format!("invalid index: {}", index)))?;
            
            Ok(Self { txid, index })
        } else {
            Err(Error::InvalidInscriptionId(format!("invalid inscription id format: {}", s)))
        }
    }
}

impl Serialize for InscriptionId {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.collect_str(&self.to_string())
    }
}

impl<'de> Deserialize<'de> for InscriptionId {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        FromStr::from_str(&s).map_err(serde::de::Error::custom)
    }
}