Skip to content

Commit

Permalink
Add PartialRegistry class and merge method (#33)
Browse files Browse the repository at this point in the history
### Description

- Add PartialRegistry class
- Add merge() method to IRegistry

### Backward compatibility

Yes

### Testing

New unit test coverage
  • Loading branch information
jmrossy authored May 20, 2024
1 parent ab3dbb0 commit 1b7c0f5
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 52 deletions.
6 changes: 6 additions & 0 deletions .changeset/brown-taxis-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@hyperlane-xyz/registry': minor
---

Add PartialRegistry class
Add merge() method to IRegistry
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ 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 { PartialRegistry, PartialRegistryOptions } from './registry/PartialRegistry.js';
export { ChainAddresses, ChainAddressesSchema } from './types.js';
5 changes: 5 additions & 0 deletions src/registry/BaseRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Logger } from 'pino';
import type { ChainMap, ChainMetadata, ChainName, WarpCoreConfig } from '@hyperlane-xyz/sdk';
import type { ChainAddresses, MaybePromise } from '../types.js';
import type { IRegistry, RegistryContent, RegistryType } from './IRegistry.js';
import { MergedRegistry } from './MergedRegistry.js';

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

Expand Down Expand Up @@ -72,4 +73,8 @@ export abstract class BaseRegistry implements IRegistry {
}): MaybePromise<void>;
abstract removeChain(chain: ChainName): MaybePromise<void>;
abstract addWarpRoute(config: WarpCoreConfig): MaybePromise<void>;

merge(otherRegistry: IRegistry): IRegistry {
return new MergedRegistry({ registries: [this, otherRegistry], logger: this.logger });
}
}
5 changes: 5 additions & 0 deletions src/registry/GithubRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ interface TreeNode {
url: string;
}

/**
* 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 implements IRegistry {
public readonly type = RegistryType.Github;
public readonly url: URL;
Expand Down
3 changes: 3 additions & 0 deletions src/registry/IRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export enum RegistryType {
Github = 'github',
Local = 'local',
Merged = 'merged',
Partial = 'partial',
}

export interface IRegistry {
Expand Down Expand Up @@ -50,4 +51,6 @@ export interface IRegistry {

addWarpRoute(config: WarpCoreConfig): MaybePromise<void>;
// TODO define more deployment artifact related methods

merge(otherRegistry: IRegistry): IRegistry;
}
57 changes: 10 additions & 47 deletions src/registry/LocalRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,26 @@ import type { ChainMap, ChainMetadata, ChainName, WarpCoreConfig } from '@hyperl
import { SCHEMA_REF } from '../consts.js';
import { ChainAddresses, ChainAddressesSchema } from '../types.js';
import { toYamlString } from '../utils.js';
import { BaseRegistry, CHAIN_FILE_REGEX } from './BaseRegistry.js';
import { CHAIN_FILE_REGEX } from './BaseRegistry.js';
import {
RegistryType,
type ChainFiles,
type IRegistry,
type RegistryContent,
} from './IRegistry.js';
import { SynchronousRegistry } from './SynchronousRegistry.js';
import { warpConfigToWarpAddresses } from './warp-utils.js';

export interface LocalRegistryOptions {
uri: string;
logger?: Logger;
}

export class LocalRegistry extends BaseRegistry implements IRegistry {
/**
* A registry that uses a local file system path as its data source.
* Requires file system access so it cannot be used in the browser.
*/
export class LocalRegistry extends SynchronousRegistry implements IRegistry {
public readonly type = RegistryType.Local;

constructor(options: LocalRegistryOptions) {
Expand All @@ -48,10 +53,6 @@ export class LocalRegistry extends BaseRegistry implements IRegistry {
return (this.listContentCache = { chains, deployments: {} });
}

getChains(): Array<ChainName> {
return Object.keys(this.listRegistryContent().chains);
}

getMetadata(): ChainMap<ChainMetadata> {
if (this.metadataCache) return this.metadataCache;
const chainMetadata: ChainMap<ChainMetadata> = {};
Expand All @@ -64,11 +65,6 @@ export class LocalRegistry extends BaseRegistry implements IRegistry {
return (this.metadataCache = chainMetadata);
}

getChainMetadata(chainName: ChainName): ChainMetadata | null {
const metadata = this.getMetadata();
return metadata[chainName] ?? null;
}

getAddresses(): ChainMap<ChainAddresses> {
if (this.addressCache) return this.addressCache;
const chainAddresses: ChainMap<ChainAddresses> = {};
Expand All @@ -81,43 +77,10 @@ export class LocalRegistry extends BaseRegistry implements IRegistry {
return (this.addressCache = chainAddresses);
}

getChainAddresses(chainName: ChainName): ChainAddresses | null {
const addresses = this.getAddresses();
return addresses[chainName] ?? null;
}

addChain(chain: {
chainName: ChainName;
metadata?: ChainMetadata;
addresses?: ChainAddresses;
}): void {
const currentChains = this.listRegistryContent().chains;
if (currentChains[chain.chainName])
throw new Error(`Chain ${chain.chainName} already exists in registry`);

this.createOrUpdateChain(chain);
}

updateChain(chain: {
chainName: ChainName;
metadata?: ChainMetadata;
addresses?: ChainAddresses;
}): void {
const currentChains = this.listRegistryContent();
if (!currentChains.chains[chain.chainName]) {
this.logger.debug(`Chain ${chain.chainName} not found in registry, adding it now`);
}
this.createOrUpdateChain(chain);
}

removeChain(chainName: ChainName): void {
const currentChains = this.listRegistryContent().chains;
if (!currentChains[chainName]) throw new Error(`Chain ${chainName} does not exist in registry`);

this.removeFiles(Object.values(currentChains[chainName]));
if (this.listContentCache?.chains[chainName]) delete this.listContentCache.chains[chainName];
if (this.metadataCache?.[chainName]) delete this.metadataCache[chainName];
if (this.addressCache?.[chainName]) delete this.addressCache[chainName];
const chainFiles = this.listRegistryContent().chains[chainName];
super.removeChain(chainName);
this.removeFiles(Object.values(chainFiles));
}

protected listFiles(dirPath: string): string[] {
Expand Down
26 changes: 23 additions & 3 deletions src/registry/MergedRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,30 @@ 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 {
/**
* A registry that accepts multiple sub-registries.
* Read methods are performed on all sub-registries and the results are merged.
* Write methods are performed on all sub-registries.
* Can be created manually or by calling `.merge()` on an existing registry.
*/
export class MergedRegistry implements IRegistry {
public readonly type = RegistryType.Merged;
public readonly uri = '__merged_registry__';
public readonly registries: Array<IRegistry>;
protected readonly logger: Logger;

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;
// @ts-ignore
this.logger = logger || console;
}

async listRegistryContent(): Promise<RegistryContent> {
Expand Down Expand Up @@ -51,6 +59,11 @@ export class MergedRegistry extends BaseRegistry implements IRegistry {
return (await this.getAddresses())[chainName] || null;
}

async getChainLogoUri(chainName: ChainName): Promise<string | null> {
const results = await this.multiRegistryRead((r) => r.getChainLogoUri(chainName));
return results.find((uri) => !!uri) || null;
}

async addChain(chain: {
chainName: ChainName;
metadata?: ChainMetadata;
Expand Down Expand Up @@ -111,4 +124,11 @@ export class MergedRegistry extends BaseRegistry implements IRegistry {
}
}
}

merge(otherRegistry: IRegistry): IRegistry {
return new MergedRegistry({
registries: [...this.registries, otherRegistry],
logger: this.logger,
});
}
}
74 changes: 74 additions & 0 deletions src/registry/PartialRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { Logger } from 'pino';

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

const PARTIAL_URI_PLACEHOLDER = '__partial_registry__';

/**
* A registry that accepts partial data, such as incomplete chain metadata or addresses.
* Useful for merging with other registries force overrides of subsets of data.
*/
export interface PartialRegistryOptions {
chainMetadata?: ChainMap<Partial<ChainMetadata>>;
chainAddresses?: ChainMap<Partial<ChainAddresses>>;
// TODO add more fields here as needed
logger?: Logger;
}

export class PartialRegistry extends SynchronousRegistry implements IRegistry {
public readonly type = RegistryType.Partial;
public chainMetadata: ChainMap<Partial<ChainMetadata>>;
public chainAddresses: ChainMap<Partial<ChainAddresses>>;

constructor({ chainMetadata, chainAddresses, logger }: PartialRegistryOptions) {
super({ uri: PARTIAL_URI_PLACEHOLDER, logger });
this.chainMetadata = chainMetadata || {};
this.chainAddresses = chainAddresses || {};
}

listRegistryContent(): RegistryContent {
const chains: ChainMap<ChainFiles> = {};
Object.keys(this.chainMetadata).forEach((c) => {
chains[c] ||= {};
chains[c].metadata = PARTIAL_URI_PLACEHOLDER;
});
Object.keys(this.chainAddresses).forEach((c) => {
chains[c] ||= {};
chains[c].addresses = PARTIAL_URI_PLACEHOLDER;
});
return {
chains,
deployments: {},
};
}

getMetadata(): ChainMap<ChainMetadata> {
return this.chainMetadata as ChainMap<ChainMetadata>;
}

getAddresses(): ChainMap<ChainAddresses> {
return this.chainAddresses as ChainMap<ChainAddresses>;
}

removeChain(chainName: ChainName): void {
super.removeChain(chainName);
if (this.chainMetadata?.[chainName]) delete this.chainMetadata[chainName];
if (this.chainAddresses?.[chainName]) delete this.chainAddresses[chainName];
}

addWarpRoute(_config: WarpCoreConfig): void {
throw new Error('Method not implemented.');
}

protected createOrUpdateChain(chain: {
chainName: ChainName;
metadata?: ChainMetadata;
addresses?: ChainAddresses;
}): void {
if (chain.metadata) this.chainMetadata[chain.chainName] = chain.metadata;
if (chain.addresses) this.chainAddresses[chain.chainName] = chain.addresses;
}
}
71 changes: 71 additions & 0 deletions src/registry/SynchronousRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { ChainMap, ChainMetadata, ChainName, WarpCoreConfig } from '@hyperlane-xyz/sdk';

import { ChainAddresses } from '../types.js';
import { BaseRegistry } from './BaseRegistry.js';
import { IRegistry, RegistryContent } from './IRegistry.js';

/**
* Shared code for sync registries like the FileSystem and Partial registries.
* This is required because of the inconsistent sync/async methods across registries.
* If the Infra package can be updated to work with async-only methods, this code can be moved to the BaseRegistry class.
*/
export abstract class SynchronousRegistry extends BaseRegistry implements IRegistry {
abstract listRegistryContent(): RegistryContent;

getChains(): Array<ChainName> {
return Object.keys(this.listRegistryContent().chains);
}

abstract getMetadata(): ChainMap<ChainMetadata>;

getChainMetadata(chainName: ChainName): ChainMetadata | null {
return this.getMetadata()[chainName] || null;
}

abstract getAddresses(): ChainMap<ChainAddresses>;

getChainAddresses(chainName: ChainName): ChainAddresses | null {
return this.getAddresses()[chainName] || null;
}

addChain(chain: {
chainName: ChainName;
metadata?: ChainMetadata;
addresses?: ChainAddresses;
}): void {
const currentChains = this.listRegistryContent().chains;
if (currentChains[chain.chainName])
throw new Error(`Chain ${chain.chainName} already exists in registry`);

this.createOrUpdateChain(chain);
}

updateChain(chain: {
chainName: ChainName;
metadata?: ChainMetadata;
addresses?: ChainAddresses;
}): void {
const currentChains = this.listRegistryContent();
if (!currentChains.chains[chain.chainName]) {
this.logger.debug(`Chain ${chain.chainName} not found in registry, adding it now`);
}
this.createOrUpdateChain(chain);
}

removeChain(chainName: ChainName): void {
const currentChains = this.listRegistryContent().chains;
if (!currentChains[chainName]) throw new Error(`Chain ${chainName} does not exist in registry`);

if (this.listContentCache?.chains[chainName]) delete this.listContentCache.chains[chainName];
if (this.metadataCache?.[chainName]) delete this.metadataCache[chainName];
if (this.addressCache?.[chainName]) delete this.addressCache[chainName];
}

abstract addWarpRoute(config: WarpCoreConfig): void;

protected abstract createOrUpdateChain(chain: {
chainName: ChainName;
metadata?: ChainMetadata;
addresses?: ChainAddresses;
}): void;
}
Loading

0 comments on commit 1b7c0f5

Please sign in to comment.