// Copyright © Aptos Foundation // SPDX-License-Identifier: Apache-2.0 /** * This file contains the underlying implementations for exposed API surface in * the {@link api/name}. By moving the methods out into a separate file, * other namespaces and processes can access these methods without depending on the entire * name namespace and without having a dependency cycle error. * @group Implementation */ import { AptosConfig } from "../api/aptosConfig"; import { AccountAddress, AccountAddressInput } from "../core"; import { InputGenerateTransactionOptions, InputEntryFunctionData } from "../transactions/types"; import { AnsName, AnsTokenStandard, ExpirationStatus, MoveAddressType, OrderByArg, PaginationArgs, RawANSName, SubdomainExpirationPolicy, WhereArg, } from "../types"; import { GetNamesQuery } from "../types/generated/operations"; import { GetNames } from "../types/generated/queries"; import { CurrentAptosNamesBoolExp } from "../types/generated/types"; import { Network } from "../utils/apiEndpoints"; import { queryIndexer } from "./general"; import { view } from "./view"; import { generateTransaction } from "./transactionSubmission"; import { SimpleTransaction } from "../transactions/instances/simpleTransaction"; let GRACE_PERIOD_IN_SECONDS: number | undefined; const RENEWAL_MONTHS_WINDOW = 6; export const VALIDATION_RULES_DESCRIPTION = [ "A name must be between 3 and 63 characters long,", "and can only contain lowercase a-z, 0-9, and hyphens.", "A name may not start or end with a hyphen.", ].join(" "); /** * Validate if a given fragment is a valid ANS segment. * This function checks the length and character constraints of the fragment to ensure it meets the ANS standards. * * @param fragment - A fragment of a name, either the domain or subdomain. * @returns A boolean indicating if the fragment is a valid fragment. * @group Implementation */ export function isValidANSSegment(fragment: string): boolean { if (!fragment) return false; if (fragment.length < 3) return false; if (fragment.length > 63) return false; // only lowercase a-z and 0-9 are allowed, along with -. a domain may not start or end with a hyphen if (!/^[a-z\d][a-z\d-]{1,61}[a-z\d]$/.test(fragment)) return false; return true; } /** * Checks if an ANS name is valid or not. * * @param name - A string of the domain name, which can include or exclude the .apt suffix. * @group Implementation */ export function isValidANSName(name: string): { domainName: string; subdomainName?: string } { const [first, second, ...rest] = name.replace(/\.apt$/, "").split("."); if (rest.length > 0) { throw new Error(`${name} is invalid. A name can only have two parts, a domain and a subdomain separated by a "."`); } if (!isValidANSSegment(first)) { throw new Error(`${first} is not valid. ${VALIDATION_RULES_DESCRIPTION}`); } if (second && !isValidANSSegment(second)) { throw new Error(`${second} is not valid. ${VALIDATION_RULES_DESCRIPTION}`); } return { domainName: second || first, subdomainName: second ? first : undefined, }; } /** * Determines the status of an ANS name's expiration. * * @param name - An ANS name returned from one of the functions of the SDK. * @returns An ExpirationStatus indicating whether the name is Active, InGracePeriod, or Expired. * @group Implementation */ export function getANSExpirationStatus({ name, gracePeriod, }: { name: RawANSName; aptosConfig: AptosConfig; gracePeriod: number; }): ExpirationStatus { if (!name) return ExpirationStatus.Expired; const gracePeriodMs = gracePeriod * 1000; // Convert to milliseconds const now = Date.now(); const tldExpirationTime = new Date(name.domain_expiration_timestamp).getTime(); const nameExpirationTime = new Date(name.expiration_timestamp).getTime(); const isTLDExpired = tldExpirationTime < now; const isNameExpired = nameExpirationTime < now; const isInGracePeriod = isNameExpired && now - nameExpirationTime < gracePeriodMs; const isTLDInGracePeriod = isTLDExpired && now - tldExpirationTime < gracePeriodMs; // If we are a subdomain, if our parent is expired we are always expired if (name.subdomain && isTLDExpired && !isTLDInGracePeriod) { return ExpirationStatus.Expired; } // If we are a subdomain and our expiration policy is to follow the domain, we // follow the parent's status (since we know our parent is not fully expired by this point) if (name.subdomain && name.subdomain_expiration_policy === SubdomainExpirationPolicy.FollowsDomain) { if (isTLDInGracePeriod) return ExpirationStatus.InGracePeriod; return ExpirationStatus.Active; } // At this point, we are either a TLD or a subdomain with an independent // expiration policy, check the name's expiration status if (isInGracePeriod) return ExpirationStatus.InGracePeriod; if (isNameExpired) return ExpirationStatus.Expired; return ExpirationStatus.Active; } export const LOCAL_ANS_ACCOUNT_PK = process.env.ANS_TEST_ACCOUNT_PRIVATE_KEY ?? "ed25519-priv-0x37368b46ce665362562c6d1d4ec01a08c8644c488690df5a17e13ba163e20221"; export const LOCAL_ANS_ACCOUNT_ADDRESS = process.env.ANS_TEST_ACCOUNT_ADDRESS ?? "0x585fc9f0f0c54183b039ffc770ca282ebd87307916c215a3e692f2f8e4305e82"; const NetworkToAnsContract: Record = { [Network.TESTNET]: "0x5f8fd2347449685cf41d4db97926ec3a096eaf381332be4f1318ad4d16a8497c", [Network.MAINNET]: "0x867ed1f6bf916171b1de3ee92849b8978b7d1b9e0a8cc982a3d19d535dfd9c0c", [Network.LOCAL]: LOCAL_ANS_ACCOUNT_ADDRESS, [Network.CUSTOM]: null, [Network.DEVNET]: null, [Network.SHELBYNET]: null, [Network.NETNA]: null, }; /** * Retrieves the address of the ANS contract based on the specified Aptos network configuration. * * @param aptosConfig - The configuration object for the Aptos network. * @param aptosConfig.network - The network for which to retrieve the ANS contract address. * * @throws Throws an error if the ANS contract is not deployed to the specified network. * @group Implementation */ function getRouterAddress(aptosConfig: AptosConfig): string { const address = NetworkToAnsContract[aptosConfig.network]; if (!address) throw new Error(`The ANS contract is not deployed to ${aptosConfig.network}`); return address; } const unwrapOption = (option: any): T | undefined => { if (!!option && typeof option === "object" && "vec" in option && Array.isArray(option.vec)) { return option.vec[0]; } return undefined; }; /** * Retrieve the owner address of a specified domain or subdomain. * * @param args - The arguments for retrieving the owner address. * @param args.aptosConfig - The Aptos configuration object. * @param args.name - The name of the domain or subdomain to query. * @returns The account address of the owner, or undefined if not found. * @group Implementation */ export async function getOwnerAddress(args: { aptosConfig: AptosConfig; name: string; }): Promise { const { aptosConfig, name } = args; const routerAddress = getRouterAddress(aptosConfig); const { domainName, subdomainName } = isValidANSName(name); const res = await view({ aptosConfig, payload: { function: `${routerAddress}::router::get_owner_addr`, functionArguments: [domainName, subdomainName], }, }); const owner = unwrapOption(res[0]); return owner ? AccountAddress.from(owner) : undefined; } /** * Parameters for registering a name in the Aptos network. * * @param aptosConfig - Configuration settings for the Aptos network. * @param sender - The account initiating the name registration. * @param name - The name to be registered. * @param expiration - The expiration policy for the name registration. * @param transferable - Whether the name can be transferred to another owner. * @param toAddress - The address that will be set as the owner_address of the name. * @param targetAddress - The address that this name will resolve to. * @group Implementation */ export interface RegisterNameParameters { aptosConfig: AptosConfig; sender: AccountAddressInput; name: string; expiration: | { policy: "domain"; years?: 1 } | { policy: "subdomain:follow-domain" } | { policy: "subdomain:independent"; expirationDate: number }; transferable?: boolean; toAddress?: AccountAddressInput; targetAddress?: AccountAddressInput; options?: InputGenerateTransactionOptions; } /** * Registers a domain or subdomain with the specified parameters. This function ensures that the provided names and expiration * policies are valid before proceeding with the registration process. * * @param args - The parameters required for registering a name. * @param args.aptosConfig - The configuration settings for Aptos. * @param args.expiration - The expiration details for the registration. * @param args.name - The name to be registered, which can be a domain or subdomain. * @param args.sender - The account details of the sender initiating the registration. * @param args.targetAddress - The target address for the registration, which is the address the name will resolve to. * @param args.toAddress - The address that will be set as the owner_address in records, defaults to sender if not provided. * @param args.options - Additional options for the registration process. * @param args.transferable - Indicates whether the registered name is transferable to another account. * * @throws Error if the provided expiration policy is invalid for subdomains. * @throws Error if the domain does not exist. * @throws Error if the subdomain expiration time exceeds the domain expiration time. * * @returns A transaction object representing the registration process. * @group Implementation */ export async function registerName( args: RegisterNameParameters, ): Promise<{ transaction: SimpleTransaction; data: InputEntryFunctionData }> { const { aptosConfig, expiration, name, sender, targetAddress, toAddress, options, transferable } = args; const routerAddress = getRouterAddress(aptosConfig); const { domainName, subdomainName } = isValidANSName(name); const hasSubdomainPolicy = expiration.policy === "subdomain:independent" || expiration.policy === "subdomain:follow-domain"; if (subdomainName && !hasSubdomainPolicy) { throw new Error( "Subdomains must have an expiration policy of either 'subdomain:independent' or 'subdomain:follow-domain'", ); } if (hasSubdomainPolicy && !subdomainName) { throw new Error(`Policy is set to ${expiration.policy} but no subdomain was provided`); } if (expiration.policy === "domain") { const years = expiration.years ?? 1; if (years !== 1) { throw new Error("For now, names can only be registered for 1 year at a time"); } const secondsInYear = 31536000; const registrationDuration = years * secondsInYear; const data: InputEntryFunctionData = { function: `${routerAddress}::router::register_domain`, functionArguments: [domainName, registrationDuration, targetAddress, toAddress], }; const transaction = await generateTransaction({ aptosConfig, sender: AccountAddress.from(sender).toString(), data, options, }); return { transaction, data, }; } // We are a subdomain if (!subdomainName) { throw new Error(`${expiration.policy} requires a subdomain to be provided.`); } const tldExpiration = await getExpiration({ aptosConfig, name: domainName }); if (!tldExpiration) { throw new Error("The domain does not exist"); } const expirationDateInMillisecondsSinceEpoch = expiration.policy === "subdomain:independent" ? expiration.expirationDate : tldExpiration; if (expirationDateInMillisecondsSinceEpoch > tldExpiration) { throw new Error("The subdomain expiration time cannot be greater than the domain expiration time"); } const data: InputEntryFunctionData = { function: `${routerAddress}::router::register_subdomain`, functionArguments: [ domainName, subdomainName, Math.round(expirationDateInMillisecondsSinceEpoch / 1000), expiration.policy === "subdomain:follow-domain" ? 1 : 0, !!transferable, targetAddress, toAddress, ], }; const transaction = await generateTransaction({ aptosConfig, sender: AccountAddress.from(sender).toString(), data, options, }); return { transaction, data, }; } /** * Retrieves the expiration time of a specified domain or subdomain in epoch milliseconds. * * @param args - The arguments for the function. * @param args.aptosConfig - The configuration object for Aptos. * @param args.name - The name of the domain or subdomain to check. * @returns The expiration time in epoch milliseconds, or undefined if an error occurs. * @group Implementation */ export async function getExpiration(args: { aptosConfig: AptosConfig; name: string }): Promise { const { aptosConfig, name } = args; const routerAddress = getRouterAddress(aptosConfig); const { domainName, subdomainName } = isValidANSName(name); try { const res = await view({ aptosConfig, payload: { function: `${routerAddress}::router::get_expiration`, functionArguments: [domainName, subdomainName], }, }); // Normalize expiration time from epoch seconds to epoch milliseconds return Number(res[0]) * 1000; } catch { return undefined; } } /** * Retrieves the primary name associated with a given account address. * This function helps in obtaining the complete domain name by combining the subdomain and domain names. * * @param args - The arguments for retrieving the primary name. * @param args.aptosConfig - The Aptos configuration object. * @param args.address - The account address for which to retrieve the primary name. * @returns The primary name as a string, or undefined if no domain name exists. * @group Implementation */ export async function getPrimaryName(args: { aptosConfig: AptosConfig; address: AccountAddressInput; }): Promise { const { aptosConfig, address } = args; const routerAddress = getRouterAddress(aptosConfig); const res = await view({ aptosConfig, payload: { function: `${routerAddress}::router::get_primary_name`, functionArguments: [AccountAddress.from(address).toString()], }, }); const domainName = unwrapOption(res[1]); const subdomainName = unwrapOption(res[0]); if (!domainName) return undefined; return [subdomainName, domainName].filter(Boolean).join("."); } /** * Sets the primary name for the specified account, allowing for the association of a domain or subdomain with the account. * If no name is provided, it clears the existing primary name. * * @param args - The arguments for setting the primary name. * @param args.aptosConfig - The Aptos configuration object. * @param args.sender - The account that is sending the transaction. * @param args.name - The name to set as the primary name. If omitted, the function will clear the primary name. * @param args.options - Optional transaction generation options. * @returns A transaction object representing the operation. * @group Implementation */ export async function setPrimaryName(args: { aptosConfig: AptosConfig; sender: AccountAddressInput; name?: string; options?: InputGenerateTransactionOptions; }): Promise<{ transaction: SimpleTransaction; data: InputEntryFunctionData }> { const { aptosConfig, sender, name, options } = args; const routerAddress = getRouterAddress(aptosConfig); if (!name) { const data: InputEntryFunctionData = { function: `${routerAddress}::router::clear_primary_name`, functionArguments: [], }; const transaction = await generateTransaction({ aptosConfig, sender: AccountAddress.from(sender).toString(), data, options, }); return { transaction, data, }; } const { domainName, subdomainName } = isValidANSName(name); const data: InputEntryFunctionData = { function: `${routerAddress}::router::set_primary_name`, functionArguments: [domainName, subdomainName], }; const transaction = await generateTransaction({ aptosConfig, sender: AccountAddress.from(sender).toString(), data, options, }); return { transaction, data, }; } /** * Retrieves the target address associated with a given domain name and subdomain name. * The target address is different from the owner address - it's the address this name * resolves to, which may be different from who owns the name. * * @param args - The arguments for retrieving the target address. * @param args.aptosConfig - The Aptos configuration object. * @param args.name - The name of the domain, which may include a subdomain. * @returns The target address as an AccountAddress, or undefined if not found. * @group Implementation */ export async function getTargetAddress(args: { aptosConfig: AptosConfig; name: string; }): Promise { const { aptosConfig, name } = args; const routerAddress = getRouterAddress(aptosConfig); const { domainName, subdomainName } = isValidANSName(name); const res = await view({ aptosConfig, payload: { function: `${routerAddress}::router::get_target_addr`, functionArguments: [domainName, subdomainName], }, }); const target = unwrapOption(res[0]); return target ? AccountAddress.from(target) : undefined; } /** * Sets the target address for a specified domain and subdomain in the Aptos network. * This function helps to associate a given address with a domain name, allowing for easier access and management of resources. * * @param args - The arguments for setting the target address. * @param args.aptosConfig - The configuration settings for the Aptos network. * @param args.sender - The account that is sending the transaction. * @param args.name - The name of the domain or subdomain to be set. * @param args.address - The address to be associated with the domain or subdomain. * @param args.options - Optional parameters for generating the transaction. * * @returns A transaction object representing the set target address operation. * @group Implementation */ export async function setTargetAddress(args: { aptosConfig: AptosConfig; sender: AccountAddressInput; name: string; address: AccountAddressInput; options?: InputGenerateTransactionOptions; }): Promise<{ transaction: SimpleTransaction; data: InputEntryFunctionData }> { const { aptosConfig, sender, name, address, options } = args; const routerAddress = getRouterAddress(aptosConfig); const { domainName, subdomainName } = isValidANSName(name); const data: InputEntryFunctionData = { function: `${routerAddress}::router::set_target_addr`, functionArguments: [domainName, subdomainName, address], }; const transaction = await generateTransaction({ aptosConfig, sender: AccountAddress.from(sender).toString(), data, options, }); return { transaction, data, }; } /** * Clears the target address for a specified domain and subdomain in the Aptos network. * This function removes the target address association, effectively clearing where the name resolves to. * * @param args - The arguments for clearing the target address. * @param args.aptosConfig - The configuration settings for the Aptos network. * @param args.sender - The account that is sending the transaction. * @param args.name - The name of the domain or subdomain to clear the target address for. * @param args.options - Optional parameters for generating the transaction. * * @returns A transaction object representing the clear target address operation. * @group Implementation */ export async function clearTargetAddress(args: { aptosConfig: AptosConfig; sender: AccountAddressInput; name: string; options?: InputGenerateTransactionOptions; }): Promise<{ transaction: SimpleTransaction; data: InputEntryFunctionData }> { const { aptosConfig, sender, name, options } = args; const routerAddress = getRouterAddress(aptosConfig); const { domainName, subdomainName } = isValidANSName(name); const data: InputEntryFunctionData = { function: `${routerAddress}::router::clear_target_addr`, functionArguments: [domainName, subdomainName ?? null], }; const transaction = await generateTransaction({ aptosConfig, sender: AccountAddress.from(sender).toString(), data, options, }); return { transaction, data, }; } /** * Retrieves the active Aptos name associated with the specified domain and subdomain. * * @param args - The parameters for the function. * @param args.aptosConfig - The configuration object for Aptos. * @param args.name - The name to look up, which includes the domain and optional subdomain. * @returns The active Aptos name if it exists; otherwise, returns undefined. * @group Implementation */ export async function getName(args: { aptosConfig: AptosConfig; name: string }): Promise { const { aptosConfig, name } = args; const gracePeriod = await getANSGracePeriod({ aptosConfig }); const { domainName, subdomainName = "" } = isValidANSName(name); const where: CurrentAptosNamesBoolExp = { domain: { _eq: domainName }, subdomain: { _eq: subdomainName }, }; const data = await queryIndexer({ aptosConfig, query: { query: GetNames, variables: { where_condition: where, limit: 1, }, }, originMethod: "getName", }); // Convert the expiration_timestamp from an ISO string to milliseconds since epoch const res = data.current_aptos_names[0]; return res ? sanitizeANSName({ aptosConfig, name: res, gracePeriod }) : undefined; } /** * Options for querying names, including pagination, ordering, and filtering criteria. * * @param options - Pagination and filtering options for the query. * @group Implementation */ interface QueryNamesOptions { options?: PaginationArgs & OrderByArg & WhereArg; } /** * Arguments for retrieving account names based on the specified account address. * * @param accountAddress - The address of the account for which names are to be retrieved. * @group Implementation */ export interface GetAccountNamesArgs extends QueryNamesOptions { accountAddress: AccountAddressInput; } /** * Retrieves all the current Aptos names owned by an account. This uses the * owner_address field, not the registered_address field. * * @param args - The arguments for retrieving account names. * @param args.aptosConfig - The configuration object for Aptos. * @param args.options - Optional parameters for querying account names. * @param args.options.limit - The maximum number of names to retrieve. * @param args.options.offset - The number of names to skip before starting to collect the result set. * @param args.options.orderBy - The field by which to order the results. * @param args.options.where - Additional conditions to filter the results. * @param args.accountAddress - The address of the account for which to retrieve names. * * @returns An array of sanitized Aptos names associated with the specified account address. * @group Implementation */ export async function getAccountNames( args: { aptosConfig: AptosConfig } & GetAccountNamesArgs, ): Promise<{ names: AnsName[]; total: number }> { const { aptosConfig, options, accountAddress } = args; const gracePeriod = await getANSGracePeriod({ aptosConfig }); const expirationDate = await getANSExpirationDate({ aptosConfig }); const data = await queryIndexer({ aptosConfig, originMethod: "getAccountNames", query: { query: GetNames, variables: { limit: options?.limit, offset: options?.offset, order_by: options?.orderBy, where_condition: { owner_address: { _eq: accountAddress.toString() }, expiration_timestamp: { _gte: expirationDate }, ...(args.options?.where ?? {}), }, }, }, }); const names = data.current_aptos_names.map((name) => sanitizeANSName({ aptosConfig, name, gracePeriod })); const total = data.current_aptos_names_aggregate.aggregate?.count ?? 0; return { names, total }; } /** * Arguments for retrieving the domains associated with a specific account. * * @param accountAddress - The address of the account for which to fetch domains. * @group Implementation */ export interface GetAccountDomainsArgs extends QueryNamesOptions { accountAddress: AccountAddressInput; } /** * Retrieves the list of top-level domains owned by a specified account, * using the owner_address field. * * @param args - The arguments for retrieving account domains. * @param args.aptosConfig - The Aptos configuration object. * @param args.options - Optional parameters for the query. * @param args.options.limit - The maximum number of results to return. * @param args.options.offset - The number of results to skip before starting to collect the result set. * @param args.options.orderBy - The field by which to order the results. * @param args.options.where - Additional conditions to filter the results. * @param args.options.where.owner_address - The address of the account whose domains are being queried. * @param args.options.where.expiration_timestamp - The minimum expiration timestamp for the domains. * @param args.options.where.subdomain - The specific subdomain to filter by. * * @returns An array of sanitized domain names owned by the specified account. * @group Implementation */ export async function getAccountDomains( args: { aptosConfig: AptosConfig } & GetAccountDomainsArgs, ): Promise<{ names: AnsName[]; total: number }> { const { aptosConfig, options, accountAddress } = args; const gracePeriod = await getANSGracePeriod({ aptosConfig }); const expirationDate = await getANSExpirationDate({ aptosConfig }); const data = await queryIndexer({ aptosConfig, originMethod: "getAccountDomains", query: { query: GetNames, variables: { limit: options?.limit, offset: options?.offset, order_by: options?.orderBy, where_condition: { owner_address: { _eq: accountAddress.toString() }, expiration_timestamp: { _gte: expirationDate }, subdomain: { _eq: "" }, ...(args.options?.where ?? {}), }, }, }, }); const names = data.current_aptos_names.map((name) => sanitizeANSName({ aptosConfig, name, gracePeriod })); const total = data.current_aptos_names_aggregate.aggregate?.count ?? 0; return { names, total }; } /** * Arguments for retrieving subdomains associated with a specific account. * * @param accountAddress - The address of the account for which to fetch subdomains. * @group Implementation */ export interface GetAccountSubdomainsArgs extends QueryNamesOptions { accountAddress: AccountAddressInput; } /** * Retrieves a list of subdomains owned by a specified account address, determined * by the owner_address field. * * @param args - The arguments for retrieving account subdomains. * @param args.aptosConfig - The configuration object for Aptos. * @param args.options - Optional parameters for the query. * @param args.options.limit - The maximum number of results to return. * @param args.options.offset - The number of results to skip before starting to collect the result set. * @param args.options.orderBy - The field by which to order the results. * @param args.options.where - Additional conditions to filter the results. * @param args.options.where.owner_address - The address of the account to filter by. * @param args.options.where.expiration_timestamp - The expiration timestamp to filter by. * @param args.options.where.subdomain - The subdomain condition to filter by. * @param args.accountAddress - The address of the account whose subdomains are being queried. * @group Implementation */ export async function getAccountSubdomains( args: { aptosConfig: AptosConfig } & GetAccountSubdomainsArgs, ): Promise<{ names: AnsName[]; total: number }> { const { aptosConfig, options, accountAddress } = args; const gracePeriod = await getANSGracePeriod({ aptosConfig }); const data = await queryIndexer({ aptosConfig, originMethod: "getAccountSubdomains", query: { query: GetNames, variables: { limit: options?.limit, offset: options?.offset, order_by: options?.orderBy, where_condition: { owner_address: { _eq: accountAddress.toString() }, subdomain: { _neq: "" }, ...(args.options?.where ?? {}), }, }, }, }); const names = data.current_aptos_names.map((name) => sanitizeANSName({ aptosConfig, name, gracePeriod })); const total = data.current_aptos_names_aggregate.aggregate?.count ?? 0; return { names, total }; } /** * Arguments for retrieving subdomains associated with a specific domain. * * @param domain - The domain for which to fetch subdomains. * @group Implementation */ export interface GetDomainSubdomainsArgs extends QueryNamesOptions { domain: string; } /** * Retrieve the active subdomains associated with a specified domain, * regardless of who is in possession of the domain. * * This function queries by domain name only, not by owner, so it will return * all subdomains of a domain even if they're owned by different accounts. * * @param args - The arguments for retrieving subdomains. * @param args.aptosConfig - The configuration settings for Aptos. * @param args.options - Optional parameters for the query. * @param args.options.limit - The maximum number of results to return. * @param args.options.offset - The number of results to skip before starting to collect the results. * @param args.options.orderBy - The field by which to order the results. * @param args.options.where - Additional conditions to filter the results. * @param args.domain - The domain for which to retrieve subdomains. * * @returns An array of active subdomain names. * @group Implementation */ export async function getDomainSubdomains( args: { aptosConfig: AptosConfig } & GetDomainSubdomainsArgs, ): Promise<{ names: AnsName[]; total: number }> { const { aptosConfig, options, domain } = args; const gracePeriod = await getANSGracePeriod({ aptosConfig }); const data = await queryIndexer({ aptosConfig, originMethod: "getDomainSubdomains", query: { query: GetNames, variables: { limit: options?.limit, offset: options?.offset, order_by: options?.orderBy, where_condition: { domain: { _eq: domain }, subdomain: { _neq: "" }, ...(args.options?.where ?? {}), }, }, }, }); const names = data.current_aptos_names.map((name) => sanitizeANSName({ aptosConfig, name, gracePeriod })); const total = data.current_aptos_names_aggregate.aggregate?.count ?? 0; return { names, total }; } /** * This function returns the expiration date in which a name is fully expired as * defined by the contract. The grace period allows for names to be past * expiration for a certain amount of time before they are released to the * public. The names will not function as normal, but the owner can renew * without others taking ownership of the name. At the time of writing, the * contract specified 30 days. * * @param args - The arguments for the function. * @param args.aptosConfig - An AptosConfig object containing the configuration settings. * @returns The expiration date in ISO 8601 format. * @group Implementation */ async function getANSExpirationDate(args: { aptosConfig: AptosConfig }): Promise { const { aptosConfig } = args; const gracePeriodInSeconds = await getANSGracePeriod({ aptosConfig }); const gracePeriodInDays = gracePeriodInSeconds / 60 / 60 / 24; const now = () => new Date(); return new Date(now().setDate(now().getDate() - gracePeriodInDays)).toISOString(); } /** * This function returns the grace period in seconds as defined by the contract. * A name that is past expiration but within the grace period is considered in * grace period and can't be claimed by others. * * @param args - The arguments for the function. * @param args.aptosConfig - An AptosConfig object containing the configuration settings. * @returns The grace period in seconds. * @group Implementation */ export async function getANSGracePeriod(args: { aptosConfig: AptosConfig }): Promise { if (GRACE_PERIOD_IN_SECONDS) { return GRACE_PERIOD_IN_SECONDS; } const { aptosConfig } = args; const routerAddress = getRouterAddress(aptosConfig); const [gracePeriodInSeconds] = await view<[number]>({ aptosConfig, payload: { function: `${routerAddress}::config::reregistration_grace_sec`, functionArguments: [], }, }); GRACE_PERIOD_IN_SECONDS = gracePeriodInSeconds; return gracePeriodInSeconds; } /** * Renews a domain for a specified duration. This function allows you to extend the registration of a domain for one year. * * @param args - The parameters required to renew the domain. * @param args.aptosConfig - The configuration settings for Aptos. * @param args.sender - The account that is sending the renewal transaction. * @param args.name - The name of the domain to renew. * @param args.years - The number of years to renew the domain for. Currently, only 1 year renewals are supported. (optional, default is 1) * @param args.options - Additional options for generating the transaction. (optional) * @throws Error if the name contains a subdomain or if the years parameter is not equal to 1. * @group Implementation */ export async function renewDomain(args: { aptosConfig: AptosConfig; sender: AccountAddressInput; name: string; years?: 1; options?: InputGenerateTransactionOptions; }): Promise<{ transaction: SimpleTransaction; data: InputEntryFunctionData }> { const { aptosConfig, sender, name, years = 1, options } = args; const routerAddress = getRouterAddress(aptosConfig); const renewalDuration = years * 31536000; const { domainName, subdomainName } = isValidANSName(name); if (subdomainName) { throw new Error("Subdomains cannot be renewed"); } if (years !== 1) { throw new Error("Currently, only 1 year renewals are supported"); } const data: InputEntryFunctionData = { function: `${routerAddress}::router::renew_domain`, functionArguments: [domainName, renewalDuration], }; const transaction = await generateTransaction({ aptosConfig, sender: AccountAddress.from(sender).toString(), data, options, }); return { transaction, data, }; } /** * The indexer returns ISO strings for expiration, however the contract works in * epoch milliseconds. This function converts the ISO string to epoch * milliseconds. In the future, if other properties need sanitization, this can * be extended. * * @param name - The ANS name response to sanitize. * @param name.expiration_timestamp - The expiration timestamp in ISO string format. * @group Implementation */ function sanitizeANSName({ name, aptosConfig, gracePeriod, }: { aptosConfig: AptosConfig; name: RawANSName; gracePeriod: number; }): AnsName { const expiration_timestamp = `${name.expiration_timestamp}Z`; const domain_expiration_timestamp = `${name.domain_expiration_timestamp}Z`; const isSubdomain = !!name.subdomain; const expirationPolicy = name.subdomain_expiration_policy as SubdomainExpirationPolicy; const expiration = isSubdomain && expirationPolicy === SubdomainExpirationPolicy.FollowsDomain ? domain_expiration_timestamp : expiration_timestamp; const expiration_status = getANSExpirationStatus({ aptosConfig, name, gracePeriod }); let isInRenewablePeriod = false; if (expiration_status === ExpirationStatus.InGracePeriod) { isInRenewablePeriod = true; } else if (expiration_status === ExpirationStatus.Active) { // Check if the name is within the renewal window (6 months before expiration) const renewalWindowDate = new Date(); renewalWindowDate.setMonth(renewalWindowDate.getMonth() + RENEWAL_MONTHS_WINDOW); const expirationDate = new Date(expiration); isInRenewablePeriod = expirationDate < renewalWindowDate; } return { domain: name.domain!, subdomain: name.subdomain || undefined, expiration_timestamp, expiration_status, domain_expiration_timestamp: domain_expiration_timestamp!, expiration: new Date(expiration), token_standard: name.token_standard! as AnsTokenStandard, is_primary: name.is_primary!, subdomain_expiration_policy: name.subdomain_expiration_policy ?? SubdomainExpirationPolicy.FollowsDomain, owner_address: name.owner_address ?? "", registered_address: name.registered_address!, isInRenewablePeriod, }; }