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

Burn fuses updates #37

Merged
merged 15 commits into from
Aug 26, 2022
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