{"version":3,"file":"lookup.cjs","sources":["../../src/lookup-entry.ts"],"sourcesContent":["import { AlgorandClient } from '@algorandfoundation/algokit-utils'\nimport { Address, decodeUint64 } from 'algosdk'\n\nimport { DefaultSender, NfdRegistryId } from './constants'\nimport { getAllBoxes } from './utils/internal/boxes'\nimport { isZeroBytes } from './utils/internal/bytes'\nimport { buildNfdRecord, type NfdView } from './utils/internal/nfd-record'\nimport {\n  decodeAppIdFromNameBox,\n  getAddressBoxName,\n  getNameBoxName,\n} from './utils/internal/registry-box'\nimport { isValidName } from './utils/nfd'\n\nimport type { Nfd, ResolveOptions, ReverseLookupOptions } from './types'\nimport type { Arc56Contract } from '@algorandfoundation/algokit-utils/types/app-arc56'\nimport type { AppClient } from '@algorandfoundation/algokit-utils/types/app-client'\n\nexport type { Nfd, ResolveOptions, ReverseLookupOptions } from './types'\nexport { NfdRegistryId } from './constants'\n\n/**\n * A minimal ARC-56 app spec. The generic `AppClient` requires a spec, but\n * lookups only use `getGlobalState`/`getBoxNames`/`getBoxValue`, none of which\n * touch the ABI — so an empty spec is sufficient and avoids bundling the large\n * generated contract specs.\n */\nconst MINIMAL_APP_SPEC: Arc56Contract = {\n  arcs: [4, 56],\n  name: 'NfdLookup',\n  structs: {},\n  methods: [],\n  state: {\n    schema: {\n      global: { ints: 0, bytes: 0 },\n      local: { ints: 0, bytes: 0 },\n    },\n    keys: { global: {}, local: {}, box: {} },\n    maps: { global: {}, local: {}, box: {} },\n  },\n  bareActions: { create: [], call: [] },\n}\n\n/** Configuration for {@link NfdResolver} */\nexport interface NfdResolverConfig {\n  /** An `AlgorandClient` to use for RPC calls. Defaults to MainNet. */\n  algorand?: AlgorandClient\n  /** The NFD registry app ID. Defaults to MainNet. */\n  registryId?: number | bigint\n}\n\nfunction isErrorWith404(error: unknown): boolean {\n  return error instanceof Error && error.message.includes('404')\n}\n\nfunction toResolveView(\n  view: ReverseLookupOptions['view'],\n): NfdView | undefined {\n  // The on-chain reader supports 'tiny' | 'brief' | 'full'. Map the API-only\n  // 'thumbnail' view onto 'tiny'.\n  if (view === 'thumbnail') return 'tiny'\n  return view\n}\n\n/**\n * Lightweight, lookup-only NFD resolver that reads directly from on-chain\n * state. Unlike the full `NfdClient`, it does not pull in the generated typed\n * contract clients or the NFD HTTP API client, resulting in a dramatically\n * smaller bundle. It supports forward resolution (name/app ID → record) and\n * on-chain reverse resolution (address → record).\n */\nexport class NfdResolver {\n  private readonly algorand: AlgorandClient\n  private readonly registryId: bigint\n  private readonly defaultSender: string\n\n  constructor(config: NfdResolverConfig = {}) {\n    this.algorand = config.algorand ?? AlgorandClient.mainNet()\n    this.registryId = BigInt(config.registryId ?? NfdRegistryId.MAINNET)\n    this.defaultSender =\n      this.registryId === BigInt(NfdRegistryId.TESTNET)\n        ? DefaultSender.TESTNET\n        : DefaultSender.MAINNET\n  }\n\n  /** Create a resolver configured for MainNet. */\n  static mainNet(): NfdResolver {\n    return new NfdResolver({\n      algorand: AlgorandClient.mainNet(),\n      registryId: NfdRegistryId.MAINNET,\n    })\n  }\n\n  /** Create a resolver configured for TestNet. */\n  static testNet(): NfdResolver {\n    return new NfdResolver({\n      algorand: AlgorandClient.testNet(),\n      registryId: NfdRegistryId.TESTNET,\n    })\n  }\n\n  private appClientFor(appId: bigint): AppClient {\n    return this.algorand.client.getAppClientById({\n      appId,\n      appSpec: MINIMAL_APP_SPEC,\n      defaultSender: this.defaultSender,\n    })\n  }\n\n  private get registryClient(): AppClient {\n    return this.appClientFor(this.registryId)\n  }\n\n  private async getAppIdFromName(name: string): Promise<bigint | null> {\n    const boxName = await getNameBoxName(name)\n    try {\n      const appIdBytes = await this.registryClient.getBoxValue(boxName)\n      if (!appIdBytes) return null\n      return decodeAppIdFromNameBox(appIdBytes)\n    } catch (error) {\n      if (isErrorWith404(error)) return null\n      throw error\n    }\n  }\n\n  private async parseAppId(\n    nameOrAppId: string | number | bigint,\n  ): Promise<bigint> {\n    if (typeof nameOrAppId !== 'string') {\n      return BigInt(nameOrAppId)\n    }\n\n    const parsedNumber = parseInt(nameOrAppId)\n    if (!isNaN(parsedNumber)) {\n      return BigInt(parsedNumber)\n    }\n\n    if (!isValidName(nameOrAppId)) {\n      throw new Error(\n        `Invalid NFD name: ${nameOrAppId}. Name must be in the format 'name.algo' or 'segment.name.algo'`,\n      )\n    }\n\n    const appId = await this.getAppIdFromName(nameOrAppId)\n    if (appId === null) {\n      throw new Error(`NFD not found: ${nameOrAppId}`)\n    }\n\n    return appId\n  }\n\n  /**\n   * Resolve an NFD by name or application ID by reading directly from the\n   * blockchain.\n   * @param nameOrAppId - The NFD name or application ID to resolve\n   * @param options - Optional parameters\n   * @returns The NFD record\n   * @throws If the NFD name is invalid or not found\n   */\n  async resolve(\n    nameOrAppId: string | number | bigint,\n    options: ResolveOptions = {},\n  ): Promise<Nfd> {\n    const nfdAppId = await this.parseAppId(nameOrAppId)\n    const instance = this.appClientFor(nfdAppId)\n\n    // Names and values arrive together, so this is one request per page rather\n    // than one per box\n    const [globalState, boxes] = await Promise.all([\n      instance.getGlobalState(),\n      getAllBoxes(this.algorand.client.algod, nfdAppId),\n    ])\n\n    return buildNfdRecord({\n      appId: nfdAppId,\n      appAddress: instance.appAddress.toString(),\n      globalState,\n      boxes,\n      view: options.view,\n    })\n  }\n\n  /**\n   * Read the registry's reverse-index box for an address and return the linked\n   * NFD application IDs (primary first). Only verified links are indexed\n   * on-chain, so `ReverseLookupOptions.allowUnverified` has no effect here.\n   */\n  private async getAddressAppIds(address: string | Address): Promise<bigint[]> {\n    const boxName = await getAddressBoxName(address)\n    let boxValue: Uint8Array\n    try {\n      boxValue = await this.registryClient.getBoxValue(boxName)\n    } catch (error) {\n      if (isErrorWith404(error)) return []\n      throw error\n    }\n    if (!boxValue || boxValue.length === 0) return []\n\n    const appIds: bigint[] = []\n    for (let i = 0; i + 8 <= boxValue.length; i += 8) {\n      const chunk = boxValue.slice(i, i + 8)\n      if (isZeroBytes(chunk)) continue\n      appIds.push(decodeUint64(chunk, 'bigint'))\n    }\n    return appIds\n  }\n\n  /**\n   * Reverse lookup: resolve an address to its primary NFD by reading the\n   * registry's on-chain reverse index.\n   * @param address - The address to resolve\n   * @param options - Optional parameters\n   * @returns The address's primary NFD, or `null` if none is linked\n   */\n  async resolveAddress(\n    address: string | Address,\n    options: ReverseLookupOptions = {},\n  ): Promise<Nfd | null> {\n    const appIds = await this.getAddressAppIds(address)\n    const primaryAppId = appIds[0]\n    if (primaryAppId === undefined) return null\n\n    return this.resolve(primaryAppId, { view: toResolveView(options.view) })\n  }\n\n  /**\n   * Reverse lookup for multiple addresses. Returns a record mapping each\n   * address (that has a linked NFD) to its primary NFD.\n   * @param addresses - The addresses to resolve\n   * @param options - Optional parameters\n   * @returns A record of address string → primary NFD\n   */\n  async resolveAddresses(\n    addresses: Array<string | Address>,\n    options: ReverseLookupOptions = {},\n  ): Promise<Record<string, Nfd>> {\n    const entries = await Promise.all(\n      addresses.map(async (address) => {\n        const addressStr =\n          typeof address === 'string' ? address : address.toString()\n        const nfd = await this.resolveAddress(address, options)\n        return [addressStr, nfd] as const\n      }),\n    )\n\n    const result: Record<string, Nfd> = {}\n    for (const [addressStr, nfd] of entries) {\n      if (nfd) result[addressStr] = nfd\n    }\n    return result\n  }\n}\n"],"names":["MINIMAL_APP_SPEC","isErrorWith404","error","toResolveView","view","NfdResolver","config","__publicField","AlgorandClient","NfdRegistryId","DefaultSender","appId","name","boxName","getNameBoxName","appIdBytes","decodeAppIdFromNameBox","nameOrAppId","parsedNumber","isValidName","options","nfdAppId","instance","globalState","boxes","getAllBoxes","buildNfdRecord","address","getAddressBoxName","boxValue","appIds","i","chunk","isZeroBytes","decodeUint64","primaryAppId","addresses","entries","addressStr","nfd","result"],"mappings":"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"}