Skip to content

Commit

Permalink
www: add ens support (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
malonehedges authored Sep 1, 2022
1 parent d16ac8c commit 535d3fd
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 14 deletions.
96 changes: 82 additions & 14 deletions www/components/CreateRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,49 @@ import classNames from 'classnames'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { isErrorResponse, createMerkleRoot } from 'utils/api'
import Button from 'components/Button'
import { parseAddressesFromText } from 'utils/addressParsing'
import { parseAddressesFromText, prepareAddresses } from 'utils/addressParsing'
import { useRouter } from 'next/router'
import { randomBytes } from 'crypto'
import { resolveEnsDomains } from 'utils/ens'

const useCreateMerkleRoot = () => {
const [{ value, status }, create] = useAsync(
async (addresses: string[]) => await createMerkleRoot(addresses),
const [ensMap, setEnsMap] = useState<Record<string, string>>({})

const [{ value, status, error: reqError }, create] = useAsync(
async (addressesOrENSNames: string[]) => {
let prepared = prepareAddresses(addressesOrENSNames, ensMap)

if (prepared.unresolvedEnsNames.length > 0) {
const ensAddresses = await resolveEnsDomains(
prepared.unresolvedEnsNames,
)

setEnsMap((prev) => ({
...prev,
...ensAddresses,
}))

prepared = prepareAddresses(addressesOrENSNames, {
...ensMap,
...ensAddresses,
})

if (prepared.unresolvedEnsNames.length > 0) {
throw new Error(`Could not resolve all ENS names`)
}
}

if (prepared.addresses.length !== prepared.dedupedAddresses.length) {
return (
await Promise.all([
createMerkleRoot(prepared.dedupedAddresses),
createMerkleRoot(prepared.addresses),
])
)[0]
}

return await createMerkleRoot(prepared.dedupedAddresses)
},
)

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

const error = useMemo(() => {
if (isErrorResponse(value)) return value
if (reqError !== undefined)
return { error: true, message: reqError.message }
return undefined
}, [value])
}, [value, reqError])

return { merkleRoot, error, status, create }
return { merkleRoot, error, status, create, parsedEnsNames: ensMap }
}

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

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

const parsedAddresses = useMemo(() => {
if (addressInput.trim().length === 0) {
return []
}

return parseAddressesFromText(addressInput)
}, [addressInput])
const parsedAddresses = useMemo(
() => parseAddressesFromText(addressInput),
[addressInput],
)

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

const handleRemoveInvalidENSNames = useCallback(() => {
const addresses = parsedAddresses
.filter((address) => {
return (
!address.includes('.') ||
parsedEnsNames[address.toLowerCase()] !== undefined
)
})
.join('\n')
addressInputSet(addresses)
}, [parsedAddresses, parsedEnsNames])

const showRemoveInvalidENSNames = useMemo(
// show the button if the error message contains `Could not resolve all ENS names`
() =>
errorResponse?.message?.includes('Could not resolve all ENS names') ??
false,
[errorResponse?.message],
)

const buttonPending = status === 'loading' || merkleRoot !== undefined

return (
Expand All @@ -90,7 +146,7 @@ export default function CreateRoot() {
)}
value={addressInput}
onChange={(e) => addressInputSet(e.target.value)}
placeholder="Paste addresses here, separated by commas, spaces or new lines"
placeholder="Paste addresses or ENS names here, separated by commas, spaces or new lines"
/>
</div>

Expand Down Expand Up @@ -118,9 +174,21 @@ export default function CreateRoot() {
)}
</div>

{status === 'success' && errorResponse !== undefined && (
{errorResponse !== undefined && (
<div className="text-center sm:text-left w-full">
Error: {errorResponse.message}
{showRemoveInvalidENSNames && (
<>
{' '}
<button
className="underline"
onClick={handleRemoveInvalidENSNames}
type="button"
>
Remove invalid ENS names
</button>
</>
)}
</div>
)}
</div>
Expand Down
46 changes: 46 additions & 0 deletions www/utils/addressParsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,49 @@ export function parseAddressesFromText(text: string) {
.map((s) => s.trim())
.filter((s) => s.length > 0)
}

export const prepareAddresses = (
addressesOrENSNames: string[],
ensMap: Record<string, string>,
): {
addresses: string[]
dedupedAddresses: string[]
unresolvedEnsNames: string[]
} => {
const seenAddresses = new Set<string>()
const unresolvedEnsNames: string[] = []
const addresses: string[] = []
const dedupedAddresses: string[] = []

for (const addressOrENSName of addressesOrENSNames) {
const lowercasedAddressOrENSName = addressOrENSName.toLowerCase()
if (addressOrENSName.includes('.')) {
const addressFromEns: string | undefined =
ensMap[lowercasedAddressOrENSName]?.toLowerCase()

if (addressFromEns !== undefined) {
addresses.push(addressFromEns)
if (seenAddresses.has(addressFromEns)) {
continue
}
seenAddresses.add(addressFromEns)
dedupedAddresses.push(addressFromEns)
} else {
unresolvedEnsNames.push(lowercasedAddressOrENSName)
}
} else {
addresses.push(addressOrENSName)
if (seenAddresses.has(lowercasedAddressOrENSName)) {
continue
}
seenAddresses.add(lowercasedAddressOrENSName)
dedupedAddresses.push(addressOrENSName)
}
}

return {
addresses,
dedupedAddresses,
unresolvedEnsNames,
}
}
66 changes: 66 additions & 0 deletions www/utils/ens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const ensSubgraphUrl = 'https://api.thegraph.com/subgraphs/name/ensdomains/ens'

const query = `
query DomainsQuery($names: [String!]!) {
domains(where: { name_in: $names }) {
resolvedAddress {
id
}
name
}
}
`

const resolveEnsDomainsBatch = async (
ensNames: string[],
): Promise<{ [name: string]: string }> => {
const variables = {
names: ensNames,
}
const response = await fetch(ensSubgraphUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query, variables }),
})
const {
data,
}: {
data?: {
domains: { resolvedAddress: { id: string } | null; name: string }[]
}
} = await response.json()
if (!data) {
throw new Error('No data returned from subgraph')
}

return data.domains.reduce((acc, domain) => {
if (domain.resolvedAddress !== null) {
acc[domain.name] = domain.resolvedAddress.id
}
return acc
}, {} as { [name: string]: string })
}

export const resolveEnsDomains = async (
ensNames: string[],
): Promise<{ [name: string]: string }> => {
const batches = chunk(ensNames, 100)
const results = await Promise.all(batches.map(resolveEnsDomainsBatch))
return results.reduce((acc, batch) => {
return { ...acc, ...batch }
}, {} as { [name: string]: string })
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const chunk = (arr: any[], size: number): any[][] => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const chunks: any[][] = []
let i = 0
while (i < arr.length) {
chunks.push(arr.slice(i, i + size))
i += size
}
return chunks
}

1 comment on commit 535d3fd

@vercel
Copy link

@vercel vercel bot commented on 535d3fd Sep 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

lanyard – ./

lanyard-git-main.context.wtf
allowlist.context.wtf
lanyard.context.wtf

Please sign in to comment.