titan_types/
inscription_id.rs

1use {
2    bitcoin::{hashes::Hash, Txid},
3    borsh::{BorshDeserialize, BorshSerialize},
4    core::str,
5    serde::{Deserialize, Serialize},
6    std::{
7        fmt::{self, Display, Formatter},
8        io::{self, Read, Write},
9        str::FromStr,
10    },
11};
12
13#[derive(Debug, Eq, PartialEq, Clone, Hash, Serialize, Deserialize)]
14pub struct InscriptionId {
15    pub txid: Txid,
16    pub index: u32,
17}
18
19impl BorshSerialize for InscriptionId {
20    fn serialize<W: Write>(&self, writer: &mut W) -> io::Result<()> {
21        // Serialize txid bytes (32 bytes)
22        BorshSerialize::serialize(&self.txid.as_raw_hash().to_byte_array(), writer)?;
23
24        // Serialize index
25        BorshSerialize::serialize(&self.index, writer)?;
26
27        Ok(())
28    }
29}
30
31impl BorshDeserialize for InscriptionId {
32    fn deserialize_reader<R: Read>(reader: &mut R) -> io::Result<Self> {
33        // Read 32 bytes for txid
34        let txid_bytes = <[u8; 32]>::deserialize_reader(reader)?;
35
36        // Deserialize index
37        let index = u32::deserialize_reader(reader)?;
38
39        Ok(Self {
40            txid: Txid::from_byte_array(txid_bytes),
41            index,
42        })
43    }
44}
45
46impl Display for InscriptionId {
47    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
48        write!(f, "{}i{}", self.txid, self.index)
49    }
50}
51
52#[derive(Debug)]
53pub enum ParseError {
54    Character(char),
55    Length(usize),
56    Separator(char),
57    Txid(bitcoin::hex::HexToArrayError),
58    Index(std::num::ParseIntError),
59}
60
61impl Display for ParseError {
62    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
63        match self {
64            Self::Character(c) => write!(f, "invalid character: '{c}'"),
65            Self::Length(len) => write!(f, "invalid length: {len}"),
66            Self::Separator(c) => write!(f, "invalid separator: `{c}`"),
67            Self::Txid(err) => write!(f, "invalid txid: {err}"),
68            Self::Index(err) => write!(f, "invalid index: {err}"),
69        }
70    }
71}
72
73impl std::error::Error for ParseError {}
74
75impl FromStr for InscriptionId {
76    type Err = ParseError;
77
78    fn from_str(s: &str) -> Result<Self, Self::Err> {
79        if let Some(char) = s.chars().find(|char| !char.is_ascii()) {
80            return Err(ParseError::Character(char));
81        }
82
83        const TXID_LEN: usize = 64;
84        const MIN_LEN: usize = TXID_LEN + 2;
85
86        if s.len() < MIN_LEN {
87            return Err(ParseError::Length(s.len()));
88        }
89
90        let txid = &s[..TXID_LEN];
91
92        let separator = s.chars().nth(TXID_LEN).unwrap();
93
94        if separator != 'i' {
95            return Err(ParseError::Separator(separator));
96        }
97
98        let vout = &s[TXID_LEN + 1..];
99
100        Ok(Self {
101            txid: txid.parse().map_err(ParseError::Txid)?,
102            index: vout.parse().map_err(ParseError::Index)?,
103        })
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use bitcoin::Txid;
110
111    use super::*;
112
113    fn inscription_id(n: u32) -> InscriptionId {
114        let hex = format!("{n:x}");
115
116        if hex.is_empty() || hex.len() > 1 {
117            panic!();
118        }
119
120        format!("{}i{n}", hex.repeat(64)).parse().unwrap()
121    }
122
123    fn txid(n: u32) -> Txid {
124        let hex = format!("{n:x}");
125
126        if hex.is_empty() || hex.len() > 1 {
127            panic!();
128        }
129
130        hex.repeat(64).parse().unwrap()
131    }
132
133    #[test]
134    fn display() {
135        assert_eq!(
136            inscription_id(1).to_string(),
137            "1111111111111111111111111111111111111111111111111111111111111111i1",
138        );
139        assert_eq!(
140            InscriptionId {
141                txid: txid(1),
142                index: 0,
143            }
144            .to_string(),
145            "1111111111111111111111111111111111111111111111111111111111111111i0",
146        );
147        assert_eq!(
148            InscriptionId {
149                txid: txid(1),
150                index: 0xFFFFFFFF,
151            }
152            .to_string(),
153            "1111111111111111111111111111111111111111111111111111111111111111i4294967295",
154        );
155    }
156
157    #[test]
158    fn from_str() {
159        assert_eq!(
160            "1111111111111111111111111111111111111111111111111111111111111111i1"
161                .parse::<InscriptionId>()
162                .unwrap(),
163            inscription_id(1),
164        );
165        assert_eq!(
166            "1111111111111111111111111111111111111111111111111111111111111111i4294967295"
167                .parse::<InscriptionId>()
168                .unwrap(),
169            InscriptionId {
170                txid: txid(1),
171                index: 0xFFFFFFFF,
172            },
173        );
174        assert_eq!(
175            "1111111111111111111111111111111111111111111111111111111111111111i4294967295"
176                .parse::<InscriptionId>()
177                .unwrap(),
178            InscriptionId {
179                txid: txid(1),
180                index: 0xFFFFFFFF,
181            },
182        );
183    }
184}