1use crate::api::FullnodeClient;
42use crate::config::Network;
43use crate::error::{AptosError, AptosResult};
44use crate::transaction::EntryFunction;
45use crate::types::AccountAddress;
46
47const MAINNET_ROUTER: &str = "0x867ed1f6bf916171b1de3ee92849b8978b7d1b9e0a8cc982a3d19d535dfd9c0c";
49const TESTNET_ROUTER: &str = "0xb1ae61606dfbe0ea5b5c45ffdb4fb08da0dba18c5125182ff63ab280a450ecf4";
51const LOCAL_ROUTER: &str = "0x585fc9f0f0c54183b039ffc770ca282ebd87307916c215a3e692f2f8e4305e82";
54
55const MIN_SEGMENT_LEN: usize = 3;
57const MAX_SEGMENT_LEN: usize = 63;
59
60const SEGMENT_RULES: &str = "a segment must be 3-63 characters of lowercase a-z, 0-9, and hyphens, \
62 and may not start or end with a hyphen";
63
64#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct AnsName {
85 domain: String,
86 subdomain: Option<String>,
87}
88
89impl AnsName {
90 pub fn parse(name: &str) -> AptosResult<Self> {
101 let trimmed = name.strip_suffix(".apt").unwrap_or(name);
102 let mut parts = trimmed.split('.');
103 let first = parts.next().unwrap_or("");
104 let second = parts.next();
105 if parts.next().is_some() {
106 return Err(invalid_name(name, "a name may have at most two segments"));
107 }
108
109 if !is_valid_segment(first) {
110 return Err(invalid_name(first, SEGMENT_RULES));
111 }
112 if let Some(second) = second
113 && !is_valid_segment(second)
114 {
115 return Err(invalid_name(second, SEGMENT_RULES));
116 }
117
118 match second {
119 Some(domain) => Ok(Self {
121 domain: domain.to_string(),
122 subdomain: Some(first.to_string()),
123 }),
124 None => Ok(Self {
126 domain: first.to_string(),
127 subdomain: None,
128 }),
129 }
130 }
131
132 #[must_use]
134 pub fn domain(&self) -> &str {
135 &self.domain
136 }
137
138 #[must_use]
140 pub fn subdomain(&self) -> Option<&str> {
141 self.subdomain.as_deref()
142 }
143
144 #[must_use]
146 pub fn is_subdomain(&self) -> bool {
147 self.subdomain.is_some()
148 }
149
150 #[must_use]
153 pub fn full_name(&self) -> String {
154 match &self.subdomain {
155 Some(sub) => format!("{sub}.{}", self.domain),
156 None => self.domain.clone(),
157 }
158 }
159}
160
161impl std::fmt::Display for AnsName {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 write!(f, "{}.apt", self.full_name())
164 }
165}
166
167fn invalid_name(value: &str, reason: &str) -> AptosError {
168 AptosError::Other(anyhow::anyhow!("invalid ANS name '{value}': {reason}"))
169}
170
171fn is_valid_segment(segment: &str) -> bool {
173 let len = segment.len();
174 if !(MIN_SEGMENT_LEN..=MAX_SEGMENT_LEN).contains(&len) {
175 return false;
176 }
177 let bytes = segment.as_bytes();
178 let is_alnum = |b: u8| b.is_ascii_lowercase() || b.is_ascii_digit();
179 if !is_alnum(bytes[0]) || !is_alnum(bytes[len - 1]) {
181 return false;
182 }
183 bytes.iter().all(|&b| is_alnum(b) || b == b'-')
184}
185
186fn domain_subdomain_args(name: &AnsName) -> AptosResult<Vec<Vec<u8>>> {
189 Ok(vec![
190 bcs_string(name.domain())?,
191 bcs_option_string(name.subdomain())?,
192 ])
193}
194
195#[derive(Debug, Clone)]
200pub struct AnsClient {
201 fullnode: FullnodeClient,
202 router_override: Option<AccountAddress>,
205}
206
207impl AnsClient {
208 #[must_use]
215 pub fn new(fullnode: FullnodeClient) -> Self {
216 Self {
217 fullnode,
218 router_override: None,
219 }
220 }
221
222 #[must_use]
228 pub fn with_router_address(fullnode: FullnodeClient, router_address: AccountAddress) -> Self {
229 Self {
230 fullnode,
231 router_override: Some(router_address),
232 }
233 }
234
235 pub fn router_address(&self) -> AptosResult<AccountAddress> {
242 if let Some(addr) = self.router_override {
243 return Ok(addr);
244 }
245 let hex = match self.fullnode.config().network() {
246 Network::Mainnet => MAINNET_ROUTER,
247 Network::Testnet => TESTNET_ROUTER,
248 Network::Local => LOCAL_ROUTER,
249 other => {
250 return Err(AptosError::Config(format!(
251 "ANS is not deployed on {}; construct the client with \
252 AnsClient::with_router_address to supply the router contract address",
253 other.as_str()
254 )));
255 }
256 };
257 AccountAddress::from_hex(hex)
258 }
259
260 pub async fn get_target_address(&self, name: &str) -> AptosResult<Option<AccountAddress>> {
274 let name = AnsName::parse(name)?;
275 let values = self
276 .view_router("get_target_addr", domain_subdomain_args(&name)?)
277 .await?;
278 option_address(values.first())
279 }
280
281 pub async fn get_owner_address(&self, name: &str) -> AptosResult<Option<AccountAddress>> {
289 let name = AnsName::parse(name)?;
290 let values = self
291 .view_router("get_owner_addr", domain_subdomain_args(&name)?)
292 .await?;
293 option_address(values.first())
294 }
295
296 pub async fn get_primary_name(&self, address: AccountAddress) -> AptosResult<Option<String>> {
305 let args = vec![bcs_address(address)?];
306 let values = self.view_router("get_primary_name", args).await?;
307
308 if values.len() < 2 {
312 return Err(AptosError::Internal(format!(
313 "ANS get_primary_name returned {} value(s); expected 2 (subdomain, domain)",
314 values.len()
315 )));
316 }
317 let subdomain = option_string(&values[0])?;
318 let domain = option_string(&values[1])?;
319
320 Ok(domain.map(|domain| match subdomain {
321 Some(sub) => format!("{sub}.{domain}"),
322 None => domain,
323 }))
324 }
325
326 pub async fn get_expiration(&self, name: &str) -> AptosResult<Option<u64>> {
341 let name = AnsName::parse(name)?;
342 let args = domain_subdomain_args(&name)?;
343 match self.view_router("get_expiration", args).await {
344 Ok(values) => {
349 let raw = values.first().ok_or_else(|| {
350 AptosError::Internal("ANS get_expiration returned no value".into())
351 })?;
352 let secs = parse_u64(raw).ok_or_else(|| {
353 AptosError::Internal(format!(
354 "ANS get_expiration returned a non-u64 value: {raw}"
355 ))
356 })?;
357 Ok(Some(secs))
358 }
359 Err(AptosError::Api {
365 status_code: 400,
366 vm_error_code: Some(_),
367 ..
368 }) => Ok(None),
369 Err(e) => Err(e),
370 }
371 }
372
373 pub async fn lookup(&self, name: &str) -> AptosResult<AccountAddress> {
385 self.get_target_address(name).await?.ok_or_else(|| {
386 AptosError::NotFound(format!("ANS name '{name}' does not resolve to an address"))
387 })
388 }
389
390 pub async fn reverse_lookup(&self, address: AccountAddress) -> AptosResult<Option<String>> {
399 self.get_primary_name(address).await
400 }
401
402 pub fn register_domain_payload(
417 &self,
418 name: &str,
419 registration_duration_secs: u64,
420 target_address: Option<AccountAddress>,
421 to_address: Option<AccountAddress>,
422 ) -> AptosResult<EntryFunction> {
423 let name = AnsName::parse(name)?;
424 if name.is_subdomain() {
425 return Err(invalid_name(
426 &name.full_name(),
427 "register_domain_payload expects a top-level domain, not a subdomain",
428 ));
429 }
430 let args = vec![
431 bcs_string(name.domain())?,
432 bcs_u64(registration_duration_secs)?,
433 bcs_option_address(target_address)?,
434 bcs_option_address(to_address)?,
435 ];
436 self.router_entry("register_domain", args)
437 }
438
439 pub fn set_primary_name_payload(&self, name: &str) -> AptosResult<EntryFunction> {
446 let name = AnsName::parse(name)?;
447 self.router_entry("set_primary_name", domain_subdomain_args(&name)?)
448 }
449
450 pub fn clear_primary_name_payload(&self) -> AptosResult<EntryFunction> {
456 self.router_entry("clear_primary_name", vec![])
457 }
458
459 pub fn set_target_address_payload(
467 &self,
468 name: &str,
469 address: AccountAddress,
470 ) -> AptosResult<EntryFunction> {
471 let name = AnsName::parse(name)?;
472 let mut args = domain_subdomain_args(&name)?;
473 args.push(bcs_address(address)?);
474 self.router_entry("set_target_addr", args)
475 }
476
477 pub fn clear_target_address_payload(&self, name: &str) -> AptosResult<EntryFunction> {
484 let name = AnsName::parse(name)?;
485 self.router_entry("clear_target_addr", domain_subdomain_args(&name)?)
486 }
487
488 async fn view_router(
492 &self,
493 function: &str,
494 args: Vec<Vec<u8>>,
495 ) -> AptosResult<Vec<serde_json::Value>> {
496 let router = self.router_address()?;
497 let function_id = format!("{router}::router::{function}");
498 let response = self
499 .fullnode
500 .view_bcs_args(&function_id, vec![], args)
501 .await?;
502 Ok(response.into_inner())
503 }
504
505 fn router_entry(&self, function: &str, args: Vec<Vec<u8>>) -> AptosResult<EntryFunction> {
507 let router = self.router_address()?;
508 let function_id = format!("{router}::router::{function}");
509 EntryFunction::from_function_id(&function_id, vec![], args)
510 }
511}
512
513fn bcs_string(value: &str) -> AptosResult<Vec<u8>> {
516 aptos_bcs::to_bytes(&value.to_string()).map_err(AptosError::bcs)
517}
518
519fn bcs_option_string(value: Option<&str>) -> AptosResult<Vec<u8>> {
520 aptos_bcs::to_bytes(&value.map(ToString::to_string)).map_err(AptosError::bcs)
521}
522
523fn bcs_address(value: AccountAddress) -> AptosResult<Vec<u8>> {
524 aptos_bcs::to_bytes(&value).map_err(AptosError::bcs)
525}
526
527fn bcs_option_address(value: Option<AccountAddress>) -> AptosResult<Vec<u8>> {
528 aptos_bcs::to_bytes(&value).map_err(AptosError::bcs)
529}
530
531fn bcs_u64(value: u64) -> AptosResult<Vec<u8>> {
532 aptos_bcs::to_bytes(&value).map_err(AptosError::bcs)
533}
534
535fn unwrap_option(value: &serde_json::Value) -> AptosResult<Option<&serde_json::Value>> {
545 if let Some(vec) = value.get("vec").and_then(serde_json::Value::as_array) {
546 return Ok(vec.first());
547 }
548 if let Some(arr) = value.as_array() {
549 return Ok(arr.first());
550 }
551 Err(AptosError::Internal(format!(
552 "ANS view returned a value that is not a Move Option: {value}"
553 )))
554}
555
556fn option_address(value: Option<&serde_json::Value>) -> AptosResult<Option<AccountAddress>> {
563 let value = value.ok_or_else(|| {
564 AptosError::Internal(
565 "ANS view returned no value where an Option<address> was expected".into(),
566 )
567 })?;
568 let Some(inner) = unwrap_option(value)? else {
569 return Ok(None);
570 };
571 let s = inner.as_str().ok_or_else(|| {
572 AptosError::Internal(format!("ANS view returned a non-string address: {inner}"))
573 })?;
574 let address = AccountAddress::from_hex(s).map_err(|e| {
575 AptosError::Internal(format!("ANS view returned a malformed address '{s}': {e}"))
576 })?;
577 Ok(Some(address))
578}
579
580fn option_string(value: &serde_json::Value) -> AptosResult<Option<String>> {
585 let Some(inner) = unwrap_option(value)? else {
586 return Ok(None);
587 };
588 let s = inner.as_str().ok_or_else(|| {
589 AptosError::Internal(format!("ANS view returned a non-string value: {inner}"))
590 })?;
591 Ok(if s.is_empty() {
592 None
593 } else {
594 Some(s.to_string())
595 })
596}
597
598fn parse_u64(value: &serde_json::Value) -> Option<u64> {
600 if let Some(n) = value.as_u64() {
601 return Some(n);
602 }
603 value.as_str().and_then(|s| s.parse().ok())
604}
605
606#[cfg(test)]
607mod tests {
608 use super::*;
609 use crate::config::AptosConfig;
610 use wiremock::matchers::{method, path};
611 use wiremock::{Mock, MockServer, ResponseTemplate};
612
613 fn ans_for(server: &MockServer) -> AnsClient {
614 let url = format!("{}/v1", server.uri());
615 let config = AptosConfig::custom(&url).unwrap().without_retry();
616 let fullnode = FullnodeClient::new(config).unwrap();
617 AnsClient::with_router_address(fullnode, AccountAddress::from_hex("0x1").unwrap())
619 }
620
621 #[test]
622 fn parse_plain_domain() {
623 let name = AnsName::parse("alice.apt").unwrap();
624 assert_eq!(name.domain(), "alice");
625 assert_eq!(name.subdomain(), None);
626 assert!(!name.is_subdomain());
627 assert_eq!(name.full_name(), "alice");
628 assert_eq!(name.to_string(), "alice.apt");
629 }
630
631 #[test]
632 fn parse_without_suffix() {
633 let name = AnsName::parse("alice").unwrap();
634 assert_eq!(name.domain(), "alice");
635 assert_eq!(name.subdomain(), None);
636 }
637
638 #[test]
639 fn parse_subdomain() {
640 let name = AnsName::parse("wallet.alice.apt").unwrap();
641 assert_eq!(name.domain(), "alice");
642 assert_eq!(name.subdomain(), Some("wallet"));
643 assert!(name.is_subdomain());
644 assert_eq!(name.full_name(), "wallet.alice");
645 }
646
647 #[test]
648 fn parse_rejects_invalid_names() {
649 assert!(AnsName::parse("ab").is_err());
651 assert!(AnsName::parse("-abc").is_err());
653 assert!(AnsName::parse("abc-").is_err());
655 assert!(AnsName::parse("Alice").is_err());
657 assert!(AnsName::parse("a.b.c").is_err());
659 assert!(AnsName::parse("ali_ce").is_err());
661 }
662
663 #[test]
664 fn segment_validation_boundaries() {
665 assert!(is_valid_segment("abc"));
666 assert!(is_valid_segment("a-c"));
667 assert!(is_valid_segment("a1c"));
668 assert!(is_valid_segment(&"a".repeat(63)));
669 assert!(!is_valid_segment(&"a".repeat(64)));
670 assert!(!is_valid_segment("ab"));
671 assert!(!is_valid_segment("-bc"));
672 assert!(!is_valid_segment("ab-"));
673 }
674
675 #[test]
676 fn router_address_unsupported_network_errors() {
677 let fullnode = FullnodeClient::new(AptosConfig::devnet()).unwrap();
679 let ans = AnsClient::new(fullnode);
680 assert!(matches!(ans.router_address(), Err(AptosError::Config(_))));
681 }
682
683 #[test]
684 fn router_address_known_networks() {
685 let mainnet = AnsClient::new(FullnodeClient::new(AptosConfig::mainnet()).unwrap());
689 assert_eq!(
690 mainnet.router_address().unwrap(),
691 AccountAddress::from_hex(
692 "0x867ed1f6bf916171b1de3ee92849b8978b7d1b9e0a8cc982a3d19d535dfd9c0c"
693 )
694 .unwrap()
695 );
696 let testnet = AnsClient::new(FullnodeClient::new(AptosConfig::testnet()).unwrap());
697 assert_eq!(
698 testnet.router_address().unwrap(),
699 AccountAddress::from_hex(
700 "0xb1ae61606dfbe0ea5b5c45ffdb4fb08da0dba18c5125182ff63ab280a450ecf4"
701 )
702 .unwrap()
703 );
704 let local = AnsClient::new(FullnodeClient::new(AptosConfig::local()).unwrap());
705 assert_eq!(
706 local.router_address().unwrap(),
707 AccountAddress::from_hex(
708 "0x585fc9f0f0c54183b039ffc770ca282ebd87307916c215a3e692f2f8e4305e82"
709 )
710 .unwrap()
711 );
712 }
713
714 #[tokio::test]
715 async fn get_target_address_resolves() {
716 let server = MockServer::start().await;
717 let addr = "0x0000000000000000000000000000000000000000000000000000000000000abc";
718 Mock::given(method("POST"))
719 .and(path("/v1/view"))
720 .respond_with(
721 ResponseTemplate::new(200).set_body_json(serde_json::json!([{"vec": [addr]}])),
722 )
723 .expect(1)
724 .mount(&server)
725 .await;
726
727 let ans = ans_for(&server);
728 let resolved = ans.get_target_address("alice.apt").await.unwrap();
729 assert_eq!(resolved, Some(AccountAddress::from_hex(addr).unwrap()));
730 }
731
732 #[tokio::test]
733 async fn get_target_address_none_when_unregistered() {
734 let server = MockServer::start().await;
735 Mock::given(method("POST"))
736 .and(path("/v1/view"))
737 .respond_with(
738 ResponseTemplate::new(200).set_body_json(serde_json::json!([{"vec": []}])),
739 )
740 .expect(1)
741 .mount(&server)
742 .await;
743
744 let ans = ans_for(&server);
745 let resolved = ans.get_target_address("alice.apt").await.unwrap();
746 assert_eq!(resolved, None);
747 }
748
749 #[tokio::test]
750 async fn get_primary_name_combines_subdomain_and_domain() {
751 let server = MockServer::start().await;
752 Mock::given(method("POST"))
753 .and(path("/v1/view"))
754 .respond_with(ResponseTemplate::new(200).set_body_json(
755 serde_json::json!([{"vec": ["wallet"]}, {"vec": ["alice"]}]),
757 ))
758 .expect(1)
759 .mount(&server)
760 .await;
761
762 let ans = ans_for(&server);
763 let name = ans.get_primary_name(AccountAddress::ONE).await.unwrap();
764 assert_eq!(name, Some("wallet.alice".to_string()));
765 }
766
767 #[tokio::test]
768 async fn get_primary_name_domain_only() {
769 let server = MockServer::start().await;
770 Mock::given(method("POST"))
771 .and(path("/v1/view"))
772 .respond_with(
773 ResponseTemplate::new(200)
774 .set_body_json(serde_json::json!([{"vec": []}, {"vec": ["alice"]}])),
775 )
776 .expect(1)
777 .mount(&server)
778 .await;
779
780 let ans = ans_for(&server);
781 let name = ans.get_primary_name(AccountAddress::ONE).await.unwrap();
782 assert_eq!(name, Some("alice".to_string()));
783 }
784
785 #[tokio::test]
786 async fn get_primary_name_none() {
787 let server = MockServer::start().await;
788 Mock::given(method("POST"))
789 .and(path("/v1/view"))
790 .respond_with(
791 ResponseTemplate::new(200)
792 .set_body_json(serde_json::json!([{"vec": []}, {"vec": []}])),
793 )
794 .expect(1)
795 .mount(&server)
796 .await;
797
798 let ans = ans_for(&server);
799 let name = ans.get_primary_name(AccountAddress::ONE).await.unwrap();
800 assert_eq!(name, None);
801 }
802
803 #[tokio::test]
804 async fn get_expiration_parses_seconds() {
805 let server = MockServer::start().await;
806 Mock::given(method("POST"))
807 .and(path("/v1/view"))
808 .respond_with(
809 ResponseTemplate::new(200).set_body_json(serde_json::json!(["1700000000"])),
810 )
811 .expect(1)
812 .mount(&server)
813 .await;
814
815 let ans = ans_for(&server);
816 let expiration = ans.get_expiration("alice.apt").await.unwrap();
817 assert_eq!(expiration, Some(1_700_000_000));
818 }
819
820 #[tokio::test]
821 async fn get_expiration_none_on_move_abort() {
822 let server = MockServer::start().await;
823 Mock::given(method("POST"))
824 .and(path("/v1/view"))
825 .respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
826 "message": "Move abort",
827 "error_code": "invalid_input",
828 "vm_error_code": 196609
829 })))
830 .expect(1)
831 .mount(&server)
832 .await;
833
834 let ans = ans_for(&server);
835 let expiration = ans.get_expiration("alice.apt").await.unwrap();
836 assert_eq!(expiration, None);
837 }
838
839 #[tokio::test]
840 async fn lookup_errors_when_unresolved() {
841 let server = MockServer::start().await;
842 Mock::given(method("POST"))
843 .and(path("/v1/view"))
844 .respond_with(
845 ResponseTemplate::new(200).set_body_json(serde_json::json!([{"vec": []}])),
846 )
847 .mount(&server)
848 .await;
849
850 let ans = ans_for(&server);
851 let err = ans.lookup("alice.apt").await.unwrap_err();
852 assert!(matches!(err, AptosError::NotFound(_)));
853 }
854
855 #[test]
856 fn set_primary_name_payload_encodes_arguments() {
857 let router = AccountAddress::from_hex("0x1").unwrap();
858 let fullnode = FullnodeClient::new(AptosConfig::mainnet()).unwrap();
859 let ans = AnsClient::with_router_address(fullnode, router);
860
861 let payload = ans.set_primary_name_payload("alice.apt").unwrap();
862 assert_eq!(payload.module.address, router);
863 assert_eq!(payload.module.name.as_str(), "router");
864 assert_eq!(payload.function, "set_primary_name");
865 assert_eq!(payload.args.len(), 2);
867 assert_eq!(
868 payload.args[0],
869 aptos_bcs::to_bytes(&"alice".to_string()).unwrap()
870 );
871 assert_eq!(
872 payload.args[1],
873 aptos_bcs::to_bytes(&Option::<String>::None).unwrap()
874 );
875 }
876
877 #[test]
878 fn set_target_address_payload_encodes_arguments() {
879 let router = AccountAddress::from_hex("0x1").unwrap();
880 let target = AccountAddress::from_hex("0xabc").unwrap();
881 let fullnode = FullnodeClient::new(AptosConfig::mainnet()).unwrap();
882 let ans = AnsClient::with_router_address(fullnode, router);
883
884 let payload = ans
885 .set_target_address_payload("wallet.alice.apt", target)
886 .unwrap();
887 assert_eq!(payload.function, "set_target_addr");
888 assert_eq!(payload.args.len(), 3);
890 assert_eq!(
891 payload.args[0],
892 aptos_bcs::to_bytes(&"alice".to_string()).unwrap()
893 );
894 assert_eq!(
895 payload.args[1],
896 aptos_bcs::to_bytes(&Some("wallet".to_string())).unwrap()
897 );
898 assert_eq!(payload.args[2], aptos_bcs::to_bytes(&target).unwrap());
899 }
900
901 #[test]
902 fn register_domain_payload_rejects_subdomain() {
903 let fullnode = FullnodeClient::new(AptosConfig::mainnet()).unwrap();
904 let ans =
905 AnsClient::with_router_address(fullnode, AccountAddress::from_hex("0x1").unwrap());
906 let result = ans.register_domain_payload("wallet.alice.apt", 31_536_000, None, None);
907 assert!(matches!(result, Err(AptosError::Other(_))));
908 }
909
910 #[test]
911 fn register_domain_payload_encodes_arguments() {
912 let router = AccountAddress::from_hex("0x1").unwrap();
913 let target = AccountAddress::from_hex("0xabc").unwrap();
914 let fullnode = FullnodeClient::new(AptosConfig::mainnet()).unwrap();
915 let ans = AnsClient::with_router_address(fullnode, router);
916
917 let payload = ans
918 .register_domain_payload("alice.apt", 31_536_000, Some(target), None)
919 .unwrap();
920 assert_eq!(payload.function, "register_domain");
921 assert_eq!(payload.args.len(), 4);
923 assert_eq!(
924 payload.args[0],
925 aptos_bcs::to_bytes(&"alice".to_string()).unwrap()
926 );
927 assert_eq!(
928 payload.args[1],
929 aptos_bcs::to_bytes(&31_536_000u64).unwrap()
930 );
931 assert_eq!(payload.args[2], aptos_bcs::to_bytes(&Some(target)).unwrap());
932 assert_eq!(
933 payload.args[3],
934 aptos_bcs::to_bytes(&Option::<AccountAddress>::None).unwrap()
935 );
936 }
937
938 #[test]
939 fn clear_primary_name_payload_has_no_args() {
940 let fullnode = FullnodeClient::new(AptosConfig::mainnet()).unwrap();
941 let ans =
942 AnsClient::with_router_address(fullnode, AccountAddress::from_hex("0x1").unwrap());
943 let payload = ans.clear_primary_name_payload().unwrap();
944 assert_eq!(payload.function, "clear_primary_name");
945 assert!(payload.args.is_empty());
946 }
947
948 #[tokio::test]
949 async fn get_owner_address_resolves() {
950 let server = MockServer::start().await;
951 let addr = "0x0000000000000000000000000000000000000000000000000000000000000abc";
952 Mock::given(method("POST"))
953 .and(path("/v1/view"))
954 .respond_with(
955 ResponseTemplate::new(200).set_body_json(serde_json::json!([{"vec": [addr]}])),
956 )
957 .expect(1)
958 .mount(&server)
959 .await;
960
961 let ans = ans_for(&server);
962 let owner = ans.get_owner_address("alice.apt").await.unwrap();
963 assert_eq!(owner, Some(AccountAddress::from_hex(addr).unwrap()));
964 }
965
966 #[tokio::test]
967 async fn lookup_resolves_to_target_address() {
968 let server = MockServer::start().await;
969 let addr = "0x0000000000000000000000000000000000000000000000000000000000000abc";
970 Mock::given(method("POST"))
971 .and(path("/v1/view"))
972 .respond_with(
973 ResponseTemplate::new(200).set_body_json(serde_json::json!([{"vec": [addr]}])),
974 )
975 .mount(&server)
976 .await;
977
978 let ans = ans_for(&server);
979 let resolved = ans.lookup("alice.apt").await.unwrap();
980 assert_eq!(resolved, AccountAddress::from_hex(addr).unwrap());
981 }
982
983 #[tokio::test]
984 async fn reverse_lookup_returns_primary_name() {
985 let server = MockServer::start().await;
986 Mock::given(method("POST"))
987 .and(path("/v1/view"))
988 .respond_with(
989 ResponseTemplate::new(200)
990 .set_body_json(serde_json::json!([{"vec": []}, {"vec": ["alice"]}])),
991 )
992 .mount(&server)
993 .await;
994
995 let ans = ans_for(&server);
996 let name = ans.reverse_lookup(AccountAddress::ONE).await.unwrap();
997 assert_eq!(name, Some("alice".to_string()));
998 }
999
1000 #[test]
1001 fn clear_target_address_payload_encodes_arguments() {
1002 let router = AccountAddress::from_hex("0x1").unwrap();
1003 let fullnode = FullnodeClient::new(AptosConfig::mainnet()).unwrap();
1004 let ans = AnsClient::with_router_address(fullnode, router);
1005
1006 let payload = ans.clear_target_address_payload("alice.apt").unwrap();
1007 assert_eq!(payload.function, "clear_target_addr");
1008 assert_eq!(payload.args.len(), 2);
1010 assert_eq!(
1011 payload.args[0],
1012 aptos_bcs::to_bytes(&"alice".to_string()).unwrap()
1013 );
1014 assert_eq!(
1015 payload.args[1],
1016 aptos_bcs::to_bytes(&Option::<String>::None).unwrap()
1017 );
1018 }
1019
1020 #[tokio::test]
1021 async fn get_expiration_propagates_server_error() {
1022 let server = MockServer::start().await;
1023 Mock::given(method("POST"))
1024 .and(path("/v1/view"))
1025 .respond_with(ResponseTemplate::new(500).set_body_json(serde_json::json!({
1026 "message": "internal error"
1027 })))
1028 .mount(&server)
1029 .await;
1030
1031 let ans = ans_for(&server);
1032 let result = ans.get_expiration("alice.apt").await;
1035 assert!(matches!(
1036 result,
1037 Err(AptosError::Api {
1038 status_code: 500,
1039 ..
1040 })
1041 ));
1042 }
1043
1044 #[tokio::test]
1045 async fn get_target_address_errors_on_malformed_address() {
1046 let server = MockServer::start().await;
1047 Mock::given(method("POST"))
1051 .and(path("/v1/view"))
1052 .respond_with(
1053 ResponseTemplate::new(200)
1054 .set_body_json(serde_json::json!([{"vec": ["not-an-address"]}])),
1055 )
1056 .mount(&server)
1057 .await;
1058
1059 let ans = ans_for(&server);
1060 let err = ans.get_target_address("alice.apt").await.unwrap_err();
1061 assert!(matches!(err, AptosError::Internal(_)));
1062 }
1063
1064 #[tokio::test]
1065 async fn get_expiration_errors_on_malformed_success() {
1066 let server = MockServer::start().await;
1067 Mock::given(method("POST"))
1070 .and(path("/v1/view"))
1071 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1072 .mount(&server)
1073 .await;
1074
1075 let ans = ans_for(&server);
1076 let err = ans.get_expiration("alice.apt").await.unwrap_err();
1077 assert!(matches!(err, AptosError::Internal(_)));
1078 }
1079
1080 #[tokio::test]
1081 async fn get_target_address_none_on_move_option_none() {
1082 let server = MockServer::start().await;
1083 Mock::given(method("POST"))
1086 .and(path("/v1/view"))
1087 .respond_with(
1088 ResponseTemplate::new(200).set_body_json(serde_json::json!([{"vec": []}])),
1089 )
1090 .mount(&server)
1091 .await;
1092
1093 let ans = ans_for(&server);
1094 assert_eq!(ans.get_target_address("alice.apt").await.unwrap(), None);
1095 }
1096
1097 #[tokio::test]
1098 async fn get_target_address_errors_on_non_option_value() {
1099 let server = MockServer::start().await;
1100 Mock::given(method("POST"))
1104 .and(path("/v1/view"))
1105 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["0x1"])))
1106 .mount(&server)
1107 .await;
1108
1109 let ans = ans_for(&server);
1110 let err = ans.get_target_address("alice.apt").await.unwrap_err();
1111 assert!(matches!(err, AptosError::Internal(_)));
1112 }
1113
1114 #[tokio::test]
1115 async fn get_primary_name_errors_on_short_response() {
1116 let server = MockServer::start().await;
1117 Mock::given(method("POST"))
1120 .and(path("/v1/view"))
1121 .respond_with(
1122 ResponseTemplate::new(200).set_body_json(serde_json::json!([{"vec": ["alice"]}])),
1123 )
1124 .mount(&server)
1125 .await;
1126
1127 let ans = ans_for(&server);
1128 let err = ans.get_primary_name(AccountAddress::ONE).await.unwrap_err();
1129 assert!(matches!(err, AptosError::Internal(_)));
1130 }
1131
1132 #[tokio::test]
1133 async fn get_primary_name_errors_on_non_string_value() {
1134 let server = MockServer::start().await;
1135 Mock::given(method("POST"))
1137 .and(path("/v1/view"))
1138 .respond_with(
1139 ResponseTemplate::new(200)
1140 .set_body_json(serde_json::json!([{"vec": []}, {"vec": [42]}])),
1141 )
1142 .mount(&server)
1143 .await;
1144
1145 let ans = ans_for(&server);
1146 let err = ans.get_primary_name(AccountAddress::ONE).await.unwrap_err();
1147 assert!(matches!(err, AptosError::Internal(_)));
1148 }
1149}