/* eslint-disable @typescript-eslint/naming-convention */ import { gql } from 'graphql-request' import type { ClientWithPns } from '../../contracts/consts.js' import { // FilterKeyRequiredError, // InvalidFilterKeyError, InvalidOrderByError, } from '../../errors/subgraph.js' import { EMPTY_ADDRESS, GRACE_PERIOD_SECONDS } from '../../utils/consts.js' import { createSubgraphClient } from './client.js' import { getExpiryDateOrderFilter, type DomainFilter, getCreatedAtOrderFilter, } from './filters.js' import { domainDetailsFragment, registrationDetailsFragment, wrappedDomainDetailsFragment, type SubgraphDomain, } from './fragments.js' import { makeNameObject, type Name } from './utils.js' // const supportedOwnerFilters = [ // 'owner', // 'registrant', // 'wrappedOwner', // 'resolvedAddress', // ] as const type GetGracePeriodNamesOrderBy = | 'expiryDate' | 'name' | 'labelName' | 'createdAt' type GetGracePeriodNamesRelation = { /** Names with registrant as address (default: true) */ registrant?: boolean /** Names with owner as address (default: true) */ owner?: boolean /** Names with wrapped owner as address (default: true) */ wrappedOwner?: boolean /** Names with matching resolving address (default: true) */ resolvedAddress?: boolean } type GetGracePeriodNamesFilter = GetGracePeriodNamesRelation & { /** Search string filter for name */ searchString?: string /** Search string filter type (default: `labelName`) */ searchType?: 'labelName' | 'name' /** Allows reverse record nodes to be included (default: false) */ allowReverseRecord?: boolean /** Allows deleted names to be included (default: false) */ allowDeleted?: boolean /** Allows expires/expired names or both to be included (default: all) */ status?: 'expired' | 'expires' | 'all' } export type GetGracePeriodNamesParameters = { /** Names to get, in relation to address */ filter?: GetGracePeriodNamesFilter /** Parameter to order names by (default: name) */ orderBy?: GetGracePeriodNamesOrderBy /** Direction to order names in (default: asc) */ orderDirection?: 'asc' | 'desc' /** Previous page of names, used for pagination */ previousPage?: NameWithRelation[] /** Page size (default: 100) */ pageSize?: number } export type NameWithRelation = Name & { relation: GetGracePeriodNamesRelation } export type GetGracePeriodNamesReturnType = NameWithRelation[] type SubgraphResult = { domains: SubgraphDomain[] } const getOrderByFilter = ({ orderBy, orderDirection, previousPage, }: Required< Pick< GetGracePeriodNamesParameters, 'orderBy' | 'orderDirection' | 'previousPage' > >): DomainFilter => { const lastDomain = previousPage[previousPage.length - 1] const operator = orderDirection === 'asc' ? 'gt' : 'lt' switch (orderBy) { case 'expiryDate': { return getExpiryDateOrderFilter({ orderDirection, lastDomain, }) } case 'name': { return { [`name_${operator}`]: lastDomain.name ?? '', } } case 'labelName': { return { [`labelName_${operator}`]: lastDomain.labelName ?? '', } } case 'createdAt': { return getCreatedAtOrderFilter({ lastDomain, orderDirection }) } default: throw new InvalidOrderByError({ orderBy: orderBy || '', supportedOrderBys: ['expiryDate', 'name', 'labelName', 'createdAt'], }) } } /** * Gets the names for an address from the subgraph. * @param client - {@link ClientWithPns} * @param parameters - {@link GetGracePeriodNamesParameters} * @returns Name array. {@link GetGracePeriodNamesReturnType} * * @example * import { createPublicClient, http } from 'viem' * import { mainnet } from 'viem/chains' * import { addPnsContracts } from '@pnsdomains/pnsjs' * import { getGracePeriodNames } from '@pnsdomains/pnsjs/subgraph' * * const client = createPublicClient({ * chain: addPnsContracts(mainnet), * transport: http(), * }) * const result = await getGracePeriodNames(client) */ const getGracePeriodNames = async ( client: ClientWithPns, { filter: _filter, orderBy = 'name', orderDirection = 'asc', pageSize = 100, previousPage, }: GetGracePeriodNamesParameters, ): Promise => { const filter = { owner: true, registrant: true, resolvedAddress: true, wrappedOwner: true, allowDeleted: false, allowReverseRecord: false, searchType: 'labelName', ..._filter, } as const const subgraphClient = createSubgraphClient({ client }) const { allowDeleted, allowReverseRecord, searchString, searchType, // ...filters } = filter // const ownerWhereFilters: DomainFilter[] = Object.entries(filters).reduce( // (prev, [key, value]) => { // if (value) { // if (!supportedOwnerFilters.includes(key as any)) // throw new InvalidFilterKeyError({ // filterKey: key, // supportedFilterKeys: supportedOwnerFilters, // }) // return prev // } // return prev // }, // [] as DomainFilter[], // ) // const hasFilterApplied = ownerWhereFilters.length > 0 // if (!hasFilterApplied) // throw new FilterKeyRequiredError({ // supportedFilterKeys: supportedOwnerFilters, // details: 'At least one ownership filter must be enabled', // }) // const ownerWhereFilter: DomainFilter = // ownerWhereFilters.length > 1 // ? { or: ownerWhereFilters } // : ownerWhereFilters[0] const whereFilters: DomainFilter[] = [] if (previousPage?.length) { whereFilters.push( getOrderByFilter({ orderBy, orderDirection, previousPage, }), ) } if (!allowReverseRecord) { // Exclude domains with parent addr.reverse // namehash of addr.reverse = 0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2 whereFilters.push({ parent_not: '0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2', }) } if (filter.status === 'expires') { whereFilters.push({ expiryDate_lte: `${Math.floor(Date.now() / 1000) + GRACE_PERIOD_SECONDS}`, expiryDate_gte: `${Math.floor(Date.now() / 1000)}` }) } else if (filter.status === 'expired') { whereFilters.push({ expiryDate_lte: `${Math.floor(Date.now() / 1000)}` }) } else { whereFilters.push({ expiryDate_lte: `${Math.floor(Date.now() / 1000) + GRACE_PERIOD_SECONDS}`, }) } if (!allowDeleted) { // exclude "deleted" domains // when owner/resolver/registrant = null whereFilters.push({ or: [ { owner_not: EMPTY_ADDRESS, }, { resolver_not: null, }, { and: [ { registrant_not: EMPTY_ADDRESS, }, { registrant_not: null, }, ], }, ], }) } if (searchString) { whereFilters.push({ [`${searchType}_contains`]: searchString, }) } const whereFilter: DomainFilter = whereFilters.length > 1 ? { and: whereFilters } : whereFilters[0] const query = gql` query getGracePeriodNames( $orderBy: Domain_orderBy $orderDirection: OrderDirection $first: Int $whereFilter: Domain_filter ) { domains( orderBy: $orderBy orderDirection: $orderDirection first: $first where: $whereFilter ) { ...DomainDetails registration { ...RegistrationDetails } wrappedDomain { ...WrappedDomainDetails } } } ${domainDetailsFragment} ${registrationDetailsFragment} ${wrappedDomainDetailsFragment} ` const result = await subgraphClient.request< SubgraphResult, { orderBy: string orderDirection: string first: number whereFilter: DomainFilter } >(query, { orderBy, orderDirection, first: pageSize, whereFilter, }) if (!result) return [] const names = result.domains.map((domain) => { const relation: GetGracePeriodNamesRelation = {} if (domain.owner) { relation.owner = false } if (domain.registrant) { relation.registrant = false } if (domain.wrappedOwner) { relation.wrappedOwner = false } if (domain.resolvedAddress) { relation.resolvedAddress = false } return { ...makeNameObject(domain), relation, } }) return names } export default getGracePeriodNames