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 = "0x5f8fd2347449685cf41d4db97926ec3a096eaf381332be4f1318ad4d16a8497c";
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());
686 assert_eq!(
687 mainnet.router_address().unwrap(),
688 AccountAddress::from_hex(MAINNET_ROUTER).unwrap()
689 );
690 let testnet = AnsClient::new(FullnodeClient::new(AptosConfig::testnet()).unwrap());
691 assert_eq!(
692 testnet.router_address().unwrap(),
693 AccountAddress::from_hex(TESTNET_ROUTER).unwrap()
694 );
695 }
696
697 #[tokio::test]
698 async fn get_target_address_resolves() {
699 let server = MockServer::start().await;
700 let addr = "0x0000000000000000000000000000000000000000000000000000000000000abc";
701 Mock::given(method("POST"))
702 .and(path("/v1/view"))
703 .respond_with(
704 ResponseTemplate::new(200).set_body_json(serde_json::json!([{"vec": [addr]}])),
705 )
706 .expect(1)
707 .mount(&server)
708 .await;
709
710 let ans = ans_for(&server);
711 let resolved = ans.get_target_address("alice.apt").await.unwrap();
712 assert_eq!(resolved, Some(AccountAddress::from_hex(addr).unwrap()));
713 }
714
715 #[tokio::test]
716 async fn get_target_address_none_when_unregistered() {
717 let server = MockServer::start().await;
718 Mock::given(method("POST"))
719 .and(path("/v1/view"))
720 .respond_with(
721 ResponseTemplate::new(200).set_body_json(serde_json::json!([{"vec": []}])),
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, None);
730 }
731
732 #[tokio::test]
733 async fn get_primary_name_combines_subdomain_and_domain() {
734 let server = MockServer::start().await;
735 Mock::given(method("POST"))
736 .and(path("/v1/view"))
737 .respond_with(ResponseTemplate::new(200).set_body_json(
738 serde_json::json!([{"vec": ["wallet"]}, {"vec": ["alice"]}]),
740 ))
741 .expect(1)
742 .mount(&server)
743 .await;
744
745 let ans = ans_for(&server);
746 let name = ans.get_primary_name(AccountAddress::ONE).await.unwrap();
747 assert_eq!(name, Some("wallet.alice".to_string()));
748 }
749
750 #[tokio::test]
751 async fn get_primary_name_domain_only() {
752 let server = MockServer::start().await;
753 Mock::given(method("POST"))
754 .and(path("/v1/view"))
755 .respond_with(
756 ResponseTemplate::new(200)
757 .set_body_json(serde_json::json!([{"vec": []}, {"vec": ["alice"]}])),
758 )
759 .expect(1)
760 .mount(&server)
761 .await;
762
763 let ans = ans_for(&server);
764 let name = ans.get_primary_name(AccountAddress::ONE).await.unwrap();
765 assert_eq!(name, Some("alice".to_string()));
766 }
767
768 #[tokio::test]
769 async fn get_primary_name_none() {
770 let server = MockServer::start().await;
771 Mock::given(method("POST"))
772 .and(path("/v1/view"))
773 .respond_with(
774 ResponseTemplate::new(200)
775 .set_body_json(serde_json::json!([{"vec": []}, {"vec": []}])),
776 )
777 .expect(1)
778 .mount(&server)
779 .await;
780
781 let ans = ans_for(&server);
782 let name = ans.get_primary_name(AccountAddress::ONE).await.unwrap();
783 assert_eq!(name, None);
784 }
785
786 #[tokio::test]
787 async fn get_expiration_parses_seconds() {
788 let server = MockServer::start().await;
789 Mock::given(method("POST"))
790 .and(path("/v1/view"))
791 .respond_with(
792 ResponseTemplate::new(200).set_body_json(serde_json::json!(["1700000000"])),
793 )
794 .expect(1)
795 .mount(&server)
796 .await;
797
798 let ans = ans_for(&server);
799 let expiration = ans.get_expiration("alice.apt").await.unwrap();
800 assert_eq!(expiration, Some(1_700_000_000));
801 }
802
803 #[tokio::test]
804 async fn get_expiration_none_on_move_abort() {
805 let server = MockServer::start().await;
806 Mock::given(method("POST"))
807 .and(path("/v1/view"))
808 .respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
809 "message": "Move abort",
810 "error_code": "invalid_input",
811 "vm_error_code": 196609
812 })))
813 .expect(1)
814 .mount(&server)
815 .await;
816
817 let ans = ans_for(&server);
818 let expiration = ans.get_expiration("alice.apt").await.unwrap();
819 assert_eq!(expiration, None);
820 }
821
822 #[tokio::test]
823 async fn lookup_errors_when_unresolved() {
824 let server = MockServer::start().await;
825 Mock::given(method("POST"))
826 .and(path("/v1/view"))
827 .respond_with(
828 ResponseTemplate::new(200).set_body_json(serde_json::json!([{"vec": []}])),
829 )
830 .mount(&server)
831 .await;
832
833 let ans = ans_for(&server);
834 let err = ans.lookup("alice.apt").await.unwrap_err();
835 assert!(matches!(err, AptosError::NotFound(_)));
836 }
837
838 #[test]
839 fn set_primary_name_payload_encodes_arguments() {
840 let router = AccountAddress::from_hex("0x1").unwrap();
841 let fullnode = FullnodeClient::new(AptosConfig::mainnet()).unwrap();
842 let ans = AnsClient::with_router_address(fullnode, router);
843
844 let payload = ans.set_primary_name_payload("alice.apt").unwrap();
845 assert_eq!(payload.module.address, router);
846 assert_eq!(payload.module.name.as_str(), "router");
847 assert_eq!(payload.function, "set_primary_name");
848 assert_eq!(payload.args.len(), 2);
850 assert_eq!(
851 payload.args[0],
852 aptos_bcs::to_bytes(&"alice".to_string()).unwrap()
853 );
854 assert_eq!(
855 payload.args[1],
856 aptos_bcs::to_bytes(&Option::<String>::None).unwrap()
857 );
858 }
859
860 #[test]
861 fn set_target_address_payload_encodes_arguments() {
862 let router = AccountAddress::from_hex("0x1").unwrap();
863 let target = AccountAddress::from_hex("0xabc").unwrap();
864 let fullnode = FullnodeClient::new(AptosConfig::mainnet()).unwrap();
865 let ans = AnsClient::with_router_address(fullnode, router);
866
867 let payload = ans
868 .set_target_address_payload("wallet.alice.apt", target)
869 .unwrap();
870 assert_eq!(payload.function, "set_target_addr");
871 assert_eq!(payload.args.len(), 3);
873 assert_eq!(
874 payload.args[0],
875 aptos_bcs::to_bytes(&"alice".to_string()).unwrap()
876 );
877 assert_eq!(
878 payload.args[1],
879 aptos_bcs::to_bytes(&Some("wallet".to_string())).unwrap()
880 );
881 assert_eq!(payload.args[2], aptos_bcs::to_bytes(&target).unwrap());
882 }
883
884 #[test]
885 fn register_domain_payload_rejects_subdomain() {
886 let fullnode = FullnodeClient::new(AptosConfig::mainnet()).unwrap();
887 let ans =
888 AnsClient::with_router_address(fullnode, AccountAddress::from_hex("0x1").unwrap());
889 let result = ans.register_domain_payload("wallet.alice.apt", 31_536_000, None, None);
890 assert!(matches!(result, Err(AptosError::Other(_))));
891 }
892
893 #[test]
894 fn register_domain_payload_encodes_arguments() {
895 let router = AccountAddress::from_hex("0x1").unwrap();
896 let target = AccountAddress::from_hex("0xabc").unwrap();
897 let fullnode = FullnodeClient::new(AptosConfig::mainnet()).unwrap();
898 let ans = AnsClient::with_router_address(fullnode, router);
899
900 let payload = ans
901 .register_domain_payload("alice.apt", 31_536_000, Some(target), None)
902 .unwrap();
903 assert_eq!(payload.function, "register_domain");
904 assert_eq!(payload.args.len(), 4);
906 assert_eq!(
907 payload.args[0],
908 aptos_bcs::to_bytes(&"alice".to_string()).unwrap()
909 );
910 assert_eq!(
911 payload.args[1],
912 aptos_bcs::to_bytes(&31_536_000u64).unwrap()
913 );
914 assert_eq!(payload.args[2], aptos_bcs::to_bytes(&Some(target)).unwrap());
915 assert_eq!(
916 payload.args[3],
917 aptos_bcs::to_bytes(&Option::<AccountAddress>::None).unwrap()
918 );
919 }
920
921 #[test]
922 fn clear_primary_name_payload_has_no_args() {
923 let fullnode = FullnodeClient::new(AptosConfig::mainnet()).unwrap();
924 let ans =
925 AnsClient::with_router_address(fullnode, AccountAddress::from_hex("0x1").unwrap());
926 let payload = ans.clear_primary_name_payload().unwrap();
927 assert_eq!(payload.function, "clear_primary_name");
928 assert!(payload.args.is_empty());
929 }
930
931 #[tokio::test]
932 async fn get_owner_address_resolves() {
933 let server = MockServer::start().await;
934 let addr = "0x0000000000000000000000000000000000000000000000000000000000000abc";
935 Mock::given(method("POST"))
936 .and(path("/v1/view"))
937 .respond_with(
938 ResponseTemplate::new(200).set_body_json(serde_json::json!([{"vec": [addr]}])),
939 )
940 .expect(1)
941 .mount(&server)
942 .await;
943
944 let ans = ans_for(&server);
945 let owner = ans.get_owner_address("alice.apt").await.unwrap();
946 assert_eq!(owner, Some(AccountAddress::from_hex(addr).unwrap()));
947 }
948
949 #[tokio::test]
950 async fn lookup_resolves_to_target_address() {
951 let server = MockServer::start().await;
952 let addr = "0x0000000000000000000000000000000000000000000000000000000000000abc";
953 Mock::given(method("POST"))
954 .and(path("/v1/view"))
955 .respond_with(
956 ResponseTemplate::new(200).set_body_json(serde_json::json!([{"vec": [addr]}])),
957 )
958 .mount(&server)
959 .await;
960
961 let ans = ans_for(&server);
962 let resolved = ans.lookup("alice.apt").await.unwrap();
963 assert_eq!(resolved, AccountAddress::from_hex(addr).unwrap());
964 }
965
966 #[tokio::test]
967 async fn reverse_lookup_returns_primary_name() {
968 let server = MockServer::start().await;
969 Mock::given(method("POST"))
970 .and(path("/v1/view"))
971 .respond_with(
972 ResponseTemplate::new(200)
973 .set_body_json(serde_json::json!([{"vec": []}, {"vec": ["alice"]}])),
974 )
975 .mount(&server)
976 .await;
977
978 let ans = ans_for(&server);
979 let name = ans.reverse_lookup(AccountAddress::ONE).await.unwrap();
980 assert_eq!(name, Some("alice".to_string()));
981 }
982
983 #[test]
984 fn clear_target_address_payload_encodes_arguments() {
985 let router = AccountAddress::from_hex("0x1").unwrap();
986 let fullnode = FullnodeClient::new(AptosConfig::mainnet()).unwrap();
987 let ans = AnsClient::with_router_address(fullnode, router);
988
989 let payload = ans.clear_target_address_payload("alice.apt").unwrap();
990 assert_eq!(payload.function, "clear_target_addr");
991 assert_eq!(payload.args.len(), 2);
993 assert_eq!(
994 payload.args[0],
995 aptos_bcs::to_bytes(&"alice".to_string()).unwrap()
996 );
997 assert_eq!(
998 payload.args[1],
999 aptos_bcs::to_bytes(&Option::<String>::None).unwrap()
1000 );
1001 }
1002
1003 #[tokio::test]
1004 async fn get_expiration_propagates_server_error() {
1005 let server = MockServer::start().await;
1006 Mock::given(method("POST"))
1007 .and(path("/v1/view"))
1008 .respond_with(ResponseTemplate::new(500).set_body_json(serde_json::json!({
1009 "message": "internal error"
1010 })))
1011 .mount(&server)
1012 .await;
1013
1014 let ans = ans_for(&server);
1015 let result = ans.get_expiration("alice.apt").await;
1018 assert!(matches!(
1019 result,
1020 Err(AptosError::Api {
1021 status_code: 500,
1022 ..
1023 })
1024 ));
1025 }
1026
1027 #[tokio::test]
1028 async fn get_target_address_errors_on_malformed_address() {
1029 let server = MockServer::start().await;
1030 Mock::given(method("POST"))
1034 .and(path("/v1/view"))
1035 .respond_with(
1036 ResponseTemplate::new(200)
1037 .set_body_json(serde_json::json!([{"vec": ["not-an-address"]}])),
1038 )
1039 .mount(&server)
1040 .await;
1041
1042 let ans = ans_for(&server);
1043 let err = ans.get_target_address("alice.apt").await.unwrap_err();
1044 assert!(matches!(err, AptosError::Internal(_)));
1045 }
1046
1047 #[tokio::test]
1048 async fn get_expiration_errors_on_malformed_success() {
1049 let server = MockServer::start().await;
1050 Mock::given(method("POST"))
1053 .and(path("/v1/view"))
1054 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1055 .mount(&server)
1056 .await;
1057
1058 let ans = ans_for(&server);
1059 let err = ans.get_expiration("alice.apt").await.unwrap_err();
1060 assert!(matches!(err, AptosError::Internal(_)));
1061 }
1062
1063 #[tokio::test]
1064 async fn get_target_address_none_on_move_option_none() {
1065 let server = MockServer::start().await;
1066 Mock::given(method("POST"))
1069 .and(path("/v1/view"))
1070 .respond_with(
1071 ResponseTemplate::new(200).set_body_json(serde_json::json!([{"vec": []}])),
1072 )
1073 .mount(&server)
1074 .await;
1075
1076 let ans = ans_for(&server);
1077 assert_eq!(ans.get_target_address("alice.apt").await.unwrap(), None);
1078 }
1079
1080 #[tokio::test]
1081 async fn get_target_address_errors_on_non_option_value() {
1082 let server = MockServer::start().await;
1083 Mock::given(method("POST"))
1087 .and(path("/v1/view"))
1088 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["0x1"])))
1089 .mount(&server)
1090 .await;
1091
1092 let ans = ans_for(&server);
1093 let err = ans.get_target_address("alice.apt").await.unwrap_err();
1094 assert!(matches!(err, AptosError::Internal(_)));
1095 }
1096
1097 #[tokio::test]
1098 async fn get_primary_name_errors_on_short_response() {
1099 let server = MockServer::start().await;
1100 Mock::given(method("POST"))
1103 .and(path("/v1/view"))
1104 .respond_with(
1105 ResponseTemplate::new(200).set_body_json(serde_json::json!([{"vec": ["alice"]}])),
1106 )
1107 .mount(&server)
1108 .await;
1109
1110 let ans = ans_for(&server);
1111 let err = ans.get_primary_name(AccountAddress::ONE).await.unwrap_err();
1112 assert!(matches!(err, AptosError::Internal(_)));
1113 }
1114
1115 #[tokio::test]
1116 async fn get_primary_name_errors_on_non_string_value() {
1117 let server = MockServer::start().await;
1118 Mock::given(method("POST"))
1120 .and(path("/v1/view"))
1121 .respond_with(
1122 ResponseTemplate::new(200)
1123 .set_body_json(serde_json::json!([{"vec": []}, {"vec": [42]}])),
1124 )
1125 .mount(&server)
1126 .await;
1127
1128 let ans = ans_for(&server);
1129 let err = ans.get_primary_name(AccountAddress::ONE).await.unwrap_err();
1130 assert!(matches!(err, AptosError::Internal(_)));
1131 }
1132}