Skip to content

Commit

Permalink
Additions to serve Hyperlane Explorer (#25)
Browse files Browse the repository at this point in the history
### Description

- Add `getChainLogoUri` method to IRegistry
- Improve caching of single-chain data queries
- Avoid errors on missing chain files

### Backward compatibility

Yes

### Testing

Tested in widget lib manually
  • Loading branch information
jmrossy authored May 16, 2024
1 parent 46e0384 commit 46b136f
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 30 deletions.
5 changes: 5 additions & 0 deletions .changeset/gold-hairs-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperlane-xyz/registry': patch
---

Add getChainLogoUri method and improve caching
14 changes: 13 additions & 1 deletion src/registry/BaseRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ChainMap, ChainMetadata, ChainName, WarpCoreConfig } from '@hyperl
import type { ChainAddresses, MaybePromise } from '../types.js';
import type { IRegistry, RegistryContent, RegistryType } from './IRegistry.js';

export const CHAIN_FILE_REGEX = /chains\/([a-z0-9]+)\/([a-z]+)\.yaml/;
export const CHAIN_FILE_REGEX = /chains\/([a-z0-9]+)\/([a-z]+)\.(yaml|svg)/;

export abstract class BaseRegistry implements IRegistry {
public abstract type: RegistryType;
Expand All @@ -14,7 +14,9 @@ export abstract class BaseRegistry implements IRegistry {
// Caches
protected listContentCache?: RegistryContent;
protected metadataCache?: ChainMap<ChainMetadata>;
protected isMetadataCacheFull: boolean = false;
protected addressCache?: ChainMap<ChainAddresses>;
protected isAddressCacheFull: boolean = false;

constructor({ uri, logger }: { uri: string; logger?: Logger }) {
this.uri = uri;
Expand Down Expand Up @@ -43,11 +45,21 @@ export abstract class BaseRegistry implements IRegistry {
}

abstract listRegistryContent(): MaybePromise<RegistryContent>;

abstract getChains(): MaybePromise<Array<ChainName>>;

abstract getMetadata(): MaybePromise<ChainMap<ChainMetadata>>;
abstract getChainMetadata(chainName: ChainName): MaybePromise<ChainMetadata | null>;

abstract getAddresses(): MaybePromise<ChainMap<ChainAddresses>>;
abstract getChainAddresses(chainName: ChainName): MaybePromise<ChainAddresses | null>;

async getChainLogoUri(chainName: ChainName): Promise<string | null> {
const registryContent = await this.listRegistryContent();
const chain = registryContent.chains[chainName];
return chain?.logo ?? null;
}

abstract addChain(chain: {
chainName: ChainName;
metadata?: ChainMetadata;
Expand Down
49 changes: 32 additions & 17 deletions src/registry/GithubRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { parse as yamlParse } from 'yaml';
import type { ChainMap, ChainMetadata, ChainName, WarpCoreConfig } from '@hyperlane-xyz/sdk';

import { DEFAULT_GITHUB_REGISTRY, GITHUB_FETCH_CONCURRENCY_LIMIT } from '../consts.js';
import { ChainAddresses, ChainAddressesSchema } from '../types.js';
import { ChainAddresses } from '../types.js';
import { concurrentMap } from '../utils.js';
import { BaseRegistry, CHAIN_FILE_REGEX } from './BaseRegistry.js';
import {
Expand Down Expand Up @@ -60,11 +60,11 @@ export class GithubRegistry extends BaseRegistry implements IRegistry {
const chains: ChainMap<ChainFiles> = {};
for (const node of tree) {
if (CHAIN_FILE_REGEX.test(node.path)) {
const [_, chainName, fileName] = node.path.match(CHAIN_FILE_REGEX)!;
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}.yaml`,
`${chainPath}/${chainName}/${fileName}.${extension}`,
);
}

Expand All @@ -80,31 +80,33 @@ export class GithubRegistry extends BaseRegistry implements IRegistry {
}

async getMetadata(): Promise<ChainMap<ChainMetadata>> {
if (this.metadataCache) return this.metadataCache;
if (this.metadataCache && this.isMetadataCacheFull) return this.metadataCache;
const chainMetadata = await this.fetchChainFiles<ChainMetadata>('metadata');
this.isMetadataCacheFull = true;
return (this.metadataCache = chainMetadata);
}

async getChainMetadata(chainName: ChainName): Promise<ChainMetadata> {
async getChainMetadata(chainName: ChainName): Promise<ChainMetadata | null> {
if (this.metadataCache?.[chainName]) return this.metadataCache[chainName];
const url = this.getRawContentUrl(`${this.getChainsPath()}/${chainName}/metadata.yaml`);
const response = await this.fetch(url);
const data = await response.text();
return yamlParse(data);
const data = await this.fetchSingleChainFile<ChainMetadata>('metadata', chainName);
if (!data) return null;
this.metadataCache = { ...this.metadataCache, [chainName]: data };
return data;
}

async getAddresses(): Promise<ChainMap<ChainAddresses>> {
if (this.addressCache) return this.addressCache;
if (this.addressCache && this.isAddressCacheFull) return this.addressCache;
const chainAddresses = await this.fetchChainFiles<ChainAddresses>('addresses');
this.isAddressCacheFull = true;
return (this.addressCache = chainAddresses);
}

async getChainAddresses(chainName: ChainName): Promise<ChainAddresses> {
async getChainAddresses(chainName: ChainName): Promise<ChainAddresses | null> {
if (this.addressCache?.[chainName]) return this.addressCache[chainName];
const url = this.getRawContentUrl(`${this.getChainsPath()}/${chainName}/addresses.yaml`);
const response = await this.fetch(url);
const data = await response.text();
return ChainAddressesSchema.parse(yamlParse(data));
const data = await this.fetchSingleChainFile<ChainAddresses>('addresses', chainName);
if (!data) return null;
this.addressCache = { ...this.addressCache, [chainName]: data };
return data;
}

async addChain(_chains: {
Expand Down Expand Up @@ -133,9 +135,14 @@ export class GithubRegistry extends BaseRegistry implements IRegistry {
return `https://raw.githubusercontent.com/${this.repoOwner}/${this.repoName}/${this.branch}/${path}`;
}

protected async fetchChainFiles<T>(fileName: keyof ChainFiles): Promise<ChainMap<T>> {
// Fetches all files of a particular type in parallel
// Defaults to all known chains if chainNames is not provided
protected async fetchChainFiles<T>(
fileName: keyof ChainFiles,
chainNames?: ChainName[],
): Promise<ChainMap<T>> {
const repoContents = await this.listRegistryContent();
const chainNames = Object.keys(repoContents.chains);
chainNames ||= Object.keys(repoContents.chains);

const fileUrls = chainNames.reduce<ChainMap<string>>((acc, chainName) => {
const fileUrl = repoContents.chains[chainName][fileName];
Expand All @@ -156,6 +163,14 @@ export class GithubRegistry extends BaseRegistry implements IRegistry {
return Object.fromEntries(results);
}

protected async fetchSingleChainFile<T>(
fileName: keyof ChainFiles,
chainName: ChainName,
): Promise<T | null> {
const results = await this.fetchChainFiles<T>(fileName, [chainName]);
return results[chainName] ?? null;
}

protected async fetch(url: string): Promise<Response> {
this.logger.debug(`Fetching from github: ${url}`);
const response = await fetch(url);
Expand Down
6 changes: 6 additions & 0 deletions src/registry/IRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ChainAddresses, MaybePromise } from '../types.js';
export interface ChainFiles {
metadata?: string;
addresses?: string;
logo?: string;
}

export interface RegistryContent {
Expand All @@ -25,10 +26,15 @@ export interface IRegistry {
listRegistryContent(): MaybePromise<RegistryContent>;

getChains(): MaybePromise<Array<ChainName>>;

getMetadata(): MaybePromise<ChainMap<ChainMetadata>>;
getChainMetadata(chainName: ChainName): MaybePromise<ChainMetadata | null>;

getAddresses(): MaybePromise<ChainMap<ChainAddresses>>;
getChainAddresses(chainName: ChainName): MaybePromise<ChainAddresses | null>;

getChainLogoUri(chainName: ChainName): Promise<string | null>;

addChain(chain: {
chainName: ChainName;
metadata?: ChainMetadata;
Expand Down
12 changes: 4 additions & 8 deletions src/registry/LocalRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,9 @@ export class LocalRegistry extends BaseRegistry implements IRegistry {
return (this.metadataCache = chainMetadata);
}

getChainMetadata(chainName: ChainName): ChainMetadata {
getChainMetadata(chainName: ChainName): ChainMetadata | null {
const metadata = this.getMetadata();
if (!metadata[chainName])
throw new Error(`Metadata not found in registry for chain: ${chainName}`);
return metadata[chainName];
return metadata[chainName] ?? null;
}

getAddresses(): ChainMap<ChainAddresses> {
Expand All @@ -83,11 +81,9 @@ export class LocalRegistry extends BaseRegistry implements IRegistry {
return (this.addressCache = chainAddresses);
}

getChainAddresses(chainName: ChainName): ChainAddresses {
getChainAddresses(chainName: ChainName): ChainAddresses | null {
const addresses = this.getAddresses();
if (!addresses[chainName])
throw new Error(`Addresses not found in registry for chain: ${chainName}`);
return addresses[chainName];
return addresses[chainName] ?? null;
}

addChain(chain: {
Expand Down
10 changes: 6 additions & 4 deletions test/unit/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('Registry utilities', () => {

it(`Fetches single chain metadata for ${registry.type} registry`, async () => {
const metadata = await registry.getChainMetadata('ethereum');
expect(metadata.chainId).to.eql(1);
expect(metadata!.chainId).to.eql(1);
}).timeout(5_000);

it(`Fetches chain addresses for ${registry.type} registry`, async () => {
Expand All @@ -47,7 +47,7 @@ describe('Registry utilities', () => {

it(`Fetches single chain addresses for ${registry.type} registry`, async () => {
const addresses = await registry.getChainAddresses('ethereum');
expect(addresses.mailbox.substring(0, 2)).to.eql('0x');
expect(addresses!.mailbox.substring(0, 2)).to.eql('0x');
}).timeout(5_000);

it(`Caches correctly for ${registry.type} registry`, async () => {
Expand All @@ -63,10 +63,10 @@ describe('Registry utilities', () => {

it(`Adds a new chain for ${registry.type} registry`, async () => {
const mockMetadata: ChainMetadata = {
...(await registry.getChainMetadata('ethereum')),
...(await registry.getChainMetadata('ethereum'))!,
name: MOCK_CHAIN_NAME,
};
const mockAddresses: ChainAddresses = await registry.getChainAddresses('ethereum');
const mockAddresses: ChainAddresses = await registry.getChainAddresses('ethereum')!;
await registry.addChain({
chainName: MOCK_CHAIN_NAME,
metadata: mockMetadata,
Expand Down Expand Up @@ -105,5 +105,7 @@ describe('Registry regex', () => {
expect(CHAIN_FILE_REGEX.test('chains/ethereum/metadata.yaml')).to.be.true;
expect(CHAIN_FILE_REGEX.test('chains/ancient8/addresses.yaml')).to.be.true;
expect(CHAIN_FILE_REGEX.test('chains/_NotAChain/addresses.yaml')).to.be.false;
expect(CHAIN_FILE_REGEX.test('chains/foobar/logo.svg')).to.be.true;
expect(CHAIN_FILE_REGEX.test('chains/foobar/randomfile.txt')).to.be.false;
});
});

0 comments on commit 46b136f

Please sign in to comment.