Skip to content

Commit

Permalink
Merge pull request #37 from ensdomains/burn-fuses-updates
Browse files Browse the repository at this point in the history
Burn fuses updates
  • Loading branch information
LeonmanRolls authored Aug 26, 2022
2 parents 4523f13 + eb07539 commit 9af2fe8
Show file tree
Hide file tree
Showing 9 changed files with 1,037 additions and 599 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.yarn/*
node_modules
node_modules/

.env

Expand Down
138 changes: 123 additions & 15 deletions packages/ensjs/src/functions/burnFuses.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,128 @@ describe('burnFuses', () => {
beforeEach(async () => {
await revert()
})
it('should return a burnFuses transaction and succeed', async () => {
const tx = await ENSInstance.burnFuses('wrapped.eth', {
fusesToBurn: {
cannotUnwrap: true,
cannotCreateSubdomain: true,
cannotSetTtl: true,
},
addressOrIndex: accounts[1],
})
expect(tx).toBeTruthy()
await tx.wait()

const nameWrapper = await ENSInstance.contracts!.getNameWrapper()!
const [fuses] = await nameWrapper.getFuses(namehash('wrapped.eth'))
expect(fuses).toBe(113)
describe('Array', () => {
it('should return a burnFuses transaction from a named fuse array and succeed', async () => {
const tx = await ENSInstance.burnFuses('wrapped.eth', {
namedFusesToBurn: [
'CANNOT_UNWRAP',
'CANNOT_CREATE_SUBDOMAIN',
'CANNOT_SET_TTL',
],
addressOrIndex: accounts[1],
})
expect(tx).toBeTruthy()
await tx.wait()

const nameWrapper = await ENSInstance.contracts!.getNameWrapper()!
const [fuses] = await nameWrapper.getFuses(namehash('wrapped.eth'))
expect(fuses).toBe(113)
})
it('should return a burnFuses transaction from an unnamed fuse array and succeed', async () => {
const tx0 = await ENSInstance.burnFuses('wrapped.eth', {
namedFusesToBurn: ['CANNOT_UNWRAP'],
addressOrIndex: accounts[1],
})
expect(tx0).toBeTruthy()
await tx0.wait()

const tx = await ENSInstance.burnFuses('wrapped.eth', {
unnamedFusesToBurn: [128, 256, 512],
addressOrIndex: accounts[1],
})
expect(tx).toBeTruthy()
await tx.wait()

const nameWrapper = await ENSInstance.contracts!.getNameWrapper()!
const [fuses] = await nameWrapper.getFuses(namehash('wrapped.eth'))
expect(fuses).toBe(961)
})
it('should return a burnFuses transaction from both an unnamed and named fuse array and succeed', async () => {
const tx = await ENSInstance.burnFuses('wrapped.eth', {
namedFusesToBurn: [
'CANNOT_UNWRAP',
'CANNOT_CREATE_SUBDOMAIN',
'CANNOT_SET_TTL',
],
unnamedFusesToBurn: [128, 256, 512],
addressOrIndex: accounts[1],
})
expect(tx).toBeTruthy()
await tx.wait()

const nameWrapper = await ENSInstance.contracts!.getNameWrapper()!
const [fuses] = await nameWrapper.getFuses(namehash('wrapped.eth'))
expect(fuses).toBe(1009)
})
it('should throw an error when trying to burn a named fuse in an unnamed fuse array', async () => {
try {
await ENSInstance.burnFuses('wrapped.eth', {
unnamedFusesToBurn: [64] as any,
})
expect(false).toBeTruthy()
} catch (e: any) {
expect(e.message).toBe(
'64 is not a valid unnamed fuse. If you are trying to burn a named fuse, use the namedFusesToBurn property.',
)
}
})
it('should throw an error when trying to burn an unnamed fuse in a named fuse array', async () => {
try {
await ENSInstance.burnFuses('wrapped.eth', {
namedFusesToBurn: ['COOL_SWAG_FUSE'] as any,
})
expect(false).toBeTruthy()
} catch (e: any) {
expect(e.message).toBe('COOL_SWAG_FUSE is not a valid named fuse.')
}
})
})
describe('Number', () => {
it('should return a burnFuses transaction from a number and succeed', async () => {
const tx = await ENSInstance.burnFuses('wrapped.eth', {
fuseNumberToBurn: 49,
addressOrIndex: accounts[1],
})
expect(tx).toBeTruthy()
await tx.wait()

const nameWrapper = await ENSInstance.contracts!.getNameWrapper()!
const [fuses] = await nameWrapper.getFuses(namehash('wrapped.eth'))
expect(fuses).toBe(113)
})
it('should throw an error if the number is too high', async () => {
try {
await ENSInstance.burnFuses('wrapped.eth', {
fuseNumberToBurn: 4294967297,
})
expect(false).toBeTruthy()
} catch (e: any) {
expect(e.message).toBe(
'Fuse number must be limited to uint32, 4294967297 was too high.',
)
}
})
it('should throw an error if the number is too low', async () => {
try {
await ENSInstance.burnFuses('wrapped.eth', {
fuseNumberToBurn: -1,
})
expect(false).toBeTruthy()
} catch (e: any) {
expect(e.message).toBe(
'Fuse number must be limited to uint32, -1 was too low.',
)
}
})
it('should throw an error if the number is not an integer', async () => {
try {
await ENSInstance.burnFuses('wrapped.eth', {
fuseNumberToBurn: 7.5,
})
expect(false).toBeTruthy()
} catch (e: any) {
expect(e.message).toBe('Fuse number must be an integer, 7.5 was not.')
}
})
})
})
111 changes: 101 additions & 10 deletions packages/ensjs/src/functions/burnFuses.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,112 @@
import { ENSArgs } from '..'
import { FuseOptions } from '../@types/FuseOptions'
import generateFuseInput from '../utils/generateFuseInput'
import { fuseEnum, unnamedFuses } from '../utils/fuses'
import { namehash } from '../utils/normalise'

export default async function (
type FuseObj = typeof fuseEnum
type UnnamedFuseType = typeof unnamedFuses
type Fuse = keyof FuseObj
type NamedFuseValues = FuseObj[Fuse]
type UnnamedFuseValues = UnnamedFuseType[number]

// We need this type so that the following type isn't infinite. This type limits the max length of the fuse array to 7.
type FuseArrayPossibilities =
| [Fuse]
| [Fuse, Fuse]
| [Fuse, Fuse, Fuse]
| [Fuse, Fuse, Fuse, Fuse]
| [Fuse, Fuse, Fuse, Fuse, Fuse]
| [Fuse, Fuse, Fuse, Fuse, Fuse, Fuse]
| [Fuse, Fuse, Fuse, Fuse, Fuse, Fuse, Fuse]

/**
* This type creates a type error if there are any duplicate fuses.
* It effectively works like a reduce function, starting with 0 included types, adding a type each time, and then checking for duplicates.
*
* @template A The array to check for duplicates.
* @template B The union of all checked existing types.
*/
// CLAUSE A: This extension unwraps the type as a fuse tuple.
type FusesWithoutDuplicates<A, B = never> = A extends FuseArrayPossibilities
? // CLAUSE A > TRUE: CLAUSE B: Pick out the first item in the current array, separating the current item from the rest.
A extends [infer Head, ...infer Tail]
? // CLAUSE B > TRUE: CLAUSE C: Check if the current item is a duplicate based on the input union.
Head extends B
? // CLAUSE C > TRUE: Duplicate found, return an empty array to throw a type error.
[]
: // CLAUSE C > FALSE: Return a new array to continue the recursion, adds the current item type to the union.
[Head, ...FusesWithoutDuplicates<Tail, Head | B>]
: // CLAUSE B > FALSE: Return the input array as there is no more array elements to check.
A
: // CLAUSE A > FALSE: Return an empty array as it isn't a fuse tuple.
[]

type FusePropsNamedArray<A extends FuseArrayPossibilities> = {
namedFusesToBurn: FusesWithoutDuplicates<A>
}

type FusePropsUnnamedArray = {
unnamedFusesToBurn: UnnamedFuseValues[]
}

type FusePropsNumber = {
fuseNumberToBurn: number
}

type FuseProps<A extends FuseArrayPossibilities> =
| (Partial<FusePropsNamedArray<A>> & FusePropsUnnamedArray)
| (FusePropsNamedArray<A> & Partial<FusePropsUnnamedArray>)
| FusePropsNumber

export default async function <A extends FuseArrayPossibilities>(
{ contracts, signer }: ENSArgs<'contracts' | 'signer'>,
name: string,
{
fusesToBurn,
}: {
fusesToBurn: FuseOptions
},
props: FuseProps<A>,
) {
const isNumber = 'fuseNumberToBurn' in props
const hasNamedArray = 'namedFusesToBurn' in props
const hasUnnamedArray = 'unnamedFusesToBurn' in props

let encodedFuses: number = 0

if (isNumber) {
if (props.fuseNumberToBurn > 2 ** 32 || props.fuseNumberToBurn < 1) {
throw new Error(
`Fuse number must be limited to uint32, ${
props.fuseNumberToBurn
} was too ${props.fuseNumberToBurn < 1 ? 'low' : 'high'}.`,
)
} else if (props.fuseNumberToBurn % 1 !== 0) {
throw new Error(
`Fuse number must be an integer, ${props.fuseNumberToBurn} was not.`,
)
}
encodedFuses = props.fuseNumberToBurn
} else {
if (!hasNamedArray && !hasUnnamedArray) {
throw new Error('Please provide fuses to burn')
}
if (hasNamedArray) {
for (const fuse of props.namedFusesToBurn!) {
if (!(fuse in fuseEnum)) {
throw new Error(`${fuse} is not a valid named fuse.`)
}
encodedFuses |= fuseEnum[fuse]
}
}
if (hasUnnamedArray) {
for (const fuse of props.unnamedFusesToBurn!) {
if (!unnamedFuses.includes(fuse)) {
throw new Error(
`${fuse} is not a valid unnamed fuse. If you are trying to burn a named fuse, use the namedFusesToBurn property.`,
)
}
encodedFuses |= fuse
}
}
}

const nameWrapper = (await contracts?.getNameWrapper()!).connect(signer)
const hash = namehash(name)

const encodedFuses = generateFuseInput(fusesToBurn)

return nameWrapper.populateTransaction.setFuses(hash, encodedFuses)
}
48 changes: 33 additions & 15 deletions packages/ensjs/src/functions/getFuses.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ethers } from 'ethers'
import { BigNumber, ethers } from 'ethers'
import { ENS } from '..'
import setup from '../tests/setup'

Expand All @@ -9,6 +9,22 @@ let provider: ethers.providers.JsonRpcProvider
let accounts: string[]
let withWrappedSnapshot: any

const unwrappedNameDefault = {
expiryDate: new Date(0).toString(),
fuseObj: {
CANNOT_BURN_FUSES: false,
CANNOT_CREATE_SUBDOMAIN: false,
CANNOT_SET_RESOLVER: false,
CANNOT_SET_TTL: false,
CANNOT_TRANSFER: false,
CANNOT_UNWRAP: false,
PARENT_CANNOT_CONTROL: false,
canDoEverything: true,
},
owner: '0x0000000000000000000000000000000000000000',
rawFuses: BigNumber.from(0),
}

beforeAll(async () => {
;({ ENSInstance, revert, provider, createSnapshot } = await setup())
accounts = await provider.listAccounts()
Expand All @@ -26,9 +42,11 @@ afterAll(async () => {
})

describe('getFuses', () => {
it('should return null for an unwrapped name', async () => {
it('should return default data for an unwrapped name', async () => {
const result = await ENSInstance.getFuses('with-profile.eth')
expect(result).toBeUndefined()
expect({ ...result, expiryDate: result?.expiryDate.toString() }).toEqual(
unwrappedNameDefault,
)
})
it('should return with canDoEverything set to true for a name with no fuses burned', async () => {
const nameWrapper = await ENSInstance.contracts!.getNameWrapper()!
Expand All @@ -47,11 +65,11 @@ describe('getFuses', () => {
})
it('should return with other correct fuses', async () => {
const tx = await ENSInstance.burnFuses('wrapped.eth', {
fusesToBurn: {
cannotUnwrap: true,
cannotSetTtl: true,
cannotCreateSubdomain: true,
},
namedFusesToBurn: [
'CANNOT_UNWRAP',
'CANNOT_CREATE_SUBDOMAIN',
'CANNOT_SET_TTL',
],
addressOrIndex: 1,
})
await tx.wait()
Expand All @@ -60,13 +78,13 @@ describe('getFuses', () => {
expect(result).toBeTruthy()
if (result) {
expect(result.fuseObj).toMatchObject({
cannotUnwrap: true,
cannotBurnFuses: false,
cannotTransfer: false,
cannotSetResolver: false,
cannotSetTtl: true,
cannotCreateSubdomain: true,
parentCannotControl: true,
CANNOT_UNWRAP: true,
CANNOT_BURN_FUSES: false,
CANNOT_TRANSFER: false,
CANNOT_SET_RESOLVER: false,
CANNOT_SET_TTL: true,
CANNOT_CREATE_SUBDOMAIN: true,
PARENT_CANNOT_CONTROL: true,
canDoEverything: false,
})
expect(result.rawFuses.toHexString()).toBe('0x71')
Expand Down
Loading

0 comments on commit 9af2fe8

Please sign in to comment.