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#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
13pub enum OrdParser {
14 Ordinal(Nft),
16 Brc20(Brc20),
18}
19
20impl OrdParser {
21 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 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 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 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(), script_sig: ScriptBuf::new(), sequence: Sequence::ZERO, 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![], };
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}