1#![deny(unsafe_code)]
10#![deny(non_upper_case_globals)]
11#![deny(non_camel_case_types)]
12#![deny(non_snake_case)]
13#![deny(unused_mut)]
14#![deny(dead_code)]
15#![deny(unused_imports)]
16#![deny(missing_docs)]
17
18#[cfg(not(any(feature = "std")))]
19compile_error!("`std` must be enabled");
20
21use bitcoin::{Transaction, Txid, taproot::LeafVersion};
22use std::fmt;
23use std::str::FromStr;
24
25pub mod envelope;
26pub mod message;
27pub mod varint;
28
29pub const TAPROOT_ANNEX_DATA_TAG: u8 = 0;
31
32#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
34pub enum ScriptType {
35 Legacy,
37 Tapscript,
39}
40
41#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
43pub enum EmbeddingType {
44 OpReturn,
46 TaprootAnnex,
48 WitnessEnvelope(ScriptType),
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum EmbeddingLocation {
55 OpReturn {
57 output: usize,
59 },
60
61 TaprootAnnex {
63 input: usize,
65 },
66
67 WitnessEnvelope {
73 input: usize,
75 index: usize,
77 pushes: Vec<usize>,
79 script_type: ScriptType,
81 },
82}
83
84impl EmbeddingLocation {
85 pub fn to_type(&self) -> EmbeddingType {
87 match self {
88 EmbeddingLocation::OpReturn { .. } => EmbeddingType::OpReturn,
89 EmbeddingLocation::TaprootAnnex { .. } => EmbeddingType::TaprootAnnex,
90 EmbeddingLocation::WitnessEnvelope { script_type, .. } => {
91 EmbeddingType::WitnessEnvelope(*script_type)
92 }
93 }
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
99pub struct EmbeddingId {
100 pub txid: Txid,
102 pub embedding_type: EmbeddingType,
104 pub index: usize,
106 pub sub_index: Option<usize>,
108 _private: bool,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum EmbeddingIdError {
115 InvalidFormat,
117 InvalidTxid,
119 InvalidType,
121 InvalidIndex,
123}
124
125#[derive(Debug, Clone, PartialEq)]
127pub struct Embedding {
128 pub bytes: Vec<u8>,
130 pub txid: Txid,
132 pub location: EmbeddingLocation,
134}
135
136impl Embedding {
137 pub fn id(&self) -> EmbeddingId {
139 let embedding_type = self.location.to_type();
140
141 let (index, sub_index) = match self.location {
142 EmbeddingLocation::OpReturn { output } => (output, None),
143 EmbeddingLocation::TaprootAnnex { input } => (input, None),
144 EmbeddingLocation::WitnessEnvelope { input, index, .. } => (input, Some(index)),
145 };
146
147 EmbeddingId {
148 txid: self.txid,
149 embedding_type,
150 index,
151 sub_index,
152 _private: false,
153 }
154 }
155
156 pub fn to_type(&self) -> EmbeddingType {
158 self.location.to_type()
159 }
160
161 pub fn from_transaction(tx: &Transaction) -> Vec<Self> {
163 let mut embeddings = Vec::new();
164 let txid = tx.compute_txid();
165
166 for (output, txout) in tx.output.iter().enumerate() {
168 if !txout.script_pubkey.is_op_return() {
169 continue;
170 }
171
172 let location = EmbeddingLocation::OpReturn { output };
173
174 embeddings.push(Self {
175 bytes: txout.script_pubkey.to_bytes()[1..].to_vec(),
176 txid,
177 location,
178 });
179 }
180
181 for (input, txin) in tx.input.iter().enumerate() {
183 let mut script = None;
184 let mut script_type = None;
185 let witness = &txin.witness;
186
187 if let Some(leaf_script) = witness.taproot_leaf_script() {
189 if leaf_script.version == LeafVersion::TapScript {
190 script = Some(leaf_script.script);
191 script_type = Some(ScriptType::Tapscript);
192 }
193 }
194
195 if script.is_none() && witness.taproot_annex().is_none() && witness.len() > 1 {
197 if let Some(witness_script) = witness.witness_script() {
198 script = Some(witness_script);
199 script_type = Some(ScriptType::Legacy);
200 }
201 }
202
203 let (Some(script), Some(script_type)) = (script, script_type) else {
204 continue;
205 };
206
207 let envelopes = envelope::from_script(script);
208
209 for (index, envelope) in envelopes.into_iter().enumerate() {
210 let mut bytes = Vec::new();
211 let mut pushes = Vec::new();
212
213 for chunk in envelope {
214 bytes.extend(chunk.clone());
215 pushes.push(chunk.len());
216 }
217
218 let location = EmbeddingLocation::WitnessEnvelope {
219 input,
220 index,
221 pushes,
222 script_type,
223 };
224
225 embeddings.push(Self {
226 bytes,
227 txid,
228 location,
229 });
230 }
231 }
232
233 for (input, txin) in tx.input.iter().enumerate() {
235 if let Some(annex) = txin.witness.taproot_annex() {
236 if annex.len() > 2 && annex[1] == TAPROOT_ANNEX_DATA_TAG {
237 let location = EmbeddingLocation::TaprootAnnex { input };
238
239 embeddings.push(Self {
240 bytes: annex[2..].to_vec(),
241 txid,
242 location,
243 });
244 }
245 }
246 }
247
248 embeddings
249 }
250}
251
252impl fmt::Display for ScriptType {
253 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
254 match self {
255 ScriptType::Legacy => write!(f, "Legacy P2WSH"),
256 ScriptType::Tapscript => write!(f, "Tapscript"),
257 }
258 }
259}
260
261impl fmt::Display for EmbeddingType {
262 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
263 match self {
264 EmbeddingType::OpReturn => write!(f, "OP_RETURN"),
265 EmbeddingType::TaprootAnnex => write!(f, "Taproot Annex"),
266 EmbeddingType::WitnessEnvelope(script_type) => write!(f, "{} Envelope", script_type),
267 }
268 }
269}
270
271impl fmt::Display for EmbeddingLocation {
272 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
273 match self {
274 EmbeddingLocation::OpReturn { output } => {
275 write!(f, "OP_RETURN at output {}", output)
276 }
277 EmbeddingLocation::TaprootAnnex { input } => {
278 write!(f, "Taproot Annex at input {}", input)
279 }
280 EmbeddingLocation::WitnessEnvelope {
281 input,
282 index,
283 script_type,
284 ..
285 } => {
286 write!(
287 f,
288 "{} Envelope at input {} (index {})",
289 script_type, input, index
290 )
291 }
292 }
293 }
294}
295
296impl fmt::Display for EmbeddingId {
297 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298 match self.embedding_type {
299 EmbeddingType::OpReturn => {
300 write!(f, "{}:rt:{}", self.txid, self.index)
301 }
302 EmbeddingType::TaprootAnnex => {
303 write!(f, "{}:ta:{}", self.txid, self.index)
304 }
305 EmbeddingType::WitnessEnvelope(script_type) => {
306 let type_code = match script_type {
307 ScriptType::Legacy => "le",
308 ScriptType::Tapscript => "te",
309 };
310
311 if let Some(sub_index) = self.sub_index {
312 if sub_index > 0 {
313 return write!(
314 f,
315 "{}:{}:{}:{}",
316 self.txid, type_code, self.index, sub_index
317 );
318 }
319 }
320
321 write!(f, "{}:{}:{}", self.txid, type_code, self.index)
322 }
323 }
324 }
325}
326
327impl FromStr for EmbeddingId {
328 type Err = EmbeddingIdError;
329
330 fn from_str(s: &str) -> Result<Self, Self::Err> {
331 let parts: Vec<&str> = s.split(':').collect();
332
333 if parts.len() < 3 || parts.len() > 4 {
334 return Err(EmbeddingIdError::InvalidFormat);
335 }
336
337 let txid = Txid::from_str(parts[0]).map_err(|_| EmbeddingIdError::InvalidTxid)?;
338
339 let embedding_type = match parts[1] {
340 "rt" => EmbeddingType::OpReturn,
341 "ta" => EmbeddingType::TaprootAnnex,
342 "le" => EmbeddingType::WitnessEnvelope(ScriptType::Legacy),
343 "te" => EmbeddingType::WitnessEnvelope(ScriptType::Tapscript),
344 _ => return Err(EmbeddingIdError::InvalidType),
345 };
346
347 let index = parts[2]
348 .parse::<usize>()
349 .map_err(|_| EmbeddingIdError::InvalidIndex)?;
350
351 let mut sub_index = if parts.len() == 4 {
352 Some(
353 parts[3]
354 .parse::<usize>()
355 .map_err(|_| EmbeddingIdError::InvalidIndex)?,
356 )
357 } else {
358 None
359 };
360
361 match embedding_type {
363 EmbeddingType::WitnessEnvelope(_) => {
364 if sub_index.is_none() {
365 sub_index = Some(0);
366 }
367 }
368 _ if sub_index.is_some() => return Err(EmbeddingIdError::InvalidFormat),
369 _ => {}
370 }
371
372 Ok(Self {
373 txid,
374 embedding_type,
375 index,
376 sub_index,
377 _private: false,
378 })
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use bitcoin::{
386 Amount, OutPoint, ScriptBuf, Sequence, TxIn, TxOut, Witness,
387 absolute::LockTime,
388 hashes::Hash,
389 script::Builder,
390 taproot::{TAPROOT_ANNEX_PREFIX, TAPROOT_CONTROL_BASE_SIZE, TAPROOT_LEAF_TAPSCRIPT},
391 transaction::Version,
392 };
393
394 #[test]
395 fn test_embedding_to_type() {
396 let op_return_loc = EmbeddingLocation::OpReturn { output: 0 };
397 let annex_loc = EmbeddingLocation::TaprootAnnex { input: 1 };
398 let legacy_loc = EmbeddingLocation::WitnessEnvelope {
399 input: 2,
400 index: 0,
401 pushes: vec![4, 8],
402 script_type: ScriptType::Legacy,
403 };
404 let tapscript_loc = EmbeddingLocation::WitnessEnvelope {
405 input: 3,
406 index: 0,
407 pushes: vec![5, 10],
408 script_type: ScriptType::Tapscript,
409 };
410
411 assert_eq!(op_return_loc.to_type(), EmbeddingType::OpReturn);
412 assert_eq!(annex_loc.to_type(), EmbeddingType::TaprootAnnex);
413 assert_eq!(
414 legacy_loc.to_type(),
415 EmbeddingType::WitnessEnvelope(ScriptType::Legacy)
416 );
417 assert_eq!(
418 tapscript_loc.to_type(),
419 EmbeddingType::WitnessEnvelope(ScriptType::Tapscript)
420 );
421
422 for loc in [op_return_loc, annex_loc, legacy_loc, tapscript_loc] {
423 let embedding = Embedding {
424 bytes: vec![1, 2, 3],
425 txid: Txid::all_zeros(),
426 location: loc.clone(),
427 };
428
429 assert_eq!(embedding.to_type(), loc.to_type());
430 }
431 }
432
433 #[test]
434 fn test_from_transaction_op_return() {
435 let mut tx = Transaction {
437 version: Version::ONE,
438 lock_time: LockTime::ZERO,
439 input: vec![],
440 output: vec![TxOut {
441 value: Amount::ZERO,
442 script_pubkey: ScriptBuf::from_hex("6a48656c6c6f").unwrap(), }],
444 };
445
446 let embeddings = Embedding::from_transaction(&tx);
447
448 assert_eq!(embeddings.len(), 1);
449 assert_eq!(embeddings[0].bytes, b"Hello");
450 assert_eq!(embeddings[0].txid, tx.compute_txid());
451 assert_eq!(
452 embeddings[0].location,
453 EmbeddingLocation::OpReturn { output: 0 }
454 );
455
456 tx.output.push(TxOut {
458 value: Amount::from_sat(1000),
459 script_pubkey: ScriptBuf::new(),
460 });
461 tx.output.push(TxOut {
462 value: Amount::ZERO,
463 script_pubkey: ScriptBuf::from_hex("6a576f726c64").unwrap(), });
465
466 let embeddings = Embedding::from_transaction(&tx);
467
468 assert_eq!(embeddings.len(), 2);
469 assert_eq!(embeddings[0].bytes, b"Hello");
470 assert_eq!(embeddings[0].txid, tx.compute_txid());
471 assert_eq!(
472 embeddings[0].location,
473 EmbeddingLocation::OpReturn { output: 0 }
474 );
475 assert_eq!(embeddings[1].bytes, b"World");
476 assert_eq!(embeddings[1].txid, tx.compute_txid());
477 assert_eq!(
478 embeddings[1].location,
479 EmbeddingLocation::OpReturn { output: 2 }
480 );
481 }
482
483 #[test]
484 fn test_from_transaction_p2wsh_envelope() {
485 let mut builder = envelope::append_bytes_to_builder(b"data", Builder::new());
487 builder = envelope::append_bytes_to_builder(b"data-two", builder);
488 let witness0 = Witness::from_slice(&[vec![1], builder.into_bytes()]);
489
490 let builder = envelope::append_to_builder(
491 vec![b"data-three".to_vec(), b"<extension>".to_vec()],
492 Builder::new(),
493 );
494 let witness1 = Witness::from_slice(&[vec![1], builder.into_bytes()]);
495
496 let tx = Transaction {
497 version: Version::ONE,
498 lock_time: LockTime::ZERO,
499 input: vec![
500 TxIn {
501 previous_output: OutPoint::null(),
502 script_sig: ScriptBuf::new(),
503 sequence: Sequence::ZERO,
504 witness: witness0,
505 },
506 TxIn {
507 previous_output: OutPoint::null(),
508 script_sig: ScriptBuf::new(),
509 sequence: Sequence::ZERO,
510 witness: witness1,
511 },
512 ],
513 output: vec![],
514 };
515
516 let embeddings = Embedding::from_transaction(&tx);
517
518 assert_eq!(embeddings.len(), 3);
519
520 assert_eq!(embeddings[0].bytes, b"data");
521 assert_eq!(embeddings[0].txid, tx.compute_txid());
522 assert_eq!(
523 embeddings[0].location,
524 EmbeddingLocation::WitnessEnvelope {
525 input: 0,
526 index: 0,
527 pushes: vec![4],
528 script_type: ScriptType::Legacy,
529 }
530 );
531
532 assert_eq!(embeddings[1].bytes, b"data-two");
533 assert_eq!(embeddings[1].txid, tx.compute_txid());
534 assert_eq!(
535 embeddings[1].location,
536 EmbeddingLocation::WitnessEnvelope {
537 input: 0,
538 index: 1,
539 pushes: vec![8],
540 script_type: ScriptType::Legacy,
541 }
542 );
543
544 assert_eq!(embeddings[2].bytes, b"data-three<extension>");
545 assert_eq!(embeddings[2].txid, tx.compute_txid());
546 assert_eq!(
547 embeddings[2].location,
548 EmbeddingLocation::WitnessEnvelope {
549 input: 1,
550 index: 0,
551 pushes: vec![10, 11],
552 script_type: ScriptType::Legacy,
553 }
554 );
555 }
556
557 #[test]
558 fn test_from_transaction_tapscript_envelope() {
559 let builder = envelope::append_bytes_to_builder(b"data", Builder::new());
561 let witness = Witness::from_slice(&[
562 builder.into_bytes(),
563 vec![TAPROOT_LEAF_TAPSCRIPT; TAPROOT_CONTROL_BASE_SIZE],
564 ]);
565
566 let tx = Transaction {
567 version: Version::ONE,
568 lock_time: LockTime::ZERO,
569 input: vec![TxIn {
570 previous_output: OutPoint::null(),
571 script_sig: ScriptBuf::new(),
572 sequence: Sequence::ZERO,
573 witness,
574 }],
575 output: vec![],
576 };
577
578 let embeddings = Embedding::from_transaction(&tx);
579
580 assert_eq!(embeddings.len(), 1);
581
582 assert_eq!(embeddings[0].bytes, b"data");
583 assert_eq!(embeddings[0].txid, tx.compute_txid());
584 assert_eq!(
585 embeddings[0].location,
586 EmbeddingLocation::WitnessEnvelope {
587 input: 0,
588 index: 0,
589 pushes: vec![4],
590 script_type: ScriptType::Tapscript,
591 }
592 );
593 }
594
595 #[test]
596 fn test_from_transaction_taproot_annex() {
597 let witness0 = Witness::from_slice(&[
599 vec![1],
600 [
601 vec![TAPROOT_ANNEX_PREFIX, TAPROOT_ANNEX_DATA_TAG],
602 b"Hello".to_vec(),
603 ]
604 .concat(),
605 ]);
606
607 let witness1 = Witness::from_slice(&[
608 vec![1],
609 [
610 vec![TAPROOT_ANNEX_PREFIX, TAPROOT_ANNEX_DATA_TAG],
611 b"World".to_vec(),
612 ]
613 .concat(),
614 ]);
615
616 let tx = Transaction {
617 version: Version::ONE,
618 lock_time: LockTime::ZERO,
619 input: vec![
620 TxIn {
621 previous_output: OutPoint::null(),
622 script_sig: ScriptBuf::new(),
623 sequence: Sequence::ZERO,
624 witness: witness0,
625 },
626 TxIn {
627 previous_output: OutPoint::null(),
628 script_sig: ScriptBuf::new(),
629 sequence: Sequence::ZERO,
630 witness: Witness::new(),
631 },
632 TxIn {
633 previous_output: OutPoint::null(),
634 script_sig: ScriptBuf::new(),
635 sequence: Sequence::ZERO,
636 witness: witness1,
637 },
638 ],
639 output: vec![],
640 };
641
642 let embeddings = Embedding::from_transaction(&tx);
643
644 assert_eq!(embeddings.len(), 2);
645 assert_eq!(embeddings[0].bytes, b"Hello");
646 assert_eq!(embeddings[0].txid, tx.compute_txid());
647 assert_eq!(
648 embeddings[0].location,
649 EmbeddingLocation::TaprootAnnex { input: 0 }
650 );
651 assert_eq!(embeddings[1].bytes, b"World");
652 assert_eq!(embeddings[1].txid, tx.compute_txid());
653 assert_eq!(
654 embeddings[1].location,
655 EmbeddingLocation::TaprootAnnex { input: 2 }
656 );
657 }
658
659 #[test]
660 fn test_from_transaction_complex() {
661 let op_return_output0 = TxOut {
663 value: Amount::ZERO,
664 script_pubkey: ScriptBuf::from_hex("6a48656c6c6f").unwrap(), };
666
667 let op_return_output1 = TxOut {
668 value: Amount::ZERO,
669 script_pubkey: ScriptBuf::from_hex("6a576f726c64").unwrap(), };
671
672 let p2wsh_builder = envelope::append_bytes_to_builder(b"p2wsh-data", Builder::new());
674 let p2wsh_witness = Witness::from_slice(&[vec![1], p2wsh_builder.into_bytes()]);
675
676 let mut tapscript_builder =
678 envelope::append_bytes_to_builder(b"tapscript-data1", Builder::new());
679 tapscript_builder = envelope::append_to_builder(
680 vec![b"multi".to_vec(), b"part".to_vec(), b"data".to_vec()],
681 tapscript_builder,
682 );
683 let tapscript_witness = Witness::from_slice(&[
684 tapscript_builder.into_bytes(),
685 vec![TAPROOT_LEAF_TAPSCRIPT; TAPROOT_CONTROL_BASE_SIZE],
686 ]);
687
688 let annex_witness = Witness::from_slice(&[
690 vec![1],
691 [
692 vec![TAPROOT_ANNEX_PREFIX, TAPROOT_ANNEX_DATA_TAG],
693 b"annex-data".to_vec(),
694 ]
695 .concat(),
696 ]);
697
698 let tx = Transaction {
700 version: Version::ONE,
701 lock_time: LockTime::ZERO,
702 input: vec![
703 TxIn {
705 previous_output: OutPoint::null(),
706 script_sig: ScriptBuf::new(),
707 sequence: Sequence::ZERO,
708 witness: Witness::new(),
709 },
710 TxIn {
712 previous_output: OutPoint::null(),
713 script_sig: ScriptBuf::new(),
714 sequence: Sequence::ZERO,
715 witness: p2wsh_witness,
716 },
717 TxIn {
719 previous_output: OutPoint::null(),
720 script_sig: ScriptBuf::new(),
721 sequence: Sequence::ZERO,
722 witness: tapscript_witness,
723 },
724 TxIn {
726 previous_output: OutPoint::null(),
727 script_sig: ScriptBuf::new(),
728 sequence: Sequence::ZERO,
729 witness: annex_witness,
730 },
731 ],
732 output: vec![
733 op_return_output0,
734 TxOut {
735 value: Amount::from_sat(10000),
736 script_pubkey: ScriptBuf::new(),
737 },
738 op_return_output1,
739 ],
740 };
741
742 let embeddings = Embedding::from_transaction(&tx);
743
744 assert_eq!(embeddings.len(), 6);
745
746 assert_eq!(embeddings[0].bytes, b"Hello");
747 assert_eq!(embeddings[0].txid, tx.compute_txid());
748 assert_eq!(
749 embeddings[0].location,
750 EmbeddingLocation::OpReturn { output: 0 }
751 );
752
753 assert_eq!(embeddings[1].bytes, b"World");
754 assert_eq!(embeddings[1].txid, tx.compute_txid());
755 assert_eq!(
756 embeddings[1].location,
757 EmbeddingLocation::OpReturn { output: 2 }
758 );
759
760 assert_eq!(embeddings[2].bytes, b"p2wsh-data");
761 assert_eq!(embeddings[2].txid, tx.compute_txid());
762 assert_eq!(
763 embeddings[2].location,
764 EmbeddingLocation::WitnessEnvelope {
765 input: 1,
766 index: 0,
767 pushes: vec![10],
768 script_type: ScriptType::Legacy,
769 }
770 );
771
772 assert_eq!(embeddings[3].bytes, b"tapscript-data1");
773 assert_eq!(embeddings[3].txid, tx.compute_txid());
774 assert_eq!(
775 embeddings[3].location,
776 EmbeddingLocation::WitnessEnvelope {
777 input: 2,
778 index: 0,
779 pushes: vec![15],
780 script_type: ScriptType::Tapscript,
781 }
782 );
783
784 assert_eq!(embeddings[4].bytes, b"multipartdata");
785 assert_eq!(embeddings[4].txid, tx.compute_txid());
786 assert_eq!(
787 embeddings[4].location,
788 EmbeddingLocation::WitnessEnvelope {
789 input: 2,
790 index: 1,
791 pushes: vec![5, 4, 4],
792 script_type: ScriptType::Tapscript,
793 }
794 );
795
796 assert_eq!(embeddings[5].bytes, b"annex-data");
797 assert_eq!(embeddings[5].txid, tx.compute_txid());
798 assert_eq!(
799 embeddings[5].location,
800 EmbeddingLocation::TaprootAnnex { input: 3 }
801 );
802
803 for embedding in &embeddings {
804 assert_eq!(embedding.txid, tx.compute_txid());
805 }
806 }
807
808 #[test]
809 fn test_embedding_id_from_str() {
810 let txid_str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
811
812 let op_return_id = EmbeddingId::from_str(&format!("{}:rt:2", txid_str)).unwrap();
814 assert_eq!(op_return_id.embedding_type, EmbeddingType::OpReturn);
815 assert_eq!(op_return_id.index, 2);
816 assert_eq!(op_return_id.sub_index, None);
817
818 let annex_id = EmbeddingId::from_str(&format!("{}:ta:1", txid_str)).unwrap();
820 assert_eq!(annex_id.embedding_type, EmbeddingType::TaprootAnnex);
821 assert_eq!(annex_id.index, 1);
822 assert_eq!(annex_id.sub_index, None);
823
824 let legacy_id = EmbeddingId::from_str(&format!("{}:le:0:3", txid_str)).unwrap();
826 assert_eq!(
827 legacy_id.embedding_type,
828 EmbeddingType::WitnessEnvelope(ScriptType::Legacy)
829 );
830 assert_eq!(legacy_id.index, 0);
831 assert_eq!(legacy_id.sub_index, Some(3));
832
833 let legacy_id2 = EmbeddingId::from_str(&format!("{}:le:0", txid_str)).unwrap();
835 assert_eq!(
836 legacy_id2.embedding_type,
837 EmbeddingType::WitnessEnvelope(ScriptType::Legacy)
838 );
839 assert_eq!(legacy_id2.index, 0);
840 assert_eq!(legacy_id2.sub_index, Some(0));
841
842 let tapscript_id = EmbeddingId::from_str(&format!("{}:te:2:1", txid_str)).unwrap();
844 assert_eq!(
845 tapscript_id.embedding_type,
846 EmbeddingType::WitnessEnvelope(ScriptType::Tapscript)
847 );
848 assert_eq!(tapscript_id.index, 2);
849 assert_eq!(tapscript_id.sub_index, Some(1));
850 }
851
852 #[test]
853 fn test_embedding_id_to_string() {
854 let txid = Txid::all_zeros();
855
856 let op_return_id = EmbeddingId {
858 txid,
859 embedding_type: EmbeddingType::OpReturn,
860 index: 2,
861 sub_index: None,
862 _private: false,
863 };
864
865 let annex_id = EmbeddingId {
867 txid,
868 embedding_type: EmbeddingType::TaprootAnnex,
869 index: 1,
870 sub_index: None,
871 _private: false,
872 };
873
874 let legacy_id = EmbeddingId {
876 txid,
877 embedding_type: EmbeddingType::WitnessEnvelope(ScriptType::Legacy),
878 index: 0,
879 sub_index: Some(3),
880 _private: false,
881 };
882
883 let tapscript_id = EmbeddingId {
885 txid,
886 embedding_type: EmbeddingType::WitnessEnvelope(ScriptType::Tapscript),
887 index: 2,
888 sub_index: Some(0),
889 _private: false,
890 };
891
892 let txid_str = txid.to_string();
893
894 assert_eq!(op_return_id.to_string(), format!("{}:rt:2", txid_str));
895 assert_eq!(annex_id.to_string(), format!("{}:ta:1", txid_str));
896 assert_eq!(legacy_id.to_string(), format!("{}:le:0:3", txid_str));
897 assert_eq!(tapscript_id.to_string(), format!("{}:te:2", txid_str));
898 }
899
900 #[test]
901 fn test_embedding_id_invalid_parsing() {
902 let txid_str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
903
904 let err = EmbeddingId::from_str(txid_str).unwrap_err();
906 assert_eq!(err, EmbeddingIdError::InvalidFormat);
907
908 let err = EmbeddingId::from_str(&format!("{}:rt:2:3:extra", txid_str)).unwrap_err();
910 assert_eq!(err, EmbeddingIdError::InvalidFormat);
911
912 let err = EmbeddingId::from_str("invalid:rt:2").unwrap_err();
914 assert_eq!(err, EmbeddingIdError::InvalidTxid);
915
916 let err = EmbeddingId::from_str(&format!("{}:invalid:2", txid_str)).unwrap_err();
918 assert_eq!(err, EmbeddingIdError::InvalidType);
919
920 let err = EmbeddingId::from_str(&format!("{}:rt:abc", txid_str)).unwrap_err();
922 assert_eq!(err, EmbeddingIdError::InvalidIndex);
923
924 let err = EmbeddingId::from_str(&format!("{}:te:2:abc", txid_str)).unwrap_err();
926 assert_eq!(err, EmbeddingIdError::InvalidIndex);
927
928 let err = EmbeddingId::from_str(&format!("{}:rt:2:1", txid_str)).unwrap_err();
930 assert_eq!(err, EmbeddingIdError::InvalidFormat);
931
932 let err = EmbeddingId::from_str(&format!("{}:ta:1:2", txid_str)).unwrap_err();
934 assert_eq!(err, EmbeddingIdError::InvalidFormat);
935 }
936
937 #[test]
938 fn test_embedding_id_from_embedding() {
939 let txid = Txid::all_zeros();
940
941 let op_return_embedding = Embedding {
943 bytes: vec![1, 2, 3],
944 txid,
945 location: EmbeddingLocation::OpReturn { output: 2 },
946 };
947
948 let op_return_id = op_return_embedding.id();
949 assert_eq!(op_return_id.txid, txid);
950 assert_eq!(op_return_id.embedding_type, EmbeddingType::OpReturn);
951 assert_eq!(op_return_id.index, 2);
952 assert_eq!(op_return_id.sub_index, None);
953
954 let annex_embedding = Embedding {
956 bytes: vec![4, 5, 6],
957 txid,
958 location: EmbeddingLocation::TaprootAnnex { input: 1 },
959 };
960
961 let annex_id = annex_embedding.id();
962 assert_eq!(annex_id.txid, txid);
963 assert_eq!(annex_id.embedding_type, EmbeddingType::TaprootAnnex);
964 assert_eq!(annex_id.index, 1);
965 assert_eq!(annex_id.sub_index, None);
966
967 let legacy_embedding0 = Embedding {
969 bytes: vec![7, 8, 9],
970 txid,
971 location: EmbeddingLocation::WitnessEnvelope {
972 input: 3,
973 index: 0,
974 pushes: vec![3],
975 script_type: ScriptType::Legacy,
976 },
977 };
978
979 let legacy_id0 = legacy_embedding0.id();
980 assert_eq!(legacy_id0.txid, txid);
981 assert_eq!(
982 legacy_id0.embedding_type,
983 EmbeddingType::WitnessEnvelope(ScriptType::Legacy)
984 );
985 assert_eq!(legacy_id0.index, 3);
986 assert_eq!(legacy_id0.sub_index, Some(0));
987
988 let legacy_embedding1 = Embedding {
990 bytes: vec![7, 8, 9],
991 txid,
992 location: EmbeddingLocation::WitnessEnvelope {
993 input: 3,
994 index: 2,
995 pushes: vec![3],
996 script_type: ScriptType::Legacy,
997 },
998 };
999
1000 let legacy_id1 = legacy_embedding1.id();
1001 assert_eq!(legacy_id1.txid, txid);
1002 assert_eq!(
1003 legacy_id1.embedding_type,
1004 EmbeddingType::WitnessEnvelope(ScriptType::Legacy)
1005 );
1006 assert_eq!(legacy_id1.index, 3);
1007 assert_eq!(legacy_id1.sub_index, Some(2));
1008
1009 let tapscript_embedding = Embedding {
1011 bytes: vec![10, 11, 12],
1012 txid,
1013 location: EmbeddingLocation::WitnessEnvelope {
1014 input: 4,
1015 index: 1,
1016 pushes: vec![3],
1017 script_type: ScriptType::Tapscript,
1018 },
1019 };
1020
1021 let tapscript_id = tapscript_embedding.id();
1022 assert_eq!(tapscript_id.txid, txid);
1023 assert_eq!(
1024 tapscript_id.embedding_type,
1025 EmbeddingType::WitnessEnvelope(ScriptType::Tapscript)
1026 );
1027 assert_eq!(tapscript_id.index, 4);
1028 assert_eq!(tapscript_id.sub_index, Some(1));
1029 }
1030
1031 #[test]
1032 fn test_embedding_id_roundtrip() {
1033 let txid = Txid::all_zeros();
1034
1035 let op_return_id = EmbeddingId {
1037 txid,
1038 embedding_type: EmbeddingType::OpReturn,
1039 index: 2,
1040 sub_index: None,
1041 _private: false,
1042 };
1043
1044 let op_return_str = op_return_id.to_string();
1045 let op_return_id2 = EmbeddingId::from_str(&op_return_str).unwrap();
1046 assert_eq!(op_return_id.txid, op_return_id2.txid);
1047 assert_eq!(op_return_id.embedding_type, op_return_id2.embedding_type);
1048 assert_eq!(op_return_id.index, op_return_id2.index);
1049 assert_eq!(op_return_id.sub_index, op_return_id2.sub_index);
1050
1051 let annex_id = EmbeddingId {
1053 txid,
1054 embedding_type: EmbeddingType::TaprootAnnex,
1055 index: 1,
1056 sub_index: None,
1057 _private: false,
1058 };
1059
1060 let annex_str = annex_id.to_string();
1061 let annex_id2 = EmbeddingId::from_str(&annex_str).unwrap();
1062 assert_eq!(annex_id.txid, annex_id2.txid);
1063 assert_eq!(annex_id.embedding_type, annex_id2.embedding_type);
1064 assert_eq!(annex_id.index, annex_id2.index);
1065 assert_eq!(annex_id.sub_index, annex_id2.sub_index);
1066
1067 let legacy_id = EmbeddingId {
1069 txid,
1070 embedding_type: EmbeddingType::WitnessEnvelope(ScriptType::Legacy),
1071 index: 0,
1072 sub_index: Some(3),
1073 _private: false,
1074 };
1075
1076 let legacy_str = legacy_id.to_string();
1077 let legacy_id2 = EmbeddingId::from_str(&legacy_str).unwrap();
1078 assert_eq!(legacy_id.txid, legacy_id2.txid);
1079 assert_eq!(legacy_id.embedding_type, legacy_id2.embedding_type);
1080 assert_eq!(legacy_id.index, legacy_id2.index);
1081 assert_eq!(legacy_id.sub_index, legacy_id2.sub_index);
1082
1083 let tapscript_id = EmbeddingId {
1085 txid,
1086 embedding_type: EmbeddingType::WitnessEnvelope(ScriptType::Tapscript),
1087 index: 2,
1088 sub_index: Some(0),
1089 _private: false,
1090 };
1091
1092 let tapscript_str = tapscript_id.to_string();
1093 let tapscript_id2 = EmbeddingId::from_str(&tapscript_str).unwrap();
1094 assert_eq!(tapscript_id.txid, tapscript_id2.txid);
1095 assert_eq!(tapscript_id.embedding_type, tapscript_id2.embedding_type);
1096 assert_eq!(tapscript_id.index, tapscript_id2.index);
1097 assert_eq!(Some(0), tapscript_id2.sub_index);
1098 }
1099}