@fuel-infrastructure/fuel-hyperlane-registry
Version:
A collection of configs, artifacts, and schemas for Hyperlane
200 lines (199 loc) • 8.68 kB
JavaScript
import { parse as yamlParse } from 'yaml';
import { CHAIN_FILE_REGEX, DEFAULT_GITHUB_REGISTRY, GITHUB_FETCH_CONCURRENCY_LIMIT, WARP_ROUTE_CONFIG_FILE_REGEX, WARP_ROUTE_DEPLOY_FILE_REGEX, } from '../consts.js';
import { concurrentMap, parseGitHubPath, stripLeadingSlash } from '../utils.js';
import { BaseRegistry } from './BaseRegistry.js';
import { RegistryType, } from './IRegistry.js';
import { filterWarpRoutesIds, warpRouteConfigPathToId, warpRouteDeployConfigPathToId, } from './warp-utils.js';
export const GITHUB_API_URL = 'https://api.github.com';
export const GITHUB_API_VERSION = '2022-11-28';
/**
* A registry that uses a github repository as its data source.
* Reads are performed via the github API and github's raw content URLs.
* Writes are not yet supported (TODO)
*/
export class GithubRegistry extends BaseRegistry {
type = RegistryType.Github;
url;
branch;
repoOwner;
repoName;
proxyUrl;
authToken;
baseApiHeaders = {
'X-GitHub-Api-Version': GITHUB_API_VERSION,
};
constructor(options = {}) {
super({ uri: options.uri ?? DEFAULT_GITHUB_REGISTRY, logger: options.logger });
this.url = new URL(this.uri);
const { repoOwner, repoName, repoBranch } = parseGitHubPath(this.uri);
this.repoOwner = repoOwner;
this.repoName = repoName;
if (options.branch && repoBranch)
throw new Error('Branch is set in both options and url.');
this.branch = options.branch ?? repoBranch ?? 'main';
this.proxyUrl = options.proxyUrl;
this.authToken = options.authToken;
}
getUri(itemPath) {
if (!itemPath)
return super.getUri();
return this.getRawContentUrl(itemPath);
}
async listRegistryContent() {
if (this.listContentCache)
return this.listContentCache;
// This uses the tree API instead of the simpler directory list API because it
// allows us to get a full view of all files in one request.
const apiUrl = await this.getApiUrl();
const response = await this.fetch(apiUrl);
const result = await response.json();
const tree = result.tree;
const chainPath = this.getChainsPath();
const chains = {};
const warpRoutes = {};
const warpDeployConfig = {};
for (const node of tree) {
if (CHAIN_FILE_REGEX.test(node.path)) {
const [_, chainName, fileName, extension] = node.path.match(CHAIN_FILE_REGEX);
chains[chainName] ??= {};
// @ts-ignore allow dynamic key assignment
chains[chainName][fileName] = this.getRawContentUrl(`${chainPath}/${chainName}/${fileName}.${extension}`);
}
if (WARP_ROUTE_CONFIG_FILE_REGEX.test(node.path)) {
const routeId = warpRouteConfigPathToId(node.path);
warpRoutes[routeId] = this.getRawContentUrl(node.path);
}
if (WARP_ROUTE_DEPLOY_FILE_REGEX.test(node.path)) {
const routeId = warpRouteDeployConfigPathToId(node.path);
warpDeployConfig[routeId] = this.getRawContentUrl(node.path);
}
}
return (this.listContentCache = { chains, deployments: { warpRoutes, warpDeployConfig } });
}
async getChains() {
const repoContents = await this.listRegistryContent();
return Object.keys(repoContents.chains);
}
async getMetadata() {
if (this.metadataCache && this.isMetadataCacheFull)
return this.metadataCache;
const combinedDataUrl = this.getRawContentUrl(`${this.getChainsPath()}/metadata.yaml`);
const chainMetadata = await this.fetchYamlFile(combinedDataUrl);
this.isMetadataCacheFull = true;
return (this.metadataCache = chainMetadata);
}
async getChainMetadata(chainName) {
if (this.metadataCache?.[chainName])
return this.metadataCache[chainName];
const data = await this.fetchChainFile('metadata', chainName);
if (!data)
return null;
this.metadataCache = { ...this.metadataCache, [chainName]: data };
return data;
}
async getAddresses() {
if (this.addressCache && this.isAddressCacheFull)
return this.addressCache;
const combinedDataUrl = this.getRawContentUrl(`${this.getChainsPath()}/addresses.yaml`);
const chainAddresses = await this.fetchYamlFile(combinedDataUrl);
this.isAddressCacheFull = true;
return (this.addressCache = chainAddresses);
}
async getChainAddresses(chainName) {
if (this.addressCache?.[chainName])
return this.addressCache[chainName];
const data = await this.fetchChainFile('addresses', chainName);
if (!data)
return null;
this.addressCache = { ...this.addressCache, [chainName]: data };
return data;
}
async addChain(_chains) {
throw new Error('TODO: Implement');
}
async updateChain(_chains) {
throw new Error('TODO: Implement');
}
async removeChain(_chains) {
throw new Error('TODO: Implement');
}
async getWarpRoute(routeId) {
const repoContents = await this.listRegistryContent();
const routeConfigUrl = repoContents.deployments.warpRoutes[routeId];
if (!routeConfigUrl)
return null;
return this.fetchYamlFile(routeConfigUrl);
}
async getWarpDeployConfig(routeId) {
const repoContents = await this.listRegistryContent();
const routeConfigUrl = repoContents.deployments.warpDeployConfig[routeId];
if (!routeConfigUrl)
return null;
return this.fetchYamlFile(routeConfigUrl);
}
async getWarpRoutes(filter) {
const { warpRoutes } = (await this.listRegistryContent()).deployments;
const { ids: routeIds, values: routeConfigUrls } = filterWarpRoutesIds(warpRoutes, filter);
return this.readConfigs(routeIds, routeConfigUrls);
}
async getWarpDeployConfigs(filter) {
const { warpDeployConfig } = (await this.listRegistryContent()).deployments;
const { ids: routeIds, values: routeConfigUrls } = filterWarpRoutesIds(warpDeployConfig, filter);
return this.readConfigs(routeIds, routeConfigUrls);
}
async readConfigs(routeIds, routeConfigUrls) {
const configs = await this.fetchYamlFiles(routeConfigUrls);
const idsWithConfigs = routeIds.map((id, i) => [id, configs[i]]);
return Object.fromEntries(idsWithConfigs);
}
async addWarpRoute(_config) {
throw new Error('TODO: Implement');
}
async getApiUrl() {
const { remaining, reset } = await this.getApiRateLimit();
let apiHost = GITHUB_API_URL;
if (remaining === 0) {
if (!this.proxyUrl)
throw new Error(`Github API rate remaining: ${remaining}, limit reset at ${reset}.`);
apiHost = this.proxyUrl;
}
return `${apiHost}/repos/${this.repoOwner}/${this.repoName}/git/trees/${this.branch}?recursive=true`;
}
async getApiRateLimit() {
const response = await this.fetch(`${GITHUB_API_URL}/rate_limit`);
const { resources } = (await response.json());
return resources.core;
}
getRawContentUrl(path) {
path = stripLeadingSlash(path);
return `https://raw.githubusercontent.com/${this.repoOwner}/${this.repoName}/${this.branch}/${path}`;
}
async fetchChainFile(fileName, chainName) {
const repoContents = await this.listRegistryContent();
const fileUrl = repoContents.chains[chainName]?.[fileName];
if (!fileUrl)
return null;
return this.fetchYamlFile(fileUrl);
}
fetchYamlFiles(urls) {
return concurrentMap(GITHUB_FETCH_CONCURRENCY_LIMIT, urls, (url) => this.fetchYamlFile(url));
}
async fetchYamlFile(url) {
const response = await this.fetch(url);
const data = await response.text();
return yamlParse(data);
}
async fetch(url) {
this.logger.debug(`Fetching from github: ${url}`);
const isProxiedRequest = this.proxyUrl && url.startsWith(this.proxyUrl);
const headers = !isProxiedRequest && !!this.authToken
? { ...this.baseApiHeaders, Authorization: `Bearer ${this.authToken}` }
: this.baseApiHeaders;
const response = await fetch(url, {
headers,
});
if (!response.ok)
throw new Error(`Failed to fetch from github: ${response.status} ${response.statusText}`);
return response;
}
}