// Copyright © Endless // 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. */ import { EndlessConfig } from "../api/endlessConfig"; import { Account } from "../account"; import { AccountAddress, AccountAddressInput } from "../core"; import { InputGenerateTransactionOptions } from "../transactions/types"; import { MoveAddressType } from "../types"; import { Network } from "../utils/apiEndpoints"; import { view } from "./view"; import { generateTransaction } from "./transactionSubmission"; import { SimpleTransaction } from "../transactions/instances/simpleTransaction"; 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(" "); /** * * @param fragment A fragment of a name, either the domain or subdomain * @returns boolean indicating if the fragment is a valid fragment */ 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, can include or exclude the .apt suffix */ 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, }; } // export const LOCAL_ANS_ACCOUNT_PK = // process.env.ANS_TEST_ACCOUNT_PRIVATE_KEY ?? "0x37368b46ce665362562c6d1d4ec01a08c8644c488690df5a17e13ba163e20221"; export const LOCAL_ANS_ACCOUNT_PK = "0x37368b46ce665362562c6d1d4ec01a08c8644c488690df5a17e13ba163e20221"; // export const LOCAL_ANS_ACCOUNT_ADDRESS = // process.env.ANS_TEST_ACCOUNT_ADDRESS ?? "0x585fc9f0f0c54183b039ffc770ca282ebd87307916c215a3e692f2f8e4305e82"; export const LOCAL_ANS_ACCOUNT_ADDRESS = "0x585fc9f0f0c54183b039ffc770ca282ebd87307916c215a3e692f2f8e4305e82"; const NetworkToAnsContract: Record = { [Network.TESTNET]: "0x5f8fd2347449685cf41d4db97926ec3a096eaf381332be4f1318ad4d16a8497c", [Network.MAINNET]: "0x867ed1f6bf916171b1de3ee92849b8978b7d1b9e0a8cc982a3d19d535dfd9c0c", [Network.LOCAL]: LOCAL_ANS_ACCOUNT_ADDRESS, [Network.CUSTOM]: null, [Network.DEVNET]: null, }; export function getRouterAddress(endlessConfig: EndlessConfig): string { const address = NetworkToAnsContract[endlessConfig.network]; if (!address) throw new Error(`The ANS contract is not deployed to ${endlessConfig.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; }; export async function getOwnerAddress(args: { endlessConfig: EndlessConfig; name: string; }): Promise { const { endlessConfig, name } = args; const routerAddress = getRouterAddress(endlessConfig); const { domainName, subdomainName } = isValidANSName(name); const res = await view({ endlessConfig, payload: { function: `${routerAddress}::router::get_owner_addr`, functionArguments: [domainName, subdomainName], }, }); const owner = unwrapOption(res[0]); return owner ? AccountAddress.from(owner) : undefined; } export interface RegisterNameParameters { endlessConfig: EndlessConfig; sender: Account; name: string; expiration: | { policy: "domain"; years?: 1 } | { policy: "subdomain:follow-domain" } | { policy: "subdomain:independent"; expirationDate: number }; transferable?: boolean; toAddress?: AccountAddressInput; targetAddress?: AccountAddressInput; options?: InputGenerateTransactionOptions; } export async function registerName(args: RegisterNameParameters): Promise { const { endlessConfig, expiration, name, sender, targetAddress, toAddress, options, transferable } = args; const routerAddress = getRouterAddress(endlessConfig); 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 transaction = await generateTransaction({ endlessConfig, sender: sender.accountAddress.toString(), data: { function: `${routerAddress}::router::register_domain`, functionArguments: [domainName, registrationDuration, targetAddress, toAddress], }, options, }); return transaction; } // We are a subdomain if (!subdomainName) { throw new Error(`${expiration.policy} requires a subdomain to be provided.`); } const tldExpiration = await getExpiration({ endlessConfig, 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 transaction = await generateTransaction({ endlessConfig, sender: sender.accountAddress.toString(), data: { function: `${routerAddress}::router::register_subdomain`, functionArguments: [ domainName, subdomainName, Math.round(expirationDateInMillisecondsSinceEpoch / 1000), expiration.policy === "subdomain:follow-domain" ? 1 : 0, !!transferable, targetAddress, toAddress, ], }, options, }); return transaction; } export async function getExpiration(args: { endlessConfig: EndlessConfig; name: string }): Promise { const { endlessConfig, name } = args; const routerAddress = getRouterAddress(endlessConfig); const { domainName, subdomainName } = isValidANSName(name); try { const res = await view({ endlessConfig, payload: { function: `${routerAddress}::router::get_expiration`, functionArguments: [domainName, subdomainName], }, }); // Normalize expiration time from epoch seconds to epoch milliseconds return Number(res[0]) * 1000; } catch (e) { return undefined; } } export async function getPrimaryName(args: { endlessConfig: EndlessConfig; address: AccountAddressInput; }): Promise { const { endlessConfig, address } = args; const routerAddress = getRouterAddress(endlessConfig); const res = await view({ endlessConfig, 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("."); } export async function setPrimaryName(args: { endlessConfig: EndlessConfig; sender: Account; name?: string; options?: InputGenerateTransactionOptions; }): Promise { const { endlessConfig, sender, name, options } = args; const routerAddress = getRouterAddress(endlessConfig); if (!name) { const transaction = await generateTransaction({ endlessConfig, sender: sender.accountAddress.toString(), data: { function: `${routerAddress}::router::clear_primary_name`, functionArguments: [], }, options, }); return transaction; } const { domainName, subdomainName } = isValidANSName(name); const transaction = await generateTransaction({ endlessConfig, sender: sender.accountAddress.toString(), data: { function: `${routerAddress}::router::set_primary_name`, functionArguments: [domainName, subdomainName], }, options, }); return transaction; } export async function getTargetAddress(args: { endlessConfig: EndlessConfig; name: string; }): Promise { const { endlessConfig, name } = args; const routerAddress = getRouterAddress(endlessConfig); const { domainName, subdomainName } = isValidANSName(name); const res = await view({ endlessConfig, payload: { function: `${routerAddress}::router::get_target_addr`, functionArguments: [domainName, subdomainName], }, }); const target = unwrapOption(res[0]); return target ? AccountAddress.from(target) : undefined; } export async function setTargetAddress(args: { endlessConfig: EndlessConfig; sender: Account; name: string; address: AccountAddressInput; options?: InputGenerateTransactionOptions; }): Promise { const { endlessConfig, sender, name, address, options } = args; const routerAddress = getRouterAddress(endlessConfig); const { domainName, subdomainName } = isValidANSName(name); const transaction = await generateTransaction({ endlessConfig, sender: sender.accountAddress.toString(), data: { function: `${routerAddress}::router::set_target_addr`, functionArguments: [domainName, subdomainName, address], }, options, }); return transaction; } export async function renewDomain(args: { endlessConfig: EndlessConfig; sender: Account; name: string; years?: 1; options?: InputGenerateTransactionOptions; }): Promise { const { endlessConfig, sender, name, years = 1, options } = args; const routerAddress = getRouterAddress(endlessConfig); 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 transaction = await generateTransaction({ endlessConfig, sender: sender.accountAddress.toString(), data: { function: `${routerAddress}::router::renew_domain`, functionArguments: [domainName, renewalDuration], }, options, }); return transaction; }