From a8e97d3e3eff7bef5568937628dcf280ccbf9b6c Mon Sep 17 00:00:00 2001 From: dan13ram Date: Fri, 7 Oct 2022 16:41:52 +0530 Subject: [PATCH] added subgraph health store --- package.json | 2 +- src/graphql/client.ts | 5 +++ src/graphql/health.ts | 75 +++++++++++++++++++++++++++++++++++++++++++ src/graphql/index.ts | 1 + src/helpers.ts | 36 +++++++++++++++++++++ src/index.ts | 3 +- 6 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 src/graphql/health.ts create mode 100644 src/helpers.ts diff --git a/package.json b/package.json index e52ae8e..d2abb59 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@quest-chains/sdk", "description": "An SDK for building applications on top of Quest Chains", - "version": "0.1.11", + "version": "0.1.12", "license": "GPL-3.0", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/graphql/client.ts b/src/graphql/client.ts index 1d24707..220ec07 100644 --- a/src/graphql/client.ts +++ b/src/graphql/client.ts @@ -3,6 +3,7 @@ import { Client, createClient, dedupExchange, fetchExchange } from 'urql'; export type NetworkInfo = { [chainId: string]: { chainId: string; + subgraphName: string; subgraphUrl: string; }; }; @@ -10,18 +11,22 @@ export type NetworkInfo = { export const SUPPORTED_NETWORK_INFO: NetworkInfo = { '0x89': { chainId: '0x89', + subgraphName: 'dan13ram/quest-chains-polygon', subgraphUrl: 'https://api.thegraph.com/subgraphs/name/dan13ram/quest-chains-polygon', }, '0x64': { chainId: '0x64', + subgraphName: 'dan13ram/quest-chains-xdai', subgraphUrl: 'https://api.thegraph.com/subgraphs/name/dan13ram/quest-chains-xdai', }, '0x5': { chainId: '0x5', + subgraphName: 'dan13ram/quest-chains-goerli', subgraphUrl: 'https://api.thegraph.com/subgraphs/name/dan13ram/quest-chains-goerli', }, '0x13881': { chainId: '0x13881', + subgraphName: 'dan13ram/quest-chains-mumbai', subgraphUrl: 'https://api.thegraph.com/subgraphs/name/dan13ram/quest-chains-mumbai', }, }; diff --git a/src/graphql/health.ts b/src/graphql/health.ts new file mode 100644 index 0000000..5c90845 --- /dev/null +++ b/src/graphql/health.ts @@ -0,0 +1,75 @@ +/* eslint-disable no-console, no-await-in-loop */ +import { gql, request } from 'graphql-request'; + +import { SUPPORTED_NETWORK_INFO } from './client'; + +const GRAPH_HEALTH_ENDPOINT = 'https://api.thegraph.com/index-node/graphql'; + +const statusQuery = gql` + query getSubgraphStatus($subgraph: String!) { + status: indexingStatusForCurrentVersion(subgraphName: $subgraph) { + chains { + latestBlock { + number + } + } + } + } +`; + +const getLatestBlock = async (subgraph: string): Promise => { + const data = await request(GRAPH_HEALTH_ENDPOINT, statusQuery, { + subgraph, + }); + return data.status.chains[0].latestBlock.number; +}; + +const UPDATE_INTERVAL = 10000; + +class SubgraphHealthStore { + graphHealth: Record = {}; + + constructor() { + // console.debug('@quest-chains/sdk: health store init'); + this.updateSubgraphHealth(); + } + + public async updateSubgraphHealth() { + await Promise.all( + Object.values(SUPPORTED_NETWORK_INFO).map(async info => { + this.graphHealth[info.chainId] = await getLatestBlock(info.subgraphName); + }), + ); + // console.debug('@quest-chains/sdk: updated graph health:', this.graphHealth); + setTimeout(() => this.updateSubgraphHealth(), UPDATE_INTERVAL); + } + + status() { + return this.graphHealth; + } +} + +const HealthStoreSingleton = (function () { + let instance: SubgraphHealthStore; + + function createInstance() { + return new SubgraphHealthStore(); + } + + return { + getInstance: function () { + if (!instance) { + instance = createInstance(); + } + return instance; + }, + }; +})(); + +const getSubgraphStatus = () => HealthStoreSingleton.getInstance().status(); + +const initSubgraphHealthStore = getSubgraphStatus; + +initSubgraphHealthStore(); + +export const getSubgraphLatestBlock = (chainId: string): number => getSubgraphStatus()[chainId]; diff --git a/src/graphql/index.ts b/src/graphql/index.ts index 8f5d118..84e18ce 100644 --- a/src/graphql/index.ts +++ b/src/graphql/index.ts @@ -7,3 +7,4 @@ export * from './statusForUser'; export * from './rolesForUser'; export * from './types'; export * from './client'; +export * from './health'; diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 0000000..f98cd81 --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,36 @@ +/* eslint-disable no-await-in-loop */ + +import { Log, TransactionReceipt } from '@ethersproject/abstract-provider'; +import { ethers } from 'ethers'; + +import { getSubgraphLatestBlock } from './graphql/health'; + +const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); + +const UPDATE_INTERVAL = 10000; + +const MAX_RETRIES = 6; + +export const waitUntilSubgraphIndexed = async (chainId: string, block: number): Promise => { + let latestBlock = getSubgraphLatestBlock(chainId); + let tries = 0; + while (latestBlock < block && tries < MAX_RETRIES) { + await sleep(UPDATE_INTERVAL); + tries += 1; + latestBlock = getSubgraphLatestBlock(chainId); + } + return latestBlock >= block; +}; + +export const getQuestChainAddressFromTx = async (receipt: TransactionReceipt) => { + if (!receipt || !receipt.logs) return ''; + const abi = new ethers.utils.Interface(['event QuestChainCreated(uint256 id, address questChain)']); + const eventFragment = abi.events[Object.keys(abi.events)[0]]; + const eventTopic = abi.getEventTopic(eventFragment); + const event = receipt.logs.find((e: Log) => e.topics[0] === eventTopic); + if (event) { + const decodedLog = abi.decodeEventLog(eventFragment, event.data, event.topics); + return decodedLog.questChain; + } + return ''; +}; diff --git a/src/index.ts b/src/index.ts index b7ee74b..383e1d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import * as graphql from './graphql'; import * as contracts from './contracts'; import * as metadata from './metadata'; +import * as helpers from './helpers'; -export { graphql, contracts, metadata }; +export { graphql, contracts, metadata, helpers };