From 8b2f4c960a47ea6109b15ccbf243cad9c3ad6691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Tanr=C4=B1kulu?= Date: Fri, 17 Feb 2023 02:28:42 +0100 Subject: [PATCH 1/5] add batch query support & do batch subraph call --- mock/entry.mock.ts | 48 ++++++++++------------------- src/service/domain.test.ts | 57 +++++++++++++++++------------------ src/service/domain.ts | 43 +++++++++++++------------- src/service/network.ts | 2 +- src/service/subgraph.ts | 24 +++++++-------- src/utils/batchQuery.test.ts | 30 +++++++++++++++++++ src/utils/batchQuery.ts | 58 ++++++++++++++++++++++++++++++++++++ 7 files changed, 167 insertions(+), 95 deletions(-) create mode 100644 src/utils/batchQuery.test.ts create mode 100644 src/utils/batchQuery.ts diff --git a/mock/entry.mock.ts b/mock/entry.mock.ts index 4e04a8e..ffbcb7e 100644 --- a/mock/entry.mock.ts +++ b/mock/entry.mock.ts @@ -5,6 +5,7 @@ import { ADDRESS_NAME_WRAPPER } from '../src/config'; import { Metadata } from '../src/service/metadata'; import getNetwork from '../src/service/network'; import { decodeFuses } from '../src/utils/fuse'; +import { createBatchQuery } from '../src/utils/batchQuery'; import { GET_DOMAINS, GET_REGISTRATIONS, @@ -50,7 +51,7 @@ export class MockEntry { if (!registered) { this.expect = 'No results found.'; nock(SUBGRAPH_URL.origin) - .post(SUBGRAPH_URL.pathname, { + .post(SUBGRAPH_URL.pathname + SUBGRAPH_URL.search, { query: GET_DOMAINS, variables: { tokenId: this.namehash, @@ -74,7 +75,7 @@ export class MockEntry { }); this.expect = JSON.parse(JSON.stringify(unknownMetadata)); nock(SUBGRAPH_URL.origin) - .post(SUBGRAPH_URL.pathname, { + .post(SUBGRAPH_URL.pathname + SUBGRAPH_URL.search, { query: GET_DOMAINS, variables: { tokenId: this.namehash, @@ -143,19 +144,6 @@ export class MockEntry { display_type: 'date', value: expiryDate * 1000, }); - - nock(SUBGRAPH_URL.origin) - .post(SUBGRAPH_URL.pathname, { - query: GET_REGISTRATIONS, - variables: { - labelhash, - }, - operationName: 'getRegistration', - }) - .reply(statusCode, { - data: this.registrationResponse, - }) - .persist(persist); } if (version === Version.v2) { @@ -177,33 +165,29 @@ export class MockEntry { display_type: 'date', value: expiryDate * 1000, }); - - nock(SUBGRAPH_URL.origin) - .post(SUBGRAPH_URL.pathname, { - query: GET_WRAPPED_DOMAIN, - variables: { - tokenId: this.namehash, - }, - operationName: 'getWrappedDomain', - }) - .reply(statusCode, { - data: this.wrappedDomainResponse, - }) - .persist(persist); } this.expect = JSON.parse(JSON.stringify(_metadata)); //todo: find better serialization option + const newBatchQuery = createBatchQuery('getDomainInfo') + .add(GET_DOMAINS) + .add(GET_REGISTRATIONS) + .add(GET_WRAPPED_DOMAIN); + nock(SUBGRAPH_URL.origin) - .post(SUBGRAPH_URL.pathname, { - query: GET_DOMAINS, + .post(SUBGRAPH_URL.pathname + SUBGRAPH_URL.search, { + query: newBatchQuery.query(), variables: { tokenId: this.namehash, }, - operationName: 'getDomains', + operationName: 'getDomainInfo', }) .reply(statusCode, { - data: this.domainResponse, + data: { + ...this.domainResponse, + ...this.registrationResponse, + ...this.wrappedDomainResponse, + }, }) .persist(persist); } diff --git a/src/service/domain.test.ts b/src/service/domain.test.ts index 7eb1868..040169f 100644 --- a/src/service/domain.test.ts +++ b/src/service/domain.test.ts @@ -1,13 +1,18 @@ import avaTest, { ExecutionContext, TestFn } from 'ava'; -import { ethers } from 'ethers'; -import nock from 'nock'; -import { nockProvider } from '../../mock/helper'; -import { TestContext } from '../../mock/interface'; -import { NamehashMismatchError, Version } from '../base'; -import { ADDRESS_ETH_REGISTRAR } from '../config'; -import { getDomain } from './domain'; -import getNetwork from './network'; -import { GET_DOMAINS_BY_LABELHASH, GET_REGISTRATIONS } from './subgraph'; +import { ethers } from 'ethers'; +import nock from 'nock'; +import { nockProvider } from '../../mock/helper'; +import { TestContext } from '../../mock/interface'; +import { NamehashMismatchError, Version } from '../base'; +import { ADDRESS_ETH_REGISTRAR } from '../config'; +import { createBatchQuery } from '../utils/batchQuery'; +import { getDomain } from './domain'; +import getNetwork from './network'; +import { + GET_DOMAINS_BY_LABELHASH, + GET_REGISTRATIONS, + GET_WRAPPED_DOMAIN, +} from './subgraph'; const test = avaTest as TestFn; const NETWORK = 'mainnet'; @@ -60,15 +65,20 @@ test.before(async (t: ExecutionContext) => { } ); + const newBatchQuery = createBatchQuery('getDomainInfo') + .add(GET_DOMAINS_BY_LABELHASH) + .add(GET_REGISTRATIONS) + .add(GET_WRAPPED_DOMAIN); + // fake vitalik.eth with nullifier nock(SUBGRAPH_URL.origin) - .post(SUBGRAPH_URL.pathname, { - query: GET_DOMAINS_BY_LABELHASH, + .post(SUBGRAPH_URL.pathname + SUBGRAPH_URL.search, { + query: newBatchQuery.query(), variables: { tokenId: '0x3581397a478dcebdc1ee778deed625697f624c6f7dbed8bb7f780a6ac094b772', }, - operationName: 'getDomains', + operationName: 'getDomainInfo', }) .reply(200, { data: { @@ -85,18 +95,20 @@ test.before(async (t: ExecutionContext) => { resolver: null, }, ], + registrations: [], + wrappedDomain: null, }, }); // original vitalik.eth nock(SUBGRAPH_URL.origin) - .post(SUBGRAPH_URL.pathname, { - query: GET_DOMAINS_BY_LABELHASH, + .post(SUBGRAPH_URL.pathname + SUBGRAPH_URL.search, { + query: newBatchQuery.query(), variables: { tokenId: '0xaf2caa1c2ca1d027f1ac823b529d0a67cd144264b2789fa2ea4d63a67c7103cc', }, - operationName: 'getDomains', + operationName: 'getDomainInfo', }) .reply(200, { data: { @@ -116,20 +128,6 @@ test.before(async (t: ExecutionContext) => { }, }, ], - }, - }); - - nock(SUBGRAPH_URL.origin) - .post(SUBGRAPH_URL.pathname, { - query: GET_REGISTRATIONS, - variables: { - labelhash: - '0xaf2caa1c2ca1d027f1ac823b529d0a67cd144264b2789fa2ea4d63a67c7103cc', - }, - operationName: 'getRegistration', - }) - .reply(200, { - data: { registrations: [ { labelName: 'vitalik', @@ -137,6 +135,7 @@ test.before(async (t: ExecutionContext) => { expiryDate: '2032977474', }, ], + wrappedDomain: null, }, }); }); diff --git a/src/service/domain.ts b/src/service/domain.ts index 86a8df4..e212f4a 100644 --- a/src/service/domain.ts +++ b/src/service/domain.ts @@ -1,22 +1,23 @@ -import { request } from 'graphql-request'; -import { ethers } from 'ethers'; +import request from 'graphql-request'; +import { ethers } from 'ethers'; import { GET_REGISTRATIONS, GET_DOMAINS, GET_DOMAINS_BY_LABELHASH, GET_WRAPPED_DOMAIN, -} from './subgraph'; -import { Metadata } from './metadata'; -import { getAvatarImage } from './avatar'; +} from './subgraph'; +import { Metadata } from './metadata'; +import { getAvatarImage } from './avatar'; import { ExpiredNameError, NamehashMismatchError, SubgraphRecordNotFound, Version, -} from '../base'; -import { NetworkName } from './network'; -import { decodeFuses } from '../utils/fuse'; -import { getNamehash } from '../utils/namehash'; +} from '../base'; +import { NetworkName } from './network'; +import { decodeFuses } from '../utils/fuse'; +import { getNamehash } from '../utils/namehash'; +import { createBatchQuery } from '../utils/batchQuery'; const eth = '0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae'; @@ -44,11 +45,16 @@ export async function getDomain( } const queryDocument: string = version !== Version.v2 ? GET_DOMAINS_BY_LABELHASH : GET_DOMAINS; - const result = await request(SUBGRAPH_URL, queryDocument, { tokenId: hexId }); - const domain = version !== Version.v2 ? result.domains[0] : result.domain; + + const newBatch = createBatchQuery('getDomainInfo'); + newBatch.add(queryDocument).add(GET_REGISTRATIONS).add(GET_WRAPPED_DOMAIN); + + const domainQueryResult = await request(SUBGRAPH_URL, newBatch.query(), { tokenId: hexId }); + + const domain = version !== Version.v2 ? domainQueryResult.domains[0] : domainQueryResult.domain; if (!(domain && Object.keys(domain).length)) throw new SubgraphRecordNotFound(`No record for ${hexId}`); - const { name, labelhash, createdAt, parent, resolver, id: namehash } = domain; + const { name, createdAt, parent, resolver, id: namehash } = domain; /** * IMPORTANT @@ -104,11 +110,8 @@ export async function getDomain( } async function requestAttributes() { - if (parent.id === eth) { - const { registrations } = await request(SUBGRAPH_URL, GET_REGISTRATIONS, { - labelhash, - }); - const registration = registrations[0]; + if (parent.id === eth && domainQueryResult.registrations?.length) { + const registration = domainQueryResult.registrations[0]; const registered_date = registration.registrationDate * 1000; const expiration_date = registration.expiryDate * 1000; if (expiration_date + GRACE_PERIOD_MS < +new Date()) { @@ -133,12 +136,10 @@ export async function getDomain( } } - if (version === Version.v2) { + if (version === Version.v2 && domainQueryResult.wrappedDomain) { const { wrappedDomain: { fuses, expiryDate }, - } = await request(SUBGRAPH_URL, GET_WRAPPED_DOMAIN, { - tokenId: namehash, - }); + } = domainQueryResult; metadata.addAttribute({ trait_type: 'Namewrapper Fuse States', display_type: 'object', diff --git a/src/service/network.ts b/src/service/network.ts index df40f3d..e356489 100644 --- a/src/service/network.ts +++ b/src/service/network.ts @@ -74,5 +74,5 @@ export default function getNetwork(network: NetworkName): { throw new UnsupportedNetwork(`Unknown network '${network}'`, 501); } const provider = new ethers.providers.StaticJsonRpcProvider(WEB3_URL); - return { WEB3_URL, SUBGRAPH_URL, provider }; + return { WEB3_URL, SUBGRAPH_URL: `${SUBGRAPH_URL}?source=ens-metadata`, provider }; } diff --git a/src/service/subgraph.ts b/src/service/subgraph.ts index 53b9959..4246cd9 100644 --- a/src/service/subgraph.ts +++ b/src/service/subgraph.ts @@ -45,11 +45,11 @@ export const GET_DOMAINS_BY_LABELHASH = gql` `; export const GET_REGISTRATIONS = gql` - query getRegistration($labelhash: String) { + query getRegistration($tokenId: String) { registrations( orderBy: registrationDate orderDirection: desc - where: { id: $labelhash } + where: { id: $tokenId } ) { labelName registrationDate @@ -59,17 +59,17 @@ export const GET_REGISTRATIONS = gql` `; export const GET_WRAPPED_DOMAIN = gql` -query getWrappedDomain($tokenId: String) { - wrappedDomain(id: $tokenId) { - id - owner { + query getWrappedDomain($tokenId: String) { + wrappedDomain(id: $tokenId) { id - } - fuses - expiryDate - domain { - name + owner { + id + } + fuses + expiryDate + domain { + name + } } } -} `; diff --git a/src/utils/batchQuery.test.ts b/src/utils/batchQuery.test.ts new file mode 100644 index 0000000..452c47a --- /dev/null +++ b/src/utils/batchQuery.test.ts @@ -0,0 +1,30 @@ +import avaTest, { ExecutionContext, TestFn } from 'ava'; +import { gql } from 'graphql-request'; +import { TestContext } from '../../mock/interface'; +import { createBatchQuery } from './batchQuery'; + +const test = avaTest as TestFn; + +test('should retrieve letter character set for nick.eth', (t: ExecutionContext) => { + const query1 = gql` + query query1($id: String) { + domain(id: $id) { + name + } + } + `; + const query2 = gql` + query query2($name: String) { + registry(name: $name) { + id + } + } + `; + const batchedQuery = createBatchQuery('combinedQuery'); + batchedQuery.add(query1).add(query2); + console.log(batchedQuery.query()); + t.deepEqual( + batchedQuery.query(), + 'query combinedQuery($id:String, $name:String) { domain(id: $id) { name },registry(name: $name) { id } }' + ); +}); diff --git a/src/utils/batchQuery.ts b/src/utils/batchQuery.ts new file mode 100644 index 0000000..3d5837b --- /dev/null +++ b/src/utils/batchQuery.ts @@ -0,0 +1,58 @@ +import { + DocumentNode, + OperationDefinitionNode, + VariableDefinitionNode, + parse, + print, +} from 'graphql'; +import { gql } from 'graphql-request'; + +class BatchedQuery { + documentNodes: DocumentNode[] = []; + queryName: string = ''; + constructor(queryName: string) { + this.queryName = queryName; + } + + add(document: string) { + if (!document) throw Error('Parameters cannot be empty.'); + const documentNode: DocumentNode = parse(document); + this.documentNodes.push(documentNode); + return this; + } + + _genNodes() { + const variables = new Set(); + const documentNodes: string[] = []; + this.documentNodes.forEach((documentNode: DocumentNode) => { + const vars = ( + documentNode.definitions[0] as OperationDefinitionNode + ).variableDefinitions + ?.map( + (def: VariableDefinitionNode) => + `$${def.variable.name.value}:${(def.type as any).name.value}` + ) + .toString(); + variables.add(vars); + const node = print(documentNode) + .replace(/query.*\{/, '') + .slice(0, -1) + .trim(); + documentNodes.push(node); + }); + return [[...variables].join(', '), documentNodes]; + } + + query() { + const [variables, documentNodes] = this._genNodes(); + return gql` + query ${this.queryName}(${variables}) { + ${documentNodes} + } + `.replace(/\n/g, '').replace(/\s\s+/g, ' ').trim(); + } +} + +export function createBatchQuery(queryName: string) { + return new BatchedQuery(queryName); +} From a38e686d9d3688ee34882aae20a3bc1116625ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Tanr=C4=B1kulu?= Date: Thu, 18 Apr 2024 17:25:08 +0200 Subject: [PATCH 2/5] update metadata mock --- mock/entry.mock.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mock/entry.mock.ts b/mock/entry.mock.ts index 87bf43b..41a89d0 100644 --- a/mock/entry.mock.ts +++ b/mock/entry.mock.ts @@ -171,6 +171,15 @@ export class MockEntry { display_type: 'date', value: expiryDate * 1000, }); + _metadata.addAttribute({ + trait_type: 'Namewrapper State', + display_type: 'string', + value: getWrapperState(decodedFuses), + }); + + _metadata.description += _metadata.generateRuggableWarning( + _metadata.name, version, getWrapperState(decodedFuses) + ) } this.expect = JSON.parse(JSON.stringify(_metadata)); //todo: find better serialization option From e7e32052c01e4e094f34ea5a198dbbb63b676cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Tanr=C4=B1kulu?= Date: Mon, 6 May 2024 14:02:10 +0200 Subject: [PATCH 3/5] merge main into the pr, update subgraph mock with batch support --- mock/entry.mock.ts | 56 +++++++++++++++++++---------------- src/controller/ensMetadata.ts | 1 - src/service/metadata.ts | 2 +- src/utils/batchQuery.test.ts | 1 - 4 files changed, 32 insertions(+), 28 deletions(-) diff --git a/mock/entry.mock.ts b/mock/entry.mock.ts index 41a89d0..fa84b6f 100644 --- a/mock/entry.mock.ts +++ b/mock/entry.mock.ts @@ -1,27 +1,23 @@ -import { namehash } from '@ensdomains/ensjs/utils/normalise'; -import { - keccak256, - toUtf8Bytes -} from 'ethers'; -import nock from 'nock'; -import { Version } from '../src/base'; -import { ADDRESS_NAME_WRAPPER } from '../src/config'; -import { Metadata } from '../src/service/metadata'; -import getNetwork from '../src/service/network'; -import { createBatchQuery } from '../src/utils/batchQuery'; -import { decodeFuses, getWrapperState } from '../src/utils/fuse'; +import { namehash } from '@ensdomains/ensjs/utils/normalise'; +import { keccak256, toUtf8Bytes } from 'ethers'; +import nock from 'nock'; +import { Version } from '../src/base'; +import { ADDRESS_NAME_WRAPPER } from '../src/config'; +import { Metadata } from '../src/service/metadata'; +import getNetwork from '../src/service/network'; +import { createBatchQuery } from '../src/utils/batchQuery'; +import { decodeFuses, getWrapperState } from '../src/utils/fuse'; import { GET_DOMAINS, GET_REGISTRATIONS, GET_WRAPPED_DOMAIN, -} from '../src/service/subgraph'; +} from '../src/service/subgraph'; import { DomainResponse, MockEntryBody, RegistrationResponse, WrappedDomainResponse, -} from './interface'; - +} from './interface'; const { SUBGRAPH_URL: subgraph_url } = getNetwork('goerli'); const SUBGRAPH_URL = new URL(subgraph_url); @@ -54,13 +50,17 @@ export class MockEntry { if (!registered) { this.expect = 'No results found.'; + const newBatchQuery = createBatchQuery('getDomainInfo') + .add(GET_DOMAINS) + .add(GET_REGISTRATIONS) + .add(GET_WRAPPED_DOMAIN); nock(SUBGRAPH_URL.origin) - .post(SUBGRAPH_URL.pathname + SUBGRAPH_URL.search, { - query: GET_DOMAINS, + .post(SUBGRAPH_PATH, { + query: newBatchQuery.query(), variables: { tokenId: this.namehash, }, - operationName: 'getDomains', + operationName: 'getDomainInfo', }) .reply(statusCode, { data: null, @@ -78,16 +78,20 @@ export class MockEntry { version: Version.v1, }); this.expect = JSON.parse(JSON.stringify(unknownMetadata)); + const newBatchQuery = createBatchQuery('getDomainInfo') + .add(GET_DOMAINS) + .add(GET_REGISTRATIONS) + .add(GET_WRAPPED_DOMAIN); nock(SUBGRAPH_URL.origin) - .post(SUBGRAPH_URL.pathname + SUBGRAPH_URL.search, { - query: GET_DOMAINS, + .post(SUBGRAPH_PATH, { + query: newBatchQuery.query(), variables: { tokenId: this.namehash, }, - operationName: 'getDomains', + operationName: 'getDomainInfo', }) .reply(statusCode, { - data: { domain: {} }, + data: { domain: {}, registrations: {}, wrappedDomain: {} }, }) .persist(persist); return; @@ -178,8 +182,10 @@ export class MockEntry { }); _metadata.description += _metadata.generateRuggableWarning( - _metadata.name, version, getWrapperState(decodedFuses) - ) + _metadata.name, + version, + getWrapperState(decodedFuses) + ); } this.expect = JSON.parse(JSON.stringify(_metadata)); //todo: find better serialization option @@ -190,7 +196,7 @@ export class MockEntry { .add(GET_WRAPPED_DOMAIN); nock(SUBGRAPH_URL.origin) - .post(SUBGRAPH_URL.pathname + SUBGRAPH_URL.search, { + .post(SUBGRAPH_PATH, { query: newBatchQuery.query(), variables: { tokenId: this.namehash, diff --git a/src/controller/ensMetadata.ts b/src/controller/ensMetadata.ts index 7d63753..7ec0cb4 100644 --- a/src/controller/ensMetadata.ts +++ b/src/controller/ensMetadata.ts @@ -1,7 +1,6 @@ import { strict as assert } from 'assert'; import { Contract } from 'ethers'; import { Request, Response } from 'express'; -import { FetchError } from 'node-fetch'; import { ContractMismatchError, ExpiredNameError, diff --git a/src/service/metadata.ts b/src/service/metadata.ts index 9c67828..e50b562 100644 --- a/src/service/metadata.ts +++ b/src/service/metadata.ts @@ -182,7 +182,7 @@ export class Metadata { try { this.setImage('data:image/svg+xml;base64,' + base64EncodeUnicode(svg)); } catch (e) { - console.log(processedDomain, e); + console.log("generateImage", processedDomain, e); this.setImage(''); } } diff --git a/src/utils/batchQuery.test.ts b/src/utils/batchQuery.test.ts index 452c47a..c169d41 100644 --- a/src/utils/batchQuery.test.ts +++ b/src/utils/batchQuery.test.ts @@ -22,7 +22,6 @@ test('should retrieve letter character set for nick.eth', (t: ExecutionContext Date: Wed, 8 May 2024 10:47:34 +0200 Subject: [PATCH 4/5] remove unused import --- src/service/domain.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/service/domain.ts b/src/service/domain.ts index 14cea4c..744f1e7 100644 --- a/src/service/domain.ts +++ b/src/service/domain.ts @@ -1,4 +1,3 @@ -import { ethers } from 'ethers'; import { request } from 'graphql-request'; import { JsonRpcProvider, From 3b8d7252c464bb8d210c81fffdb749d45c6a0a9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Tanr=C4=B1kulu?= Date: Wed, 8 May 2024 16:48:11 +0200 Subject: [PATCH 5/5] better error message for contract query --- src/service/contract.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/service/contract.ts b/src/service/contract.ts index 123f812..ded8a0b 100644 --- a/src/service/contract.ts +++ b/src/service/contract.ts @@ -38,8 +38,22 @@ async function checkV1Contract( ); assert(isInterfaceSupported); return { tokenId: _tokenId, version: Version.v1w }; - } catch (error) { - console.warn(`checkV1Contract: nft ownership check fails for ${_tokenId}`); + } catch (error: any) { + if ( + // ethers error: given address is not contract, or does not have the supportsInterface method available + error?.info?.method === 'supportsInterface' || + // assert error: given address is a contract but given INAMEWRAPPER interface is not available + (typeof error?.actual === 'boolean' && !error?.actual) + ) { + // fail is expected for regular owners since the owner is not a contract and do not have supportsInterface method + console.warn( + `checkV1Contract: supportsInterface check fails for ${_tokenId}` + ); + } else { + console.warn( + `checkV1Contract: nft ownership check fails for ${_tokenId}` + ); + } } return { tokenId: _tokenId, version: Version.v1 }; }