Skip to content

Commit

Permalink
Migrate MergedRegistry from monorepo (#32)
Browse files Browse the repository at this point in the history
### Description

- Migrate MergedRegistry from the monorepo's CLI package to here
- Add required objMerge utility

### Backward compatibility

Yes

### Testing

New unit test coverage
  • Loading branch information
jmrossy authored May 20, 2024
1 parent b525f5b commit ab3dbb0
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/thick-weeks-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperlane-xyz/registry': minor
---

Add MergedRegistry class
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export { DEFAULT_GITHUB_REGISTRY } from './consts.js';
export { BaseRegistry, CHAIN_FILE_REGEX } from './registry/BaseRegistry.js';
export { GithubRegistry, GithubRegistryOptions } from './registry/GithubRegistry.js';
export { ChainFiles, IRegistry, RegistryContent, RegistryType } from './registry/IRegistry.js';
export { MergedRegistry, MergedRegistryOptions } from './registry/MergedRegistry.js';
export { ChainAddresses, ChainAddressesSchema } from './types.js';
1 change: 1 addition & 0 deletions src/registry/IRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface RegistryContent {
export enum RegistryType {
Github = 'github',
Local = 'local',
Merged = 'merged',
}

export interface IRegistry {
Expand Down
114 changes: 114 additions & 0 deletions src/registry/MergedRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type { Logger } from 'pino';

import type { ChainMap, ChainMetadata, ChainName, WarpCoreConfig } from '@hyperlane-xyz/sdk';
import { ChainAddresses } from '../types.js';
import { objMerge } from '../utils.js';
import { BaseRegistry } from './BaseRegistry.js';
import { IRegistry, RegistryContent, RegistryType } from './IRegistry.js';

export interface MergedRegistryOptions {
registries: Array<IRegistry>;
logger?: Logger;
}

export class MergedRegistry extends BaseRegistry implements IRegistry {
public readonly type = RegistryType.Merged;
public readonly registries: Array<IRegistry>;

constructor({ registries, logger }: MergedRegistryOptions) {
super({ uri: '__merged_registry__', logger });
if (!registries.length) throw new Error('At least one registry URI is required');
this.registries = registries;
}

async listRegistryContent(): Promise<RegistryContent> {
const results = await this.multiRegistryRead((r) => r.listRegistryContent());
return results.reduce((acc, content) => objMerge(acc, content), {
chains: {},
deployments: {},
});
}

async getChains(): Promise<Array<ChainName>> {
return Object.keys(await this.getMetadata());
}

async getMetadata(): Promise<ChainMap<ChainMetadata>> {
const results = await this.multiRegistryRead((r) => r.getMetadata());
return results.reduce((acc, content) => objMerge(acc, content), {});
}

async getChainMetadata(chainName: ChainName): Promise<ChainMetadata | null> {
return (await this.getMetadata())[chainName] || null;
}

async getAddresses(): Promise<ChainMap<ChainAddresses>> {
const results = await this.multiRegistryRead((r) => r.getAddresses());
return results.reduce((acc, content) => objMerge(acc, content), {});
}

async getChainAddresses(chainName: ChainName): Promise<ChainAddresses | null> {
return (await this.getAddresses())[chainName] || null;
}

async addChain(chain: {
chainName: ChainName;
metadata?: ChainMetadata;
addresses?: ChainAddresses;
}): Promise<void> {
return this.multiRegistryWrite(
async (registry) => await registry.addChain(chain),
`adding chain ${chain.chainName}`,
);
}

async updateChain(chain: {
chainName: ChainName;
metadata?: ChainMetadata;
addresses?: ChainAddresses;
}): Promise<void> {
return this.multiRegistryWrite(
async (registry) => await registry.updateChain(chain),
`updating chain ${chain.chainName}`,
);
}

async removeChain(chain: ChainName): Promise<void> {
return this.multiRegistryWrite(
async (registry) => await registry.removeChain(chain),
`removing chain ${chain}`,
);
}

async addWarpRoute(config: WarpCoreConfig): Promise<void> {
return this.multiRegistryWrite(
async (registry) => await registry.addWarpRoute(config),
'adding warp route',
);
}

protected multiRegistryRead<R>(readFn: (registry: IRegistry) => Promise<R> | R) {
return Promise.all(this.registries.map(readFn));
}

protected async multiRegistryWrite(
writeFn: (registry: IRegistry) => Promise<void>,
logMsg: string,
): Promise<void> {
for (const registry of this.registries) {
// TODO remove this when GithubRegistry supports write methods
if (registry.type === RegistryType.Github) {
this.logger.warn(`Skipping ${logMsg} at ${registry.type} registry`);
continue;
}
try {
this.logger.info(`Now ${logMsg} at ${registry.type} registry at ${registry.uri}`);
await writeFn(registry);
this.logger.info(`Done ${logMsg} at ${registry.type} registry`);
} catch (error) {
// To prevent loss of artifacts, MergedRegistry write methods are failure tolerant
this.logger.error(`Failure ${logMsg} at ${registry.type} registry`, error);
}
}
}
}
30 changes: 30 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,33 @@ export async function concurrentMap<A, B>(
}
return res;
}

export function isObject(item: any) {
return item && typeof item === 'object' && !Array.isArray(item);
}

// Recursively merges b into a
// Where there are conflicts, b takes priority over a
export function objMerge(a: Record<string, any>, b: Record<string, any>, max_depth = 10): any {
if (max_depth === 0) {
throw new Error('objMerge tried to go too deep');
}
if (isObject(a) && isObject(b)) {
const ret: Record<string, any> = {};
const aKeys = new Set(Object.keys(a));
const bKeys = new Set(Object.keys(b));
const allKeys = new Set([...aKeys, ...bKeys]);
for (const key of allKeys.values()) {
if (aKeys.has(key) && bKeys.has(key)) {
ret[key] = objMerge(a[key], b[key], max_depth - 1);
} else if (aKeys.has(key)) {
ret[key] = a[key];
} else {
ret[key] = b[key];
}
}
return ret;
} else {
return b ? b : a;
}
}
8 changes: 6 additions & 2 deletions test/unit/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import type { ChainMetadata } from '@hyperlane-xyz/sdk';
import fs from 'fs';
import { CHAIN_FILE_REGEX } from '../../src/registry/BaseRegistry.js';
import { GithubRegistry } from '../../src/registry/GithubRegistry.js';
import { RegistryType } from '../../src/registry/IRegistry.js';
import { LocalRegistry } from '../../src/registry/LocalRegistry.js';
import { MergedRegistry } from '../../src/registry/MergedRegistry.js';
import { ChainAddresses } from '../../src/types.js';

const MOCK_CHAIN_NAME = 'mockchain';
Expand All @@ -21,7 +23,9 @@ describe('Registry utilities', () => {
const localRegistry = new LocalRegistry({ uri: './' });
expect(localRegistry.uri).to.be.a('string');

for (const registry of [githubRegistry, localRegistry]) {
const mergedRegistry = new MergedRegistry({ registries: [githubRegistry, localRegistry] });

for (const registry of [githubRegistry, localRegistry, mergedRegistry]) {
it(`Lists all chains for ${registry.type} registry`, async () => {
const chains = await registry.getChains();
expect(chains.length).to.be.greaterThan(0);
Expand Down Expand Up @@ -59,7 +63,7 @@ describe('Registry utilities', () => {
}).timeout(250);

// TODO remove this once GitHubRegistry methods are implemented
if (registry.type === 'github') continue;
if (registry.type !== RegistryType.Local) continue;

it(`Adds a new chain for ${registry.type} registry`, async () => {
const mockMetadata: ChainMetadata = {
Expand Down

0 comments on commit ab3dbb0

Please sign in to comment.