Skip to content

Commit

Permalink
Merge pull request #138 from ensdomains/subgraph-indexing-errors
Browse files Browse the repository at this point in the history
Add ability to handle subgraph indexing error
  • Loading branch information
storywithoutend authored May 19, 2023
2 parents 0c149b8 + 7013de7 commit 7a79f7e
Show file tree
Hide file tree
Showing 16 changed files with 793 additions and 71 deletions.
17 changes: 17 additions & 0 deletions packages/ensjs/src/GqlManager.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { parse, print, visit } from 'graphql'
import traverse from 'traverse'
import { ClientError } from 'graphql-request'

import { requestMiddleware, responseMiddleware } from './GqlManager'
import { namehash } from './utils/normalise'
Expand Down Expand Up @@ -170,4 +171,20 @@ query getNames($id: ID!, $expiryDate: Int) {
)
})
})

describe('errors', () => {
beforeAll(() => {
process.env.NEXT_PUBLIC_ENSJS_DEBUG = 'on'
localStorage.setItem('ensjs-debug', 'ENSJSSubgraphError')
})

afterAll(() => {
process.env.NEXT_PUBLIC_ENSJS_DEBUG = ''
localStorage.removeItem('ensjs-debug')
})

it('should throw error when reqest middleware is run', async () => {
expect(requestMiddleware(visit, parse, print)).toThrow(ClientError)
})
})
})
7 changes: 7 additions & 0 deletions packages/ensjs/src/GqlManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
} from 'graphql'
import type { gql, GraphQLClient } from 'graphql-request'
import type Traverse from 'traverse'
import { debugSubgraphError } from './utils/errors'
import { namehash } from './utils/normalise'

const generateSelection = (selection: any) => ({
Expand Down Expand Up @@ -42,6 +43,9 @@ export const enter = (node: SelectionSetNode) => {
export const requestMiddleware =
(visit: typeof Visit, parse: typeof Parse, print: typeof Print) =>
(request: any) => {
// Debug here because response middleware will resolve any error thrown
debugSubgraphError(request)

const requestBody = JSON.parse(request.body)
const rawQuery = requestBody.query
const parsedQuery = parse(rawQuery)
Expand All @@ -60,6 +64,9 @@ export const requestMiddleware =

export const responseMiddleware =
(traverse: typeof Traverse) => (response: any) => {
// If response is of type error, we do not need to further process it
if (response instanceof Error) return response

// eslint-disable-next-line func-names
traverse(response).forEach(function (responseItem: any) {
if (responseItem instanceof Object && responseItem.name) {
Expand Down
41 changes: 41 additions & 0 deletions packages/ensjs/src/functions/batch.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ENS } from '..'
import setup from '../tests/setup'
import { ENSJSError } from '../utils/errors'

let ensInstance: ENS

Expand Down Expand Up @@ -33,4 +34,44 @@ describe('batch', () => {
expect(result[0]).toBe('Hello2')
}
})

describe('errors', () => {
beforeAll(() => {
process.env.NEXT_PUBLIC_ENSJS_DEBUG = 'on'
localStorage.setItem('ensjs-debug', 'ENSJSSubgraphError')
})

afterAll(() => {
process.env.NEXT_PUBLIC_ENSJS_DEBUG = ''
localStorage.removeItem('ensjs-debug')
})

it('should throw a single error if there is an indexing error', async () => {
try {
await ensInstance.batch(
ensInstance.getText.batch('with-profile.eth', 'description'),
ensInstance.getOwner.batch('expired.eth', { skipGraph: false }),
ensInstance.getName.batch(
'0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC',
),
)
expect(true).toBe(false)
} catch (e) {
expect(e).toBeInstanceOf(ENSJSError)
const error = e as ENSJSError<any[]>
expect(error.name).toBe('ENSJSSubgraphError')
const result = error.data as any[]
expect(result[0]).toBe('Hello2')
expect(result[1]).toEqual({
expired: true,
owner: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
ownershipLevel: 'registrar',
})
expect(result[2]).toMatchObject({
name: 'with-profile.eth',
match: true,
})
}
})
})
})
32 changes: 31 additions & 1 deletion packages/ensjs/src/functions/batch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TransactionRequest } from '@ethersproject/abstract-provider'
import { BatchFunctionResult, ENSArgs, RawFunction } from '..'
import { ENSJSError } from '../utils/errors'

const raw = async (
{ multicallWrapper }: ENSArgs<'multicallWrapper'>,
Expand All @@ -26,7 +27,7 @@ const decode = async (
const response = await multicallWrapper.decode(data, passthrough)
if (!response) return

return Promise.all(
const results = await Promise.allSettled(
response.map((ret: any, i: number) => {
if (passthrough[i].passthrough) {
return items[i].decode(
Expand All @@ -38,6 +39,35 @@ const decode = async (
return items[i].decode(ret.returnData, ...items[i].args)
}),
)

const reducedResults = results.reduce<{
errors: any[]
data: any[]
}>(
(acc, result) => {
if (result.status === 'fulfilled') {
return { ...acc, data: [...acc.data, result.value] }
}
const error =
result.reason instanceof ENSJSError
? (result.reason as ENSJSError<any>)
: undefined
const itemData = error?.data
const itemErrors = error?.errors || [{ message: 'unknown_error' }]
return {
errors: [...acc.errors, ...itemErrors],
data: [...acc.data, itemData],
}
},
{ data: [], errors: [] },
)

if (reducedResults.errors.length)
throw new ENSJSError({
data: reducedResults.data,
errors: reducedResults.errors,
})
return reducedResults.data
}

export default {
Expand Down
25 changes: 25 additions & 0 deletions packages/ensjs/src/functions/getHistory.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ENS } from '..'
import setup from '../tests/setup'
import { ENSJSError } from '../utils/errors'
import { ReturnData } from './getHistory'

let ensInstance: ENS
let revert: Awaited<ReturnType<typeof setup>>['revert']
Expand Down Expand Up @@ -46,4 +48,27 @@ describe('getHistory', () => {
expect(result).not.toHaveProperty('registration')
}
})

describe('errors', () => {
beforeAll(() => {
process.env.NEXT_PUBLIC_ENSJS_DEBUG = 'on'
localStorage.setItem('ensjs-debug', 'ENSJSSubgraphError')
})

afterAll(() => {
process.env.NEXT_PUBLIC_ENSJS_DEBUG = ''
localStorage.removeItem('ensjs-debug')
})

it('should throw an error with no data', async () => {
try {
await ensInstance.getHistory('with-profile.eth')
expect(true).toBeFalsy()
} catch (e) {
expect(e).toBeInstanceOf(ENSJSError)
const error = e as ENSJSError<ReturnData>
expect(error.data).toBeUndefined()
}
})
})
})
39 changes: 28 additions & 11 deletions packages/ensjs/src/functions/getHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { formatsByCoinType } from '@ensdomains/address-encoder'
import { hexStripZeros } from '@ethersproject/bytes'
import { ENSArgs } from '..'
import { decodeContenthash } from '../utils/contentHash'
import {
debugSubgraphLatency,
ENSJSError,
getClientErrors,
} from '../utils/errors'
import { namehash } from '../utils/normalise'
import {
AbiChanged,
Expand Down Expand Up @@ -148,6 +153,14 @@ const mapEvents = <T extends EventTypes>(eventArray: any[], type: T) =>
}),
)

type MappedEvents = ReturnType<typeof mapEvents>

export type ReturnData = {
domain: MappedEvents
registration?: MappedEvents
resolver: MappedEvents
}

export async function getHistory(
{ gqlInstance }: ENSArgs<'gqlInstance'>,
name: string,
Expand Down Expand Up @@ -279,17 +292,23 @@ export async function getHistory(
const labels = name.split('.')
const is2ldEth = checkIsDotEth(labels)

const response = await client.request(query, {
namehash: nameHash,
})
const response = await client
.request(query, {
namehash: nameHash,
})
.catch((e: unknown) => {
throw new ENSJSError({
errors: getClientErrors(e),
})
})
.finally(debugSubgraphLatency)

const domain = response?.domain

if (!domain) return
if (!domain) return undefined

const {
events: domainEvents,
resolver: { events: resolverEvents },
} = domain
const domainEvents = domain.events || []
const resolverEvents = domain.resolver?.events || []

const domainHistory = mapEvents(domainEvents, 'Domain')
const resolverHistory = mapEvents(
Expand All @@ -301,9 +320,7 @@ export async function getHistory(
)

if (is2ldEth) {
const {
registration: { events: registrationEvents },
} = domain
const registrationEvents = domain.registration?.events || []
const registrationHistory = mapEvents(registrationEvents, 'Registration')
return {
domain: domainHistory,
Expand Down
28 changes: 28 additions & 0 deletions packages/ensjs/src/functions/getNames.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ENS } from '..'
import setup from '../tests/setup'
import { Name } from './getNames'
import { names as wrappedNames } from '../../deploy/00_register_wrapped'
import { ENSJSError } from '../utils/errors'

let ensInstance: ENS
let provider: ethers.providers.JsonRpcProvider
Expand Down Expand Up @@ -390,4 +391,31 @@ describe('getNames', () => {
})
})
})

describe('error', () => {
beforeAll(() => {
process.env.NEXT_PUBLIC_ENSJS_DEBUG = 'on'
localStorage.setItem('ensjs-debug', 'ENSJSSubgraphError')
})

afterAll(() => {
process.env.NEXT_PUBLIC_ENSJS_DEBUG = ''
localStorage.removeItem('ensjs-debug')
})

it('should throw an ENSJSError for type "all"', async () => {
try {
await ensInstance.getNames({
address: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC',
type: 'all',
})
expect(true).toBeFalsy()
} catch (e) {
expect(e).toBeInstanceOf(ENSJSError)
const error = e as ENSJSError<Name[]>
expect(error.name).toBe('ENSJSSubgraphError')
expect(error.data?.length).toBe(0)
}
})
})
})
21 changes: 19 additions & 2 deletions packages/ensjs/src/functions/getNames.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { ENSArgs } from '..'
import {
getClientErrors,
ENSJSError,
debugSubgraphLatency,
} from '../utils/errors'
import { truncateFormat } from '../utils/format'
import { AllCurrentFuses, checkPCCBurned, decodeFuses } from '../utils/fuses'
import { decryptName } from '../utils/labels'
Expand Down Expand Up @@ -466,10 +471,20 @@ const getNames = async (
}
}

const response = await client.request(finalQuery, queryVars)
const response = await client
.request(finalQuery, queryVars)
.catch((e: unknown) => {
console.error(e)
throw new ENSJSError({
errors: getClientErrors(e),
data: [],
})
})
.finally(debugSubgraphLatency)
const account = response?.account

if (type === 'all') {
return [
const data = [
...(account?.domains.map(mapDomain) || []),
...(account?.registrations.map(mapRegistration) || []),
...(account?.wrappedDomains.map(mapWrappedDomain).filter((d: any) => d) ||
Expand All @@ -486,6 +501,7 @@ const getNames = async (
}
return a.createdAt.getTime() - b.createdAt.getTime()
}) as Name[]
return data
}
if (type === 'resolvedAddress') {
return (response?.domains.map(mapResolvedAddress).filter((d: any) => d) ||
Expand All @@ -499,6 +515,7 @@ const getNames = async (
.map(mapWrappedDomain)
.filter((d: any) => d) || []) as Name[]
}

return (account?.registrations.map(mapRegistration) || []) as Name[]
}

Expand Down
Loading

0 comments on commit 7a79f7e

Please sign in to comment.