From 0f43e3e30ed90952e72c49afbe2d97e454f2f738 Mon Sep 17 00:00:00 2001 From: tate Date: Wed, 8 Feb 2023 12:16:27 +1100 Subject: [PATCH 1/4] fetch fuses in getsubnames --- .../ensjs/src/functions/getSubnames.test.ts | 14 ++++++++ packages/ensjs/src/functions/getSubnames.ts | 35 ++++++++++++++----- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/packages/ensjs/src/functions/getSubnames.test.ts b/packages/ensjs/src/functions/getSubnames.test.ts index 6de809d7..b530d753 100644 --- a/packages/ensjs/src/functions/getSubnames.test.ts +++ b/packages/ensjs/src/functions/getSubnames.test.ts @@ -131,6 +131,20 @@ describe('getSubnames', () => { ) }) + it('should return fuses for wrapped subnames', 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() + }) + describe('with pagination', () => { it('should get paginated subnames for a name ordered by createdAt in desc order', async () => { const result = await ensInstance.getSubnames({ diff --git a/packages/ensjs/src/functions/getSubnames.ts b/packages/ensjs/src/functions/getSubnames.ts index ffe24932..60b4f3b0 100644 --- a/packages/ensjs/src/functions/getSubnames.ts +++ b/packages/ensjs/src/functions/getSubnames.ts @@ -1,7 +1,9 @@ import { ENSArgs } from '..' import { truncateFormat } from '../utils/format' +import { AllCurrentFuses, decodeFuses } from '../utils/fuses' import { decryptName } from '../utils/labels' import { namehash } from '../utils/normalise' +import { Domain } from '../utils/subgraph-types' type Subname = { id: string @@ -13,6 +15,8 @@ type Subname = { owner: { id: string } + fuses?: AllCurrentFuses + expiryDate?: Date } type Params = { @@ -90,6 +94,10 @@ const largeQuery = async ( owner { id } + wrappedDomain { + fuses + expiryDate + } } } } @@ -106,15 +114,26 @@ 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 = { + ...subname, + labelName: subname.labelName || null, + labelhash: subname.labelhash || '', + name: decrypted, + truncatedName: truncateFormat(decrypted), + } + + if (wrappedDomain) { + obj.fuses = decodeFuses(wrappedDomain.fuses) + obj.expiryDate = new Date(parseInt(wrappedDomain.expiryDate) * 1000) + } + + return obj + }, + ) return { subnames: subdomains, From 93a554d3e4f4c9e4772e60dc31c6753f42c573a8 Mon Sep 17 00:00:00 2001 From: tate Date: Thu, 9 Feb 2023 11:39:24 +1100 Subject: [PATCH 2/4] return undefined for 0 expiry --- packages/ensjs/deploy/00_register_wrapped.ts | 24 +++++++++- packages/ensjs/src/functions/getNames.test.ts | 2 +- .../ensjs/src/functions/getSubnames.test.ts | 44 ++++++++++++++----- packages/ensjs/src/functions/getSubnames.ts | 5 ++- 4 files changed, 61 insertions(+), 14 deletions(-) diff --git a/packages/ensjs/deploy/00_register_wrapped.ts b/packages/ensjs/deploy/00_register_wrapped.ts index 4a86e67c..06c3c8d5 100644 --- a/packages/ensjs/deploy/00_register_wrapped.ts +++ b/packages/ensjs/deploy/00_register_wrapped.ts @@ -15,6 +15,8 @@ const names: { subnames?: { label: string namedOwner: string + fuses?: number + expiry?: number }[] duration?: number }[] = [ @@ -34,6 +36,22 @@ const names: { subnames: [{ label: 'test', namedOwner: 'owner2' }], duration: 2419200, }, + { + label: 'wrapped-with-expiring-subnames', + namedOwner: 'owner', + subnames: [ + { + label: 'test', + namedOwner: 'owner2', + expiry: Math.floor(Date.now() / 1000), + }, + { + label: 'test1', + namedOwner: 'owner2', + expiry: 0, + }, + ], + }, ] const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { @@ -103,6 +121,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 +132,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..f052038f 100644 --- a/packages/ensjs/src/functions/getNames.test.ts +++ b/packages/ensjs/src/functions/getNames.test.ts @@ -172,7 +172,7 @@ describe('getNames', () => { type: 'wrappedOwner', page: 0, }) - expect(pageOne).toHaveLength(2) + expect(pageOne).toHaveLength(4) }) describe('orderBy', () => { describe('registrations', () => { diff --git a/packages/ensjs/src/functions/getSubnames.test.ts b/packages/ensjs/src/functions/getSubnames.test.ts index b530d753..9d5993ea 100644 --- a/packages/ensjs/src/functions/getSubnames.test.ts +++ b/packages/ensjs/src/functions/getSubnames.test.ts @@ -131,18 +131,42 @@ describe('getSubnames', () => { ) }) - it('should return fuses for wrapped subnames', async () => { - const result = await ensInstance.getSubnames({ - name: 'wrapped-with-subnames.eth', - pageSize: 10, - orderBy: 'createdAt', - orderDirection: 'desc', + 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[0].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.length).toBe(1) - expect(result.subnameCount).toBe(1) - expect(result.subnames[0].fuses).toBeDefined() + expect(result).toBeTruthy() + expect(result.subnames[1].expiryDate).toBeInstanceOf(Date) + }) }) describe('with pagination', () => { diff --git a/packages/ensjs/src/functions/getSubnames.ts b/packages/ensjs/src/functions/getSubnames.ts index 60b4f3b0..7d2e89e4 100644 --- a/packages/ensjs/src/functions/getSubnames.ts +++ b/packages/ensjs/src/functions/getSubnames.ts @@ -128,7 +128,10 @@ const largeQuery = async ( if (wrappedDomain) { obj.fuses = decodeFuses(wrappedDomain.fuses) - obj.expiryDate = new Date(parseInt(wrappedDomain.expiryDate) * 1000) + obj.expiryDate = + wrappedDomain.expiryDate && wrappedDomain.expiryDate !== '0' + ? new Date(parseInt(wrappedDomain.expiryDate) * 1000) + : undefined } return obj From b908fea21d2726bb55ce929993ca2e1a95d8ad7e Mon Sep 17 00:00:00 2001 From: tate Date: Fri, 10 Feb 2023 14:10:46 +1100 Subject: [PATCH 3/4] add pccExpired checks --- packages/ensjs/deploy/00_register_wrapped.ts | 14 +++++ packages/ensjs/src/functions/getNames.test.ts | 5 +- packages/ensjs/src/functions/getNames.ts | 29 +++++++++-- .../ensjs/src/functions/getSubnames.test.ts | 19 +++++-- packages/ensjs/src/functions/getSubnames.ts | 51 ++++++++++++++----- packages/ensjs/src/utils/fuses.ts | 3 ++ 6 files changed, 101 insertions(+), 20 deletions(-) diff --git a/packages/ensjs/deploy/00_register_wrapped.ts b/packages/ensjs/deploy/00_register_wrapped.ts index 06c3c8d5..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: { @@ -39,6 +40,11 @@ const names: { { label: 'wrapped-with-expiring-subnames', namedOwner: 'owner', + fuses: encodeFuses({ + child: { + named: ['CANNOT_UNWRAP'], + }, + }), subnames: [ { label: 'test', @@ -50,6 +56,14 @@ const names: { namedOwner: 'owner2', expiry: 0, }, + { + label: 'recent-pcc', + namedOwner: 'owner2', + expiry: Math.floor(Date.now() / 1000), + fuses: encodeFuses({ + parent: { named: ['PARENT_CANNOT_CONTROL'] }, + }), + }, ], }, ] diff --git a/packages/ensjs/src/functions/getNames.test.ts b/packages/ensjs/src/functions/getNames.test.ts index f052038f..c140ceb4 100644 --- a/packages/ensjs/src/functions/getNames.test.ts +++ b/packages/ensjs/src/functions/getNames.test.ts @@ -166,12 +166,15 @@ 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, }) + // 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', () => { diff --git a/packages/ensjs/src/functions/getNames.ts b/packages/ensjs/src/functions/getNames.ts index 8c781502..73e9cc91 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' @@ -70,6 +70,23 @@ const mapDomain = ({ name, ...domain }: Domain) => { } const mapWrappedDomain = (wrappedDomain: WrappedDomain) => { + 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) as Omit< ReturnType, 'registration' @@ -89,8 +106,9 @@ const mapWrappedDomain = (wrappedDomain: WrappedDomain) => { ), } } + return { - expiryDate: new Date(parseInt(wrappedDomain.expiryDate) * 1000), + expiryDate, fuses: decodeFuses(wrappedDomain.fuses), ...domain, type: 'wrappedDomain', @@ -366,7 +384,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 +403,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 9d5993ea..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,7 +131,6 @@ describe('getSubnames', () => { 'truncatedName', ) }) - describe('wrapped subnames', () => { it('should return fuses', async () => { const result = await ensInstance.getSubnames({ @@ -154,7 +154,7 @@ describe('getSubnames', () => { }) expect(result).toBeTruthy() - expect(result.subnames[0].expiryDate).toBeUndefined() + expect(result.subnames[1].expiryDate).toBeUndefined() }) it('should return expiry', async () => { const result = await ensInstance.getSubnames({ @@ -165,7 +165,20 @@ describe('getSubnames', () => { }) expect(result).toBeTruthy() - expect(result.subnames[1].expiryDate).toBeInstanceOf(Date) + 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) }) }) diff --git a/packages/ensjs/src/functions/getSubnames.ts b/packages/ensjs/src/functions/getSubnames.ts index 7d2e89e4..dd628a9e 100644 --- a/packages/ensjs/src/functions/getSubnames.ts +++ b/packages/ensjs/src/functions/getSubnames.ts @@ -1,24 +1,36 @@ 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 { 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 - } - fuses?: AllCurrentFuses - expiryDate?: Date + 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 @@ -97,6 +109,9 @@ const largeQuery = async ( wrappedDomain { fuses expiryDate + owner { + id + } } } } @@ -118,20 +133,32 @@ const largeQuery = async ( ({ wrappedDomain, ...subname }: Domain) => { const decrypted = decryptName(subname.name!) - const obj: Subname = { + 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.fuses = decodeFuses(wrappedDomain.fuses) - obj.expiryDate = + 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 @@ -139,7 +166,7 @@ const largeQuery = async ( ) 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 From a21a9fd7b7828224da2929f2d4cab83c8975a55b Mon Sep 17 00:00:00 2001 From: tate Date: Fri, 10 Feb 2023 15:52:53 +1100 Subject: [PATCH 4/4] add registration data parsing in mapDomain --- packages/ensjs/src/functions/getNames.ts | 41 +++++++++++++----------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/packages/ensjs/src/functions/getNames.ts b/packages/ensjs/src/functions/getNames.ts index 73e9cc91..f173eb72 100644 --- a/packages/ensjs/src/functions/getNames.ts +++ b/packages/ensjs/src/functions/getNames.ts @@ -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), @@ -87,25 +100,7 @@ const mapWrappedDomain = (wrappedDomain: WrappedDomain) => { return null } - 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 domain = mapDomain(wrappedDomain.domain) return { expiryDate, @@ -174,6 +169,10 @@ const getNames = async ( domains(first: 1000) { ${domainQueryData} createdAt + registration { + registrationDate + expiryDate + } } wrappedDomains(first: 1000) { expiryDate @@ -205,6 +204,10 @@ const getNames = async ( domains(orderBy: $orderBy, orderDirection: $orderDirection) { ${domainQueryData} createdAt + registration { + registrationDate + expiryDate + } } } }