@nomad-xyz/sdk
Version:
500 lines (452 loc) • 13.7 kB
text/typescript
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<string, GoldSkyMessage>;
dispatchTxToMessageHash: Map<string, string[]>;
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>): 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<Dispatch[] | undefined> {
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<Dispatch | undefined> {
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<GoldSkyMessage | 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<GoldSkyMessage[] | undefined> {
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<string | undefined> {
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<string | undefined> {
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<string | undefined> {
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<string | undefined> {
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<Date | undefined> {
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<Date | undefined> {
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<Date | undefined> {
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<Date | undefined> {
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<number | undefined> {
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<string | undefined> {
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<MessageFilter>,
limit?: number,
): Promise<GoldSkyMessage[] | undefined> {
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;
}
}