Skip to content

Commit

Permalink
Merge pull request #31 from ensdomains/feat/set-individual-record
Browse files Browse the repository at this point in the history
Allow setting individual records with setRecord
  • Loading branch information
TateB authored Aug 5, 2022
2 parents 34ece02 + d8ddae1 commit 9d1c0d7
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 20 deletions.
100 changes: 100 additions & 0 deletions packages/ensjs/src/functions/setRecord.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { ENS } from '..'
import setup from '../tests/setup'
import { decodeContenthash } from '../utils/contentHash'
import { hexEncodeName } from '../utils/hexEncodedName'
import { namehash } from '../utils/normalise'

let ENSInstance: ENS
let revert: Awaited<ReturnType<typeof setup>>['revert']

beforeAll(async () => {
;({ ENSInstance, revert } = await setup())
})

afterAll(async () => {
await revert()
})

describe('setRecord', () => {
it('should allow a text record set', async () => {
const tx = await ENSInstance.setRecord('parthtejpal.eth', {
type: 'text',
record: { key: 'foo', value: 'bar' },
})
expect(tx).toBeTruthy()
await tx.wait()

const universalResolver =
await ENSInstance.contracts!.getUniversalResolver()!
const publicResolver = await ENSInstance.contracts!.getPublicResolver()!
const encodedText = await universalResolver.resolve(
hexEncodeName('parthtejpal.eth'),
publicResolver.interface.encodeFunctionData('text', [
namehash('parthtejpal.eth'),
'foo',
]),
)
const [resultText] = publicResolver.interface.decodeFunctionResult(
'text',
encodedText[0],
)
expect(resultText).toBe('bar')
})
it('should allow an address record set', async () => {
const tx = await ENSInstance.setRecord('parthtejpal.eth', {
type: 'addr',
record: {
key: 'ETC',
value: '0x42D63ae25990889E35F215bC95884039Ba354115',
},
})
expect(tx).toBeTruthy()
await tx.wait()

const universalResolver =
await ENSInstance.contracts!.getUniversalResolver()!
const publicResolver = await ENSInstance.contracts!.getPublicResolver()!
const encodedAddr = await universalResolver.resolve(
hexEncodeName('parthtejpal.eth'),
publicResolver.interface.encodeFunctionData('addr(bytes32,uint256)', [
namehash('parthtejpal.eth'),
'61',
]),
)
const [resultAddr] = publicResolver.interface.decodeFunctionResult(
'addr(bytes32,uint256)',
encodedAddr[0],
)
expect(resultAddr).toBe(
'0x42D63ae25990889E35F215bC95884039Ba354115'.toLowerCase(),
)
})
it('should allow a contenthash record set', async () => {
const tx = await ENSInstance.setRecord('parthtejpal.eth', {
type: 'contentHash',
record:
'ipns://k51qzi5uqu5dgox2z23r6e99oqency055a6xt92xzmyvpz8mwz5ycjavm0u150',
})
expect(tx).toBeTruthy()
await tx.wait()

const universalResolver =
await ENSInstance.contracts!.getUniversalResolver()!
const publicResolver = await ENSInstance.contracts!.getPublicResolver()!
const encodedContent = await universalResolver.resolve(
hexEncodeName('parthtejpal.eth'),
publicResolver.interface.encodeFunctionData('contenthash', [
namehash('parthtejpal.eth'),
]),
)
const [resultContent] = publicResolver.interface.decodeFunctionResult(
'contenthash',
encodedContent[0],
)
const content = decodeContenthash(resultContent)
expect(content.decoded).toBe(
'k51qzi5uqu5dgox2z23r6e99oqency055a6xt92xzmyvpz8mwz5ycjavm0u150',
)
expect(content.protocolType).toBe('ipns')
})
})
63 changes: 63 additions & 0 deletions packages/ensjs/src/functions/setRecord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { ENSArgs } from '..'
import { namehash } from '../utils/normalise'
import {
generateSingleRecordCall,
RecordInput,
RecordTypes,
} from '../utils/recordHelpers'

type BaseInput = {
resolverAddress?: string
}

type ContentHashInput = {
record: RecordInput<'contentHash'>
type: 'contentHash'
}

type AddrOrTextInput = {
record: RecordInput<'addr' | 'text'>
type: 'addr' | 'text'
}

export default async function <T extends RecordTypes>(
{
contracts,
provider,
getResolver,
signer,
}: ENSArgs<'contracts' | 'provider' | 'getResolver' | 'signer'>,
name: string,
{
record,
type,
resolverAddress,
}: BaseInput & (ContentHashInput | AddrOrTextInput),
) {
if (!name.includes('.')) {
throw new Error('Input is not an ENS name')
}

let resolverToUse: string
if (resolverAddress) {
resolverToUse = resolverAddress
} else {
resolverToUse = await getResolver(name)
}

if (!resolverToUse) {
throw new Error('No resolver found for input address')
}

const resolver = (
await contracts?.getPublicResolver(provider, resolverToUse)
)?.connect(signer)!
const hash = namehash(name)

const call = generateSingleRecordCall(hash, resolver, type)(record)

return {
to: resolver.address,
data: call,
}
}
4 changes: 2 additions & 2 deletions packages/ensjs/src/functions/setRecords.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('setRecords', () => {
const publicResolver = await ENSInstance.contracts!.getPublicResolver()!
const encodedText = await universalResolver.resolve(
hexEncodeName('parthtejpal.eth'),
publicResolver.interface.encodeFunctionData('text(bytes32,string)', [
publicResolver.interface.encodeFunctionData('text', [
namehash('parthtejpal.eth'),
'foo',
]),
Expand All @@ -45,7 +45,7 @@ describe('setRecords', () => {
]),
)
const [resultText] = publicResolver.interface.decodeFunctionResult(
'text(bytes32,string)',
'text',
encodedText[0],
)
const [resultAddr] = publicResolver.interface.decodeFunctionResult(
Expand Down
7 changes: 7 additions & 0 deletions packages/ensjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import type {
} from './functions/getSpecificRecord'
import type getSubnames from './functions/getSubnames'
import type setName from './functions/setName'
import type setRecord from './functions/setRecord'
import type setRecords from './functions/setRecords'
import type setResolver from './functions/setResolver'
import type transferName from './functions/transferName'
Expand Down Expand Up @@ -543,6 +544,12 @@ export class ENS {
['contracts', 'provider', 'getResolver'],
)

public setRecord = this.generateWriteFunction<typeof setRecord>('setRecord', [
'contracts',
'provider',
'getResolver',
])

public setResolver = this.generateWriteFunction<typeof setResolver>(
'setResolver',
['contracts'],
Expand Down
70 changes: 52 additions & 18 deletions packages/ensjs/src/utils/recordHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,47 @@ export const generateSetAddr = (
)
}

export type RecordTypes = 'contentHash' | 'text' | 'addr'

export type RecordInput<T extends RecordTypes> = T extends 'contentHash'
? string
: RecordItem

export function generateSingleRecordCall<T extends RecordTypes>(
namehash: string,
resolver: PublicResolver,
type: T,
): (record: RecordInput<T>) => string {
if (type === 'contentHash') {
return (_r: RecordInput<T>) => {
const record = _r as string
let _contentHash = ''
if (record !== _contentHash) {
const encoded = encodeContenthash(record)
if (encoded.error) throw new Error(encoded.error)
_contentHash = encoded.encoded as string
}
return resolver.interface.encodeFunctionData('setContenthash', [
namehash,
_contentHash,
])
}
} else {
return (_r: RecordInput<T>) => {
const record = _r as RecordItem
if (type === 'text') {
return resolver.interface.encodeFunctionData('setText', [
namehash,
record.key,
record.value,
])
} else {
return generateSetAddr(namehash, record.key, record.value, resolver)
}
}
}
}

export const generateRecordCallArray = (
namehash: string,
records: RecordOptions,
Expand All @@ -41,31 +82,24 @@ export const generateRecordCallArray = (
const calls: string[] = []

if (records.contentHash) {
const contentHash =
records.contentHash === '' ? '' : encodeContenthash(records.contentHash)
const data = (resolver?.interface.encodeFunctionData as any)(
'setContenthash',
[namehash, contentHash],
)
const data = generateSingleRecordCall(
namehash,
resolver,
'contentHash',
)(records.contentHash)
data && calls.push(data)
}

if (records.texts && records.texts.length > 0) {
records.texts.forEach(({ key, value }: RecordItem) => {
const data = resolver?.interface.encodeFunctionData('setText', [
namehash,
key,
value,
])
data && calls.push(data)
})
records.texts
.map(generateSingleRecordCall(namehash, resolver, 'text'))
.forEach((call) => calls.push(call))
}

if (records.coinTypes && records.coinTypes.length > 0) {
records.coinTypes.forEach(({ key, value }: RecordItem) => {
const data = generateSetAddr(namehash, key, value, resolver!)
data && calls.push(data)
})
records.coinTypes
.map(generateSingleRecordCall(namehash, resolver, 'addr'))
.forEach((call) => calls.push(call))
}

return calls
Expand Down

0 comments on commit 9d1c0d7

Please sign in to comment.