diff --git a/.changeset/beige-cats-punch.md b/.changeset/beige-cats-punch.md new file mode 100644 index 000000000..32721336f --- /dev/null +++ b/.changeset/beige-cats-punch.md @@ -0,0 +1,6 @@ +--- +'@hyperlane-xyz/registry': major +--- + +Add support for reading warp route configs from registries +Add getURI method to registry classes diff --git a/.changeset/yellow-keys-peel.md b/.changeset/yellow-keys-peel.md index d56c55a69..61debb119 100644 --- a/.changeset/yellow-keys-peel.md +++ b/.changeset/yellow-keys-peel.md @@ -1,5 +1,5 @@ --- -"@hyperlane-xyz/registry": patch +'@hyperlane-xyz/registry': patch --- Add black border to Blast logo.svg diff --git a/scripts/build.ts b/scripts/build.ts index 68a354463..32f414b81 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -5,6 +5,11 @@ import fs from 'fs'; import { parse } from 'yaml'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { CoreChains } from '../src/core/chains'; +import { warpRouteConfigToId } from '../src/registry/warp-utils'; + +const chainMetadata = {}; +const chainAddresses = {}; +const warpRouteConfigs = {}; function genJsExport(data, exportName) { return `export const ${exportName} = ${JSON.stringify(data, null, 2)}`; @@ -20,102 +25,172 @@ function genChainMetadataMapExport(data, exportName) { ${genJsExport(data, exportName)} as ChainMap`; } -console.log('Preparing tmp directory'); -if (fs.existsSync('./tmp')) fs.rmSync(`./tmp`, { recursive: true }); -// Start with the contents of src, which we will add to in this script -fs.cpSync(`./src`, `./tmp`, { recursive: true }); +function genWarpRouteConfigExport(data, exportName) { + return `import type { WarpCoreConfig } from '@hyperlane-xyz/sdk'; +${genJsExport(data, exportName)} as WarpCoreConfig`; +} -const chainMetadata = {}; -const chainAddresses = {}; +function genWarpRouteConfigMapExport(data, exportName) { + return `import type { WarpCoreConfig } from '@hyperlane-xyz/sdk'; +${genJsExport(data, exportName)} as Record`; +} -console.log('Parsing and copying chain data'); -for (const file of fs.readdirSync('./chains')) { - const inDirPath = `./chains/${file}`; - const assetOutPath = `./dist/chains/${file}`; - // ts files go to the tmp dir so they can be compiled along with other generated code - const tsOutPath = `./tmp/chains/${file}`; - const stat = fs.statSync(`${inDirPath}`); - if (!stat.isDirectory()) continue; - - // Convert and copy metadata - const metadata = parse(fs.readFileSync(`${inDirPath}/metadata.yaml`, 'utf8')); - chainMetadata[metadata.name] = metadata; - fs.mkdirSync(`${assetOutPath}`, { recursive: true }); - fs.mkdirSync(`${tsOutPath}`, { recursive: true }); - fs.copyFileSync(`${inDirPath}/metadata.yaml`, `${assetOutPath}/metadata.yaml`); - fs.writeFileSync(`${assetOutPath}/metadata.json`, JSON.stringify(metadata, null, 2)); - fs.writeFileSync(`${tsOutPath}/metadata.ts`, genChainMetadataExport(metadata, 'metadata')); - - // Convert and copy addresses if there are any - if (fs.existsSync(`${inDirPath}/addresses.yaml`)) { - const addresses = parse(fs.readFileSync(`${inDirPath}/addresses.yaml`, 'utf8')); - chainAddresses[metadata.name] = addresses; - fs.copyFileSync(`${inDirPath}/addresses.yaml`, `${assetOutPath}/addresses.yaml`); - fs.writeFileSync(`${assetOutPath}/addresses.json`, JSON.stringify(addresses, null, 2)); - fs.writeFileSync(`${tsOutPath}/addresses.ts`, genJsExport(addresses, 'addresses')); +function createTmpDir() { + console.log('Preparing tmp directory'); + if (fs.existsSync('./tmp')) fs.rmSync(`./tmp`, { recursive: true }); + // Start with the contents of src, which we will add to in this script + fs.cpSync(`./src`, `./tmp`, { recursive: true }); +} + +function createChainFiles() { + console.log('Parsing and copying chain data'); + for (const file of fs.readdirSync('./chains')) { + const inDirPath = `./chains/${file}`; + const assetOutPath = `./dist/chains/${file}`; + // ts files go to the tmp dir so they can be compiled along with other generated code + const tsOutPath = `./tmp/chains/${file}`; + const stat = fs.statSync(`${inDirPath}`); + if (!stat.isDirectory()) continue; + + // Convert and copy metadata + const metadata = parse(fs.readFileSync(`${inDirPath}/metadata.yaml`, 'utf8')); + chainMetadata[metadata.name] = metadata; + fs.mkdirSync(`${assetOutPath}`, { recursive: true }); + fs.mkdirSync(`${tsOutPath}`, { recursive: true }); + fs.copyFileSync(`${inDirPath}/metadata.yaml`, `${assetOutPath}/metadata.yaml`); + fs.writeFileSync(`${assetOutPath}/metadata.json`, JSON.stringify(metadata, null, 2)); + fs.writeFileSync(`${tsOutPath}/metadata.ts`, genChainMetadataExport(metadata, 'metadata')); + + // Convert and copy addresses if there are any + if (fs.existsSync(`${inDirPath}/addresses.yaml`)) { + const addresses = parse(fs.readFileSync(`${inDirPath}/addresses.yaml`, 'utf8')); + chainAddresses[metadata.name] = addresses; + fs.copyFileSync(`${inDirPath}/addresses.yaml`, `${assetOutPath}/addresses.yaml`); + fs.writeFileSync(`${assetOutPath}/addresses.json`, JSON.stringify(addresses, null, 2)); + fs.writeFileSync(`${tsOutPath}/addresses.ts`, genJsExport(addresses, 'addresses')); + } + + // Copy the logo file + fs.copyFileSync(`${inDirPath}/logo.svg`, `${assetOutPath}/logo.svg`); } +} + +function createWarpConfigFiles() { + console.log('Parsing and copying warp config data'); + const warpPathBase = 'deployments/warp_routes'; + // Outer loop for token symbol directories + for (const warpDir of fs.readdirSync(`./${warpPathBase}`)) { + const inDirPath = `./${warpPathBase}/${warpDir}`; + const assetOutPath = `./dist/${warpPathBase}/${warpDir}`; + // ts files go to the tmp dir so they can be compiled along with other generated code + const tsOutPath = `./tmp/${warpPathBase}/${warpDir}`; + const stat = fs.statSync(`${inDirPath}`); + if (!stat.isDirectory()) continue; - // Copy the logo file - fs.copyFileSync(`${inDirPath}/logo.svg`, `${assetOutPath}/logo.svg`); + // Inner loop for individual warp route configs + for (const warpFile of fs.readdirSync(inDirPath)) { + if (!warpFile.endsWith('config.yaml')) continue; + const [warpFileName] = warpFile.split('.'); + const config = parse(fs.readFileSync(`${inDirPath}/${warpFile}`, 'utf8')); + const id = warpRouteConfigToId(config); + warpRouteConfigs[id] = config; + fs.mkdirSync(`${assetOutPath}`, { recursive: true }); + fs.mkdirSync(`${tsOutPath}`, { recursive: true }); + fs.copyFileSync(`${inDirPath}/${warpFileName}.yaml`, `${assetOutPath}/${warpFile}.yaml`); + fs.writeFileSync(`${assetOutPath}/${warpFileName}.json`, JSON.stringify(config, null, 2)); + fs.writeFileSync( + `${tsOutPath}/${warpFileName}.ts`, + genWarpRouteConfigExport(config, 'warpRouteConfig'), + ); + } + } } -console.log('Assembling typescript code'); -// Create files for the chain metadata and addresses maps -fs.writeFileSync( - `./tmp/chainMetadata.ts`, - genChainMetadataMapExport(chainMetadata, 'chainMetadata'), -); -fs.writeFileSync(`./tmp/chainAddresses.ts`, genJsExport(chainAddresses, 'chainAddresses')); -// And also alternate versions with just the core chains -const coreChainMetadata = pick(chainMetadata, CoreChains); -const coreChainAddresses = pick(chainAddresses, CoreChains); -fs.writeFileSync( - `./tmp/coreChainMetadata.ts`, - genChainMetadataMapExport(coreChainMetadata, 'coreChainMetadata'), -); -fs.writeFileSync( - `./tmp/coreChainAddresses.ts`, - genJsExport(coreChainAddresses, 'coreChainAddresses'), -); -// Add the exports for new files to the index file -fs.appendFileSync( - `./tmp/index.ts`, - ` -export { chainMetadata } from './chainMetadata.js'; -export { coreChainMetadata } from './coreChainMetadata.js'; -export { chainAddresses } from './chainAddresses.js'; -export { coreChainAddresses } from './coreChainAddresses.js'; -`, -); -// Also create individual js files for each chain -for (const name of Object.keys(chainMetadata)) { - // Create an index file for each chain folder to allow for direct, single-chain imports - fs.writeFileSync(`./tmp/chains/${name}/index.ts`, `export { metadata } from './metadata.js';\n`); - // Also add a metadata export to the root index for convenience +function generateChainTsCode() { + console.log('Assembling chain typescript code'); + // Create files for the chain metadata and addresses maps + fs.writeFileSync( + `./tmp/chainMetadata.ts`, + genChainMetadataMapExport(chainMetadata, 'chainMetadata'), + ); + fs.writeFileSync(`./tmp/chainAddresses.ts`, genJsExport(chainAddresses, 'chainAddresses')); + // And also alternate versions with just the core chains + const coreChainMetadata = pick(chainMetadata, CoreChains); + const coreChainAddresses = pick(chainAddresses, CoreChains); + fs.writeFileSync( + `./tmp/coreChainMetadata.ts`, + genChainMetadataMapExport(coreChainMetadata, 'coreChainMetadata'), + ); + fs.writeFileSync( + `./tmp/coreChainAddresses.ts`, + genJsExport(coreChainAddresses, 'coreChainAddresses'), + ); + // Add the exports for new files to the index file fs.appendFileSync( `./tmp/index.ts`, - `export { metadata as ${name} } from './chains/${name}/metadata.js';\n`, + ` + export { chainMetadata } from './chainMetadata.js'; + export { coreChainMetadata } from './coreChainMetadata.js'; + export { chainAddresses } from './chainAddresses.js'; + export { coreChainAddresses } from './coreChainAddresses.js'; + `, ); - // Ditto as above for addresses if they exist - if (chainAddresses[name]) { - fs.appendFileSync( + // Also create individual js files for each chain + for (const name of Object.keys(chainMetadata)) { + // Create an index file for each chain folder to allow for direct, single-chain imports + fs.writeFileSync( `./tmp/chains/${name}/index.ts`, - `export { addresses } from './addresses.js';\n`, + `export { metadata } from './metadata.js';\n`, ); + // Also add a metadata export to the root index for convenience fs.appendFileSync( `./tmp/index.ts`, - `export { addresses as ${name}Addresses } from './chains/${name}/addresses.js';\n`, + `export { metadata as ${name} } from './chains/${name}/metadata.js';\n`, ); + // Ditto as above for addresses if they exist + if (chainAddresses[name]) { + fs.appendFileSync( + `./tmp/chains/${name}/index.ts`, + `export { addresses } from './addresses.js';\n`, + ); + fs.appendFileSync( + `./tmp/index.ts`, + `export { addresses as ${name}Addresses } from './chains/${name}/addresses.js';\n`, + ); + } } } -console.log('Updating & copying chain JSON schemas'); -const chainSchema = zodToJsonSchema(ChainMetadataSchemaObject, 'hyperlaneChainMetadata'); -fs.writeFileSync(`./chains/schema.json`, JSON.stringify(chainSchema, null, 2), 'utf8'); -fs.copyFileSync(`./chains/schema.json`, `./dist/chains/schema.json`); -const warpSchema = zodToJsonSchema(WarpCoreConfigSchema, 'hyperlaneWarpCoreConfig'); -fs.writeFileSync( - `./deployments/warp_routes/schema.json`, - JSON.stringify(warpSchema, null, 2), - 'utf8', -); +function generateWarpConfigTsCode() { + console.log('Assembling warp config typescript code'); + // Generate a combined config map + fs.writeFileSync( + `./tmp/warpRouteConfigs.ts`, + genWarpRouteConfigMapExport(warpRouteConfigs, 'warpRouteConfigs'), + ); + // Add the export to the index file + fs.appendFileSync( + `./tmp/index.ts`, + `\nexport { warpRouteConfigs } from './warpRouteConfigs.js';`, + ); +} + +function updateJsonSchemas() { + console.log('Updating & copying chain JSON schemas'); + const chainSchema = zodToJsonSchema(ChainMetadataSchemaObject, 'hyperlaneChainMetadata'); + fs.writeFileSync(`./chains/schema.json`, JSON.stringify(chainSchema, null, 2), 'utf8'); + fs.copyFileSync(`./chains/schema.json`, `./dist/chains/schema.json`); + const warpSchema = zodToJsonSchema(WarpCoreConfigSchema, 'hyperlaneWarpCoreConfig'); + fs.writeFileSync( + `./deployments/warp_routes/schema.json`, + JSON.stringify(warpSchema, null, 2), + 'utf8', + ); +} + +createTmpDir(); +createChainFiles(); +createWarpConfigFiles(); +generateChainTsCode(); +generateWarpConfigTsCode(); +updateJsonSchemas(); diff --git a/src/consts.ts b/src/consts.ts index 726e4ee21..070fff0ce 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -1,3 +1,7 @@ export const SCHEMA_REF = '# yaml-language-server: $schema=../schema.json'; + export const DEFAULT_GITHUB_REGISTRY = 'https://github.com/hyperlane-xyz/hyperlane-registry'; export const GITHUB_FETCH_CONCURRENCY_LIMIT = 5; + +export const CHAIN_FILE_REGEX = /chains\/([a-z0-9]+)\/([a-z]+)\.(yaml|svg)/; +export const WARP_ROUTE_CONFIG_FILE_REGEX = /warp_routes\/([a-zA-Z0-9]+)\/([a-z0-9-]+)-config.yaml/; diff --git a/src/index.ts b/src/index.ts index 76677f28b..9d50cd3d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,19 @@ export { CoreChain, CoreChainName, CoreChains, CoreMainnets, CoreTestnets } from './core/chains.js'; -export { DEFAULT_GITHUB_REGISTRY } from './consts.js'; -export { BaseRegistry, CHAIN_FILE_REGEX } from './registry/BaseRegistry.js'; +export { + CHAIN_FILE_REGEX, + DEFAULT_GITHUB_REGISTRY, + WARP_ROUTE_CONFIG_FILE_REGEX, +} from './consts.js'; +export { BaseRegistry } 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 { + filterWarpRoutesIds, + warpConfigToWarpAddresses, + warpRouteConfigPathToId, + warpRouteConfigToId, +} from './registry/warp-utils.js'; export { ChainAddresses, ChainAddressesSchema } from './types.js'; diff --git a/src/registry/BaseRegistry.ts b/src/registry/BaseRegistry.ts index 07396206a..cc83c481f 100644 --- a/src/registry/BaseRegistry.ts +++ b/src/registry/BaseRegistry.ts @@ -2,11 +2,17 @@ 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 { WarpRouteConfigMap } from '../types.js'; +import { stripLeadingSlash } from '../utils.js'; +import type { + IRegistry, + RegistryContent, + RegistryType, + UpdateChainParams, + WarpRouteFilterParams, +} from './IRegistry.js'; import { MergedRegistry } from './MergedRegistry.js'; -export const CHAIN_FILE_REGEX = /chains\/([a-z0-9]+)\/([a-z]+)\.(yaml|svg)/; - export abstract class BaseRegistry implements IRegistry { public abstract type: RegistryType; public readonly uri: string; @@ -27,11 +33,20 @@ export abstract class BaseRegistry implements IRegistry { this.logger = logger || console; } + getUri(itemPath?: string): string { + if (itemPath) itemPath = stripLeadingSlash(itemPath); + return itemPath ? `${this.uri}/${itemPath}` : this.uri; + } + protected getChainsPath(): string { return 'chains'; } - protected getWarpArtifactsPaths({ tokens }: WarpCoreConfig) { + protected getWarpRoutesPath(): string { + return 'deployments/warp_routes'; + } + + protected getWarpRoutesArtifactPaths({ tokens }: WarpCoreConfig) { if (!tokens.length) throw new Error('No tokens provided in config'); const symbols = new Set(tokens.map((token) => token.symbol.toUpperCase())); if (symbols.size !== 1) @@ -41,7 +56,7 @@ export abstract class BaseRegistry implements IRegistry { .map((token) => token.chainName) .sort() .join('-'); - const basePath = `deployments/warp_routes/${symbol}/${chains}`; + const basePath = `${this.getWarpRoutesPath()}/${symbol}/${chains}`; return { configPath: `${basePath}-config.yaml`, addressesPath: `${basePath}-addresses.yaml` }; } @@ -61,17 +76,12 @@ export abstract class BaseRegistry implements IRegistry { return chain?.logo ?? null; } - abstract addChain(chain: { - chainName: ChainName; - metadata?: ChainMetadata; - addresses?: ChainAddresses; - }): MaybePromise; - abstract updateChain(chain: { - chainName: ChainName; - metadata?: ChainMetadata; - addresses?: ChainAddresses; - }): MaybePromise; + abstract addChain(chain: UpdateChainParams): MaybePromise; + abstract updateChain(chain: UpdateChainParams): MaybePromise; abstract removeChain(chain: ChainName): MaybePromise; + + abstract getWarpRoute(routeId: string): MaybePromise; + abstract getWarpRoutes(filter?: WarpRouteFilterParams): MaybePromise; abstract addWarpRoute(config: WarpCoreConfig): MaybePromise; merge(otherRegistry: IRegistry): IRegistry { diff --git a/src/registry/FileSystemRegistry.ts b/src/registry/FileSystemRegistry.ts index 1251c2ae2..d26453a41 100644 --- a/src/registry/FileSystemRegistry.ts +++ b/src/registry/FileSystemRegistry.ts @@ -5,18 +5,18 @@ import { parse as yamlParse } from 'yaml'; import type { ChainMap, ChainMetadata, ChainName, WarpCoreConfig } from '@hyperlane-xyz/sdk'; -import { SCHEMA_REF } from '../consts.js'; -import { ChainAddresses, ChainAddressesSchema } from '../types.js'; +import { CHAIN_FILE_REGEX, SCHEMA_REF, WARP_ROUTE_CONFIG_FILE_REGEX } from '../consts.js'; +import { ChainAddresses, ChainAddressesSchema, WarpRouteId } from '../types.js'; import { toYamlString } from '../utils.js'; -import { CHAIN_FILE_REGEX } from './BaseRegistry.js'; import { RegistryType, + UpdateChainParams, type ChainFiles, type IRegistry, type RegistryContent, } from './IRegistry.js'; import { SynchronousRegistry } from './SynchronousRegistry.js'; -import { warpConfigToWarpAddresses } from './warp-utils.js'; +import { warpConfigToWarpAddresses, warpRouteConfigPathToId } from './warp-utils.js'; export interface FileSystemRegistryOptions { uri: string; @@ -34,23 +34,34 @@ export class FileSystemRegistry extends SynchronousRegistry implements IRegistry super(options); } + getUri(itemPath?: string): string { + if (!itemPath) return super.getUri(); + return path.join(this.uri, itemPath); + } + listRegistryContent(): RegistryContent { if (this.listContentCache) return this.listContentCache; const chainFileList = this.listFiles(path.join(this.uri, this.getChainsPath())); const chains: ChainMap = {}; - for (const chainFilePath of chainFileList) { - const matches = chainFilePath.match(CHAIN_FILE_REGEX); + for (const filePath of chainFileList) { + const matches = filePath.match(CHAIN_FILE_REGEX); if (!matches) continue; const [_, chainName, fileName] = matches; chains[chainName] ??= {}; // @ts-ignore allow dynamic key assignment - chains[chainName][fileName] = chainFilePath; + chains[chainName][fileName] = filePath; } - // TODO add handling for deployment artifact files here too + const warpRoutes: RegistryContent['deployments']['warpRoutes'] = {}; + const warpRouteFiles = this.listFiles(path.join(this.uri, this.getWarpRoutesPath())); + for (const filePath of warpRouteFiles) { + if (!WARP_ROUTE_CONFIG_FILE_REGEX.test(filePath)) continue; + const routeId = warpRouteConfigPathToId(filePath); + warpRoutes[routeId] = filePath; + } - return (this.listContentCache = { chains, deployments: {} }); + return (this.listContentCache = { chains, deployments: { warpRoutes } }); } getMetadata(): ChainMap { @@ -83,6 +94,17 @@ export class FileSystemRegistry extends SynchronousRegistry implements IRegistry this.removeFiles(Object.values(chainFiles)); } + addWarpRoute(config: WarpCoreConfig): void { + let { configPath, addressesPath } = this.getWarpRoutesArtifactPaths(config); + + configPath = path.join(this.uri, configPath); + this.createFile({ filePath: configPath, data: toYamlString(config, SCHEMA_REF) }); + + addressesPath = path.join(this.uri, addressesPath); + const addresses = warpConfigToWarpAddresses(config); + this.createFile({ filePath: addressesPath, data: toYamlString(addresses) }); + } + protected listFiles(dirPath: string): string[] { if (!fs.existsSync(dirPath)) return []; @@ -95,22 +117,7 @@ export class FileSystemRegistry extends SynchronousRegistry implements IRegistry return filePaths.flat(); } - addWarpRoute(config: WarpCoreConfig): void { - let { configPath, addressesPath } = this.getWarpArtifactsPaths(config); - - configPath = path.join(this.uri, configPath); - this.createFile({ filePath: configPath, data: toYamlString(config, SCHEMA_REF) }); - - addressesPath = path.join(this.uri, addressesPath); - const addresses = warpConfigToWarpAddresses(config); - this.createFile({ filePath: addressesPath, data: toYamlString(addresses) }); - } - - protected createOrUpdateChain(chain: { - chainName: ChainName; - metadata?: ChainMetadata; - addresses?: ChainAddresses; - }): void { + protected createOrUpdateChain(chain: UpdateChainParams): void { if (!chain.metadata && !chain.addresses) throw new Error(`Chain ${chain.chainName} must have metadata or addresses, preferably both`); @@ -166,4 +173,15 @@ export class FileSystemRegistry extends SynchronousRegistry implements IRegistry fs.rmdirSync(parentDir); } } + + protected getWarpRoutesForIds(ids: WarpRouteId[]): WarpCoreConfig[] { + const configs: WarpCoreConfig[] = []; + const warpRoutes = this.listRegistryContent().deployments.warpRoutes; + for (const [id, filePath] of Object.entries(warpRoutes)) { + if (!ids.includes(id)) continue; + const data = fs.readFileSync(filePath, 'utf8'); + configs.push(yamlParse(data)); + } + return configs; + } } diff --git a/src/registry/GithubRegistry.ts b/src/registry/GithubRegistry.ts index 15196da56..7b15a227d 100644 --- a/src/registry/GithubRegistry.ts +++ b/src/registry/GithubRegistry.ts @@ -3,16 +3,24 @@ 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 } from '../types.js'; -import { concurrentMap } from '../utils.js'; -import { BaseRegistry, CHAIN_FILE_REGEX } from './BaseRegistry.js'; import { + CHAIN_FILE_REGEX, + DEFAULT_GITHUB_REGISTRY, + GITHUB_FETCH_CONCURRENCY_LIMIT, + WARP_ROUTE_CONFIG_FILE_REGEX, +} from '../consts.js'; +import { ChainAddresses, WarpRouteConfigMap, WarpRouteId } from '../types.js'; +import { concurrentMap, stripLeadingSlash } from '../utils.js'; +import { BaseRegistry } from './BaseRegistry.js'; +import { + ChainFiles, + IRegistry, + RegistryContent, RegistryType, - type ChainFiles, - type IRegistry, - type RegistryContent, + UpdateChainParams, + WarpRouteFilterParams, } from './IRegistry.js'; +import { filterWarpRoutesIds, warpRouteConfigPathToId } from './warp-utils.js'; export interface GithubRegistryOptions { uri?: string; @@ -51,6 +59,11 @@ export class GithubRegistry extends BaseRegistry implements IRegistry { this.repoName = pathSegments.at(-1)!; } + getUri(itemPath?: string): string { + if (!itemPath) return super.getUri(); + return this.getRawContentUrl(itemPath); + } + async listRegistryContent(): Promise { if (this.listContentCache) return this.listContentCache; @@ -62,7 +75,8 @@ export class GithubRegistry extends BaseRegistry implements IRegistry { const tree = result.tree as TreeNode[]; const chainPath = this.getChainsPath(); - const chains: ChainMap = {}; + const chains: RegistryContent['chains'] = {}; + const warpRoutes: RegistryContent['deployments']['warpRoutes'] = {}; for (const node of tree) { if (CHAIN_FILE_REGEX.test(node.path)) { const [_, chainName, fileName, extension] = node.path.match(CHAIN_FILE_REGEX)!; @@ -73,10 +87,13 @@ export class GithubRegistry extends BaseRegistry implements IRegistry { ); } - // TODO add handling for deployment artifact files here too + if (WARP_ROUTE_CONFIG_FILE_REGEX.test(node.path)) { + const routeId = warpRouteConfigPathToId(node.path); + warpRoutes[routeId] = this.getRawContentUrl(node.path); + } } - return (this.listContentCache = { chains, deployments: {} }); + return (this.listContentCache = { chains, deployments: { warpRoutes } }); } async getChains(): Promise> { @@ -93,7 +110,7 @@ export class GithubRegistry extends BaseRegistry implements IRegistry { async getChainMetadata(chainName: ChainName): Promise { if (this.metadataCache?.[chainName]) return this.metadataCache[chainName]; - const data = await this.fetchSingleChainFile('metadata', chainName); + const data = await this.fetchChainFile('metadata', chainName); if (!data) return null; this.metadataCache = { ...this.metadataCache, [chainName]: data }; return data; @@ -108,35 +125,43 @@ export class GithubRegistry extends BaseRegistry implements IRegistry { async getChainAddresses(chainName: ChainName): Promise { if (this.addressCache?.[chainName]) return this.addressCache[chainName]; - const data = await this.fetchSingleChainFile('addresses', chainName); + const data = await this.fetchChainFile('addresses', chainName); if (!data) return null; this.addressCache = { ...this.addressCache, [chainName]: data }; return data; } - async addChain(_chains: { - chainName: ChainName; - metadata?: ChainMetadata; - addresses?: ChainAddresses; - }): Promise { + async addChain(_chains: UpdateChainParams): Promise { throw new Error('TODO: Implement'); } - async updateChain(_chains: { - chainName: ChainName; - metadata?: ChainMetadata; - addresses?: ChainAddresses; - }): Promise { + async updateChain(_chains: UpdateChainParams): Promise { throw new Error('TODO: Implement'); } async removeChain(_chains: ChainName): Promise { throw new Error('TODO: Implement'); } + async getWarpRoute(routeId: string): Promise { + const repoContents = await this.listRegistryContent(); + const routeConfigUrl = repoContents.deployments.warpRoutes[routeId]; + if (!routeConfigUrl) return null; + return this.fetchYamlFile(routeConfigUrl); + } + + async getWarpRoutes(filter?: WarpRouteFilterParams): Promise { + const warpRoutes = (await this.listRegistryContent()).deployments.warpRoutes; + const { ids: routeIds, values: routeConfigUrls } = filterWarpRoutesIds(warpRoutes, filter); + const configs = await this.fetchYamlFiles(routeConfigUrls); + const idsWithConfigs = routeIds.map((id, i): [WarpRouteId, WarpCoreConfig] => [id, configs[i]]); + return Object.fromEntries(idsWithConfigs); + } + async addWarpRoute(_config: WarpCoreConfig): Promise { throw new Error('TODO: Implement'); } protected getRawContentUrl(path: string): string { + path = stripLeadingSlash(path); return `https://raw.githubusercontent.com/${this.repoOwner}/${this.repoName}/${this.branch}/${path}`; } @@ -155,20 +180,15 @@ export class GithubRegistry extends BaseRegistry implements IRegistry { return acc; }, {}); - const results = await concurrentMap( - GITHUB_FETCH_CONCURRENCY_LIMIT, - Object.entries(fileUrls), - async ([chainName, fileUrl]): Promise<[ChainName, T]> => { - const response = await this.fetch(fileUrl); - const data = await response.text(); - return [chainName, yamlParse(data)]; - }, - ); - - return Object.fromEntries(results); + const results = await this.fetchYamlFiles(Object.values(fileUrls)); + const chainNameWithResult = chainNames.map((chainName, i): [ChainName, T] => [ + chainName, + results[i], + ]); + return Object.fromEntries(chainNameWithResult); } - protected async fetchSingleChainFile( + protected async fetchChainFile( fileName: keyof ChainFiles, chainName: ChainName, ): Promise { @@ -176,6 +196,16 @@ export class GithubRegistry extends BaseRegistry implements IRegistry { return results[chainName] ?? null; } + protected fetchYamlFiles(urls: string[]): Promise { + return concurrentMap(GITHUB_FETCH_CONCURRENCY_LIMIT, urls, (url) => this.fetchYamlFile(url)); + } + + protected async fetchYamlFile(url: string): Promise { + const response = await this.fetch(url); + const data = await response.text(); + return yamlParse(data); + } + protected async fetch(url: string): Promise { this.logger.debug(`Fetching from github: ${url}`); const response = await fetch(url); diff --git a/src/registry/IRegistry.ts b/src/registry/IRegistry.ts index 3e7533b62..d19475823 100644 --- a/src/registry/IRegistry.ts +++ b/src/registry/IRegistry.ts @@ -1,5 +1,5 @@ import type { ChainMap, ChainMetadata, ChainName, WarpCoreConfig } from '@hyperlane-xyz/sdk'; -import type { ChainAddresses, MaybePromise } from '../types.js'; +import { ChainAddresses, MaybePromise, WarpRouteConfigMap, WarpRouteId } from '../types.js'; export interface ChainFiles { metadata?: string; @@ -8,12 +8,25 @@ export interface ChainFiles { } export interface RegistryContent { + // Chain name to file type to file URI chains: ChainMap; deployments: { - // TODO define deployment artifact shape here + // Warp route ID to config URI + warpRoutes: Record; }; } +export interface UpdateChainParams { + chainName: ChainName; + metadata?: ChainMetadata; + addresses?: ChainAddresses; +} + +export interface WarpRouteFilterParams { + symbol?: string; + chainName?: ChainName; +} + export enum RegistryType { Github = 'github', FileSystem = 'filesystem', @@ -25,6 +38,8 @@ export interface IRegistry { type: RegistryType; uri: string; + getUri(itemPath?: string): string; + listRegistryContent(): MaybePromise; getChains(): MaybePromise>; @@ -37,19 +52,14 @@ export interface IRegistry { getChainLogoUri(chainName: ChainName): Promise; - addChain(chain: { - chainName: ChainName; - metadata?: ChainMetadata; - addresses?: ChainAddresses; - }): MaybePromise; - updateChain(chain: { - chainName: ChainName; - metadata?: ChainMetadata; - addresses?: ChainAddresses; - }): MaybePromise; + addChain(chain: UpdateChainParams): MaybePromise; + updateChain(chain: UpdateChainParams): MaybePromise; removeChain(chain: ChainName): MaybePromise; + getWarpRoute(routeId: string): MaybePromise; + getWarpRoutes(filter?: WarpRouteFilterParams): MaybePromise; addWarpRoute(config: WarpCoreConfig): MaybePromise; + // TODO define more deployment artifact related methods merge(otherRegistry: IRegistry): IRegistry; diff --git a/src/registry/MergedRegistry.ts b/src/registry/MergedRegistry.ts index 4833dd640..ce15c6634 100644 --- a/src/registry/MergedRegistry.ts +++ b/src/registry/MergedRegistry.ts @@ -1,9 +1,15 @@ import type { Logger } from 'pino'; import type { ChainMap, ChainMetadata, ChainName, WarpCoreConfig } from '@hyperlane-xyz/sdk'; -import { ChainAddresses } from '../types.js'; +import { ChainAddresses, WarpRouteConfigMap, WarpRouteId } from '../types.js'; import { objMerge } from '../utils.js'; -import { IRegistry, RegistryContent, RegistryType } from './IRegistry.js'; +import { + IRegistry, + RegistryContent, + RegistryType, + UpdateChainParams, + WarpRouteFilterParams, +} from './IRegistry.js'; export interface MergedRegistryOptions { registries: Array; @@ -29,11 +35,17 @@ export class MergedRegistry implements IRegistry { this.logger = logger || console; } + getUri(): string { + throw new Error('getUri method not applicable to MergedRegistry'); + } + async listRegistryContent(): Promise { const results = await this.multiRegistryRead((r) => r.listRegistryContent()); return results.reduce((acc, content) => objMerge(acc, content), { chains: {}, - deployments: {}, + deployments: { + warpRoutes: {}, + }, }); } @@ -64,22 +76,14 @@ export class MergedRegistry implements IRegistry { return results.find((uri) => !!uri) || null; } - async addChain(chain: { - chainName: ChainName; - metadata?: ChainMetadata; - addresses?: ChainAddresses; - }): Promise { + async addChain(chain: UpdateChainParams): Promise { return this.multiRegistryWrite( async (registry) => await registry.addChain(chain), `adding chain ${chain.chainName}`, ); } - async updateChain(chain: { - chainName: ChainName; - metadata?: ChainMetadata; - addresses?: ChainAddresses; - }): Promise { + async updateChain(chain: UpdateChainParams): Promise { return this.multiRegistryWrite( async (registry) => await registry.updateChain(chain), `updating chain ${chain.chainName}`, @@ -93,6 +97,16 @@ export class MergedRegistry implements IRegistry { ); } + async getWarpRoute(id: WarpRouteId): Promise { + const results = await this.multiRegistryRead((r) => r.getWarpRoute(id)); + return results.find((r) => !!r) || null; + } + + async getWarpRoutes(filter?: WarpRouteFilterParams): Promise { + const results = await this.multiRegistryRead((r) => r.getWarpRoutes(filter)); + return results.reduce((acc, content) => objMerge(acc, content), {}); + } + async addWarpRoute(config: WarpCoreConfig): Promise { return this.multiRegistryWrite( async (registry) => await registry.addWarpRoute(config), diff --git a/src/registry/PartialRegistry.ts b/src/registry/PartialRegistry.ts index 5addc04ce..54c228d80 100644 --- a/src/registry/PartialRegistry.ts +++ b/src/registry/PartialRegistry.ts @@ -1,9 +1,10 @@ import type { Logger } from 'pino'; import type { ChainMap, ChainMetadata, ChainName, WarpCoreConfig } from '@hyperlane-xyz/sdk'; -import { ChainAddresses } from '../types.js'; +import { ChainAddresses, DeepPartial, WarpRouteId } from '../types.js'; import { ChainFiles, IRegistry, RegistryContent, RegistryType } from './IRegistry.js'; import { SynchronousRegistry } from './SynchronousRegistry.js'; +import { warpRouteConfigToId } from './warp-utils.js'; const PARTIAL_URI_PLACEHOLDER = '__partial_registry__'; @@ -12,21 +13,24 @@ const PARTIAL_URI_PLACEHOLDER = '__partial_registry__'; * Useful for merging with other registries force overrides of subsets of data. */ export interface PartialRegistryOptions { - chainMetadata?: ChainMap>; - chainAddresses?: ChainMap>; + chainMetadata?: ChainMap>; + chainAddresses?: ChainMap>; + warpRoutes?: Array>; // 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>; + public chainMetadata: ChainMap>; + public chainAddresses: ChainMap>; + public warpRoutes: Array>; - constructor({ chainMetadata, chainAddresses, logger }: PartialRegistryOptions) { + constructor({ chainMetadata, chainAddresses, warpRoutes, logger }: PartialRegistryOptions) { super({ uri: PARTIAL_URI_PLACEHOLDER, logger }); this.chainMetadata = chainMetadata || {}; this.chainAddresses = chainAddresses || {}; + this.warpRoutes = warpRoutes || []; } listRegistryContent(): RegistryContent { @@ -39,9 +43,22 @@ export class PartialRegistry extends SynchronousRegistry implements IRegistry { chains[c] ||= {}; chains[c].addresses = PARTIAL_URI_PLACEHOLDER; }); + + const warpRoutes = this.warpRoutes.reduce( + (acc, r) => { + // Cast is useful because this handles partials and safe because the fn validates data + const id = warpRouteConfigToId(r as WarpCoreConfig); + acc[id] = PARTIAL_URI_PLACEHOLDER; + return acc; + }, + {}, + ); + return { chains, - deployments: {}, + deployments: { + warpRoutes, + }, }; } @@ -63,6 +80,13 @@ export class PartialRegistry extends SynchronousRegistry implements IRegistry { throw new Error('Method not implemented.'); } + protected getWarpRoutesForIds(ids: WarpRouteId[]): WarpCoreConfig[] { + return this.warpRoutes.filter((r) => { + const id = warpRouteConfigToId(r as WarpCoreConfig); + return ids.includes(id); + }) as WarpCoreConfig[]; + } + protected createOrUpdateChain(chain: { chainName: ChainName; metadata?: ChainMetadata; diff --git a/src/registry/SynchronousRegistry.ts b/src/registry/SynchronousRegistry.ts index 3d81bc330..e55b58d45 100644 --- a/src/registry/SynchronousRegistry.ts +++ b/src/registry/SynchronousRegistry.ts @@ -1,8 +1,14 @@ import type { ChainMap, ChainMetadata, ChainName, WarpCoreConfig } from '@hyperlane-xyz/sdk'; -import { ChainAddresses } from '../types.js'; +import { ChainAddresses, WarpRouteConfigMap, WarpRouteId } from '../types.js'; import { BaseRegistry } from './BaseRegistry.js'; -import { IRegistry, RegistryContent } from './IRegistry.js'; +import { + IRegistry, + RegistryContent, + UpdateChainParams, + WarpRouteFilterParams, +} from './IRegistry.js'; +import { filterWarpRoutesIds } from './warp-utils.js'; /** * Shared code for sync registries like the FileSystem and Partial registries. @@ -28,11 +34,7 @@ export abstract class SynchronousRegistry extends BaseRegistry implements IRegis return this.getAddresses()[chainName] || null; } - addChain(chain: { - chainName: ChainName; - metadata?: ChainMetadata; - addresses?: ChainAddresses; - }): void { + addChain(chain: UpdateChainParams): void { const currentChains = this.listRegistryContent().chains; if (currentChains[chain.chainName]) throw new Error(`Chain ${chain.chainName} already exists in registry`); @@ -40,11 +42,7 @@ export abstract class SynchronousRegistry extends BaseRegistry implements IRegis this.createOrUpdateChain(chain); } - updateChain(chain: { - chainName: ChainName; - metadata?: ChainMetadata; - addresses?: ChainAddresses; - }): void { + updateChain(chain: UpdateChainParams): void { const currentChains = this.listRegistryContent(); if (!currentChains.chains[chain.chainName]) { this.logger.debug(`Chain ${chain.chainName} not found in registry, adding it now`); @@ -61,11 +59,21 @@ export abstract class SynchronousRegistry extends BaseRegistry implements IRegis if (this.addressCache?.[chainName]) delete this.addressCache[chainName]; } + getWarpRoute(routeId: string): WarpCoreConfig | null { + return this.getWarpRoutesForIds([routeId])[0] || null; + } + + getWarpRoutes(filter?: WarpRouteFilterParams): WarpRouteConfigMap { + const warpRoutes = this.listRegistryContent().deployments.warpRoutes; + const { ids: routeIds } = filterWarpRoutesIds(warpRoutes, filter); + const configs = this.getWarpRoutesForIds(routeIds); + const idsWithConfigs = routeIds.map((id, i): [WarpRouteId, WarpCoreConfig] => [id, configs[i]]); + return Object.fromEntries(idsWithConfigs); + } + abstract addWarpRoute(config: WarpCoreConfig): void; - protected abstract createOrUpdateChain(chain: { - chainName: ChainName; - metadata?: ChainMetadata; - addresses?: ChainAddresses; - }): void; + protected abstract createOrUpdateChain(chain: UpdateChainParams): void; + + protected abstract getWarpRoutesForIds(ids: WarpRouteId[]): WarpCoreConfig[]; } diff --git a/src/registry/warp-utils.ts b/src/registry/warp-utils.ts index 4378e2d36..c8d2c87a4 100644 --- a/src/registry/warp-utils.ts +++ b/src/registry/warp-utils.ts @@ -1,6 +1,11 @@ -import type { ChainMap, TokenStandard, WarpCoreConfig } from '@hyperlane-xyz/sdk'; -import { ChainAddresses } from '../types.js'; +import type { ChainMap, ChainName, TokenStandard, WarpCoreConfig } from '@hyperlane-xyz/sdk'; +import { WARP_ROUTE_CONFIG_FILE_REGEX } from '../consts.js'; +import { ChainAddresses, WarpRouteId } from '../types.js'; +import { WarpRouteFilterParams } from './IRegistry.js'; +/** + * Converts from a full warp config to a map of chain addresses. + */ export function warpConfigToWarpAddresses(config: WarpCoreConfig): ChainMap { return config.tokens.reduce>((acc, token) => { const addressKey = getWarpAddressKey(token.standard); @@ -19,3 +24,62 @@ function getWarpAddressKey(standard: TokenStandard): string | null { if (standardValue.includes('native')) return 'native'; else return null; } + +/** + * Gets a warp route ID from a warp route config path. + * @param configRelativePath A relative path in the deployments dir + * (e.g. `warp_routes/USDC/ethereum-arbitrum-config.yaml`) + */ +export function warpRouteConfigPathToId(configRelativePath: string): WarpRouteId { + const matches = configRelativePath.match(WARP_ROUTE_CONFIG_FILE_REGEX); + if (!matches || matches.length < 3) + throw new Error(`Invalid warp route config path: ${configRelativePath}`); + const [_, tokenSymbol, chains] = matches; + return createWarpRouteConfigId(tokenSymbol, chains.split('-')); +} + +/** + * Gets a warp route ID from a warp route config. + * This uses the first symbol in the lift. Situations where a config contains multiple + * symbols are not officially supported yet. + */ +export function warpRouteConfigToId(config: WarpCoreConfig): WarpRouteId { + if (!config?.tokens?.length) throw new Error('Cannot generate ID for empty warp config'); + const tokenSymbol = config.tokens[0].symbol; + if (!tokenSymbol) throw new Error('Cannot generate warp config ID without a token symbol'); + const chains = config.tokens.map((token) => token.chainName); + return createWarpRouteConfigId(tokenSymbol, chains); +} + +export function createWarpRouteConfigId(tokenSymbol: string, chains: ChainName[]): WarpRouteId { + const sortedChains = [...chains].sort(); + return `${tokenSymbol}/${sortedChains.join('-')}`; +} + +export function parseWarpRouteConfigId(routeId: WarpRouteId): { + tokenSymbol: string; + chainNames: ChainName[]; +} { + const [tokenSymbol, chains] = routeId.split('/'); + return { tokenSymbol, chainNames: chains.split('-') }; +} + +/** + * Filters a list of warp route IDs based on the provided filter params. + */ +export function filterWarpRoutesIds( + idMap: Record, + filter?: WarpRouteFilterParams, +): { ids: WarpRouteId[]; values: T[]; idMap: Record } { + const filterChainName = filter?.chainName?.toLowerCase(); + const filterSymbol = filter?.symbol?.toLowerCase(); + const filtered = Object.entries(idMap).filter(([routeId]) => { + const { tokenSymbol, chainNames } = parseWarpRouteConfigId(routeId); + if (filterSymbol && tokenSymbol.toLowerCase() !== filterSymbol) return false; + if (filterChainName && !chainNames.includes(filterChainName)) return false; + return true; + }); + const ids = filtered.map(([routeId]) => routeId); + const values = filtered.map(([, value]) => value); + return { ids, values, idMap: Object.fromEntries(filtered) }; +} diff --git a/src/types.ts b/src/types.ts index 057d9b871..eadc4b058 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,4 @@ +import type { WarpCoreConfig } from '@hyperlane-xyz/sdk'; import { z } from 'zod'; // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-5.html#the-awaited-type-and-promise-improvements @@ -5,3 +6,12 @@ export type MaybePromise = T | Promise | PromiseLike; export const ChainAddressesSchema = z.record(z.string()); export type ChainAddresses = z.infer; + +export type WarpRouteId = string; +export type WarpRouteConfigMap = Record; + +export type DeepPartial = T extends object + ? { + [P in keyof T]?: DeepPartial; + } + : T; diff --git a/src/utils.ts b/src/utils.ts index 5280ec0b2..f9c6eca9d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,6 +5,10 @@ export function toYamlString(data: any, prefix?: string): string { return prefix ? `${prefix}\n${yamlString}` : yamlString; } +export function stripLeadingSlash(path: string): string { + return path.startsWith('/') || path.startsWith('\\') ? path.slice(1) : path; +} + export async function concurrentMap( concurrency: number, xs: A[], diff --git a/test/unit/registry.test.ts b/test/unit/registry.test.ts index 480738424..fb3bf610d 100644 --- a/test/unit/registry.test.ts +++ b/test/unit/registry.test.ts @@ -3,7 +3,7 @@ import { expect } from 'chai'; import type { ChainMetadata } from '@hyperlane-xyz/sdk'; import fs from 'fs'; -import { CHAIN_FILE_REGEX } from '../../src/registry/BaseRegistry.js'; +import { CHAIN_FILE_REGEX } from '../../src/consts.js'; import { FileSystemRegistry } from '../../src/registry/FileSystemRegistry.js'; import { GithubRegistry } from '../../src/registry/GithubRegistry.js'; import { RegistryType } from '../../src/registry/IRegistry.js'; @@ -29,6 +29,7 @@ describe('Registry utilities', () => { const partialRegistry = new PartialRegistry({ chainMetadata: { ethereum: { chainId: 1, displayName: MOCK_DISPLAY_NAME } }, chainAddresses: { ethereum: { mailbox: MOCK_ADDRESS } }, + warpRoutes: [{ tokens: [{ chainName: 'ethereum', symbol: 'USDT' }] }], }); const mergedRegistry = new MergedRegistry({ @@ -77,6 +78,16 @@ describe('Registry utilities', () => { // Note the short timeout to ensure result is coming from cache }).timeout(250); + it(`Fetches warp route configs for ${registry.type} registry`, async () => { + const routes = await registry.getWarpRoutes(); + const routeIds = Object.keys(routes); + expect(routeIds.length).to.be.greaterThan(0); + const firstRoute = await registry.getWarpRoute(routeIds[0]); + expect(firstRoute!.tokens.length).to.be.greaterThan(0); + const noRoutes = await registry.getWarpRoutes({ symbol: 'NOTFOUND' }); + expect(Object.keys(noRoutes).length).to.eql(0); + }); + // TODO remove this once GitHubRegistry methods are implemented if (registry.type !== RegistryType.FileSystem) continue; diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts new file mode 100644 index 000000000..8d1c0dcfc --- /dev/null +++ b/test/unit/utils.test.ts @@ -0,0 +1,39 @@ +import type { WarpCoreConfig } from '@hyperlane-xyz/sdk'; +import { expect } from 'chai'; +import { + filterWarpRoutesIds, + parseWarpRouteConfigId, + warpRouteConfigPathToId, + warpRouteConfigToId, +} from '../../src/registry/warp-utils.js'; + +const WARP_ROUTE_ID = 'USDT/arbitrum-ethereum'; + +describe('Warp utils', () => { + it('Computes a warp ID from a config path', () => { + expect(warpRouteConfigPathToId('warp_routes/USDT/arbitrum-ethereum-config.yaml')).to.eql( + WARP_ROUTE_ID, + ); + }); + it('Computes a warp ID from a config', () => { + const mockConfig = { + tokens: [ + { chainName: 'ethereum', symbol: 'USDT', standard: 'EvmHypCollateral' }, + { chainName: 'arbitrum', symbol: 'USDT', standard: 'EvmHypCollateral' }, + ], + } as WarpCoreConfig; + expect(warpRouteConfigToId(mockConfig)).to.eql(WARP_ROUTE_ID); + }); + it('Parses a warp ID', () => { + expect(parseWarpRouteConfigId(WARP_ROUTE_ID)).to.eql({ + tokenSymbol: 'USDT', + chainNames: ['arbitrum', 'ethereum'], + }); + }); + it('Filters warp route ID maps', () => { + const idMap = { [WARP_ROUTE_ID]: 'path' }; + expect(filterWarpRoutesIds(idMap).ids.length).to.eql(1); + expect(filterWarpRoutesIds(idMap, { chainName: 'fakechain' }).ids.length).to.eql(0); + expect(filterWarpRoutesIds(idMap, { symbol: 'fakesymbol' }).ids.length).to.eql(0); + }); +});