Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add getDecryptedName + add decrypted name fetching in getProfile #103

Merged
merged 5 commits into from
Jan 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/ensjs/deploy/00_register_legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,20 @@ const names: {
{ label: 'addr', namedOwner: 'owner2' },
],
},
{
label: 'with-unknown-subnames',
namedOwner: 'owner',
namedAddr: 'owner',
subnames: [
{ label: 'aaa123', namedOwner: 'owner2' },
{ label: 'not-known', namedOwner: 'owner2' },
],
},
{
label: 'aaa123',
namedOwner: 'owner',
namedAddr: 'owner',
},
{
label: 'with-type-1-abi',
namedOwner: 'owner',
Expand Down
49 changes: 49 additions & 0 deletions packages/ensjs/src/functions/getDecryptedName.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ENS } from '../index'
import setup from '../tests/setup'

let ensInstance: ENS

beforeAll(async () => {
;({ ensInstance } = await setup())
})

describe('getDecryptedName', () => {
it('should decrypt a wrapped name with on-chain data', async () => {
const result = await ensInstance.getDecryptedName(
'[9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658].wrapped-with-subnames.eth',
)
expect(result).toBeDefined()
expect(result).toBe('test.wrapped-with-subnames.eth')
})
it('should decrypt a name via namehash lookup', async () => {
const result = await ensInstance.getDecryptedName(
'[f81b517a242b218999ec8eec0ea6e2ddbef2a367a14e93f4a32a39e260f686ad].eth',
)
expect(result).toBeDefined()
expect(result).toBe('test123.eth')
})
it('should decrypt a name via labelhash lookup', async () => {
const result = await ensInstance.getDecryptedName(
'[4d2920c35d976f8478bee89292ba85074d1bbea73f1571363b41a1629e1bac68].with-unknown-subnames.eth',
)
expect(result).toBeDefined()
expect(result).toBe('aaa123.with-unknown-subnames.eth')
})
it('should partially decrypt a name when allowIncomplete is true', async () => {
const result = await ensInstance.getDecryptedName(
'[7bffb6e3ebf801bbc438fea5c11d957ba49978bdc8d52b71cba974139d22edea].[6c14e1739568670447af1d5af8a571008f7a582068af18bcd7ac2dbc13bb37c1].eth',
true,
)
expect(result).toBeDefined()
expect(result).toBe(
'[7bffb6e3ebf801bbc438fea5c11d957ba49978bdc8d52b71cba974139d22edea].with-unknown-subnames.eth',
)
})
it('should not partially decrypt a name when allowIncomplete is false', async () => {
const result = await ensInstance.getDecryptedName(
'[7bffb6e3ebf801bbc438fea5c11d957ba49978bdc8d52b71cba974139d22edea].[6c14e1739568670447af1d5af8a571008f7a582068af18bcd7ac2dbc13bb37c1].eth',
false,
)
expect(result).toBeUndefined()
})
})
119 changes: 119 additions & 0 deletions packages/ensjs/src/functions/getDecryptedName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { hexStripZeros } from '@ethersproject/bytes'
import { ENSArgs } from '../index'
import { hexDecodeName } from '../utils/hexEncodedName'
import {
checkIsDecrypted,
decodeLabelhash,
getEncryptedLabelAmount,
isEncodedLabelhash,
} from '../utils/labels'
import { namehash } from '../utils/normalise'

const raw = async (
{ contracts }: ENSArgs<'contracts'>,
name: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
allowIncomplete?: boolean,
) => {
const nameWrapper = await contracts?.getNameWrapper()!

return {
to: nameWrapper.address,
data: nameWrapper.interface.encodeFunctionData('names', [namehash(name)]),
}
}

const generateNameQuery = (labels: string[]) => {
let query = ''

for (let i = 0; i < labels.length; i += 1) {
const label = labels[i]
if (isEncodedLabelhash(label)) {
query += `
label${i}: domains(where: { labelhash: "${decodeLabelhash(
label,
).toLowerCase()}", labelName_not: null }) {
labelName
}
`
}
}

return query
}

const decode = async (
{ contracts, gqlInstance }: ENSArgs<'contracts' | 'gqlInstance'>,
data: string,
name: string,
allowIncomplete: boolean = false,
) => {
if (data !== null) {
const nameWrapper = await contracts?.getNameWrapper()!
try {
const result = nameWrapper.interface.decodeFunctionResult('names', data)
if (hexStripZeros(result['0']) !== '0x') {
const decoded = hexDecodeName(result['0'])
if (decoded && decoded !== '.') return decoded
}
} catch (e: any) {
console.error('Error decoding name: ', e)
return
}
}

if (checkIsDecrypted(name)) return name
// if the name isn't wrapped, try to fetch the name from an existing Domain entity
// also try to fetch the label names from any Domain entities that have a corresponding labelhash
const labels = name.split('.')
const decryptedNameQuery = gqlInstance.gql`
query decodedName($id: String!) {
namehashLookup: domains(where: { id: $id }) {
name
}
${generateNameQuery(labels)}
}
`

const decryptedNameResult = await gqlInstance.client.request(
decryptedNameQuery,
{
id: namehash(name),
},
)

if (!decryptedNameResult) return

const namehashLookupResult = decryptedNameResult?.namehashLookup[0]?.name
let bestResult: string | undefined = namehashLookupResult
if (namehashLookupResult && checkIsDecrypted(namehashLookupResult)) {
return namehashLookupResult
}

const { namehashLookup: _, ...labelNames } = decryptedNameResult
if (Object.keys(labelNames).length !== 0) {
for (const [key, value] of Object.entries<[{ labelName?: string }]>(
labelNames,
)) {
if (value.length && value[0].labelName) {
labels[parseInt(key.replace('label', ''))] = value[0].labelName
}
}
const labelLookupResult = labels.join('.')
if (
!namehashLookupResult ||
getEncryptedLabelAmount(namehashLookupResult) >
getEncryptedLabelAmount(labelLookupResult)
)
bestResult = labelLookupResult
}

if (!bestResult || (!allowIncomplete && !checkIsDecrypted(bestResult))) return

return bestResult
}

export default {
raw,
decode,
}
2 changes: 1 addition & 1 deletion packages/ensjs/src/functions/getNames.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const letterItems = [
]

const domainLetterItems = [
'[',
...Array(3).fill('['),
'x',
'w',
't',
Expand Down
7 changes: 7 additions & 0 deletions packages/ensjs/src/functions/getProfile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ describe('getProfile', () => {
deploymentAddresses.LegacyPublicResolver,
)
})
it('should return the decoded name for a name with encoded labels', async () => {
const result = await ensInstance.getProfile(
'[9dd2c369a187b4e6b9c402f030e50743e619301ea62aa4c0737d4ef7e10a3d49].with-subnames.eth',
)
expect(result).toBeDefined()
expect(result?.decryptedName).toBe('xyz.with-subnames.eth')
})
it('should return undefined for an unregistered name', async () => {
const result = await ensInstance.getProfile('test123123123cool.eth')
expect(result).toBeUndefined()
Expand Down
15 changes: 12 additions & 3 deletions packages/ensjs/src/functions/getProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type ResolvedProfile = {
createdAt: string | null
address?: string
name?: string | null
decryptedName?: string | null
match?: boolean
message?: string
records?: {
Expand Down Expand Up @@ -360,6 +361,7 @@ const graphFetch = async (
const query = gqlInstance.gql`
query getRecords($id: String!) {
domain(id: $id) {
name
isMigrated
createdAt
resolver {
Expand All @@ -377,6 +379,7 @@ const graphFetch = async (
const customResolverQuery = gqlInstance.gql`
query getRecordsWithCustomResolver($id: String!, $resolverId: String!) {
domain(id: $id) {
name
isMigrated
createdAt
}
Expand Down Expand Up @@ -414,11 +417,12 @@ const graphFetch = async (

if (!domain) return

const { isMigrated, createdAt } = domain
const { isMigrated, createdAt, name: decryptedName } = domain

const returnedRecords: ProfileResponse = {}

if (!resolverResponse || !wantedRecords) return { isMigrated, createdAt }
if (!resolverResponse || !wantedRecords)
return { isMigrated, createdAt, decryptedName }

Object.keys(wantedRecords).forEach((key: string) => {
const data = wantedRecords[key as keyof ProfileOptions]
Expand All @@ -433,6 +437,7 @@ const graphFetch = async (

return {
...returnedRecords,
decryptedName,
isMigrated,
createdAt,
}
Expand Down Expand Up @@ -486,6 +491,7 @@ const getProfileFromName = async (
)
let isMigrated: boolean | null = null
let createdAt: string | null = null
let decryptedName: string | null = null
let result: Awaited<ReturnType<typeof getDataForName>> | null = null
if (!graphResult) {
if (!fallback) return
Expand All @@ -506,13 +512,16 @@ const getProfileFromName = async (
const {
isMigrated: _isMigrated,
createdAt: _createdAt,
decryptedName: _decryptedName,
...wantedRecords
}: {
isMigrated: boolean
createdAt: string
decryptedName: string
} & InternalProfileOptions = graphResult
isMigrated = _isMigrated
createdAt = _createdAt
decryptedName = _decryptedName
let recordsWithFallback = usingOptions
? wantedRecords
: (_options as InternalProfileOptions)
Expand Down Expand Up @@ -547,7 +556,7 @@ const getProfileFromName = async (
? "Records fetch didn't complete"
: "Name doesn't have a resolver",
}
return { ...result, isMigrated, createdAt, message: undefined }
return { ...result, isMigrated, createdAt, decryptedName, message: undefined }
}

const getProfileFromAddress = async (
Expand Down
2 changes: 2 additions & 0 deletions packages/ensjs/src/functions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type commitName from './commitName'
import type createSubname from './createSubname'
import type deleteSubname from './deleteSubname'
import type getAvailable from './getAvailable'
import type getDecryptedName from './getDecryptedName'
import type getDNSOwner from './getDNSOwner'
import type getExpiry from './getExpiry'
import type { getHistory } from './getHistory'
Expand Down Expand Up @@ -61,6 +62,7 @@ type Function = {
createSubname: typeof createSubname
deleteSubname: typeof deleteSubname
getAvailable: typeof getAvailable
getDecryptedName: typeof getDecryptedName
getDNSOwner: typeof getDNSOwner
getExpiry: typeof getExpiry
getHistory: typeof getHistory
Expand Down
4 changes: 4 additions & 0 deletions packages/ensjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,10 @@ export class ENS {
['contracts'],
)

public getDecryptedName = this.generateRawFunction<
FunctionTypes['getDecryptedName']
>('getDecryptedName', ['contracts', 'gqlInstance'])

public universalWrapper = this.generateRawFunction<
FunctionTypes['universalWrapper']
>('initialGetters', ['contracts'], 'universalWrapper')
Expand Down
2 changes: 2 additions & 0 deletions packages/ensjs/src/tests/func-imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import commitName from '../functions/commitName'
import createSubname from '../functions/createSubname'
import deleteSubname from '../functions/deleteSubname'
import getAvailable from '../functions/getAvailable'
import getDecryptedName from '../functions/getDecryptedName'
import getDNSOwner from '../functions/getDNSOwner'
import getExpiry from '../functions/getExpiry'
import { getHistory } from '../functions/getHistory'
Expand Down Expand Up @@ -60,6 +61,7 @@ export default {
createSubname,
deleteSubname,
getAvailable,
getDecryptedName,
getDNSOwner,
getExpiry,
getHistory,
Expand Down
3 changes: 3 additions & 0 deletions packages/ensjs/src/utils/hexEncodedName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ import packet from 'dns-packet'

export const hexEncodeName = (name: string) =>
`0x${packet.name.encode(name).toString('hex')}`

export const hexDecodeName = (hex: string): string =>
packet.name.decode(Buffer.from(hex.slice(2), 'hex')).toString()
24 changes: 13 additions & 11 deletions packages/ensjs/src/utils/labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,21 +64,19 @@ export function saveLabel(label: string) {

export function saveName(name: string) {
const nameArray = name.split('.')
nameArray.forEach((label: any) => {
saveLabel(label)
})
for (const label of nameArray) {
if (!isEncodedLabelhash(label)) {
saveLabel(label)
}
}
}

// eslint-disable-next-line consistent-return
export function checkLabel(hash: string): string | undefined {
export function checkLabel(hash: string): string {
const labels = getLabels()
if (isEncodedLabelhash(hash)) {
return labels[decodeLabelhash(hash)]
}

if (hash.startsWith('0x')) {
return labels[`${hash.slice(2)}`]
return labels[decodeLabelhash(hash)] || hash
}
return hash
}

export function encodeLabel(label: any) {
Expand All @@ -101,7 +99,7 @@ export function checkIsDecrypted(string: string | string[]) {
export function decryptName(name: string) {
return name
.split('.')
.map((label: any) => checkLabel(label) || label)
.map((label: any) => checkLabel(label))
.join('.')
}

Expand All @@ -119,3 +117,7 @@ export function checkLocalStorageSize() {
? `${3 + (allStrings.length * 16) / (8 * 1024)} KB`
: 'Empty (0 KB)'
}

const encodedLabelRegex = /\[[a-fA-F0-9]{64}\]/g
export const getEncryptedLabelAmount = (name: string) =>
name.match(encodedLabelRegex)?.length || 0