Skip to content

Commit 535d3fd

Browse files
authored
www: add ens support (#62)
1 parent d16ac8c commit 535d3fd

File tree

3 files changed

+194
-14
lines changed

3 files changed

+194
-14
lines changed

www/components/CreateRoot.tsx

Lines changed: 82 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,49 @@ import classNames from 'classnames'
33
import { useCallback, useEffect, useMemo, useState } from 'react'
44
import { isErrorResponse, createMerkleRoot } from 'utils/api'
55
import Button from 'components/Button'
6-
import { parseAddressesFromText } from 'utils/addressParsing'
6+
import { parseAddressesFromText, prepareAddresses } from 'utils/addressParsing'
77
import { useRouter } from 'next/router'
88
import { randomBytes } from 'crypto'
9+
import { resolveEnsDomains } from 'utils/ens'
910

1011
const useCreateMerkleRoot = () => {
11-
const [{ value, status }, create] = useAsync(
12-
async (addresses: string[]) => await createMerkleRoot(addresses),
12+
const [ensMap, setEnsMap] = useState<Record<string, string>>({})
13+
14+
const [{ value, status, error: reqError }, create] = useAsync(
15+
async (addressesOrENSNames: string[]) => {
16+
let prepared = prepareAddresses(addressesOrENSNames, ensMap)
17+
18+
if (prepared.unresolvedEnsNames.length > 0) {
19+
const ensAddresses = await resolveEnsDomains(
20+
prepared.unresolvedEnsNames,
21+
)
22+
23+
setEnsMap((prev) => ({
24+
...prev,
25+
...ensAddresses,
26+
}))
27+
28+
prepared = prepareAddresses(addressesOrENSNames, {
29+
...ensMap,
30+
...ensAddresses,
31+
})
32+
33+
if (prepared.unresolvedEnsNames.length > 0) {
34+
throw new Error(`Could not resolve all ENS names`)
35+
}
36+
}
37+
38+
if (prepared.addresses.length !== prepared.dedupedAddresses.length) {
39+
return (
40+
await Promise.all([
41+
createMerkleRoot(prepared.dedupedAddresses),
42+
createMerkleRoot(prepared.addresses),
43+
])
44+
)[0]
45+
}
46+
47+
return await createMerkleRoot(prepared.dedupedAddresses)
48+
},
1349
)
1450

1551
const merkleRoot = useMemo(() => {
@@ -20,10 +56,12 @@ const useCreateMerkleRoot = () => {
2056

2157
const error = useMemo(() => {
2258
if (isErrorResponse(value)) return value
59+
if (reqError !== undefined)
60+
return { error: true, message: reqError.message }
2361
return undefined
24-
}, [value])
62+
}, [value, reqError])
2563

26-
return { merkleRoot, error, status, create }
64+
return { merkleRoot, error, status, create, parsedEnsNames: ensMap }
2765
}
2866

2967
const randomAddress = () => `0x${randomBytes(20).toString('hex')}`
@@ -34,6 +72,7 @@ export default function CreateRoot() {
3472
error: errorResponse,
3573
status,
3674
create,
75+
parsedEnsNames,
3776
} = useCreateMerkleRoot()
3877
const [addressInput, addressInputSet] = useState('')
3978

@@ -46,13 +85,10 @@ export default function CreateRoot() {
4685
create(addresses)
4786
}, [addressInput, create])
4887

49-
const parsedAddresses = useMemo(() => {
50-
if (addressInput.trim().length === 0) {
51-
return []
52-
}
53-
54-
return parseAddressesFromText(addressInput)
55-
}, [addressInput])
88+
const parsedAddresses = useMemo(
89+
() => parseAddressesFromText(addressInput),
90+
[addressInput],
91+
)
5692

5793
const parsedAddressesCount = useMemo(
5894
() => parsedAddresses.length,
@@ -75,6 +111,26 @@ export default function CreateRoot() {
75111
addressInputSet(addresses.join('\n'))
76112
}, [])
77113

114+
const handleRemoveInvalidENSNames = useCallback(() => {
115+
const addresses = parsedAddresses
116+
.filter((address) => {
117+
return (
118+
!address.includes('.') ||
119+
parsedEnsNames[address.toLowerCase()] !== undefined
120+
)
121+
})
122+
.join('\n')
123+
addressInputSet(addresses)
124+
}, [parsedAddresses, parsedEnsNames])
125+
126+
const showRemoveInvalidENSNames = useMemo(
127+
// show the button if the error message contains `Could not resolve all ENS names`
128+
() =>
129+
errorResponse?.message?.includes('Could not resolve all ENS names') ??
130+
false,
131+
[errorResponse?.message],
132+
)
133+
78134
const buttonPending = status === 'loading' || merkleRoot !== undefined
79135

80136
return (
@@ -90,7 +146,7 @@ export default function CreateRoot() {
90146
)}
91147
value={addressInput}
92148
onChange={(e) => addressInputSet(e.target.value)}
93-
placeholder="Paste addresses here, separated by commas, spaces or new lines"
149+
placeholder="Paste addresses or ENS names here, separated by commas, spaces or new lines"
94150
/>
95151
</div>
96152

@@ -118,9 +174,21 @@ export default function CreateRoot() {
118174
)}
119175
</div>
120176

121-
{status === 'success' && errorResponse !== undefined && (
177+
{errorResponse !== undefined && (
122178
<div className="text-center sm:text-left w-full">
123179
Error: {errorResponse.message}
180+
{showRemoveInvalidENSNames && (
181+
<>
182+
{' '}
183+
<button
184+
className="underline"
185+
onClick={handleRemoveInvalidENSNames}
186+
type="button"
187+
>
188+
Remove invalid ENS names
189+
</button>
190+
</>
191+
)}
124192
</div>
125193
)}
126194
</div>

www/utils/addressParsing.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,49 @@ export function parseAddressesFromText(text: string) {
66
.map((s) => s.trim())
77
.filter((s) => s.length > 0)
88
}
9+
10+
export const prepareAddresses = (
11+
addressesOrENSNames: string[],
12+
ensMap: Record<string, string>,
13+
): {
14+
addresses: string[]
15+
dedupedAddresses: string[]
16+
unresolvedEnsNames: string[]
17+
} => {
18+
const seenAddresses = new Set<string>()
19+
const unresolvedEnsNames: string[] = []
20+
const addresses: string[] = []
21+
const dedupedAddresses: string[] = []
22+
23+
for (const addressOrENSName of addressesOrENSNames) {
24+
const lowercasedAddressOrENSName = addressOrENSName.toLowerCase()
25+
if (addressOrENSName.includes('.')) {
26+
const addressFromEns: string | undefined =
27+
ensMap[lowercasedAddressOrENSName]?.toLowerCase()
28+
29+
if (addressFromEns !== undefined) {
30+
addresses.push(addressFromEns)
31+
if (seenAddresses.has(addressFromEns)) {
32+
continue
33+
}
34+
seenAddresses.add(addressFromEns)
35+
dedupedAddresses.push(addressFromEns)
36+
} else {
37+
unresolvedEnsNames.push(lowercasedAddressOrENSName)
38+
}
39+
} else {
40+
addresses.push(addressOrENSName)
41+
if (seenAddresses.has(lowercasedAddressOrENSName)) {
42+
continue
43+
}
44+
seenAddresses.add(lowercasedAddressOrENSName)
45+
dedupedAddresses.push(addressOrENSName)
46+
}
47+
}
48+
49+
return {
50+
addresses,
51+
dedupedAddresses,
52+
unresolvedEnsNames,
53+
}
54+
}

www/utils/ens.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
const ensSubgraphUrl = 'https://api.thegraph.com/subgraphs/name/ensdomains/ens'
2+
3+
const query = `
4+
query DomainsQuery($names: [String!]!) {
5+
domains(where: { name_in: $names }) {
6+
resolvedAddress {
7+
id
8+
}
9+
name
10+
}
11+
}
12+
`
13+
14+
const resolveEnsDomainsBatch = async (
15+
ensNames: string[],
16+
): Promise<{ [name: string]: string }> => {
17+
const variables = {
18+
names: ensNames,
19+
}
20+
const response = await fetch(ensSubgraphUrl, {
21+
method: 'POST',
22+
headers: {
23+
'Content-Type': 'application/json',
24+
},
25+
body: JSON.stringify({ query, variables }),
26+
})
27+
const {
28+
data,
29+
}: {
30+
data?: {
31+
domains: { resolvedAddress: { id: string } | null; name: string }[]
32+
}
33+
} = await response.json()
34+
if (!data) {
35+
throw new Error('No data returned from subgraph')
36+
}
37+
38+
return data.domains.reduce((acc, domain) => {
39+
if (domain.resolvedAddress !== null) {
40+
acc[domain.name] = domain.resolvedAddress.id
41+
}
42+
return acc
43+
}, {} as { [name: string]: string })
44+
}
45+
46+
export const resolveEnsDomains = async (
47+
ensNames: string[],
48+
): Promise<{ [name: string]: string }> => {
49+
const batches = chunk(ensNames, 100)
50+
const results = await Promise.all(batches.map(resolveEnsDomainsBatch))
51+
return results.reduce((acc, batch) => {
52+
return { ...acc, ...batch }
53+
}, {} as { [name: string]: string })
54+
}
55+
56+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
57+
const chunk = (arr: any[], size: number): any[][] => {
58+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
59+
const chunks: any[][] = []
60+
let i = 0
61+
while (i < arr.length) {
62+
chunks.push(arr.slice(i, i + size))
63+
i += size
64+
}
65+
return chunks
66+
}

0 commit comments

Comments
 (0)