ord_rs/wallet/
parser.rs

1mod envelope;
2
3use bitcoin::script::{Builder as ScriptBuilder, PushBytesBuf};
4use bitcoin::Transaction;
5use serde::{Deserialize, Serialize};
6
7use self::envelope::ParsedEnvelope;
8use crate::wallet::RedeemScriptPubkey;
9use crate::{Brc20, Inscription, InscriptionId, InscriptionParseError, Nft, OrdError, OrdResult};
10
11/// Encapsulates inscription parsing logic for both Ordinals and BRC20s.
12#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
13pub enum OrdParser {
14    /// Denotes a parsed [Nft] inscription.
15    Ordinal(Nft),
16    /// Denotes a parsed [Brc20] inscription.
17    Brc20(Brc20),
18}
19
20impl OrdParser {
21    /// Parses all inscriptions from a given transaction and categorizes them as either `Self::Brc20` or `Self::Ordinal`.
22    ///
23    /// This function extracts all inscription data from the transaction, attempts to parse each inscription,
24    /// and returns a vector of categorized inscriptions with their corresponding IDs.
25    ///
26    /// # Errors
27    ///
28    /// Will return an error if any inscription data cannot be parsed correctly,
29    /// or if no valid inscriptions are found in the transaction.
30    pub fn parse_all(tx: &Transaction) -> OrdResult<Vec<(InscriptionId, Self)>> {
31        let txid = tx.txid();
32
33        ParsedEnvelope::from_transaction(tx)
34            .into_iter()
35            .map(|envelope| {
36                let inscription_id = InscriptionId {
37                    txid,
38                    index: envelope.input,
39                };
40
41                let raw_body = envelope.payload.body.as_ref().ok_or_else(|| {
42                    OrdError::InscriptionParser(InscriptionParseError::ParsedEnvelope(
43                        "Empty payload body in envelope".to_string(),
44                    ))
45                })?;
46
47                if let Some(brc20) = Self::parse_brc20(raw_body) {
48                    Ok((inscription_id, Self::Brc20(brc20)))
49                } else {
50                    Ok((inscription_id, Self::Ordinal(envelope.payload)))
51                }
52            })
53            .collect::<Result<Vec<(InscriptionId, Self)>, OrdError>>()
54    }
55
56    /// Parses a single inscription from a transaction at a specified index, returning the
57    /// parsed inscription along with its ID.
58    ///
59    /// This method specifically targets one inscription identified by its index within the transaction's inputs.
60    /// It extracts the inscription data, attempts to parse it, and categorizes it as either `Self::Brc20` or `Self::Ordinal`.
61    ///
62    /// # Errors
63    ///
64    /// Returns an error if the inscription data at the specified index cannot be parsed,
65    /// if there is no data at the specified index, or if the data at the index does not contain a valid payload.
66    pub fn parse_one(tx: &Transaction, index: usize) -> OrdResult<(InscriptionId, Self)> {
67        let envelope = ParsedEnvelope::from_transaction_input(tx, index).ok_or_else(|| {
68            OrdError::InscriptionParser(InscriptionParseError::ParsedEnvelope(
69                "No data found in envelope at specified index".to_string(),
70            ))
71        })?;
72
73        let raw_body = envelope.payload.body.as_ref().ok_or_else(|| {
74            OrdError::InscriptionParser(InscriptionParseError::ParsedEnvelope(
75                "Empty payload body in envelope".to_string(),
76            ))
77        })?;
78
79        let inscription_id = InscriptionId {
80            txid: tx.txid(),
81            index: envelope.input,
82        };
83
84        if let Some(brc20) = Self::parse_brc20(raw_body) {
85            Ok((inscription_id, Self::Brc20(brc20)))
86        } else {
87            Ok((inscription_id, Self::Ordinal(envelope.payload)))
88        }
89    }
90
91    /// Attempts to parse the raw data as a BRC20 inscription.
92    /// Returns `Some(Brc20)` if successful, otherwise `None`.
93    fn parse_brc20(raw_body: &[u8]) -> Option<Brc20> {
94        serde_json::from_slice::<Brc20>(raw_body).ok()
95    }
96}
97
98impl From<Brc20> for OrdParser {
99    fn from(inscription: Brc20) -> Self {
100        Self::Brc20(inscription)
101    }
102}
103
104impl From<Nft> for OrdParser {
105    fn from(inscription: Nft) -> Self {
106        Self::Ordinal(inscription)
107    }
108}
109
110impl TryFrom<OrdParser> for Nft {
111    type Error = OrdError;
112
113    fn try_from(parser: OrdParser) -> Result<Self, Self::Error> {
114        match parser {
115            OrdParser::Ordinal(nft) => Ok(nft),
116            _ => Err(OrdError::InscriptionParser(
117                InscriptionParseError::NotOrdinal,
118            )),
119        }
120    }
121}
122
123impl TryFrom<&OrdParser> for Nft {
124    type Error = OrdError;
125
126    fn try_from(parser: &OrdParser) -> Result<Self, Self::Error> {
127        match parser {
128            OrdParser::Ordinal(nft) => Ok(nft.clone()),
129            _ => Err(OrdError::InscriptionParser(
130                InscriptionParseError::NotOrdinal,
131            )),
132        }
133    }
134}
135
136impl TryFrom<OrdParser> for Brc20 {
137    type Error = OrdError;
138
139    fn try_from(parser: OrdParser) -> Result<Self, Self::Error> {
140        match parser {
141            OrdParser::Brc20(brc20) => Ok(brc20),
142            _ => Err(OrdError::InscriptionParser(InscriptionParseError::NotBrc20)),
143        }
144    }
145}
146
147impl TryFrom<&OrdParser> for Brc20 {
148    type Error = OrdError;
149
150    fn try_from(parser: &OrdParser) -> Result<Self, Self::Error> {
151        match parser {
152            OrdParser::Brc20(brc20) => Ok(brc20.clone()),
153            _ => Err(OrdError::InscriptionParser(InscriptionParseError::NotBrc20)),
154        }
155    }
156}
157
158impl Inscription for OrdParser {
159    fn content_type(&self) -> String {
160        match self {
161            Self::Brc20(inscription) => inscription.content_type(),
162            Self::Ordinal(inscription) => Inscription::content_type(inscription),
163        }
164    }
165
166    fn data(&self) -> OrdResult<PushBytesBuf> {
167        match self {
168            Self::Brc20(inscription) => inscription.data(),
169            Self::Ordinal(inscription) => inscription.data(),
170        }
171    }
172
173    fn generate_redeem_script(
174        &self,
175        builder: ScriptBuilder,
176        pubkey: RedeemScriptPubkey,
177    ) -> OrdResult<ScriptBuilder> {
178        match self {
179            Self::Brc20(inscription) => inscription.generate_redeem_script(builder, pubkey),
180            Self::Ordinal(inscription) => inscription.generate_redeem_script(builder, pubkey),
181        }
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use bitcoin::absolute::LockTime;
188    use bitcoin::script::{Builder as ScriptBuilder, PushBytes};
189    use bitcoin::transaction::Version;
190    use bitcoin::{opcodes, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, Witness};
191
192    use super::*;
193    use crate::utils::test_utils::get_transaction_by_id;
194
195    #[tokio::test]
196    async fn ord_parser_should_parse_one() {
197        let transaction = get_transaction_by_id(
198            "b61b0172d95e266c18aea0c624db987e971a5d6d4ebc2aaed85da4642d635735",
199            Network::Bitcoin,
200        )
201        .await
202        .unwrap();
203
204        let (inscription_id, parsed_inscription) = OrdParser::parse_one(&transaction, 0).unwrap();
205
206        assert_eq!(inscription_id.index, 0);
207        assert_eq!(inscription_id.txid, transaction.txid());
208
209        let brc20 = Brc20::try_from(parsed_inscription).unwrap();
210        assert_eq!(
211            brc20,
212            Brc20::deploy("ordi", 21000000, Some(1000), None, None)
213        );
214    }
215
216    #[tokio::test]
217    async fn ord_parser_should_parse_valid_brc20_inscription_mainnet() {
218        let transaction = get_transaction_by_id(
219            "b61b0172d95e266c18aea0c624db987e971a5d6d4ebc2aaed85da4642d635735",
220            Network::Bitcoin,
221        )
222        .await
223        .unwrap();
224
225        let parsed_data = OrdParser::parse_all(&transaction).unwrap();
226        let (parsed_brc20, brc20_iid) = (&parsed_data[0].1, parsed_data[0].0);
227
228        assert_eq!(brc20_iid.txid, transaction.txid());
229        assert_eq!(brc20_iid.index, 0);
230
231        let brc20 = Brc20::try_from(parsed_brc20).unwrap();
232        assert_eq!(
233            brc20,
234            Brc20::deploy("ordi", 21000000, Some(1000), None, None)
235        );
236    }
237
238    #[tokio::test]
239    async fn ord_parser_should_not_parse_a_non_brc20_inscription_mainnet() {
240        let transaction = get_transaction_by_id(
241            "37777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8",
242            bitcoin::Network::Bitcoin,
243        )
244        .await
245        .unwrap();
246
247        assert!(OrdParser::parse_all(&transaction).unwrap().is_empty());
248    }
249
250    #[tokio::test]
251    async fn ord_parser_should_not_parse_a_non_brc20_inscription_testnet() {
252        let transaction = get_transaction_by_id(
253            "5b8ee749df4a3cfc37344892a97f1819fac80fb2432289a474dc0f0fd3711208",
254            bitcoin::Network::Testnet,
255        )
256        .await
257        .unwrap();
258
259        assert!(OrdParser::parse_all(&transaction).unwrap().is_empty());
260    }
261
262    #[test]
263    fn ord_parser_should_return_a_valid_brc20_from_raw_transaction_data() {
264        let brc20 = br#"{
265            "p": "brc-20",
266            "op": "deploy",
267            "tick": "kobp",
268            "max": "1000",
269            "lim": "10",
270            "dec": "8",
271            "self_mint": "true"
272        }"#;
273
274        let script = ScriptBuilder::new()
275            .push_opcode(opcodes::OP_FALSE)
276            .push_opcode(opcodes::all::OP_IF)
277            .push_slice(b"ord")
278            .push_slice([1])
279            .push_slice(b"text/plain;charset=utf-8")
280            .push_slice([])
281            .push_slice::<&PushBytes>(brc20.as_slice().try_into().unwrap())
282            .push_opcode(opcodes::all::OP_ENDIF)
283            .into_script();
284
285        let witnesses = &[Witness::from_slice(&[script.into_bytes(), Vec::new()])];
286
287        let transaction = Transaction {
288            version: Version::ONE,
289            lock_time: LockTime::ZERO,
290            input: witnesses
291                .iter()
292                .map(|witness| TxIn {
293                    previous_output: OutPoint::null(),
294                    script_sig: ScriptBuf::new(),
295                    sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
296                    witness: witness.clone(),
297                })
298                .collect(),
299            output: Vec::new(),
300        };
301
302        let parsed_data = OrdParser::parse_all(&transaction).unwrap();
303        let (parsed_brc20, brc20_iid) = (&parsed_data[0].1, parsed_data[0].0);
304
305        assert_eq!(brc20_iid.txid, transaction.txid());
306        assert_eq!(brc20_iid.index, 0);
307
308        let brc20 = Brc20::try_from(parsed_brc20).unwrap();
309
310        assert_eq!(
311            brc20,
312            Brc20::deploy("kobp", 1000, Some(10), Some(8), Some(true))
313        );
314    }
315
316    #[test]
317    fn ord_parser_should_parse_valid_multiple_inscriptions_from_a_single_input_witness() {
318        let brc20 = br#"{
319            "p": "brc-20",
320            "op": "deploy",
321            "tick": "kobp",
322            "max": "1000",
323            "lim": "10",
324            "dec": "8",
325            "self_mint": "true"
326        }"#;
327
328        let script = ScriptBuilder::new()
329            .push_opcode(opcodes::OP_FALSE)
330            .push_opcode(opcodes::all::OP_IF)
331            .push_slice(b"ord")
332            .push_slice([1])
333            .push_slice(b"text/plain;charset=utf-8")
334            .push_slice([])
335            .push_slice::<&PushBytes>(brc20.as_slice().try_into().unwrap())
336            .push_opcode(opcodes::all::OP_ENDIF)
337            .push_opcode(opcodes::OP_FALSE)
338            .push_opcode(opcodes::all::OP_IF)
339            .push_slice(b"ord")
340            .push_slice([1])
341            .push_slice(b"text/plain;charset=utf-8")
342            .push_slice([])
343            .push_slice(b"Hello, world!")
344            .push_opcode(opcodes::all::OP_ENDIF)
345            .into_script();
346
347        let witnesses = &[Witness::from_slice(&[script.into_bytes(), Vec::new()])];
348
349        let transaction = Transaction {
350            version: Version::ONE,
351            lock_time: LockTime::ZERO,
352            input: witnesses
353                .iter()
354                .map(|witness| TxIn {
355                    previous_output: OutPoint::null(),
356                    script_sig: ScriptBuf::new(),
357                    sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
358                    witness: witness.clone(),
359                })
360                .collect(),
361            output: Vec::new(),
362        };
363
364        let parsed_data = OrdParser::parse_all(&transaction).unwrap();
365
366        let (parsed_brc20, brc20_iid) = (&parsed_data[0].1, parsed_data[0].0);
367        assert_eq!(brc20_iid.txid, transaction.txid());
368        assert_eq!(brc20_iid.index, 0);
369
370        assert_eq!(
371            Brc20::try_from(parsed_brc20).unwrap(),
372            Brc20::deploy("kobp", 1000, Some(10), Some(8), Some(true))
373        );
374
375        let (parsed_nft, nft_iid) = (&parsed_data[1].1, parsed_data[1].0);
376        assert_eq!(nft_iid.txid, transaction.txid());
377        assert_eq!(nft_iid.index, 0);
378
379        let nft = Nft::try_from(parsed_nft).unwrap();
380        assert_eq!(nft.content_type().unwrap(), "text/plain;charset=utf-8");
381        assert_eq!(nft.body().unwrap(), "Hello, world!");
382    }
383
384    #[test]
385    fn ord_parser_should_parse_valid_multiple_inscriptions_from_multiple_input_witnesses() {
386        let brc20 = br#"{
387        "p": "brc-20",
388        "op": "deploy",
389        "tick": "kobp",
390        "max": "1000",
391        "lim": "10",
392        "dec": "8",
393        "self_mint": "true"
394    }"#;
395
396        let brc20_script = ScriptBuilder::new()
397            .push_opcode(opcodes::OP_FALSE)
398            .push_opcode(opcodes::all::OP_IF)
399            .push_slice(b"ord")
400            .push_slice([1])
401            .push_slice(b"text/plain;charset=utf-8")
402            .push_slice([])
403            .push_slice::<&PushBytes>(brc20.as_slice().try_into().unwrap())
404            .push_opcode(opcodes::all::OP_ENDIF)
405            .into_script();
406
407        let nft_script = ScriptBuilder::new()
408            .push_opcode(opcodes::OP_FALSE)
409            .push_opcode(opcodes::all::OP_IF)
410            .push_slice(b"ord")
411            .push_slice([1])
412            .push_slice(b"text/plain;charset=utf-8")
413            .push_slice([])
414            .push_slice(b"Hello, world!")
415            .push_opcode(opcodes::all::OP_ENDIF)
416            .into_script();
417
418        let brc20_witness = Witness::from_slice(&[brc20_script.into_bytes(), Vec::new()]);
419        let nft_witness = Witness::from_slice(&[nft_script.into_bytes(), Vec::new()]);
420
421        let transaction = Transaction {
422            version: Version::ONE,
423            lock_time: LockTime::ZERO,
424            input: vec![
425                TxIn {
426                    previous_output: OutPoint::null(),
427                    script_sig: ScriptBuf::new(),
428                    sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
429                    witness: brc20_witness,
430                },
431                TxIn {
432                    previous_output: OutPoint::null(),
433                    script_sig: ScriptBuf::new(),
434                    sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
435                    witness: nft_witness,
436                },
437            ],
438            output: Vec::new(),
439        };
440
441        let parsed_data = OrdParser::parse_all(&transaction).unwrap();
442
443        let (brc20_iid, parsed_brc20) = (&parsed_data[0].0, &parsed_data[0].1);
444        assert_eq!(brc20_iid.txid, transaction.txid());
445        assert_eq!(brc20_iid.index, 0);
446        assert_eq!(
447            Brc20::try_from(parsed_brc20).unwrap(),
448            Brc20::deploy("kobp", 1000, Some(10), Some(8), Some(true))
449        );
450
451        let (nft_iid, parsed_nft) = (&parsed_data[1].0, &parsed_data[1].1);
452        assert_eq!(nft_iid.txid, transaction.txid());
453        assert_eq!(nft_iid.index, 1);
454        let nft = Nft::try_from(parsed_nft).unwrap();
455        assert_eq!(nft.content_type().unwrap(), "text/plain;charset=utf-8");
456        assert_eq!(nft.body().unwrap(), "Hello, world!");
457    }
458
459    #[tokio::test]
460    async fn test_should_parse_bitcoin_nft() {
461        let tx: MempoolApiTx = reqwest::get("https://mempool.space/api/tx/276e858872a00b1b07312b093c5f2c1fcdd5a2d9379b9ec47d4b91be17aeaf8d")
462            .await
463            .unwrap()
464            .json()
465            .await
466            .unwrap();
467
468        // make transaction
469        let tx = Transaction {
470            version: Version::TWO,
471            lock_time: LockTime::ZERO,
472            input: tx
473                .vin
474                .into_iter()
475                .map(|vin| TxIn {
476                    previous_output: OutPoint::null(), // not used
477                    script_sig: ScriptBuf::new(),      // not used
478                    sequence: Sequence::ZERO,          // not used
479                    witness: Witness::from_slice(
480                        vin.witness
481                            .iter()
482                            .map(|w| hex::decode(w).unwrap())
483                            .collect::<Vec<Vec<u8>>>()
484                            .as_slice(),
485                    ),
486                })
487                .collect::<Vec<_>>(),
488            output: vec![], // we don't need outputs for this test
489        };
490
491        let nft = OrdParser::parse_all(&tx)
492            .unwrap()
493            .into_iter()
494            .find(|(_, ins)| matches!(ins, OrdParser::Ordinal(_)))
495            .unwrap()
496            .1;
497        let nft = Nft::try_from(nft).unwrap();
498        assert_eq!(nft.content_type().unwrap(), "image/gif");
499        assert_eq!(nft.body.unwrap().len(), 592);
500    }
501
502    #[derive(Debug, Clone, Deserialize)]
503    struct MempoolApiTx {
504        vin: Vec<MempoolApiVin>,
505    }
506
507    #[derive(Debug, Clone, Deserialize)]
508    struct MempoolApiVin {
509        witness: Vec<String>,
510    }
511}