Skip to content

Commit

Permalink
Merge pull request #39 from XYOracleNetwork/feature/xns-validators
Browse files Browse the repository at this point in the history
xns name validators and helper class
  • Loading branch information
jonesmac authored Sep 4, 2024
2 parents e6cd5b2 + a94e38e commit b523db0
Show file tree
Hide file tree
Showing 32 changed files with 808 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
"--run",
"--inspect-brk",
"--no-file-parallelism",
"packages/payload/packages/xns/plugins/record/src/diviner/lib/spec/parseEstimatesFromArray.spec.ts"
"packages/payloadset/packages/xns/plugins/record/src/validation/name/spec/Name.spec.ts"
],
"sourceMaps": true,
"resolveSourceMapLocations": [
Expand Down
2 changes: 2 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"decentralnetworkservices",
"deregistering",
"deregisters",
"doejohn",
"dotenv",
"emittery",
"etherchain",
Expand All @@ -47,6 +48,7 @@
"interoperably",
"IPFS",
"IXSCAN",
"johndoe",
"jsdom",
"jsonpatch",
"keccak",
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@
"coverage": "yarn jest --coverage --forceExit",
"deploy": "xy deploy",
"lint-pkg": "npmPkgJsonLint .",
"test": "jest --no-cache --forceExit",
"test-esm": "node $(yarn jest --no-cache --forceExit)"
"test": "vitest run"
},
"resolutions": {
"axios": "^1",
Expand Down
2 changes: 1 addition & 1 deletion packages/payload/packages/xns/plugins/record/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@xylabs/hex": "^4.0.9",
"@xyo-network/boundwitness-model": "^3.1.4",
"@xyo-network/boundwitness-validator": "^3.1.4",
"@xyo-network/diviner-hash-lease": "^3.1.4",
"@xyo-network/module-model": "^3.1.4",
"@xyo-network/payload-builder": "^3.1.4",
"@xyo-network/payload-model": "^3.1.4",
Expand All @@ -43,7 +44,6 @@
"@xylabs/tsconfig": "^4.0.7",
"@xyo-network/account": "^3.1.4",
"@xyo-network/boundwitness-builder": "^3.1.4",
"@xyo-network/diviner-hash-lease": "^3.1.4",
"typescript": "^5.5.4"
},
"publishConfig": {
Expand Down
4 changes: 4 additions & 0 deletions packages/payloadset/packages/xns/plugins/record/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@
"module": "dist/neutral/index.mjs",
"types": "dist/neutral/index.d.ts",
"dependencies": {
"@xylabs/assert": "^4.0.9",
"@xylabs/exists": "^4.0.9",
"@xylabs/hex": "^4.0.9",
"@xylabs/promise": "^4.0.9",
"@xyo-network/boundwitness-model": "^3.1.4",
"@xyo-network/diviner-hash-lease": "^3.1.4",
"@xyo-network/module-model": "^3.1.5",
"@xyo-network/payload-builder": "^3.1.4",
"@xyo-network/payload-model": "^3.1.4",
"@xyo-network/xns-record-payload-plugins": "workspace:^"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lib/index.ts'
3 changes: 2 additions & 1 deletion packages/payloadset/packages/xns/plugins/record/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './lib/index.ts'
export * from './estimate/index.ts'
export * from './validation/index.ts'
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './name/index.ts'
export * from './validation/index.ts'
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { assertEx } from '@xylabs/assert'
import { isHash } from '@xylabs/hex'
import type { Promisable } from '@xylabs/promise'
import type { Payload } from '@xyo-network/payload-model'
import type { DomainFields, TopLevelDomain } from '@xyo-network/xns-record-payload-plugins'
import { DomainSchema } from '@xyo-network/xns-record-payload-plugins'

import { MAX_DOMAIN_LENGTH, XnsNameSimpleValidators } from '../validation/index.ts'
import { removeDisallowedCharacters } from './lib/index.ts'
import type { ValidSourceTypes } from './types/index.ts'

export class XnsNameHelper {
static ValidTLDs = ['.xyo'] as const

private _xnsName: Payload<DomainFields>

private constructor(xnsName: Payload<DomainFields>) {
this._xnsName = xnsName
}

get domain() {
return assertEx(this.xnsName.domain, () => 'domain not found in payload')
}

get name() {
return `${this.domain}.${this.tld}`
}

get tld() {
return assertEx(this.xnsName.tld, () => 'tld not found in payload')
}

get xnsName() {
return assertEx(this._xnsName, () => 'XnsNameHelper xnsName not set')
}

/**
* Create an XnsNameHelper from a domain payload
* @param {Domain} domain
* @returns Promise<XnsNameHelper>
*/
static fromPayload(domain: Payload<DomainFields>): Promisable<XnsNameHelper> {
return new XnsNameHelper(domain)
}

/**
* Create an XnsNameHelper from a string
* @param {string} xnsName
* @returns Promise<XnsNameHelper>
*/
static fromString(xnsName: string): XnsNameHelper {
const parts = xnsName.split('.')
assertEx(parts.length === 2, () => 'Unable to parse xnsName')

const domain = parts[0]
const tld = parts[1] as TopLevelDomain
return new XnsNameHelper({
schema: DomainSchema, domain, tld,
})
}

/**
* Determine if a string is a valid XNS name or hash
* @param {string} source?
* @returns ValidSourceTypes
*/
static isPotentialXnsNameOrHash(source?: string): ValidSourceTypes {
if (isHash(source)) return 'hash'
const xnsName = XnsNameHelper.ValidTLDs.some(tld => source?.endsWith(tld)) ? source : null
return xnsName ? 'xnsName' : null
}

static isValid(domain: Payload<DomainFields>) {
return XnsNameSimpleValidators.every(validator => validator(domain))
}

/**
* Mask a string to be a valid XNS name
* @param {string} str
* @returns string
*/
static mask(str: string) {
// Check if the domain name is too long
if (str.length > MAX_DOMAIN_LENGTH) {
throw new Error(`Domain name too long: ${str.length} exceeds max length: ${MAX_DOMAIN_LENGTH}`)
}

// convert to lowercase
const lowercaseXnsName = str.toLowerCase()

// Remove everything except letters, numbers, and dashes
let formattedXnsName = lowercaseXnsName.replaceAll(/[^\dA-Za-z-]+$/g, '')

// Remove leading and trailing dashes
formattedXnsName = formattedXnsName.replaceAll(/^-+|-+$/g, '')

// Filter out disallowed characters.
return removeDisallowedCharacters(formattedXnsName)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './Name.ts'
export * from './types/index.ts'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './removeDisallowedCharacters.ts'
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { DisallowedModuleIdentifierCharacters } from '@xyo-network/module-model'

/**
* A set of all the disallowed characters in module identifiers
*/
const DISALLOWED_CHARACTERS = new Set(Object.keys(DisallowedModuleIdentifierCharacters))

/**
* Iterates over a string removing disallowed characters
* @param xnsName The XNS name to remove disallowed characters from
* @returns The XNS name with disallowed characters removed
*/
export const removeDisallowedCharacters = (xnsName: string): string => {
// Create the initial result
let result = ''
// Iterate over each character in the XNS name
for (const char of xnsName) {
// If the character is not a disallowed character
if (!DISALLOWED_CHARACTERS.has(char)) {
// add it to the result
result += char
}
}
// Return the result which contains only allowed characters
return result
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import type { Domain } from '@xyo-network/xns-record-payload-plugins'
import { DomainSchema } from '@xyo-network/xns-record-payload-plugins'

import { MAX_DOMAIN_LENGTH } from '../../validation/index.ts'
import { XnsNameHelper } from '../Name.ts'

describe('XnsNameHelper', () => {
const validDomain: Domain = {
schema: DomainSchema, domain: 'example', tld: 'xyo',
}
describe('domain getter', () => {
it('should return the domain if set', async () => {
const helper = await XnsNameHelper.fromPayload(validDomain)
expect(helper.domain).toBe('example')
})

it('should throw an error if domain is not set', async () => {
const domain: Domain = {
schema: DomainSchema, domain: '', tld: 'xyo',
}
const helper = await XnsNameHelper.fromPayload(domain)
expect(() => helper.domain).toThrow('domain not found in payload')
})
})

describe('tld getter', () => {
it('should return the tld if set', async () => {
const helper = await XnsNameHelper.fromPayload(validDomain)
expect(helper.tld).toBe('xyo')
})

it('should throw an error if tld is not set', async () => {
const domain: Domain = {
schema: DomainSchema, domain: 'example', tld: '' as 'xyo',
}
const helper = await XnsNameHelper.fromPayload(domain)
expect(() => helper.tld).toThrow('tld not found in payload')
})
})

describe('xnsName getter', () => {
it('should return the xnsName if set', async () => {
const helper = await XnsNameHelper.fromPayload(validDomain)
expect(helper.xnsName).toEqual(validDomain)
})

it('should throw an error if xnsName is not set', async () => {
const helper = await XnsNameHelper.fromPayload(undefined as unknown as Domain)
expect(() => helper.xnsName).toThrow('XnsNameHelper xnsName not set')
})
})

describe('fromString', () => {
it('should create an instance from a valid xnsName string', () => {
const { domain, tld } = validDomain
const helper = XnsNameHelper.fromString(`${domain}.${tld}`)
expect(helper.domain).toBe(domain)
expect(helper.xnsName.domain).toBe(domain)
expect(helper.tld).toBe(tld)
expect(helper.xnsName.tld).toBe(tld)
})

it('should throw an error if xnsName string is invalid', () => {
expect(() => XnsNameHelper.fromString('invalid')).toThrow('Unable to parse xnsName')
})
})

describe('isXnsNameOrHash', () => {
it('should return "xnsName" if the source ends with a valid TLD', () => {
expect(XnsNameHelper.isPotentialXnsNameOrHash('example.xyo')).toBe('xnsName')
})

it('should return "hash" if the source is a valid hash', () => {
expect(XnsNameHelper.isPotentialXnsNameOrHash('c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2')).toBe('hash')
})

it('should return null if the source is neither a valid xnsName nor a hash', () => {
expect(XnsNameHelper.isPotentialXnsNameOrHash('invalid')).toBe(null)
})
})

describe('isValid', () => {
it('should return true for valid xns names', () => {
expect(XnsNameHelper.isValid(validDomain)).toBe(true)
})

it('should return false for invalid xns names', () => {
const domain: Domain = {
schema: DomainSchema, domain: 'example-', tld: 'xyo',
}
expect(XnsNameHelper.isValid(domain)).toBe(false)
})
})

describe('mask', () => {
const cases = [
['Example$123', 'example123'],
['Example/123', 'example123'],
['Example.123', 'example123'],
['Example-123', 'example-123'],
['Example 123', 'example123'],
['Example_123', 'example123'],
['-Example_123-', 'example123'],
['-Example_123', 'example123'],
['Example_123-', 'example123'],
['--Example_123', 'example123'],
['Example_123--', 'example123'],
['--Example_123--', 'example123'],
['- Example_123 -', 'example123'],
]

describe.each(cases)('mask(%s)', (input, expected) => {
it(`should return ${expected}`, () => {
expect(XnsNameHelper.mask(input)).toBe(expected)
})
})

describe('With invalid input', () => {
it('should throw an error', () => {
expect(() => XnsNameHelper.mask('a'.repeat(MAX_DOMAIN_LENGTH + 1)))
.toThrow('Domain name too long: 129 exceeds max length: 128')
})
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "NodeNext",
"module": "NodeNext",
"sourceMap": true,
"inlineSources": true
},
"extends": "@xylabs/tsconfig-jest"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type ValidSourceTypes = 'xnsName' | 'hash' | null
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ValidSources.ts'
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const MIN_DOMAIN_LENGTH = 3
export const MAX_DOMAIN_LENGTH = 128
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './validators.ts'
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "NodeNext",
"module": "NodeNext",
"sourceMap": true,
"inlineSources": true
},
"extends": "@xylabs/tsconfig-jest"
}
Loading

0 comments on commit b523db0

Please sign in to comment.