Skip to content

Commit

Permalink
Merge pull request #125 from ensdomains/fix/validation-functions
Browse files Browse the repository at this point in the history
fix: make validation functions more useful
  • Loading branch information
TateB authored Mar 31, 2023
2 parents a3f3216 + 963951a commit 8b49ee1
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 92 deletions.
10 changes: 3 additions & 7 deletions packages/ensjs/src/functions/getProfile.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { formatsByName } from '@ensdomains/address-encoder'
import { defaultAbiCoder } from '@ethersproject/abi'
import { isAddress } from '@ethersproject/address'
import { hexStripZeros, isBytesLike } from '@ethersproject/bytes'
import { ENSArgs } from '..'
import { decodeContenthash, DecodedContentHash } from '../utils/contentHash'
import { hexEncodeName } from '../utils/hexEncodedName'
import { namehash } from '../utils/normalise'
import { parseInputType } from '../utils/validation'

type InternalProfileOptions = {
contentHash?: boolean | string | DecodedContentHash
Expand Down Expand Up @@ -653,13 +653,9 @@ export default async function (
}
}

const inputType = parseInputType(nameOrAddress)
const inputIsAddress = isAddress(nameOrAddress)

if (inputType.type === 'unknown' || inputType.info === 'unsupported') {
throw new Error('Invalid input type')
}

if (inputType.type === 'address') {
if (inputIsAddress) {
return getProfileFromAddress(
{
contracts,
Expand Down
23 changes: 0 additions & 23 deletions packages/ensjs/src/functions/getRecords.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/ensjs/src/functions/initialGetters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export { default as getNames } from './getNames'
export { default as getOwner } from './getOwner'
export { default as getPrice } from './getPrice'
export { default as getProfile } from './getProfile'
export { default as getRecords } from './getRecords'
export * from './getSpecificRecord'
export { default as getSubnames } from './getSubnames'
export { default as supportsTLD } from './supportsTLD'
2 changes: 0 additions & 2 deletions packages/ensjs/src/functions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import type getNames from './getNames'
import type getOwner from './getOwner'
import type getPrice from './getPrice'
import type getProfile from './getProfile'
import type getRecords from './getRecords'
import type getResolver from './getResolver'
import type {
getABI,
Expand Down Expand Up @@ -71,7 +70,6 @@ type Function = {
getOwner: typeof getOwner
getPrice: typeof getPrice
getProfile: typeof getProfile
getRecords: typeof getRecords
getResolver: typeof getResolver
getAddr: typeof getAddr
getContentHash: typeof getContentHash
Expand Down
6 changes: 0 additions & 6 deletions packages/ensjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,12 +431,6 @@ export class ENS {
'getProfile',
)

public getRecords = this.generateFunction<FunctionTypes['getRecords']>(
'initialGetters',
['getProfile'],
'getRecords',
)

public getName = this.generateRawFunction<FunctionTypes['getName']>(
'initialGetters',
['contracts'],
Expand Down
2 changes: 0 additions & 2 deletions packages/ensjs/src/tests/func-imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import getNames from '../functions/getNames'
import getOwner from '../functions/getOwner'
import getPrice from '../functions/getPrice'
import getProfile from '../functions/getProfile'
import getRecords from '../functions/getRecords'
import getResolver from '../functions/getResolver'
import {
getABI,
Expand Down Expand Up @@ -70,7 +69,6 @@ export default {
getOwner,
getPrice,
getProfile,
getRecords,
getResolver,
getAddr,
getContentHash,
Expand Down
1 change: 1 addition & 0 deletions packages/ensjs/src/utils/consts.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export const EMPTY_ADDRESS = '0x0000000000000000000000000000000000000000'
export const MAX_INT_64 = BigInt('18446744073709551615')
export const MINIMUM_DOT_ETH_CHARS = 3
164 changes: 164 additions & 0 deletions packages/ensjs/src/utils/validation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { parseInput, validateName } from './validation'

declare namespace localStorage {
const getItem: jest.MockedFn<Storage['getItem']>
const setItem: jest.MockedFn<Storage['setItem']>
const removeItem: jest.MockedFn<Storage['removeItem']>
}

const labelsMock = {
'0x68371d7e884c168ae2022c82bd837d51837718a7f7dfb7aa3f753074a35e1d87':
'something',
'0x4f5b812789fc606be1b3b16908db13fc7a9adf7ca72641f84d75b47069d3d7f0': 'eth',
}

const labelsMockJSON = JSON.stringify(labelsMock)

describe('validateName()', () => {
beforeEach(() => {
localStorage.getItem.mockClear()
localStorage.setItem.mockClear()
localStorage.getItem.mockImplementation(() => labelsMockJSON)
})
it('should throw if the name has an empty label', () => {
expect(() => validateName('foo..bar')).toThrowError(
'Name cannot have empty labels',
)
expect(() => validateName('.foo.bar')).toThrowError(
'Name cannot have empty labels',
)
expect(() => validateName('foo.bar.')).toThrowError(
'Name cannot have empty labels',
)
})
it('should allow names with [root] as a label', () => {
expect(validateName('[root]')).toEqual('[root]')
})
it('should throw if the name has [root] as a label and is not the only label', () => {
expect(() => validateName('foo.[root].bar')).toThrowError(
'Root label must be the only label',
)
})

it('should get the decoded label hash from local storage', () => {
expect(
validateName(
'thing.[68371d7e884c168ae2022c82bd837d51837718a7f7dfb7aa3f753074a35e1d87].eth',
),
).toEqual('thing.something.eth')
expect(localStorage.getItem).toHaveBeenCalled()
})
it('should fallback to encoded label hash if the decoded label hash is not in local storage', () => {
expect(
validateName(
'something.[8c6c947d200f53fa1127b152f95b118f9e1d0eeb06fc678b6fc8a6d5c6fc5e17].eth',
),
).toEqual(
'something.[8c6c947d200f53fa1127b152f95b118f9e1d0eeb06fc678b6fc8a6d5c6fc5e17].eth',
)
expect(localStorage.getItem).toHaveBeenCalled()
expect(
validateName(
'[8c6c947d200f53fa1127b152f95b118f9e1d0eeb06fc678b6fc8a6d5c6fc5e17]',
),
).toEqual(
'[8c6c947d200f53fa1127b152f95b118f9e1d0eeb06fc678b6fc8a6d5c6fc5e17]',
)
})
it('should normalise the name', () => {
expect(validateName('aAaaA.eth')).toEqual('aaaaa.eth')
})
it('should save the normalised name to local storage', () => {
validateName('swAgCity.eth')
expect(localStorage.setItem).toHaveBeenCalledWith(
'ensjs:labels',
JSON.stringify({
...labelsMock,
'0xee3bbc5c1db3f1dbd5ec4cdfd627fb76aba6215d814354b6c0f8d74253adf1a8':
'swagcity',
}),
)
})
it('should return the normalised name', () => {
expect(validateName('swAgCity.eth')).toEqual('swagcity.eth')
})
})

describe('parseInput()', () => {
it('should parse the input', () => {
expect(parseInput('bar.eth')).toEqual({
type: 'name',
normalised: 'bar.eth',
isShort: false,
isValid: true,
is2LD: true,
isETH: true,
labelDataArray: expect.any(Array),
})
})
it('should return a normalised name', () => {
expect(parseInput('bAr.etH').normalised).toEqual('bar.eth')
})
it('should parse the input if it is invalid', () => {
expect(parseInput('bar..eth')).toEqual({
type: 'name',
normalised: undefined,
isShort: false,
isValid: false,
is2LD: false,
isETH: true,
labelDataArray: expect.any(Array),
})
})
it('should return type as label if input is a label', () => {
expect(parseInput('bar').type).toEqual('label')
})
describe('should return correct value', () => {
describe('isShort', () => {
it('should return true if input is label and less than 3 characters', () => {
expect(parseInput('ba').isShort).toEqual(true)
})
it('should return true if input is less than 3 char emoji', () => {
expect(parseInput('🇺🇸').isShort).toEqual(true)
})
it('should return false if input is 3 char emoji', () => {
expect(parseInput('🏳️‍🌈').isShort).toEqual(false)
})
it('should return false if input is label and 3 characters', () => {
expect(parseInput('bar').isShort).toEqual(false)
})
it('should return true if input is 2LD .eth name and label is less than 3 characters', () => {
expect(parseInput('ba.eth').isShort).toEqual(true)
})
it('should return false if input is 2LD .eth name and label is 3 characters', () => {
expect(parseInput('bar.eth').isShort).toEqual(false)
})
it('should return false if input is 2LD other name and label is less than 3 characters', () => {
expect(parseInput('ba.com').isShort).toEqual(false)
})
it('should return false if input is 3LD .eth name and label is less than 3 characters', () => {
expect(parseInput('ba.bar.eth').isShort).toEqual(false)
})
})
describe('is2LD', () => {
it('should return true if input is 2LD name', () => {
expect(parseInput('bar.eth').is2LD).toEqual(true)
})
it('should return false if input is 3LD name', () => {
expect(parseInput('bar.foo.eth').is2LD).toEqual(false)
})
it('should return false if input is label', () => {
expect(parseInput('bar').is2LD).toEqual(false)
})
})
describe('isETH', () => {
it('should return true if input is .eth name', () => {
expect(parseInput('bar.eth').isETH).toEqual(true)
})
it('should return false if input is other name', () => {
expect(parseInput('bar.com').isETH).toEqual(false)
})
})
})
})
102 changes: 51 additions & 51 deletions packages/ensjs/src/utils/validation.ts
Original file line number Diff line number Diff line change
@@ -1,73 +1,73 @@
import { isAddress } from '@ethersproject/address'
import { isEncodedLabelhash, saveName } from './labels'
import { normalise } from './normalise'
import { MINIMUM_DOT_ETH_CHARS } from './consts'
import { checkLabel, isEncodedLabelhash, saveName } from './labels'
import { Label, normalise, split } from './normalise'

export const validateName = (name: string) => {
const nameArray = name.split('.')
const hasEmptyLabels = nameArray.some((label) => label.length === 0)
if (hasEmptyLabels) throw new Error('Name cannot have empty labels')
const normalizedArray = nameArray.map((label) => {
const normalisedArray = nameArray.map((label) => {
if (label.length === 0) throw new Error('Name cannot have empty labels')
if (label === '[root]') {
if (name !== label) throw new Error('Root label must be the only label')
return label
}
return isEncodedLabelhash(label) ? label : normalise(label)
return isEncodedLabelhash(label)
? checkLabel(label) || label
: normalise(label)
})
const normalizedName = normalizedArray.join('.')
saveName(normalizedName)
return normalizedName
const normalisedName = normalisedArray.join('.')
saveName(normalisedName)
return normalisedName
}

export const validateTLD = (name: string) => {
const labels = name.split('.')
return validateName(labels[labels.length - 1])
export type ParsedInputResult = {
type: 'name' | 'label'
normalised: string | undefined
isValid: boolean
isShort: boolean
is2LD: boolean
isETH: boolean
labelDataArray: Label[]
}

type InputType = {
type: 'name' | 'label' | 'address' | 'unknown'
info?: 'short' | 'supported' | 'unsupported'
}

export const parseInputType = (input: string): InputType => {
const validTLD = validateTLD(input)
const regex = /[^.]+$/
export const parseInput = (input: string): ParsedInputResult => {
let nameReference = input
let isValid = false

try {
validateName(input)
} catch (e) {
return {
type: 'unknown',
}
}
nameReference = validateName(input)
isValid = true
} catch {}

if (input.indexOf('.') !== -1) {
const termArray = input.split('.')
const tld = input.match(regex) ? input.match(regex)![0] : ''
if (validTLD) {
if (tld === 'eth' && [...termArray[termArray.length - 2]].length < 3) {
// code-point length
return {
type: 'name',
info: 'short',
}
}
return {
type: 'name',
info: 'supported',
}
}
const normalisedName = isValid ? nameReference : undefined

const labels = nameReference.split('.')
const tld = labels[labels.length - 1]
const isETH = tld === 'eth'
const labelDataArray = split(nameReference)
const isShort =
(labelDataArray[0].output?.length || 0) < MINIMUM_DOT_ETH_CHARS

if (labels.length === 1) {
return {
type: 'name',
info: 'unsupported',
}
}
if (isAddress(input)) {
return {
type: 'address',
type: 'label',
normalised: normalisedName,
isShort,
isValid,
is2LD: false,
isETH,
labelDataArray,
}
}

const is2LD = labels.length === 2
return {
type: 'label',
type: 'name',
normalised: normalisedName,
isShort: isETH && is2LD ? isShort : false,
isValid,
is2LD,
isETH,
labelDataArray,
}
}

Expand Down

0 comments on commit 8b49ee1

Please sign in to comment.