use crate::api::FullnodeClient;
use crate::config::Network;
use crate::error::{AptosError, AptosResult};
use crate::transaction::EntryFunction;
use crate::types::AccountAddress;
const MAINNET_ROUTER: &str = "0x867ed1f6bf916171b1de3ee92849b8978b7d1b9e0a8cc982a3d19d535dfd9c0c";
const TESTNET_ROUTER: &str = "0x5f8fd2347449685cf41d4db97926ec3a096eaf381332be4f1318ad4d16a8497c";
const LOCAL_ROUTER: &str = "0x585fc9f0f0c54183b039ffc770ca282ebd87307916c215a3e692f2f8e4305e82";
const MIN_SEGMENT_LEN: usize = 3;
const MAX_SEGMENT_LEN: usize = 63;
const SEGMENT_RULES: &str = "a segment must be 3-63 characters of lowercase a-z, 0-9, and hyphens, \
and may not start or end with a hyphen";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AnsName {
domain: String,
subdomain: Option<String>,
}
impl AnsName {
pub fn parse(name: &str) -> AptosResult<Self> {
let trimmed = name.strip_suffix(".apt").unwrap_or(name);
let mut parts = trimmed.split('.');
let first = parts.next().unwrap_or("");
let second = parts.next();
if parts.next().is_some() {
return Err(invalid_name(name, "a name may have at most two segments"));
}
if !is_valid_segment(first) {
return Err(invalid_name(first, SEGMENT_RULES));
}
if let Some(second) = second
&& !is_valid_segment(second)
{
return Err(invalid_name(second, SEGMENT_RULES));
}
match second {
Some(domain) => Ok(Self {
domain: domain.to_string(),
subdomain: Some(first.to_string()),
}),
None => Ok(Self {
domain: first.to_string(),
subdomain: None,
}),
}
}
#[must_use]
pub fn domain(&self) -> &str {
&self.domain
}
#[must_use]
pub fn subdomain(&self) -> Option<&str> {
self.subdomain.as_deref()
}
#[must_use]
pub fn is_subdomain(&self) -> bool {
self.subdomain.is_some()
}
#[must_use]
pub fn full_name(&self) -> String {
match &self.subdomain {
Some(sub) => format!("{sub}.{}", self.domain),
None => self.domain.clone(),
}
}
}
impl std::fmt::Display for AnsName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.apt", self.full_name())
}
}
fn invalid_name(value: &str, reason: &str) -> AptosError {
AptosError::Other(anyhow::anyhow!("invalid ANS name '{value}': {reason}"))
}
fn is_valid_segment(segment: &str) -> bool {
let len = segment.len();
if !(MIN_SEGMENT_LEN..=MAX_SEGMENT_LEN).contains(&len) {
return false;
}
let bytes = segment.as_bytes();
let is_alnum = |b: u8| b.is_ascii_lowercase() || b.is_ascii_digit();
if !is_alnum(bytes[0]) || !is_alnum(bytes[len - 1]) {
return false;
}
bytes.iter().all(|&b| is_alnum(b) || b == b'-')
}
fn domain_subdomain_args(name: &AnsName) -> AptosResult<Vec<Vec<u8>>> {
Ok(vec![
bcs_string(name.domain())?,
bcs_option_string(name.subdomain())?,
])
}
#[derive(Debug, Clone)]
pub struct AnsClient {
fullnode: FullnodeClient,
router_override: Option<AccountAddress>,
}
impl AnsClient {
#[must_use]
pub fn new(fullnode: FullnodeClient) -> Self {
Self {
fullnode,
router_override: None,
}
}
#[must_use]
pub fn with_router_address(fullnode: FullnodeClient, router_address: AccountAddress) -> Self {
Self {
fullnode,
router_override: Some(router_address),
}
}
pub fn router_address(&self) -> AptosResult<AccountAddress> {
if let Some(addr) = self.router_override {
return Ok(addr);
}
let hex = match self.fullnode.config().network() {
Network::Mainnet => MAINNET_ROUTER,
Network::Testnet => TESTNET_ROUTER,
Network::Local => LOCAL_ROUTER,
other => {
return Err(AptosError::Config(format!(
"ANS is not deployed on {}; construct the client with \
AnsClient::with_router_address to supply the router contract address",
other.as_str()
)));
}
};
AccountAddress::from_hex(hex)
}
pub async fn get_target_address(&self, name: &str) -> AptosResult<Option<AccountAddress>> {
let name = AnsName::parse(name)?;
let values = self
.view_router("get_target_addr", domain_subdomain_args(&name)?)
.await?;
option_address(values.first())
}
pub async fn get_owner_address(&self, name: &str) -> AptosResult<Option<AccountAddress>> {
let name = AnsName::parse(name)?;
let values = self
.view_router("get_owner_addr", domain_subdomain_args(&name)?)
.await?;
option_address(values.first())
}
pub async fn get_primary_name(&self, address: AccountAddress) -> AptosResult<Option<String>> {
let args = vec![bcs_address(address)?];
let values = self.view_router("get_primary_name", args).await?;
if values.len() < 2 {
return Err(AptosError::Internal(format!(
"ANS get_primary_name returned {} value(s); expected 2 (subdomain, domain)",
values.len()
)));
}
let subdomain = option_string(&values[0])?;
let domain = option_string(&values[1])?;
Ok(domain.map(|domain| match subdomain {
Some(sub) => format!("{sub}.{domain}"),
None => domain,
}))
}
pub async fn get_expiration(&self, name: &str) -> AptosResult<Option<u64>> {
let name = AnsName::parse(name)?;
let args = domain_subdomain_args(&name)?;
match self.view_router("get_expiration", args).await {
Ok(values) => {
let raw = values.first().ok_or_else(|| {
AptosError::Internal("ANS get_expiration returned no value".into())
})?;
let secs = parse_u64(raw).ok_or_else(|| {
AptosError::Internal(format!(
"ANS get_expiration returned a non-u64 value: {raw}"
))
})?;
Ok(Some(secs))
}
Err(AptosError::Api {
status_code: 400,
vm_error_code: Some(_),
..
}) => Ok(None),
Err(e) => Err(e),
}
}
pub async fn lookup(&self, name: &str) -> AptosResult<AccountAddress> {
self.get_target_address(name).await?.ok_or_else(|| {
AptosError::NotFound(format!("ANS name '{name}' does not resolve to an address"))
})
}
pub async fn reverse_lookup(&self, address: AccountAddress) -> AptosResult<Option<String>> {
self.get_primary_name(address).await
}
pub fn register_domain_payload(
&self,
name: &str,
registration_duration_secs: u64,
target_address: Option<AccountAddress>,
to_address: Option<AccountAddress>,
) -> AptosResult<EntryFunction> {
let name = AnsName::parse(name)?;
if name.is_subdomain() {
return Err(invalid_name(
&name.full_name(),
"register_domain_payload expects a top-level domain, not a subdomain",
));
}
let args = vec![
bcs_string(name.domain())?,
bcs_u64(registration_duration_secs)?,
bcs_option_address(target_address)?,
bcs_option_address(to_address)?,
];
self.router_entry("register_domain", args)
}
pub fn set_primary_name_payload(&self, name: &str) -> AptosResult<EntryFunction> {
let name = AnsName::parse(name)?;
self.router_entry("set_primary_name", domain_subdomain_args(&name)?)
}
pub fn clear_primary_name_payload(&self) -> AptosResult<EntryFunction> {
self.router_entry("clear_primary_name", vec![])
}
pub fn set_target_address_payload(
&self,
name: &str,
address: AccountAddress,
) -> AptosResult<EntryFunction> {
let name = AnsName::parse(name)?;
let mut args = domain_subdomain_args(&name)?;
args.push(bcs_address(address)?);
self.router_entry("set_target_addr", args)
}
pub fn clear_target_address_payload(&self, name: &str) -> AptosResult<EntryFunction> {
let name = AnsName::parse(name)?;
self.router_entry("clear_target_addr", domain_subdomain_args(&name)?)
}
async fn view_router(
&self,
function: &str,
args: Vec<Vec<u8>>,
) -> AptosResult<Vec<serde_json::Value>> {
let router = self.router_address()?;
let function_id = format!("{router}::router::{function}");
let response = self
.fullnode
.view_bcs_args(&function_id, vec![], args)
.await?;
Ok(response.into_inner())
}
fn router_entry(&self, function: &str, args: Vec<Vec<u8>>) -> AptosResult<EntryFunction> {
let router = self.router_address()?;
let function_id = format!("{router}::router::{function}");
EntryFunction::from_function_id(&function_id, vec![], args)
}
}
fn bcs_string(value: &str) -> AptosResult<Vec<u8>> {
aptos_bcs::to_bytes(&value.to_string()).map_err(AptosError::bcs)
}
fn bcs_option_string(value: Option<&str>) -> AptosResult<Vec<u8>> {
aptos_bcs::to_bytes(&value.map(ToString::to_string)).map_err(AptosError::bcs)
}
fn bcs_address(value: AccountAddress) -> AptosResult<Vec<u8>> {
aptos_bcs::to_bytes(&value).map_err(AptosError::bcs)
}
fn bcs_option_address(value: Option<AccountAddress>) -> AptosResult<Vec<u8>> {
aptos_bcs::to_bytes(&value).map_err(AptosError::bcs)
}
fn bcs_u64(value: u64) -> AptosResult<Vec<u8>> {
aptos_bcs::to_bytes(&value).map_err(AptosError::bcs)
}
fn unwrap_option(value: &serde_json::Value) -> AptosResult<Option<&serde_json::Value>> {
if let Some(vec) = value.get("vec").and_then(serde_json::Value::as_array) {
return Ok(vec.first());
}
if let Some(arr) = value.as_array() {
return Ok(arr.first());
}
Err(AptosError::Internal(format!(
"ANS view returned a value that is not a Move Option: {value}"
)))
}
fn option_address(value: Option<&serde_json::Value>) -> AptosResult<Option<AccountAddress>> {
let value = value.ok_or_else(|| {
AptosError::Internal(
"ANS view returned no value where an Option<address> was expected".into(),
)
})?;
let Some(inner) = unwrap_option(value)? else {
return Ok(None);
};
let s = inner.as_str().ok_or_else(|| {
AptosError::Internal(format!("ANS view returned a non-string address: {inner}"))
})?;
let address = AccountAddress::from_hex(s).map_err(|e| {
AptosError::Internal(format!("ANS view returned a malformed address '{s}': {e}"))
})?;
Ok(Some(address))
}
fn option_string(value: &serde_json::Value) -> AptosResult<Option<String>> {
let Some(inner) = unwrap_option(value)? else {
return Ok(None);
};
let s = inner.as_str().ok_or_else(|| {
AptosError::Internal(format!("ANS view returned a non-string value: {inner}"))
})?;
Ok(if s.is_empty() {
None
} else {
Some(s.to_string())
})
}
fn parse_u64(value: &serde_json::Value) -> Option<u64> {
if let Some(n) = value.as_u64() {
return Some(n);
}
value.as_str().and_then(|s| s.parse().ok())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::AptosConfig;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn ans_for(server: &MockServer) -> AnsClient {
let url = format!("{}/v1", server.uri());
let config = AptosConfig::custom(&url).unwrap().without_retry();
let fullnode = FullnodeClient::new(config).unwrap();
AnsClient::with_router_address(fullnode, AccountAddress::from_hex("0x1").unwrap())
}
#[test]
fn parse_plain_domain() {
let name = AnsName::parse("alice.apt").unwrap();
assert_eq!(name.domain(), "alice");
assert_eq!(name.subdomain(), None);
assert!(!name.is_subdomain());
assert_eq!(name.full_name(), "alice");
assert_eq!(name.to_string(), "alice.apt");
}
#[test]
fn parse_without_suffix() {
let name = AnsName::parse("alice").unwrap();
assert_eq!(name.domain(), "alice");
assert_eq!(name.subdomain(), None);
}
#[test]
fn parse_subdomain() {
let name = AnsName::parse("wallet.alice.apt").unwrap();
assert_eq!(name.domain(), "alice");
assert_eq!(name.subdomain(), Some("wallet"));
assert!(name.is_subdomain());
assert_eq!(name.full_name(), "wallet.alice");
}
#[test]
fn parse_rejects_invalid_names() {
assert!(AnsName::parse("ab").is_err());
assert!(AnsName::parse("-abc").is_err());
assert!(AnsName::parse("abc-").is_err());
assert!(AnsName::parse("Alice").is_err());
assert!(AnsName::parse("a.b.c").is_err());
assert!(AnsName::parse("ali_ce").is_err());
}
#[test]
fn segment_validation_boundaries() {
assert!(is_valid_segment("abc"));
assert!(is_valid_segment("a-c"));
assert!(is_valid_segment("a1c"));
assert!(is_valid_segment(&"a".repeat(63)));
assert!(!is_valid_segment(&"a".repeat(64)));
assert!(!is_valid_segment("ab"));
assert!(!is_valid_segment("-bc"));
assert!(!is_valid_segment("ab-"));
}
#[test]
fn router_address_unsupported_network_errors() {
let fullnode = FullnodeClient::new(AptosConfig::devnet()).unwrap();
let ans = AnsClient::new(fullnode);
assert!(matches!(ans.router_address(), Err(AptosError::Config(_))));
}
#[test]
fn router_address_known_networks() {
let mainnet = AnsClient::new(FullnodeClient::new(AptosConfig::mainnet()).unwrap());
assert_eq!(
mainnet.router_address().unwrap(),
AccountAddress::from_hex(MAINNET_ROUTER).unwrap()
);
let testnet = AnsClient::new(FullnodeClient::new(AptosConfig::testnet()).unwrap());
assert_eq!(
testnet.router_address().unwrap(),
AccountAddress::from_hex(TESTNET_ROUTER).unwrap()
);
}
#[tokio::test]
async fn get_target_address_resolves() {
let server = MockServer::start().await;
let addr = "0x0000000000000000000000000000000000000000000000000000000000000abc";
Mock::given(method("POST"))
.and(path("/v1/view"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!([{"vec": [addr]}])),
)
.expect(1)
.mount(&server)
.await;
let ans = ans_for(&server);
let resolved = ans.get_target_address("alice.apt").await.unwrap();
assert_eq!(resolved, Some(AccountAddress::from_hex(addr).unwrap()));
}
#[tokio::test]
async fn get_target_address_none_when_unregistered() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/view"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!([{"vec": []}])),
)
.expect(1)
.mount(&server)
.await;
let ans = ans_for(&server);
let resolved = ans.get_target_address("alice.apt").await.unwrap();
assert_eq!(resolved, None);
}
#[tokio::test]
async fn get_primary_name_combines_subdomain_and_domain() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/view"))
.respond_with(ResponseTemplate::new(200).set_body_json(
serde_json::json!([{"vec": ["wallet"]}, {"vec": ["alice"]}]),
))
.expect(1)
.mount(&server)
.await;
let ans = ans_for(&server);
let name = ans.get_primary_name(AccountAddress::ONE).await.unwrap();
assert_eq!(name, Some("wallet.alice".to_string()));
}
#[tokio::test]
async fn get_primary_name_domain_only() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/view"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(serde_json::json!([{"vec": []}, {"vec": ["alice"]}])),
)
.expect(1)
.mount(&server)
.await;
let ans = ans_for(&server);
let name = ans.get_primary_name(AccountAddress::ONE).await.unwrap();
assert_eq!(name, Some("alice".to_string()));
}
#[tokio::test]
async fn get_primary_name_none() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/view"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(serde_json::json!([{"vec": []}, {"vec": []}])),
)
.expect(1)
.mount(&server)
.await;
let ans = ans_for(&server);
let name = ans.get_primary_name(AccountAddress::ONE).await.unwrap();
assert_eq!(name, None);
}
#[tokio::test]
async fn get_expiration_parses_seconds() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/view"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!(["1700000000"])),
)
.expect(1)
.mount(&server)
.await;
let ans = ans_for(&server);
let expiration = ans.get_expiration("alice.apt").await.unwrap();
assert_eq!(expiration, Some(1_700_000_000));
}
#[tokio::test]
async fn get_expiration_none_on_move_abort() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/view"))
.respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
"message": "Move abort",
"error_code": "invalid_input",
"vm_error_code": 196609
})))
.expect(1)
.mount(&server)
.await;
let ans = ans_for(&server);
let expiration = ans.get_expiration("alice.apt").await.unwrap();
assert_eq!(expiration, None);
}
#[tokio::test]
async fn lookup_errors_when_unresolved() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/view"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!([{"vec": []}])),
)
.mount(&server)
.await;
let ans = ans_for(&server);
let err = ans.lookup("alice.apt").await.unwrap_err();
assert!(matches!(err, AptosError::NotFound(_)));
}
#[test]
fn set_primary_name_payload_encodes_arguments() {
let router = AccountAddress::from_hex("0x1").unwrap();
let fullnode = FullnodeClient::new(AptosConfig::mainnet()).unwrap();
let ans = AnsClient::with_router_address(fullnode, router);
let payload = ans.set_primary_name_payload("alice.apt").unwrap();
assert_eq!(payload.module.address, router);
assert_eq!(payload.module.name.as_str(), "router");
assert_eq!(payload.function, "set_primary_name");
assert_eq!(payload.args.len(), 2);
assert_eq!(
payload.args[0],
aptos_bcs::to_bytes(&"alice".to_string()).unwrap()
);
assert_eq!(
payload.args[1],
aptos_bcs::to_bytes(&Option::<String>::None).unwrap()
);
}
#[test]
fn set_target_address_payload_encodes_arguments() {
let router = AccountAddress::from_hex("0x1").unwrap();
let target = AccountAddress::from_hex("0xabc").unwrap();
let fullnode = FullnodeClient::new(AptosConfig::mainnet()).unwrap();
let ans = AnsClient::with_router_address(fullnode, router);
let payload = ans
.set_target_address_payload("wallet.alice.apt", target)
.unwrap();
assert_eq!(payload.function, "set_target_addr");
assert_eq!(payload.args.len(), 3);
assert_eq!(
payload.args[0],
aptos_bcs::to_bytes(&"alice".to_string()).unwrap()
);
assert_eq!(
payload.args[1],
aptos_bcs::to_bytes(&Some("wallet".to_string())).unwrap()
);
assert_eq!(payload.args[2], aptos_bcs::to_bytes(&target).unwrap());
}
#[test]
fn register_domain_payload_rejects_subdomain() {
let fullnode = FullnodeClient::new(AptosConfig::mainnet()).unwrap();
let ans =
AnsClient::with_router_address(fullnode, AccountAddress::from_hex("0x1").unwrap());
let result = ans.register_domain_payload("wallet.alice.apt", 31_536_000, None, None);
assert!(matches!(result, Err(AptosError::Other(_))));
}
#[test]
fn register_domain_payload_encodes_arguments() {
let router = AccountAddress::from_hex("0x1").unwrap();
let target = AccountAddress::from_hex("0xabc").unwrap();
let fullnode = FullnodeClient::new(AptosConfig::mainnet()).unwrap();
let ans = AnsClient::with_router_address(fullnode, router);
let payload = ans
.register_domain_payload("alice.apt", 31_536_000, Some(target), None)
.unwrap();
assert_eq!(payload.function, "register_domain");
assert_eq!(payload.args.len(), 4);
assert_eq!(
payload.args[0],
aptos_bcs::to_bytes(&"alice".to_string()).unwrap()
);
assert_eq!(
payload.args[1],
aptos_bcs::to_bytes(&31_536_000u64).unwrap()
);
assert_eq!(payload.args[2], aptos_bcs::to_bytes(&Some(target)).unwrap());
assert_eq!(
payload.args[3],
aptos_bcs::to_bytes(&Option::<AccountAddress>::None).unwrap()
);
}
#[test]
fn clear_primary_name_payload_has_no_args() {
let fullnode = FullnodeClient::new(AptosConfig::mainnet()).unwrap();
let ans =
AnsClient::with_router_address(fullnode, AccountAddress::from_hex("0x1").unwrap());
let payload = ans.clear_primary_name_payload().unwrap();
assert_eq!(payload.function, "clear_primary_name");
assert!(payload.args.is_empty());
}
#[tokio::test]
async fn get_owner_address_resolves() {
let server = MockServer::start().await;
let addr = "0x0000000000000000000000000000000000000000000000000000000000000abc";
Mock::given(method("POST"))
.and(path("/v1/view"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!([{"vec": [addr]}])),
)
.expect(1)
.mount(&server)
.await;
let ans = ans_for(&server);
let owner = ans.get_owner_address("alice.apt").await.unwrap();
assert_eq!(owner, Some(AccountAddress::from_hex(addr).unwrap()));
}
#[tokio::test]
async fn lookup_resolves_to_target_address() {
let server = MockServer::start().await;
let addr = "0x0000000000000000000000000000000000000000000000000000000000000abc";
Mock::given(method("POST"))
.and(path("/v1/view"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!([{"vec": [addr]}])),
)
.mount(&server)
.await;
let ans = ans_for(&server);
let resolved = ans.lookup("alice.apt").await.unwrap();
assert_eq!(resolved, AccountAddress::from_hex(addr).unwrap());
}
#[tokio::test]
async fn reverse_lookup_returns_primary_name() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/view"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(serde_json::json!([{"vec": []}, {"vec": ["alice"]}])),
)
.mount(&server)
.await;
let ans = ans_for(&server);
let name = ans.reverse_lookup(AccountAddress::ONE).await.unwrap();
assert_eq!(name, Some("alice".to_string()));
}
#[test]
fn clear_target_address_payload_encodes_arguments() {
let router = AccountAddress::from_hex("0x1").unwrap();
let fullnode = FullnodeClient::new(AptosConfig::mainnet()).unwrap();
let ans = AnsClient::with_router_address(fullnode, router);
let payload = ans.clear_target_address_payload("alice.apt").unwrap();
assert_eq!(payload.function, "clear_target_addr");
assert_eq!(payload.args.len(), 2);
assert_eq!(
payload.args[0],
aptos_bcs::to_bytes(&"alice".to_string()).unwrap()
);
assert_eq!(
payload.args[1],
aptos_bcs::to_bytes(&Option::<String>::None).unwrap()
);
}
#[tokio::test]
async fn get_expiration_propagates_server_error() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/view"))
.respond_with(ResponseTemplate::new(500).set_body_json(serde_json::json!({
"message": "internal error"
})))
.mount(&server)
.await;
let ans = ans_for(&server);
let result = ans.get_expiration("alice.apt").await;
assert!(matches!(
result,
Err(AptosError::Api {
status_code: 500,
..
})
));
}
#[tokio::test]
async fn get_target_address_errors_on_malformed_address() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/view"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(serde_json::json!([{"vec": ["not-an-address"]}])),
)
.mount(&server)
.await;
let ans = ans_for(&server);
let err = ans.get_target_address("alice.apt").await.unwrap_err();
assert!(matches!(err, AptosError::Internal(_)));
}
#[tokio::test]
async fn get_expiration_errors_on_malformed_success() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/view"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
.mount(&server)
.await;
let ans = ans_for(&server);
let err = ans.get_expiration("alice.apt").await.unwrap_err();
assert!(matches!(err, AptosError::Internal(_)));
}
#[tokio::test]
async fn get_target_address_none_on_move_option_none() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/view"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!([{"vec": []}])),
)
.mount(&server)
.await;
let ans = ans_for(&server);
assert_eq!(ans.get_target_address("alice.apt").await.unwrap(), None);
}
#[tokio::test]
async fn get_target_address_errors_on_non_option_value() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/view"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["0x1"])))
.mount(&server)
.await;
let ans = ans_for(&server);
let err = ans.get_target_address("alice.apt").await.unwrap_err();
assert!(matches!(err, AptosError::Internal(_)));
}
#[tokio::test]
async fn get_primary_name_errors_on_short_response() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/view"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!([{"vec": ["alice"]}])),
)
.mount(&server)
.await;
let ans = ans_for(&server);
let err = ans.get_primary_name(AccountAddress::ONE).await.unwrap_err();
assert!(matches!(err, AptosError::Internal(_)));
}
#[tokio::test]
async fn get_primary_name_errors_on_non_string_value() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/view"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(serde_json::json!([{"vec": []}, {"vec": [42]}])),
)
.mount(&server)
.await;
let ans = ans_for(&server);
let err = ans.get_primary_name(AccountAddress::ONE).await.unwrap_err();
assert!(matches!(err, AptosError::Internal(_)));
}
}