1use core::convert::TryFrom;
5use core::fmt::Debug;
6use core::fmt::Display;
7use core::fmt::Formatter;
8use core::str::FromStr;
9
10use identity_core::common::KeyComparable;
11use identity_did::BaseDIDUrl;
12use identity_did::CoreDID;
13use identity_did::Error as DIDError;
14use identity_did::DID;
15use iota_interaction::types::base_types::ObjectID;
16use product_common::network_name::NetworkName;
17use ref_cast::ref_cast_custom;
18use ref_cast::RefCastCustom;
19use serde::Deserialize;
20use serde::Serialize;
21
22type Result<T> = std::result::Result<T, DIDError>;
24
25#[derive(Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, RefCastCustom)]
30#[repr(transparent)]
31#[serde(into = "CoreDID", try_from = "CoreDID")]
32pub struct IotaDID(CoreDID);
33
34impl IotaDID {
35 pub const SCHEME: &'static str = CoreDID::SCHEME;
37
38 pub const METHOD: &'static str = "iota";
40
41 pub const DEFAULT_NETWORK: &'static str = "iota";
45
46 pub const PLACEHOLDER_TAG: &'static str = "0x0000000000000000000000000000000000000000000000000000000000000000";
48
49 pub(crate) const TAG_BYTES_LEN: usize = 32;
51
52 #[ref_cast_custom]
62 pub(crate) const fn from_inner_ref_unchecked(did: &CoreDID) -> &Self;
63
64 pub fn new(bytes: &[u8; Self::TAG_BYTES_LEN], network_name: &NetworkName) -> Self {
83 Self::from_object_id(ObjectID::new(*bytes), network_name)
84 }
85
86 pub fn from_object_id(object_id: ObjectID, network_name: &NetworkName) -> Self {
88 Self::parse(format!("did:iota:{network_name}:{object_id}")).expect("valid IOTA DID")
89 }
90
91 pub fn to_object_id(&self) -> ObjectID {
93 let object_id_hex = self.as_str().rsplit_once(':').expect("valid IOTA DID").1;
94 ObjectID::from_hex_literal(object_id_hex).expect("valid object ID")
95 }
96
97 pub fn placeholder(network_name: &NetworkName) -> Self {
110 Self::new(&[0; 32], network_name)
111 }
112
113 pub fn is_placeholder(&self) -> bool {
125 self.tag_str() == Self::PLACEHOLDER_TAG
126 }
127
128 pub fn parse(input: impl AsRef<str>) -> Result<Self> {
134 CoreDID::parse(input.as_ref().to_lowercase()).and_then(Self::try_from_core)
135 }
136
137 pub fn try_from_core(did: CoreDID) -> Result<Self> {
143 Self::check_validity(&did)?;
144
145 Ok(Self(Self::normalize(did)))
146 }
147
148 pub fn network_str(&self) -> &str {
154 Self::denormalized_components(self.method_id()).0
155 }
156
157 pub fn tag_str(&self) -> &str {
159 Self::denormalized_components(self.method_id()).1
160 }
161
162 pub fn check_validity<D: DID>(did: &D) -> Result<()> {
172 Self::check_method(did)
173 .and_then(|_| Self::check_tag(did))
174 .and_then(|_| Self::check_network(did))
175 }
176
177 pub fn is_valid(did: &CoreDID) -> bool {
182 Self::check_validity(did).is_ok()
183 }
184
185 fn check_method<D: DID>(did: &D) -> Result<()> {
195 (did.method() == Self::METHOD)
196 .then_some(())
197 .ok_or(DIDError::InvalidMethodName)
198 }
199
200 fn check_tag<D: DID>(did: &D) -> Result<()> {
206 let (_, tag) = Self::denormalized_components(did.method_id());
207
208 prefix_hex::decode::<[u8; Self::TAG_BYTES_LEN]>(tag)
210 .map_err(|_| DIDError::InvalidMethodId)
211 .map(|_| ())
212 }
213
214 fn check_network<D: DID>(did: &D) -> Result<()> {
220 let (network_name, _) = Self::denormalized_components(did.method_id());
221 NetworkName::validate_network_name(network_name).map_err(|_| DIDError::Other("invalid network name"))
222 }
223
224 #[allow(clippy::unnecessary_to_owned)]
231 fn normalize(mut did: CoreDID) -> CoreDID {
232 let method_id = did.method_id();
233 let (network, tag) = Self::denormalized_components(method_id);
234 if tag.len() == method_id.len() || network != Self::DEFAULT_NETWORK {
235 did
236 } else {
237 did
238 .set_method_id(tag.to_owned())
239 .expect("normalizing a valid CoreDID should be Ok");
240 did
241 }
242 }
243
244 #[inline(always)]
248 fn denormalized_components(input: &str) -> (&str, &str) {
249 input
250 .find(':')
251 .map(|idx| input.split_at(idx))
252 .map(|(network, tail)| (network, &tail[1..]))
253 .unwrap_or((Self::DEFAULT_NETWORK, input))
255 }
256}
257
258impl FromStr for IotaDID {
259 type Err = DIDError;
260
261 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
262 Self::parse(s)
263 }
264}
265
266impl TryFrom<&str> for IotaDID {
267 type Error = DIDError;
268
269 fn try_from(other: &str) -> std::result::Result<Self, Self::Error> {
270 Self::parse(other)
271 }
272}
273
274impl TryFrom<String> for IotaDID {
275 type Error = DIDError;
276
277 fn try_from(other: String) -> std::result::Result<Self, Self::Error> {
278 Self::parse(other)
279 }
280}
281
282impl Display for IotaDID {
283 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
284 write!(f, "{}", self.0)
285 }
286}
287
288impl From<IotaDID> for CoreDID {
289 fn from(id: IotaDID) -> Self {
290 id.0
291 }
292}
293
294impl From<IotaDID> for String {
295 fn from(did: IotaDID) -> Self {
296 did.into_string()
297 }
298}
299
300impl TryFrom<CoreDID> for IotaDID {
301 type Error = DIDError;
302
303 fn try_from(value: CoreDID) -> std::result::Result<Self, Self::Error> {
304 Self::try_from_core(value)
305 }
306}
307
308impl TryFrom<BaseDIDUrl> for IotaDID {
309 type Error = DIDError;
310
311 fn try_from(other: BaseDIDUrl) -> Result<Self> {
312 let core_did: CoreDID = CoreDID::try_from(other)?;
313 Self::try_from(core_did)
314 }
315}
316
317impl AsRef<CoreDID> for IotaDID {
318 fn as_ref(&self) -> &CoreDID {
319 &self.0
320 }
321}
322
323impl KeyComparable for IotaDID {
324 type Key = CoreDID;
325
326 #[inline]
327 fn key(&self) -> &Self::Key {
328 self.as_ref()
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use identity_did::DIDUrl;
335 use once_cell::sync::Lazy;
336 use proptest::strategy::Strategy;
337 use proptest::*;
338
339 use super::*;
340
341 const VALID_OBJECT_ID_STR: &str = "0xf29dd16310c2100fd1bf568b345fb1cc14d71caa3bd9b5ad735d2bd6d455ca3b";
346
347 const LEN_VALID_OBJECT_ID_STR: usize = VALID_OBJECT_ID_STR.len();
348
349 static VALID_IOTA_DID_STRING: Lazy<String> = Lazy::new(|| format!("did:{}:{}", IotaDID::METHOD, VALID_OBJECT_ID_STR));
350
351 const VALID_NETWORK_NAMES: [&str; 13] = [
354 IotaDID::DEFAULT_NETWORK,
355 "main",
356 "dev",
357 "smr",
358 "rms",
359 "test",
360 "foo",
361 "foobar",
362 "123456",
363 "0",
364 "foo42",
365 "bar123",
366 "42foo",
367 ];
368
369 static VALID_IOTA_DID_STRINGS: Lazy<Vec<String>> = Lazy::new(|| {
370 let network_tag_to_did = |network, tag| format!("did:{}:{}:{}", IotaDID::METHOD, network, tag);
371
372 let valid_strings: Vec<String> = VALID_NETWORK_NAMES
373 .iter()
374 .flat_map(|network| {
375 [VALID_OBJECT_ID_STR, IotaDID::PLACEHOLDER_TAG]
376 .iter()
377 .map(move |tag| network_tag_to_did(network, tag))
378 })
379 .collect();
380
381 assert_eq!(valid_strings.len(), 2 * VALID_NETWORK_NAMES.len());
384
385 valid_strings
386 });
387
388 #[test]
393 fn invalid_check_method() {
394 let did_key_core: CoreDID = CoreDID::parse("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap();
395 assert!(matches!(
396 IotaDID::check_method(&did_key_core),
397 Err(DIDError::InvalidMethodName)
398 ));
399 }
400
401 #[test]
402 fn valid_check_method() {
403 let did_iota_core: CoreDID = CoreDID::parse(&*VALID_IOTA_DID_STRING).unwrap();
404 assert!(IotaDID::check_method(&did_iota_core).is_ok());
405 }
406
407 #[test]
408 fn valid_check_network() {
409 let assert_check_network = |input: &str| {
410 let did_core: CoreDID =
411 CoreDID::parse(input).unwrap_or_else(|_| panic!("expected {input} to parse to a valid CoreDID"));
412 assert!(
413 IotaDID::check_network(&did_core).is_ok(),
414 "test: valid_check_network failed with input {input}",
415 );
416 };
417
418 for network_name in VALID_NETWORK_NAMES {
419 let did_string = format!("did:method:{network_name}:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
420 assert_check_network(&did_string);
421 }
422
423 assert_check_network("did:method:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
424 }
425
426 #[test]
427 fn invalid_check_network() {
428 let mut check_network_executed: bool = false;
436
437 const INVALID_NETWORK_NAMES: [&str; 10] = [
438 "Main",
439 "fOo",
440 "deV",
441 "féta",
442 "",
443 " ",
444 "foo ",
445 " foo",
446 "123456789",
447 "foobar123",
448 ];
449 for network_name in INVALID_NETWORK_NAMES {
450 let did_string: String = format!("did:method:{network_name}:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
451 let did_core: CoreDID = {
452 match CoreDID::parse(did_string) {
453 Ok(did_core) => did_core,
454 Err(_) => continue,
455 }
456 };
457
458 assert!(matches!(IotaDID::check_network(&did_core), Err(DIDError::Other(_))));
459 check_network_executed = true;
460 }
461 assert!(
462 check_network_executed,
463 "IotaDID::check_network` should have been executed"
464 );
465 }
466
467 #[test]
468 fn valid_check_tag() {
469 for input in VALID_IOTA_DID_STRINGS.iter() {
470 let did_core: CoreDID = CoreDID::parse(input).unwrap();
471 assert!(
472 IotaDID::check_tag(&did_core).is_ok(),
473 "test: valid_check_method_id failed on input {input}"
474 );
475 }
476
477 let did_other_string: String = format!("did:method:{VALID_OBJECT_ID_STR}");
480 let did_other_with_network: String = format!("did:method:test:{VALID_OBJECT_ID_STR}");
481 let did_other_core: CoreDID = CoreDID::parse(did_other_string).unwrap();
482 let did_other_with_network_core: CoreDID = CoreDID::parse(did_other_with_network).unwrap();
483
484 assert!(IotaDID::check_tag(&did_other_core).is_ok());
485 assert!(IotaDID::check_tag(&did_other_with_network_core).is_ok());
486 }
487
488 #[test]
489 fn invalid_check_tag() {
490 let invalid_method_id_strings = [
491 format!("did:method:main:test:{VALID_OBJECT_ID_STR}"),
493 format!("did:method:{}", &VALID_OBJECT_ID_STR.strip_prefix("0x").unwrap()),
495 format!(
497 "did:method:{}",
498 &VALID_OBJECT_ID_STR.chars().chain("a".chars()).collect::<String>()
499 ),
500 format!("did:method:main:{}", &VALID_OBJECT_ID_STR[..65]),
502 ];
503
504 for input in invalid_method_id_strings {
505 let did_core: CoreDID = CoreDID::parse(input).unwrap();
506 assert!(
507 matches!(IotaDID::check_tag(&did_core), Err(DIDError::InvalidMethodId)),
508 "{}",
509 did_core
510 );
511 }
512 }
513
514 #[test]
519 fn placeholder_produces_a_did_with_expected_string_representation() {
520 assert_eq!(
521 IotaDID::placeholder(&NetworkName::try_from(IotaDID::DEFAULT_NETWORK).unwrap()).as_str(),
522 format!("did:{}:{}", IotaDID::METHOD, IotaDID::PLACEHOLDER_TAG)
523 );
524
525 for name in VALID_NETWORK_NAMES
526 .iter()
527 .filter(|name| *name != &IotaDID::DEFAULT_NETWORK)
528 {
529 let network_name: NetworkName = NetworkName::try_from(*name).unwrap();
530 let did: IotaDID = IotaDID::placeholder(&network_name);
531 assert_eq!(
532 did.as_str(),
533 format!("did:{}:{}:{}", IotaDID::METHOD, name, IotaDID::PLACEHOLDER_TAG)
534 );
535 }
536 }
537
538 #[test]
539 fn normalization_in_constructors() {
540 let did_with_default_network_string: String = format!(
541 "did:{}:{}:{}",
542 IotaDID::METHOD,
543 IotaDID::DEFAULT_NETWORK,
544 VALID_OBJECT_ID_STR
545 );
546 let expected_normalization_string_representation: String =
547 format!("did:{}:{}", IotaDID::METHOD, VALID_OBJECT_ID_STR);
548
549 assert_eq!(
550 IotaDID::parse(did_with_default_network_string).unwrap().as_str(),
551 expected_normalization_string_representation
552 );
553 }
554
555 #[test]
556 fn parse_valid() {
557 for did_str in VALID_IOTA_DID_STRINGS.iter() {
558 assert!(IotaDID::parse(did_str).is_ok());
559 }
560 }
561
562 #[test]
563 fn parse_invalid() {
564 let execute_assertions = |valid_object_id: &str| {
565 assert!(matches!(
566 IotaDID::parse(format!("dod:{}:{}", IotaDID::METHOD, valid_object_id)),
567 Err(DIDError::InvalidScheme)
568 ));
569
570 assert!(matches!(
571 IotaDID::parse(format!("did:key:{valid_object_id}")),
572 Err(DIDError::InvalidMethodName)
573 ));
574
575 assert!(matches!(
577 IotaDID::parse(format!("did:{}:123456789:{}", IotaDID::METHOD, valid_object_id)),
578 Err(DIDError::Other(_))
579 ));
580
581 assert!(matches!(
583 IotaDID::parse(format!("did:{}:féta:{}", IotaDID::METHOD, valid_object_id)),
584 Err(DIDError::InvalidMethodId)
585 ));
586
587 assert!(matches!(
589 IotaDID::parse(format!("did:{}:", IotaDID::METHOD)),
590 Err(DIDError::InvalidMethodId)
591 ));
592
593 assert!(matches!(
595 IotaDID::parse(format!("did:{}:test:foo:{}", IotaDID::METHOD, valid_object_id)),
596 Err(DIDError::InvalidMethodId)
597 ));
598 };
599
600 execute_assertions(IotaDID::PLACEHOLDER_TAG);
601 execute_assertions(VALID_OBJECT_ID_STR);
602 }
603
604 #[cfg(feature = "iota-client")]
609 proptest! {
610 #[test]
611 fn property_based_new(bytes in proptest::prelude::any::<[u8;32]>()) {
612 for network_name in VALID_NETWORK_NAMES.iter().map(|name| NetworkName::try_from(*name).unwrap()) {
613 IotaDID::new(&bytes, &network_name);
615 }
616 }
617 }
618
619 fn arbitrary_object_id_string_replica() -> impl Strategy<Value = String> {
620 proptest::string::string_regex(&format!("0x([a-f]|[0-9]){{{}}}", (LEN_VALID_OBJECT_ID_STR - 2)))
621 .expect("regex should be ok")
622 }
623
624 proptest! {
625 #[test]
626 fn valid_object_id_string_replicas(tag in arbitrary_object_id_string_replica()) {
627 let did : String = format!("did:{}:{}", IotaDID::METHOD, tag);
628 assert!(
629 IotaDID::parse(did).is_ok()
630 );
631 }
632 }
633
634 fn arbitrary_invalid_tag() -> impl Strategy<Value = String> {
635 proptest::string::string_regex("[[:^alpha:]|[a-z]|[1-9]]*")
636 .expect("regex should be ok")
637 .prop_map(|arb_string| {
638 if arb_string
639 .chars()
640 .all(|c| c.is_ascii_hexdigit() && c.is_ascii_lowercase())
641 && arb_string.len() == LEN_VALID_OBJECT_ID_STR
642 && arb_string.starts_with("0x")
643 {
644 let mut counter = 0;
647 arb_string
648 .chars()
649 .rev()
650 .map(|value| {
651 if value == '0' && counter == 0 {
652 counter += 1;
653 'é'
654 } else {
655 value
656 }
657 })
658 .collect::<String>()
659 } else {
660 arb_string
661 }
662 })
663 }
664
665 proptest! {
666 #[test]
667 fn invalid_tag_property_based_parse(tag in arbitrary_invalid_tag()) {
668 let did: String = format!("did:{}:{}", IotaDID::METHOD, tag);
669 assert!(
670 IotaDID::parse(did).is_err()
671 );
672 }
673 }
674
675 fn arbitrary_delimiter_mixed_in_prefix_hex() -> impl Strategy<Value = String> {
676 proptest::string::string_regex("0x([a-f]|[:]|[0-9])*").expect("regex should be ok")
677 }
678
679 proptest! {
680 #[test]
681 fn invalid_hex_mixed_with_delimiter(tag in arbitrary_delimiter_mixed_in_prefix_hex()) {
682 let did: String = format!("did:{}:{}", IotaDID::METHOD, tag);
683 assert!(IotaDID::parse(did).is_err());
684 }
685 }
686
687 #[test]
691 fn test_network() {
692 let execute_assertions = |valid_object_id: &str| {
693 let did: IotaDID = format!("did:{}:{}", IotaDID::METHOD, valid_object_id).parse().unwrap();
694 assert_eq!(did.network_str(), IotaDID::DEFAULT_NETWORK);
695
696 let did: IotaDID = format!("did:{}:dev:{}", IotaDID::METHOD, valid_object_id)
697 .parse()
698 .unwrap();
699 assert_eq!(did.network_str(), "dev");
700
701 let did: IotaDID = format!("did:{}:test:{}", IotaDID::METHOD, valid_object_id)
702 .parse()
703 .unwrap();
704 assert_eq!(did.network_str(), "test");
705
706 let did: IotaDID = format!("did:{}:custom:{}", IotaDID::METHOD, valid_object_id)
707 .parse()
708 .unwrap();
709 assert_eq!(did.network_str(), "custom");
710 };
711
712 execute_assertions(IotaDID::PLACEHOLDER_TAG);
713 execute_assertions(VALID_OBJECT_ID_STR);
714 }
715
716 #[test]
717 fn test_tag() {
718 let execute_assertions = |valid_object_id: &str| {
719 let did: IotaDID = format!("did:{}:{}", IotaDID::METHOD, valid_object_id).parse().unwrap();
720 assert_eq!(did.tag_str(), valid_object_id);
721
722 let did: IotaDID = format!(
723 "did:{}:{}:{}",
724 IotaDID::METHOD,
725 IotaDID::DEFAULT_NETWORK,
726 valid_object_id
727 )
728 .parse()
729 .unwrap();
730 assert_eq!(did.tag_str(), valid_object_id);
731
732 let did: IotaDID = format!("did:{}:dev:{}", IotaDID::METHOD, valid_object_id)
733 .parse()
734 .unwrap();
735 assert_eq!(did.tag_str(), valid_object_id);
736
737 let did: IotaDID = format!("did:{}:custom:{}", IotaDID::METHOD, valid_object_id)
738 .parse()
739 .unwrap();
740 assert_eq!(did.tag_str(), valid_object_id);
741 };
742 execute_assertions(IotaDID::PLACEHOLDER_TAG);
743 execute_assertions(VALID_OBJECT_ID_STR);
744 }
745
746 #[test]
751 fn test_parse_did_url_valid() {
752 let execute_assertions = |valid_object_id: &str| {
753 assert!(DIDUrl::parse(format!("did:{}:{}", IotaDID::METHOD, valid_object_id)).is_ok());
754 assert!(DIDUrl::parse(format!("did:{}:{}#fragment", IotaDID::METHOD, valid_object_id)).is_ok());
755 assert!(DIDUrl::parse(format!(
756 "did:{}:{}?somequery=somevalue",
757 IotaDID::METHOD,
758 valid_object_id
759 ))
760 .is_ok());
761 assert!(DIDUrl::parse(format!(
762 "did:{}:{}?somequery=somevalue#fragment",
763 IotaDID::METHOD,
764 valid_object_id
765 ))
766 .is_ok());
767
768 assert!(DIDUrl::parse(format!("did:{}:main:{}", IotaDID::METHOD, valid_object_id)).is_ok());
769 assert!(DIDUrl::parse(format!("did:{}:main:{}#fragment", IotaDID::METHOD, valid_object_id)).is_ok());
770 assert!(DIDUrl::parse(format!(
771 "did:{}:main:{}?somequery=somevalue",
772 IotaDID::METHOD,
773 valid_object_id
774 ))
775 .is_ok());
776 assert!(DIDUrl::parse(format!(
777 "did:{}:main:{}?somequery=somevalue#fragment",
778 IotaDID::METHOD,
779 valid_object_id
780 ))
781 .is_ok());
782
783 assert!(DIDUrl::parse(format!("did:{}:dev:{}", IotaDID::METHOD, valid_object_id)).is_ok());
784 assert!(DIDUrl::parse(format!("did:{}:dev:{}#fragment", IotaDID::METHOD, valid_object_id)).is_ok());
785 assert!(DIDUrl::parse(format!(
786 "did:{}:dev:{}?somequery=somevalue",
787 IotaDID::METHOD,
788 valid_object_id
789 ))
790 .is_ok());
791 assert!(DIDUrl::parse(format!(
792 "did:{}:dev:{}?somequery=somevalue#fragment",
793 IotaDID::METHOD,
794 valid_object_id
795 ))
796 .is_ok());
797
798 assert!(DIDUrl::parse(format!("did:{}:custom:{}", IotaDID::METHOD, valid_object_id)).is_ok());
799 assert!(DIDUrl::parse(format!("did:{}:custom:{}#fragment", IotaDID::METHOD, valid_object_id)).is_ok());
800 assert!(DIDUrl::parse(format!(
801 "did:{}:custom:{}?somequery=somevalue",
802 IotaDID::METHOD,
803 valid_object_id
804 ))
805 .is_ok());
806 assert!(DIDUrl::parse(format!(
807 "did:{}:custom:{}?somequery=somevalue#fragment",
808 IotaDID::METHOD,
809 valid_object_id
810 ))
811 .is_ok());
812 };
813 execute_assertions(IotaDID::PLACEHOLDER_TAG);
814 execute_assertions(VALID_OBJECT_ID_STR);
815 }
816
817 #[test]
818 fn valid_url_setters() {
819 let execute_assertions = |valid_object_id: &str| {
820 let mut did_url: DIDUrl = IotaDID::parse(format!("did:{}:{}", IotaDID::METHOD, valid_object_id))
821 .unwrap()
822 .into_url();
823
824 did_url.set_path(Some("/foo")).unwrap();
825 did_url.set_query(Some("diff=true")).unwrap();
826 did_url.set_fragment(Some("foo")).unwrap();
827
828 assert_eq!(did_url.path(), Some("/foo"));
829 assert_eq!(did_url.query(), Some("diff=true"));
830 assert_eq!(did_url.fragment(), Some("foo"));
831 };
832 execute_assertions(IotaDID::PLACEHOLDER_TAG);
833 execute_assertions(VALID_OBJECT_ID_STR);
834 }
835}