+
import { exec } from 'child_process'
+
import util, { ModuleState } from '@cardano-graphql/util'
+
import { GraphQLSchema } from 'graphql'
+
import { GraphQLClient, gql } from 'graphql-request'
+
import pRetry from 'p-retry'
+
import { dummyLogger, Logger } from 'ts-log'
+
import { Asset, Block } from './graphql_types'
+
import { AssetMetadataAndHash, AssetMetadataHashAndId, AssetWithoutTokens } from './typeAliases'
+
import { Schema } from '@cardano-ogmios/client'
+
const epochInformationNotYetAvailable = 'Epoch information not yet available. This is expected during the initial chain-sync.'
+
const withHexPrefix = (value: string) => `\\x${value !== undefined ? value : ''}`
+
export class HasuraBackgroundClient {
+
private client: GraphQLClient
+
private applyingSchemaAndMetadata: boolean
+
private state: ModuleState
+
public schema: GraphQLSchema
+
readonly hasuraCliPath: string,
+
readonly hasuraUri: string,
+
private logger: Logger = dummyLogger
+
this.applyingSchemaAndMetadata = false
+
this.client = new GraphQLClient(
+
`${this.hasuraUri}/v1/graphql`,
+
'X-Hasura-Role': 'cardano-graphql'
+
private async hasuraCli (command: string) {
+
return new Promise((resolve, reject) => {
+
`${this.hasuraCliPath} --skip-update-check --project ${path.resolve(__dirname, '..', 'hasura', 'project')} --endpoint ${this.hasuraUri} ${command}`,
+
if (stdout !== '') this.logger.debug({ module: 'HasuraBackgroundClient' }, stdout)
+
public async initialize () {
+
if (this.state !== null) return
+
this.state = 'initializing'
+
this.logger.info({ module: 'HasuraBackgroundClient' }, 'Initializing')
+
await this.applySchemaAndMetadata()
+
this.logger.debug({ module: 'HasuraBackgroundClient' }, 'graphql-engine setup')
+
await pRetry(async () => {
+
const result = await this.client.request(
+
epochs (limit: 1, order_by: { number: desc }) {
+
if (result.epochs.length === 0) {
+
this.logger.debug({ module: 'HasuraBackgroundClient' }, epochInformationNotYetAvailable)
+
throw new Error(epochInformationNotYetAvailable)
+
onFailedAttempt: util.onFailedAttemptFor(
+
'Detecting DB sync state has reached minimum progress',
+
this.logger.debug({ module: 'HasuraBackgroundClient' }, 'DB sync state has reached minimum progress')
+
this.state = 'initialized'
+
this.logger.info({ module: 'HasuraBackgroundClient' }, 'Initialized')
+
public async shutdown () {
+
public async applySchemaAndMetadata (): Promise<void> {
+
if (this.applyingSchemaAndMetadata) return
+
this.applyingSchemaAndMetadata = true
+
await pRetry(async () => {
+
await this.hasuraCli('migrate --database-name default apply --down all')
+
await this.hasuraCli('migrate --database-name default apply --up all')
+
onFailedAttempt: util.onFailedAttemptFor(
+
'Applying PostgreSQL schema migrations',
+
await pRetry(async () => {
+
await this.hasuraCli('metadata clear')
+
await this.hasuraCli('metadata apply')
+
onFailedAttempt: util.onFailedAttemptFor('Applying Hasura metadata', this.logger)
+
this.applyingSchemaAndMetadata = false
+
public async deleteAssetsAfterSlot (slotNo: Block['slotNo']): Promise<number> {
+
{ module: 'HasuraClient', slotNo },
+
'deleting assets found in tokens after slot'
+
const result = await this.client.request(
+
gql`mutation DeleteAssetsAfterSlot($slotNo: Int!) {
+
return result.delete_assets.affected_rows
+
public async hasAsset (assetId: Asset['assetId']): Promise<boolean> {
+
const result = await this.client.request(
+
where: { assetId: { _eq: $assetId }}
+
assetId: withHexPrefix(assetId)
+
const response = result.assets.length > 0
+
{ module: 'HasuraClient', assetId, hasAsset: response },
+
public async getMostRecentPointWithNewAsset (): Promise<Schema.Point | null> {
+
let point: Schema.Point | null
+
// Handles possible race condition between the internal chain-follower, which manages the Asset table,
+
// and cardano-db-sync's which managed the block table.
+
await pRetry(async () => {
+
// An offset of 1 is applied to ensure a partial block extraction is not skipped
+
const result = await this.client.request(
+
order_by: { firstAppearedInSlot: desc }
+
if (result.errors !== undefined) {
+
throw new Error(result.errors)
+
if (result.assets.length !== 0) {
+
if (result.assets[0].firstAppearedInBlock === null) {
+
throw new Error('cardano-db-sync is lagging behind the asset sync operation.')
+
const { hash, slotNo } = result.assets[0].firstAppearedInBlock
+
hash: hash.substring(2),
+
onFailedAttempt: util.onFailedAttemptFor(