diff --git a/packages/ensjs/deploy/00_register_wrapped.ts b/packages/ensjs/deploy/00_register_wrapped.ts index 4a86e67c..afcdffee 100644 --- a/packages/ensjs/deploy/00_register_wrapped.ts +++ b/packages/ensjs/deploy/00_register_wrapped.ts @@ -4,6 +4,7 @@ import { BigNumber } from '@ethersproject/bignumber' import { ethers } from 'hardhat' import { DeployFunction } from 'hardhat-deploy/types' import { HardhatRuntimeEnvironment } from 'hardhat/types' +import { encodeFuses } from '../src/utils/fuses' import { namehash } from '../src/utils/normalise' const names: { @@ -15,6 +16,8 @@ const names: { subnames?: { label: string namedOwner: string + fuses?: number + expiry?: number }[] duration?: number }[] = [ @@ -34,6 +37,35 @@ const names: { subnames: [{ label: 'test', namedOwner: 'owner2' }], duration: 2419200, }, + { + label: 'wrapped-with-expiring-subnames', + namedOwner: 'owner', + fuses: encodeFuses({ + child: { + named: ['CANNOT_UNWRAP'], + }, + }), + subnames: [ + { + label: 'test', + namedOwner: 'owner2', + expiry: Math.floor(Date.now() / 1000), + }, + { + label: 'test1', + namedOwner: 'owner2', + expiry: 0, + }, + { + label: 'recent-pcc', + namedOwner: 'owner2', + expiry: Math.floor(Date.now() / 1000), + fuses: encodeFuses({ + parent: { named: ['PARENT_CANNOT_CONTROL'] }, + }), + }, + ], + }, ] const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { @@ -103,6 +135,8 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { for (const { label: subnameLabel, namedOwner: namedSubnameOwner, + fuses: subnameFuses = 0, + expiry: subnameExpiry = BigNumber.from(2).pow(64).sub(1), } of subnames) { const subnameOwner = allNamedAccts[namedSubnameOwner] const _nameWrapper = nameWrapper.connect(await ethers.getSigner(owner)) @@ -112,8 +146,8 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { subnameOwner, resolver, '0', - '0', - BigNumber.from(2).pow(64).sub(1), + subnameFuses, + subnameExpiry, ) console.log(` - ${subnameLabel} (tx: ${setSubnameTx.hash})...`) await setSubnameTx.wait() diff --git a/packages/ensjs/src/functions/getNames.test.ts b/packages/ensjs/src/functions/getNames.test.ts index 2e9696ce..c140ceb4 100644 --- a/packages/ensjs/src/functions/getNames.test.ts +++ b/packages/ensjs/src/functions/getNames.test.ts @@ -166,13 +166,16 @@ describe('getNames', () => { }) expect(pageFive).toHaveLength(totalOwnedNames % 10) }) - it('should get wrapped domains for an address with pagination', async () => { + it('should get wrapped domains for an address with pagination, and filter out pcc expired names', async () => { const pageOne = await ensInstance.getNames({ address: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC', type: 'wrappedOwner', page: 0, }) - expect(pageOne).toHaveLength(2) + // length of page one should be all the names on 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC + // minus 1 for the PCC expired name. + // the result here implies that the PCC expired name is not returned + expect(pageOne).toHaveLength(4) }) describe('orderBy', () => { describe('registrations', () => { diff --git a/packages/ensjs/src/functions/getNames.ts b/packages/ensjs/src/functions/getNames.ts index 8c781502..f173eb72 100644 --- a/packages/ensjs/src/functions/getNames.ts +++ b/packages/ensjs/src/functions/getNames.ts @@ -1,6 +1,6 @@ import { ENSArgs } from '..' import { truncateFormat } from '../utils/format' -import { AllCurrentFuses, decodeFuses } from '../utils/fuses' +import { AllCurrentFuses, checkPCCBurned, decodeFuses } from '../utils/fuses' import { decryptName } from '../utils/labels' import { Domain, Registration, WrappedDomain } from '../utils/subgraph-types' @@ -60,8 +60,21 @@ type Params = BaseParams & const mapDomain = ({ name, ...domain }: Domain) => { const decrypted = name ? decryptName(name) : undefined + return { ...domain, + ...(domain.registration + ? { + registration: { + expiryDate: new Date( + parseInt(domain.registration.expiryDate) * 1000, + ), + registrationDate: new Date( + parseInt(domain.registration.registrationDate) * 1000, + ), + }, + } + : {}), name: decrypted, truncatedName: decrypted ? truncateFormat(decrypted) : undefined, createdAt: new Date(parseInt(domain.createdAt) * 1000), @@ -70,27 +83,27 @@ const mapDomain = ({ name, ...domain }: Domain) => { } const mapWrappedDomain = (wrappedDomain: WrappedDomain) => { - const domain = mapDomain(wrappedDomain.domain) as Omit< - ReturnType, - 'registration' - > & { - registration?: { - expiryDate: string | Date - registrationDate: string | Date - } - } - if (domain.registration) { - domain.registration = { - expiryDate: new Date( - parseInt(domain.registration.expiryDate as string) * 1000, - ), - registrationDate: new Date( - parseInt(domain.registration.registrationDate as string) * 1000, - ), - } + const expiryDate = + wrappedDomain.expiryDate && wrappedDomain.expiryDate !== '0' + ? new Date(parseInt(wrappedDomain.expiryDate) * 1000) + : undefined + if ( + expiryDate && + expiryDate < new Date() && + checkPCCBurned(wrappedDomain.fuses) + ) { + // PCC was burned previously and now the fuses are expired meaning that the + // owner is now 0x0 so we need to filter this out + // if a user's local time is out of sync with the blockchain, this could potentially + // be incorrect. the likelihood of that happening though is very low, and devs + // shouldn't be relying on this value for anything critical anyway. + return null } + + const domain = mapDomain(wrappedDomain.domain) + return { - expiryDate: new Date(parseInt(wrappedDomain.expiryDate) * 1000), + expiryDate, fuses: decodeFuses(wrappedDomain.fuses), ...domain, type: 'wrappedDomain', @@ -156,6 +169,10 @@ const getNames = async ( domains(first: 1000) { ${domainQueryData} createdAt + registration { + registrationDate + expiryDate + } } wrappedDomains(first: 1000) { expiryDate @@ -187,6 +204,10 @@ const getNames = async ( domains(orderBy: $orderBy, orderDirection: $orderDirection) { ${domainQueryData} createdAt + registration { + registrationDate + expiryDate + } } } } @@ -366,7 +387,8 @@ const getNames = async ( return [ ...(account?.domains.map(mapDomain) || []), ...(account?.registrations.map(mapRegistration) || []), - ...(account?.wrappedDomains.map(mapWrappedDomain) || []), + ...(account?.wrappedDomains.map(mapWrappedDomain).filter((d: any) => d) || + []), ].sort((a, b) => { if (orderDirection === 'desc') { if (orderBy === 'labelName') { @@ -384,7 +406,9 @@ const getNames = async ( return (account?.domains.map(mapDomain) || []) as Name[] } if (type === 'wrappedOwner') { - return (account?.wrappedDomains.map(mapWrappedDomain) || []) as Name[] + return (account?.wrappedDomains + .map(mapWrappedDomain) + .filter((d: any) => d) || []) as Name[] } return (account?.registrations.map(mapRegistration) || []) as Name[] } diff --git a/packages/ensjs/src/functions/getSubnames.test.ts b/packages/ensjs/src/functions/getSubnames.test.ts index 6de809d7..8731c8e3 100644 --- a/packages/ensjs/src/functions/getSubnames.test.ts +++ b/packages/ensjs/src/functions/getSubnames.test.ts @@ -1,5 +1,6 @@ import { ENS } from '..' import setup from '../tests/setup' +import { decodeFuses } from '../utils/fuses' let ensInstance: ENS @@ -130,6 +131,56 @@ describe('getSubnames', () => { 'truncatedName', ) }) + describe('wrapped subnames', () => { + it('should return fuses', async () => { + const result = await ensInstance.getSubnames({ + name: 'wrapped-with-subnames.eth', + pageSize: 10, + orderBy: 'createdAt', + orderDirection: 'desc', + }) + + expect(result).toBeTruthy() + expect(result.subnames.length).toBe(1) + expect(result.subnameCount).toBe(1) + expect(result.subnames[0].fuses).toBeDefined() + }) + it('should return expiry as undefined if 0', async () => { + const result = await ensInstance.getSubnames({ + name: 'wrapped-with-expiring-subnames.eth', + pageSize: 10, + orderBy: 'createdAt', + orderDirection: 'desc', + }) + + expect(result).toBeTruthy() + expect(result.subnames[1].expiryDate).toBeUndefined() + }) + it('should return expiry', async () => { + const result = await ensInstance.getSubnames({ + name: 'wrapped-with-expiring-subnames.eth', + pageSize: 10, + orderBy: 'createdAt', + orderDirection: 'desc', + }) + + expect(result).toBeTruthy() + expect(result.subnames[2].expiryDate).toBeInstanceOf(Date) + }) + it('should return owner as undefined, fuses as 0, and pccExpired as true if pcc expired', async () => { + const result = await ensInstance.getSubnames({ + name: 'wrapped-with-expiring-subnames.eth', + pageSize: 10, + orderBy: 'createdAt', + orderDirection: 'desc', + }) + + expect(result).toBeTruthy() + expect(result.subnames[0].owner).toBeUndefined() + expect(result.subnames[0].fuses).toStrictEqual(decodeFuses(0)) + expect(result.subnames[0].pccExpired).toBe(true) + }) + }) describe('with pagination', () => { it('should get paginated subnames for a name ordered by createdAt in desc order', async () => { diff --git a/packages/ensjs/src/functions/getSubnames.ts b/packages/ensjs/src/functions/getSubnames.ts index ffe24932..dd628a9e 100644 --- a/packages/ensjs/src/functions/getSubnames.ts +++ b/packages/ensjs/src/functions/getSubnames.ts @@ -1,20 +1,36 @@ import { ENSArgs } from '..' import { truncateFormat } from '../utils/format' +import { AllCurrentFuses, checkPCCBurned, decodeFuses } from '../utils/fuses' import { decryptName } from '../utils/labels' import { namehash } from '../utils/normalise' +import { Domain } from '../utils/subgraph-types' -type Subname = { +type BaseSubname = { id: string labelName: string | null truncatedName?: string labelhash: string isMigrated: boolean name: string - owner: { - id: string - } + owner: string | undefined +} + +type UnwrappedSubname = BaseSubname & { + fuses?: never + expiryDate?: never + pccExpired?: never + type: 'domain' } +type WrappedSubname = BaseSubname & { + fuses: AllCurrentFuses + expiryDate: Date + pccExpired: boolean + type: 'wrappedDomain' +} + +type Subname = WrappedSubname | UnwrappedSubname + type Params = { name: string page?: number @@ -90,6 +106,13 @@ const largeQuery = async ( owner { id } + wrappedDomain { + fuses + expiryDate + owner { + id + } + } } } } @@ -106,18 +129,44 @@ const largeQuery = async ( } const response = await client.request(finalQuery, queryVars) const domain = response?.domain - const subdomains = domain.subdomains.map((subname: any) => { - const decrypted = decryptName(subname.name) + const subdomains = domain.subdomains.map( + ({ wrappedDomain, ...subname }: Domain) => { + const decrypted = decryptName(subname.name!) - return { - ...subname, - name: decrypted, - truncatedName: truncateFormat(decrypted), - } - }) + const obj = { + ...subname, + labelName: subname.labelName || null, + labelhash: subname.labelhash || '', + name: decrypted, + truncatedName: truncateFormat(decrypted), + owner: subname.owner.id, + type: 'domain', + } as Subname + + if (wrappedDomain) { + obj.type = 'wrappedDomain' + const expiryDateAsDate = + wrappedDomain.expiryDate && wrappedDomain.expiryDate !== '0' + ? new Date(parseInt(wrappedDomain.expiryDate) * 1000) + : undefined + // if a user's local time is out of sync with the blockchain, this could potentially + // be incorrect. the likelihood of that happening though is very low, and devs + // shouldn't be relying on this value for anything critical anyway. + const hasExpired = expiryDateAsDate && expiryDateAsDate < new Date() + obj.expiryDate = expiryDateAsDate + obj.fuses = decodeFuses(hasExpired ? 0 : wrappedDomain.fuses) + obj.pccExpired = hasExpired + ? checkPCCBurned(wrappedDomain.fuses) + : false + obj.owner = obj.pccExpired ? undefined : wrappedDomain.owner.id + } + + return obj + }, + ) return { - subnames: subdomains, + subnames: subdomains as Subname[], subnameCount: domain.subdomainCount, } } diff --git a/packages/ensjs/src/utils/fuses.ts b/packages/ensjs/src/utils/fuses.ts index 0e727b49..3e8884c6 100644 --- a/packages/ensjs/src/utils/fuses.ts +++ b/packages/ensjs/src/utils/fuses.ts @@ -352,6 +352,9 @@ export const decodeFuses = (fuses: number) => { } } +export const checkPCCBurned = (fuses: number) => + (fuses & PARENT_CANNOT_CONTROL) === PARENT_CANNOT_CONTROL + export type AllCurrentFuses = ReturnType export default fullFuseEnum