From 7c903338a17e614598eb1851fb670542e52b4f51 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Thu, 16 May 2024 10:45:23 -0400 Subject: [PATCH 1/8] Migrate MergedRegistry from monorepo --- .changeset/thick-weeks-approve.md | 5 ++ src/index.ts | 1 + src/registry/IRegistry.ts | 1 + src/registry/MergedRegistry.ts | 114 ++++++++++++++++++++++++++++++ src/utils.ts | 30 ++++++++ 5 files changed, 151 insertions(+) create mode 100644 .changeset/thick-weeks-approve.md create mode 100644 src/registry/MergedRegistry.ts diff --git a/.changeset/thick-weeks-approve.md b/.changeset/thick-weeks-approve.md new file mode 100644 index 000000000..c1c493bb8 --- /dev/null +++ b/.changeset/thick-weeks-approve.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/registry': minor +--- + +Add MergedRegistry class diff --git a/src/index.ts b/src/index.ts index 8d05f9c64..3ca47f495 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/registry/IRegistry.ts b/src/registry/IRegistry.ts index bcc630ab9..841cd2ee3 100644 --- a/src/registry/IRegistry.ts +++ b/src/registry/IRegistry.ts @@ -17,6 +17,7 @@ export interface RegistryContent { export enum RegistryType { Github = 'github', Local = 'local', + Merged = 'merged', } export interface IRegistry { diff --git a/src/registry/MergedRegistry.ts b/src/registry/MergedRegistry.ts new file mode 100644 index 000000000..fbad37260 --- /dev/null +++ b/src/registry/MergedRegistry.ts @@ -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; + logger?: Logger; +} + +export class MergedRegistry extends BaseRegistry implements IRegistry { + public readonly type = RegistryType.Merged; + public readonly registries: Array; + + 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 { + const results = await this.multiRegistryRead((r) => r.listRegistryContent()); + return results.reduce((acc, content) => objMerge(acc, content), { + chains: {}, + deployments: {}, + }); + } + + async getChains(): Promise> { + return Object.keys(await this.getMetadata); + } + + async getMetadata(): Promise> { + const results = await this.multiRegistryRead((r) => r.getMetadata()); + return results.reduce((acc, content) => objMerge(acc, content), {}); + } + + async getChainMetadata(chainName: ChainName): Promise { + return (await this.getMetadata())[chainName] || null; + } + + async getAddresses(): Promise> { + const results = await this.multiRegistryRead((r) => r.getAddresses()); + return results.reduce((acc, content) => objMerge(acc, content), {}); + } + + async getChainAddresses(chainName: ChainName): Promise { + return (await this.getAddresses())[chainName] || null; + } + + async addChain(chain: { + chainName: ChainName; + metadata?: ChainMetadata; + addresses?: ChainAddresses; + }): Promise { + return this.multiRegistryWrite( + async (registry) => await registry.addChain(chain), + `adding chain ${chain.chainName}`, + ); + } + + async updateChain(chain: { + chainName: ChainName; + metadata?: ChainMetadata; + addresses?: ChainAddresses; + }): Promise { + return this.multiRegistryWrite( + async (registry) => await registry.updateChain(chain), + `updating chain ${chain.chainName}`, + ); + } + + async removeChain(chain: ChainName): Promise { + return this.multiRegistryWrite( + async (registry) => await registry.removeChain(chain), + `removing chain ${chain}`, + ); + } + + async addWarpRoute(config: WarpCoreConfig): Promise { + return this.multiRegistryWrite( + async (registry) => await registry.addWarpRoute(config), + 'adding warp route', + ); + } + + protected multiRegistryRead(readFn: (registry: IRegistry) => Promise | R) { + return Promise.all(this.registries.map(readFn)); + } + + protected async multiRegistryWrite( + writeFn: (registry: IRegistry) => Promise, + logMsg: string, + ): Promise { + 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); + } + } + } +} diff --git a/src/utils.ts b/src/utils.ts index b0edb9c45..5280ec0b2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -19,3 +19,33 @@ export async function concurrentMap( } 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, b: Record, 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 = {}; + 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; + } +} From f8c630cd69acd0026042ad2b33413fd987626b28 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Thu, 16 May 2024 10:52:07 -0400 Subject: [PATCH 2/8] Add unit test coverage --- src/registry/MergedRegistry.ts | 2 +- test/unit/registry.test.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/registry/MergedRegistry.ts b/src/registry/MergedRegistry.ts index fbad37260..ca2b33472 100644 --- a/src/registry/MergedRegistry.ts +++ b/src/registry/MergedRegistry.ts @@ -30,7 +30,7 @@ export class MergedRegistry extends BaseRegistry implements IRegistry { } async getChains(): Promise> { - return Object.keys(await this.getMetadata); + return Object.keys(await this.getMetadata()); } async getMetadata(): Promise> { diff --git a/test/unit/registry.test.ts b/test/unit/registry.test.ts index f1a022ae0..9720ae588 100644 --- a/test/unit/registry.test.ts +++ b/test/unit/registry.test.ts @@ -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'; @@ -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); @@ -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 = { From 1620d1c74dd38e264899db7e77936c556b9e1761 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Thu, 16 May 2024 12:26:55 -0400 Subject: [PATCH 3/8] Add metadata and address override methods to MergedRegistry --- .changeset/brown-taxis-invite.md | 5 +++ src/registry/MergedRegistry.ts | 53 ++++++++++++++++++++++++++++---- test/unit/registry.test.ts | 15 +++++++++ 3 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 .changeset/brown-taxis-invite.md diff --git a/.changeset/brown-taxis-invite.md b/.changeset/brown-taxis-invite.md new file mode 100644 index 000000000..f7a0cf29f --- /dev/null +++ b/.changeset/brown-taxis-invite.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/registry': patch +--- + +Add override methods to MergedRegistry diff --git a/src/registry/MergedRegistry.ts b/src/registry/MergedRegistry.ts index ca2b33472..18688e819 100644 --- a/src/registry/MergedRegistry.ts +++ b/src/registry/MergedRegistry.ts @@ -8,17 +8,28 @@ import { IRegistry, RegistryContent, RegistryType } from './IRegistry.js'; export interface MergedRegistryOptions { registries: Array; + chainMetadataOverrides?: ChainMap; + chainAddressesOverrides?: ChainMap>; logger?: Logger; } export class MergedRegistry extends BaseRegistry implements IRegistry { public readonly type = RegistryType.Merged; public readonly registries: Array; - - constructor({ registries, logger }: MergedRegistryOptions) { + public chainMetadataOverrides: ChainMap>; + public chainAddressesOverrides: ChainMap>; + + constructor({ + registries, + chainMetadataOverrides, + chainAddressesOverrides, + logger, + }: MergedRegistryOptions) { super({ uri: '__merged_registry__', logger }); if (!registries.length) throw new Error('At least one registry URI is required'); this.registries = registries; + this.chainMetadataOverrides = chainMetadataOverrides || {}; + this.chainAddressesOverrides = chainAddressesOverrides || {}; } async listRegistryContent(): Promise { @@ -34,19 +45,45 @@ export class MergedRegistry extends BaseRegistry implements IRegistry { } async getMetadata(): Promise> { - const results = await this.multiRegistryRead((r) => r.getMetadata()); + const results = await this.multiRegistryRead( + (r) => r.getMetadata(), + this.chainMetadataOverrides, + ); return results.reduce((acc, content) => objMerge(acc, content), {}); } + getMetadataOverrides(): ChainMap> { + return this.chainMetadataOverrides; + } + + setMetadataOverrides( + overrides: ChainMap>, + ): ChainMap> { + return (this.chainMetadataOverrides = overrides); + } + async getChainMetadata(chainName: ChainName): Promise { return (await this.getMetadata())[chainName] || null; } async getAddresses(): Promise> { - const results = await this.multiRegistryRead((r) => r.getAddresses()); + const results = await this.multiRegistryRead( + (r) => r.getAddresses(), + this.chainAddressesOverrides, + ); return results.reduce((acc, content) => objMerge(acc, content), {}); } + getAddressesOverrides(): ChainMap> { + return this.chainAddressesOverrides; + } + + setAddressesOverrides( + overrides: ChainMap>, + ): ChainMap> { + return (this.chainAddressesOverrides = overrides); + } + async getChainAddresses(chainName: ChainName): Promise { return (await this.getAddresses())[chainName] || null; } @@ -87,8 +124,12 @@ export class MergedRegistry extends BaseRegistry implements IRegistry { ); } - protected multiRegistryRead(readFn: (registry: IRegistry) => Promise | R) { - return Promise.all(this.registries.map(readFn)); + protected async multiRegistryRead( + readFn: (registry: IRegistry) => Promise | R, + overrides?: Partial, + ) { + const results = await Promise.all(this.registries.map(readFn)); + return overrides ? [...results, overrides as R] : results; } protected async multiRegistryWrite( diff --git a/test/unit/registry.test.ts b/test/unit/registry.test.ts index 9720ae588..32239a2d9 100644 --- a/test/unit/registry.test.ts +++ b/test/unit/registry.test.ts @@ -13,6 +13,7 @@ import { ChainAddresses } from '../../src/types.js'; const MOCK_CHAIN_NAME = 'mockchain'; const MOCK_CHAIN_NAME2 = 'mockchain2'; const MOCK_SYMBOL = 'MOCK'; +const MOCK_ADDRESS = '0x0000000000000000000000000000000000000001'; describe('Registry utilities', () => { const githubRegistry = new GithubRegistry(); @@ -62,6 +63,20 @@ describe('Registry utilities', () => { // Note the short timeout to ensure result is coming from cache }).timeout(250); + if (registry.type === RegistryType.Merged) { + it(`Handles metadata overrides for ${registry.type} registry`, async () => { + registry.setMetadataOverrides({ ethereum: { chainId: 2 } }); + expect(registry.getMetadataOverrides()['ethereum'].chainId).to.eql(2); + expect((await registry.getChainMetadata('ethereum'))!.chainId).to.eql(2); + }); + + it(`Handles address overrides for ${registry.type} registry`, async () => { + registry.setAddressesOverrides({ ethereum: { fakecontract: MOCK_ADDRESS } }); + expect(registry.getAddressesOverrides()['ethereum'].fakecontract).to.eql(MOCK_ADDRESS); + expect((await registry.getChainAddresses('ethereum'))!.fakecontract).to.eql(MOCK_ADDRESS); + }); + } + // TODO remove this once GitHubRegistry methods are implemented if (registry.type !== RegistryType.Local) continue; From 4a120d83434fc5c7bd43e91a4c6a78e735388bdb Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Thu, 16 May 2024 13:26:10 -0400 Subject: [PATCH 4/8] Fix incorrect override type --- src/registry/MergedRegistry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registry/MergedRegistry.ts b/src/registry/MergedRegistry.ts index 18688e819..72a654fa2 100644 --- a/src/registry/MergedRegistry.ts +++ b/src/registry/MergedRegistry.ts @@ -8,7 +8,7 @@ import { IRegistry, RegistryContent, RegistryType } from './IRegistry.js'; export interface MergedRegistryOptions { registries: Array; - chainMetadataOverrides?: ChainMap; + chainMetadataOverrides?: ChainMap>; chainAddressesOverrides?: ChainMap>; logger?: Logger; } From f0ed939a616dd4eda4a1b83214998c4e0e155339 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Fri, 17 May 2024 14:19:42 -0400 Subject: [PATCH 5/8] Replace overrides with PartialRegistry --- src/index.ts | 1 + src/registry/BaseRegistry.ts | 5 ++ src/registry/GithubRegistry.ts | 5 ++ src/registry/IRegistry.ts | 3 ++ src/registry/LocalRegistry.ts | 57 ++++----------------- src/registry/MergedRegistry.ts | 79 +++++++++++------------------ src/registry/PartialRegistry.ts | 74 +++++++++++++++++++++++++++ src/registry/SynchronousRegistry.ts | 71 ++++++++++++++++++++++++++ test/unit/registry.test.ts | 30 +++++------ 9 files changed, 212 insertions(+), 113 deletions(-) create mode 100644 src/registry/PartialRegistry.ts create mode 100644 src/registry/SynchronousRegistry.ts diff --git a/src/index.ts b/src/index.ts index 3ca47f495..76677f28b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/registry/BaseRegistry.ts b/src/registry/BaseRegistry.ts index fd820f6d9..07396206a 100644 --- a/src/registry/BaseRegistry.ts +++ b/src/registry/BaseRegistry.ts @@ -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)/; @@ -72,4 +73,8 @@ export abstract class BaseRegistry implements IRegistry { }): MaybePromise; abstract removeChain(chain: ChainName): MaybePromise; abstract addWarpRoute(config: WarpCoreConfig): MaybePromise; + + merge(otherRegistry: IRegistry): IRegistry { + return new MergedRegistry({ registries: [this, otherRegistry], logger: this.logger }); + } } diff --git a/src/registry/GithubRegistry.ts b/src/registry/GithubRegistry.ts index 8cce11fa3..15196da56 100644 --- a/src/registry/GithubRegistry.ts +++ b/src/registry/GithubRegistry.ts @@ -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; diff --git a/src/registry/IRegistry.ts b/src/registry/IRegistry.ts index 841cd2ee3..02cb665a7 100644 --- a/src/registry/IRegistry.ts +++ b/src/registry/IRegistry.ts @@ -18,6 +18,7 @@ export enum RegistryType { Github = 'github', Local = 'local', Merged = 'merged', + Partial = 'partial', } export interface IRegistry { @@ -50,4 +51,6 @@ export interface IRegistry { addWarpRoute(config: WarpCoreConfig): MaybePromise; // TODO define more deployment artifact related methods + + merge(otherRegistry: IRegistry): IRegistry; } diff --git a/src/registry/LocalRegistry.ts b/src/registry/LocalRegistry.ts index c70bdc402..faafd575e 100644 --- a/src/registry/LocalRegistry.ts +++ b/src/registry/LocalRegistry.ts @@ -8,13 +8,14 @@ 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 { @@ -22,7 +23,11 @@ export interface LocalRegistryOptions { 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) { @@ -48,10 +53,6 @@ export class LocalRegistry extends BaseRegistry implements IRegistry { return (this.listContentCache = { chains, deployments: {} }); } - getChains(): Array { - return Object.keys(this.listRegistryContent().chains); - } - getMetadata(): ChainMap { if (this.metadataCache) return this.metadataCache; const chainMetadata: ChainMap = {}; @@ -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 { if (this.addressCache) return this.addressCache; const chainAddresses: ChainMap = {}; @@ -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[] { diff --git a/src/registry/MergedRegistry.ts b/src/registry/MergedRegistry.ts index 72a654fa2..f9641b177 100644 --- a/src/registry/MergedRegistry.ts +++ b/src/registry/MergedRegistry.ts @@ -3,33 +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; - chainMetadataOverrides?: ChainMap>; - chainAddressesOverrides?: ChainMap>; 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; - public chainMetadataOverrides: ChainMap>; - public chainAddressesOverrides: ChainMap>; - - constructor({ - registries, - chainMetadataOverrides, - chainAddressesOverrides, - logger, - }: MergedRegistryOptions) { - super({ uri: '__merged_registry__', logger }); + protected readonly logger: Logger; + + constructor({ registries, logger }: MergedRegistryOptions) { if (!registries.length) throw new Error('At least one registry URI is required'); this.registries = registries; - this.chainMetadataOverrides = chainMetadataOverrides || {}; - this.chainAddressesOverrides = chainAddressesOverrides || {}; + // @ts-ignore + this.logger = logger || console; } async listRegistryContent(): Promise { @@ -45,49 +42,28 @@ export class MergedRegistry extends BaseRegistry implements IRegistry { } async getMetadata(): Promise> { - const results = await this.multiRegistryRead( - (r) => r.getMetadata(), - this.chainMetadataOverrides, - ); + const results = await this.multiRegistryRead((r) => r.getMetadata()); return results.reduce((acc, content) => objMerge(acc, content), {}); } - getMetadataOverrides(): ChainMap> { - return this.chainMetadataOverrides; - } - - setMetadataOverrides( - overrides: ChainMap>, - ): ChainMap> { - return (this.chainMetadataOverrides = overrides); - } - async getChainMetadata(chainName: ChainName): Promise { return (await this.getMetadata())[chainName] || null; } async getAddresses(): Promise> { - const results = await this.multiRegistryRead( - (r) => r.getAddresses(), - this.chainAddressesOverrides, - ); + const results = await this.multiRegistryRead((r) => r.getAddresses()); return results.reduce((acc, content) => objMerge(acc, content), {}); } - getAddressesOverrides(): ChainMap> { - return this.chainAddressesOverrides; - } - - setAddressesOverrides( - overrides: ChainMap>, - ): ChainMap> { - return (this.chainAddressesOverrides = overrides); - } - async getChainAddresses(chainName: ChainName): Promise { return (await this.getAddresses())[chainName] || null; } + async getChainLogoUri(chainName: ChainName): Promise { + const results = await this.multiRegistryRead((r) => r.getChainLogoUri(chainName)); + return results.filter((uri) => uri !== null)[0] || null; + } + async addChain(chain: { chainName: ChainName; metadata?: ChainMetadata; @@ -124,12 +100,8 @@ export class MergedRegistry extends BaseRegistry implements IRegistry { ); } - protected async multiRegistryRead( - readFn: (registry: IRegistry) => Promise | R, - overrides?: Partial, - ) { - const results = await Promise.all(this.registries.map(readFn)); - return overrides ? [...results, overrides as R] : results; + protected async multiRegistryRead(readFn: (registry: IRegistry) => Promise | R) { + return Promise.all(this.registries.map(readFn)); } protected async multiRegistryWrite( @@ -152,4 +124,11 @@ export class MergedRegistry extends BaseRegistry implements IRegistry { } } } + + merge(otherRegistry: IRegistry): IRegistry { + return new MergedRegistry({ + registries: [...this.registries, otherRegistry], + logger: this.logger, + }); + } } diff --git a/src/registry/PartialRegistry.ts b/src/registry/PartialRegistry.ts new file mode 100644 index 000000000..5addc04ce --- /dev/null +++ b/src/registry/PartialRegistry.ts @@ -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>; + chainAddresses?: ChainMap>; + // TODO add more fields here as needed + logger?: Logger; +} + +export class PartialRegistry extends SynchronousRegistry implements IRegistry { + public readonly type = RegistryType.Partial; + public chainMetadata: ChainMap>; + public chainAddresses: ChainMap>; + + constructor({ chainMetadata, chainAddresses, logger }: PartialRegistryOptions) { + super({ uri: PARTIAL_URI_PLACEHOLDER, logger }); + this.chainMetadata = chainMetadata || {}; + this.chainAddresses = chainAddresses || {}; + } + + listRegistryContent(): RegistryContent { + const chains: ChainMap = {}; + 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 { + return this.chainMetadata as ChainMap; + } + + getAddresses(): ChainMap { + return this.chainAddresses as ChainMap; + } + + 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; + } +} diff --git a/src/registry/SynchronousRegistry.ts b/src/registry/SynchronousRegistry.ts new file mode 100644 index 000000000..600adebed --- /dev/null +++ b/src/registry/SynchronousRegistry.ts @@ -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 { type IRegistry, type 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 { + return Object.keys(this.listRegistryContent().chains); + } + + abstract getMetadata(): ChainMap; + + getChainMetadata(chainName: ChainName): ChainMetadata | null { + return this.getMetadata()[chainName] || null; + } + + abstract getAddresses(): ChainMap; + + 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; +} diff --git a/test/unit/registry.test.ts b/test/unit/registry.test.ts index 32239a2d9..8f1b672b1 100644 --- a/test/unit/registry.test.ts +++ b/test/unit/registry.test.ts @@ -8,10 +8,12 @@ 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 { PartialRegistry } from '../../src/registry/PartialRegistry.js'; import { ChainAddresses } from '../../src/types.js'; const MOCK_CHAIN_NAME = 'mockchain'; const MOCK_CHAIN_NAME2 = 'mockchain2'; +const MOCK_DISPLAY_NAME = 'faketherum'; const MOCK_SYMBOL = 'MOCK'; const MOCK_ADDRESS = '0x0000000000000000000000000000000000000001'; @@ -24,9 +26,16 @@ describe('Registry utilities', () => { const localRegistry = new LocalRegistry({ uri: './' }); expect(localRegistry.uri).to.be.a('string'); - const mergedRegistry = new MergedRegistry({ registries: [githubRegistry, localRegistry] }); + const partialRegistry = new PartialRegistry({ + chainMetadata: { ethereum: { chainId: 1, displayName: MOCK_DISPLAY_NAME } }, + chainAddresses: { ethereum: { mailbox: MOCK_ADDRESS } }, + }); + + const mergedRegistry = new MergedRegistry({ + registries: [githubRegistry, localRegistry, partialRegistry], + }); - for (const registry of [githubRegistry, localRegistry, mergedRegistry]) { + for (const registry of [githubRegistry, localRegistry, partialRegistry, mergedRegistry]) { it(`Lists all chains for ${registry.type} registry`, async () => { const chains = await registry.getChains(); expect(chains.length).to.be.greaterThan(0); @@ -37,6 +46,9 @@ describe('Registry utilities', () => { const metadata = await registry.getMetadata(); expect(Object.keys(metadata).length).to.be.greaterThan(0); expect(metadata['ethereum'].chainId).to.eql(1); + if (registry.type === RegistryType.Partial || registry.type === RegistryType.Merged) { + expect(metadata['ethereum'].displayName).to.eql(MOCK_DISPLAY_NAME); + } }).timeout(10_000); it(`Fetches single chain metadata for ${registry.type} registry`, async () => { @@ -63,20 +75,6 @@ describe('Registry utilities', () => { // Note the short timeout to ensure result is coming from cache }).timeout(250); - if (registry.type === RegistryType.Merged) { - it(`Handles metadata overrides for ${registry.type} registry`, async () => { - registry.setMetadataOverrides({ ethereum: { chainId: 2 } }); - expect(registry.getMetadataOverrides()['ethereum'].chainId).to.eql(2); - expect((await registry.getChainMetadata('ethereum'))!.chainId).to.eql(2); - }); - - it(`Handles address overrides for ${registry.type} registry`, async () => { - registry.setAddressesOverrides({ ethereum: { fakecontract: MOCK_ADDRESS } }); - expect(registry.getAddressesOverrides()['ethereum'].fakecontract).to.eql(MOCK_ADDRESS); - expect((await registry.getChainAddresses('ethereum'))!.fakecontract).to.eql(MOCK_ADDRESS); - }); - } - // TODO remove this once GitHubRegistry methods are implemented if (registry.type !== RegistryType.Local) continue; From f5d6d4cf3ed660d7ac41a9e9f5a3ccab171f2a90 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Fri, 17 May 2024 14:20:36 -0400 Subject: [PATCH 6/8] Update changeset --- .changeset/brown-taxis-invite.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.changeset/brown-taxis-invite.md b/.changeset/brown-taxis-invite.md index f7a0cf29f..23d173ac2 100644 --- a/.changeset/brown-taxis-invite.md +++ b/.changeset/brown-taxis-invite.md @@ -1,5 +1,6 @@ --- -'@hyperlane-xyz/registry': patch +'@hyperlane-xyz/registry': minor --- -Add override methods to MergedRegistry +Add PartialRegistry class +Add merge() method to IRegistry From f1b866a95382d45c7c670bc7245633e1b9036e97 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Fri, 17 May 2024 14:23:22 -0400 Subject: [PATCH 7/8] Tweak test and fix typos --- src/registry/MergedRegistry.ts | 2 +- src/registry/SynchronousRegistry.ts | 2 +- test/unit/registry.test.ts | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/registry/MergedRegistry.ts b/src/registry/MergedRegistry.ts index f9641b177..cec591f3d 100644 --- a/src/registry/MergedRegistry.ts +++ b/src/registry/MergedRegistry.ts @@ -100,7 +100,7 @@ export class MergedRegistry implements IRegistry { ); } - protected async multiRegistryRead(readFn: (registry: IRegistry) => Promise | R) { + protected multiRegistryRead(readFn: (registry: IRegistry) => Promise | R) { return Promise.all(this.registries.map(readFn)); } diff --git a/src/registry/SynchronousRegistry.ts b/src/registry/SynchronousRegistry.ts index 600adebed..3d81bc330 100644 --- a/src/registry/SynchronousRegistry.ts +++ b/src/registry/SynchronousRegistry.ts @@ -2,7 +2,7 @@ import type { ChainMap, ChainMetadata, ChainName, WarpCoreConfig } from '@hyperl import { ChainAddresses } from '../types.js'; import { BaseRegistry } from './BaseRegistry.js'; -import { type IRegistry, type RegistryContent } from './IRegistry.js'; +import { IRegistry, RegistryContent } from './IRegistry.js'; /** * Shared code for sync registries like the FileSystem and Partial registries. diff --git a/test/unit/registry.test.ts b/test/unit/registry.test.ts index 8f1b672b1..8b4e6d590 100644 --- a/test/unit/registry.test.ts +++ b/test/unit/registry.test.ts @@ -48,6 +48,8 @@ describe('Registry utilities', () => { expect(metadata['ethereum'].chainId).to.eql(1); if (registry.type === RegistryType.Partial || registry.type === RegistryType.Merged) { expect(metadata['ethereum'].displayName).to.eql(MOCK_DISPLAY_NAME); + } else { + expect(metadata['ethereum'].displayName).to.eql('Ethereum'); } }).timeout(10_000); From 3da2be9170bb4f4cc25dd4b773e2b77b5eb3911d Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Mon, 20 May 2024 12:35:37 -0400 Subject: [PATCH 8/8] Replace filter with find --- src/registry/MergedRegistry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registry/MergedRegistry.ts b/src/registry/MergedRegistry.ts index cec591f3d..4833dd640 100644 --- a/src/registry/MergedRegistry.ts +++ b/src/registry/MergedRegistry.ts @@ -61,7 +61,7 @@ export class MergedRegistry implements IRegistry { async getChainLogoUri(chainName: ChainName): Promise { const results = await this.multiRegistryRead((r) => r.getChainLogoUri(chainName)); - return results.filter((uri) => uri !== null)[0] || null; + return results.find((uri) => !!uri) || null; } async addChain(chain: {