UNPKG

@nomad-xyz/sdk-bridge

Version:
279 lines (252 loc) 7.7 kB
import { GoldSkyBackend, GoldSkyMessage, MessageFilter, MessageBackend } from '@nomad-xyz/sdk'; import { request, gql } from 'graphql-request'; import * as config from '@nomad-xyz/configuration'; import { BridgeContext } from './BridgeContext'; import { nulls2undefined } from '@nomad-xyz/sdk/src/messageBackend/utils'; /** * Abstract class required for operation of NomadMessage */ export default abstract class BridgeMessageBackend extends MessageBackend { abstract sender(messageHash: string): Promise<string | undefined>; abstract receivedTx(messageHash: string): Promise<string | undefined>; } /** * GoldSky bridge message representation */ export type GoldSkyBridgeMessage = GoldSkyMessage & { origin_and_nonce?: string; send_tx?: string; sent_at?: string; send_block?: string; original_sender?: string; receive_tx?: string; receive_block?: string; received_at?: string; }; /** * GoldSky bridge backend for NomadMessage */ export class GoldSkyBridgeBackend extends GoldSkyBackend implements BridgeMessageBackend { context: BridgeContext; messageCache: Map<string, GoldSkyBridgeMessage>; constructor(env: string, secret: string, context: BridgeContext) { super(env, secret, context); this.messageCache = new Map(); this.context = context; } /** * Checks whether an environment is supported by the backend. Throws on unsupported * @param environment environment to check */ static checkEnvironment(environment: string): void { GoldSkyBackend.checkEnvironment(environment); } /** * Returns default secret for Goldsky * @returns secret as a string */ static defaultSecret(): string { return GoldSkyBackend.defaultSecret(); } /** * Creates a default GoldSky backend for an environment * @param environment environment to create the backend for * @returns backend */ static default( environment: string | config.NomadConfig = 'development', context: BridgeContext, ): GoldSkyBridgeBackend { const environmentString = typeof environment === 'string' ? environment : environment.environment; GoldSkyBridgeBackend.checkEnvironment(environmentString); const secret = process.env.GOLDSKY_SECRET || GoldSkyBridgeBackend.defaultSecret(); if (!secret) throw new Error(`GOLDSKY_SECRET not found in env`); return new GoldSkyBridgeBackend(environmentString, secret, context); } /** * Stores message into internal cache */ storeMessage(m: GoldSkyBridgeMessage): 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<GoldSkyBridgeMessage | undefined> { 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<GoldSkyBridgeMessage[] | undefined> { let ms: GoldSkyBridgeMessage[] | 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 { // messageHashes! are there as they are already tested in `enoughHashes` above // getMessage(hash)! is also there as in order to get into `messageHashes` a message needs to get fetched 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; } /** * Gets an original sender of the message * @param messageHash * @returns sender's address */ async sender(messageHash: string): Promise<string | undefined> { let m = await this.getMessage(messageHash); if (!m?.original_sender) m = await this.getMessage(messageHash, true); return m?.original_sender; } /** * Gets a transaction related to Received event * @param messageHash * @returns transaction hash */ async receivedTx(messageHash: string): Promise<string | undefined> { let m = await this.getMessage(messageHash); if (!m?.receive_tx) m = await this.getMessage(messageHash, true); return m?.receive_tx; } /** * Fetches internal message from backend * * @returns Internal message representation (if any) */ async fetchMessages( f: Partial<MessageFilter>, limit?: number, ): Promise<GoldSkyBridgeMessage[] | undefined> { const eventsTable = `${this.env}_views_bridge_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 send_tx sent_at send_block original_sender receive_tx origin_and_nonce receive_block received_at } } `; const filter = { ...GoldSkyBackend.fillFilter(f), limit: limit || null, }; const response = await request(this.uri, query, filter, this.headers); const events: GoldSkyBridgeMessage[] = nulls2undefined(response[eventsTable]); if (!events || events.length <= 0) return undefined; return events; } }