import { BigNumber } from 'ethers'; import { request, gql } from 'graphql-request'; import { Dispatch } from '../messages/types'; import { MessageBackend } from './backend'; import * as config from '@nomad-xyz/configuration'; import { NomadContext } from '..'; import { nulls2undefined } from './utils'; const defaultGoldSkySecret = "mpa%H&RAHu9;eUe"; const supportedEnvironments = [ "production", "development", ]; /** * Filter that is used to fetch data from GoldSky */ export type MessageFilter = { committedRoot: string; messageHash: string; transactionHash: string; }; /** * GoldSky NomadMessage representation */ export type GoldSkyMessage = { committed_root: string; destination_and_nonce: string; destination_domain_id: number; destination_domain_name: string; dispatch_block: string; dispatch_tx: string; dispatched_at: string; id: string; leaf_index: string; message: string; message__action__amount: string; message__action__details_hash: string; message__action__to: string; message__action__type: string; message__token__domain: string; message__token__id: string; message_body: string; message_hash: string; message_type: string; new_root: string; nonce: number; old_root: string; origin_domain_id: number; origin_domain_name: string; recipient_address: string; sender_address: string; signature: string; update_block?: string; update_chain_id?: number; update_tx?: string; updated_at?: string; relay_block?: string; relay_chain_id?: number; relay_tx?: string; relayed_at?: string; process_block?: string; process_tx?: string; processed_at?: string; }; /** * GoldSky backend for NomadMessage */ export class GoldSkyBackend extends MessageBackend { env: string; // Unused yet, because GoldSky only has Production data protected _secret: string; context: NomadContext; messageCache: Map; dispatchTxToMessageHash: Map; constructor(env: string, secret: string, context: NomadContext) { super(); this.env = env; this._secret = secret; this.messageCache = new Map(); this.dispatchTxToMessageHash = new Map(); this.context = context; } /** * Resolves current context used with the backend * @returns Nomad context */ getContext(): NomadContext { return this.context; } /** * Checks whether the backend environment is supported * @param environment name of the environment as string */ static checkEnvironment(environment: string): void { if (!supportedEnvironments.includes(environment)) { throw new Error(`Only the following environments are supported: ${supportedEnvironments.join(', ')}. Provided: ${environment}`); } } /** * Returns default secret for Goldsky * @returns secret as a string */ static defaultSecret(): string { return defaultGoldSkySecret; } /** * Default GoldSky backend for the environment * @param environment name of the environment as string * @param context Nomad context to be used with the backend * @returns */ static default( environment: string | config.NomadConfig = 'development', context?: NomadContext, ): GoldSkyBackend { const environmentString = typeof environment === 'string' ? environment : environment.environment; GoldSkyBackend.checkEnvironment(environmentString); const secret = process.env.GOLDSKY_SECRET || GoldSkyBackend.defaultSecret(); if (!secret) throw new Error(`GOLDSKY_SECRET not found in env`); return new GoldSkyBackend( environmentString, secret, context || new NomadContext(environment), ); } /** * Fills filter with default values required for fetching a message from backend * * @returns Filled event filter */ static fillFilter(f: Partial): MessageFilter { return { committedRoot: f.committedRoot || '', messageHash: f.messageHash || '', transactionHash: f.transactionHash || '', }; } /** * Prepares a URI that is used for fetching messages * * @returns uri */ get uri(): string { // return `https://${this.env}.goldsky.io/c/nomad/gql/v1/graphql` return `https://api.goldsky.io/c/nomad/gql/v1/graphql`; } /** * Prepares headers for connecting to hasura */ get headers(): { 'content-type': string; 'x-goldsky-secret': string; } { return { 'content-type': 'application/json', 'x-goldsky-secret': this._secret, }; } /** * Prepares Dispatch events from backend's internal message representation * * @returns Dispatch events assiciated with transaction (if any) */ async getDispatches( tx: string, limit?: number, ): Promise { const ms = await this.getMessagesByTx(tx, limit); if (!ms) return undefined; return ms.map((m) => ({ args: { messageHash: m.message_hash, leafIndex: BigNumber.from(m.leaf_index), destinationAndNonce: BigNumber.from(m.destination_and_nonce), committedRoot: m.committed_root, message: m.message, }, transactionHash: m.dispatch_tx, })); } /** * Prepares a Dispatch event from backend's internal message representation * * @returns Dispatch event assiciated with message hash (if any) */ async getDispatchByMessageHash( messageHash: string, ): Promise { const ms = await this.getMessage(messageHash); if (!ms) return; return ({ args: { messageHash: ms?.message_hash, leafIndex: BigNumber.from(ms.leaf_index), destinationAndNonce: BigNumber.from(ms.destination_and_nonce), committedRoot: ms.committed_root, message: ms.message, }, transactionHash: ms.dispatch_tx, }); } /** * Stores message into internal cache */ storeMessage(m: GoldSkyMessage): void { this.messageCache.set(m.message_hash, m); const messageHashes = this.dispatchTxToMessageHash.get(m.dispatch_tx); if (!messageHashes) { this.dispatchTxToMessageHash.set(m.dispatch_tx, [m.message_hash]); } else { if (!messageHashes.includes(m.message_hash)) messageHashes.push(m.message_hash); } } /** * Get the message representation associated with this message (if any) * by message hash * * @returns A message representation (if any) */ async getMessage( messageHash: string, forceFetch = false, ): Promise { let m = this.messageCache.get(messageHash); if (!m || forceFetch) { m = ( await this.fetchMessages( { messageHash, }, 1, ) )?.[0]; if (m) { this.storeMessage(m); } } return m; } /** * Get the message representation associated with this message (if any) * by dispatch transaction * * @returns A message representation (if any) */ async getMessagesByTx( tx: string, limit?: number, forceFetch = true, ): Promise { let ms: GoldSkyMessage[] | undefined; const messageHashes = this.dispatchTxToMessageHash.get(tx); const enoughMessages = limit && messageHashes && limit <= messageHashes.length; if (!enoughMessages || forceFetch) { ms = await this.fetchMessages({ transactionHash: tx, }); if (ms && ms.length) { ms.forEach((m) => this.storeMessage(m)); } } else { if (!messageHashes) throw new Error('MessageHashes are unexpectedly not existing'); ms = await Promise.all( messageHashes.map(async (hash) => { const message = await this.getMessage(hash); if (!message) throw new Error("Couldn't get a message from existing messages."); // Message must be in messageHashes return message; }), ); } return ms; } /** * Get the `Dispatch` transaction hash associated with this message (if any) * * @returns A dispatch tx (if any) */ async dispatchTx(messageHash: string): Promise { let m = await this.getMessage(messageHash); if (!m?.dispatch_tx) m = await this.getMessage(messageHash, true); return m?.dispatch_tx; } /** * Get the `Relay` transaction hash associated with this message (if any) * * @returns A relay tx (if any) */ async relayTx(messageHash: string): Promise { let m = await this.getMessage(messageHash); if (!m?.relay_tx) m = await this.getMessage(messageHash, true); return m?.relay_tx; } /** * Get the `Update` transaction hash associated with this message (if any) * * @returns A update tx (if any) */ async updateTx(messageHash: string): Promise { let m = await this.getMessage(messageHash); if (!m?.update_tx) m = await this.getMessage(messageHash, true); return m?.update_tx; } /** * Get the `Process` transaction hash associated with this message (if any) * * @returns A relay tx (if any) */ async processTx(messageHash: string): Promise { let m = await this.getMessage(messageHash); if (!m?.process_tx) m = await this.getMessage(messageHash, true); return m?.process_tx; } /** * Get the `Dispatch` transaction hash associated with this message (if any) * * @returns A dispatch tx (if any) */ async dispatchedAt(messageHash: string): Promise { let m = await this.getMessage(messageHash); if (!m?.dispatched_at) m = await this.getMessage(messageHash, true); return m ? new Date(m.dispatched_at) : undefined; } /** * Get the `Relay` transaction hash associated with this message (if any) * * @returns A relay tx (if any) */ async relayedAt(messageHash: string): Promise { let m = await this.getMessage(messageHash); if (!m?.relayed_at) m = await this.getMessage(messageHash, true); return m && m.relayed_at ? new Date(m.relayed_at) : undefined; } /** * Get the `Update` transaction hash associated with this message (if any) * * @returns A update tx (if any) */ async updatedAt(messageHash: string): Promise { let m = await this.getMessage(messageHash); if (!m?.updated_at) m = await this.getMessage(messageHash, true); return m && m.updated_at ? new Date(m.updated_at) : undefined; } /** * Get the `Process` transaction hash associated with this message (if any) * * @returns A relay tx (if any) */ async processedAt(messageHash: string): Promise { let m = await this.getMessage(messageHash); if (!m?.processed_at) m = await this.getMessage(messageHash, true); return m && m.processed_at ? new Date(m.processed_at) : undefined; } /** * Gets destination domain for a specific message * @param messageHash message hash identifier * @returns destination domain id */ async destinationDomainId(messageHash: string): Promise { let m = await this.getMessage(messageHash); if (!m?.destination_domain_id) m = await this.getMessage(messageHash, true); return m?.destination_domain_id; } /** * Get message hash associated with this message (if any) * * @returns A message hash (if any) */ async getFirstMessageHash(tx: string): Promise { const ms = await this.getMessagesByTx(tx); return ms?.[0]?.message_hash; } /** * Fetches internal message from backend * * @returns Internal message representation (if any) */ async fetchMessages( f: Partial, limit?: number, ): Promise { const eventsTable = `${this.env}_views_events`; const query = gql` query Query( $committedRoot: String $messageHash: String $transactionHash: String $limit: Int ) { ${eventsTable}( where: { _or: [ { dispatch_tx: { _eq: $transactionHash } } { message_hash: { _eq: $messageHash } } { old_root: { _eq: $committedRoot } } ] } limit: $limit ) { committed_root destination_and_nonce destination_domain_id destination_domain_name dispatch_block dispatch_tx dispatched_at id leaf_index message message__action__amount message__action__details_hash message__action__to message__action__type message__token__domain message__token__id message_body message_hash message_type new_root nonce old_root origin_domain_id origin_domain_name process_block process_tx processed_at recipient_address relay_block relay_chain_id relay_tx relayed_at sender_address signature update_block update_chain_id update_tx updated_at } } `; const filter = { ...GoldSkyBackend.fillFilter(f), limit: limit || null, }; const response = await request(this.uri, query, filter, this.headers); const events: GoldSkyMessage[] = nulls2undefined(response[eventsTable]); if (!events || events.length <= 0) return undefined; return events; } }