@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
403 lines • 15.4 kB
JavaScript
import { ProtocolType, assert, exclude, pick, rootLogger, } from '@hyperlane-xyz/utils';
import { isEvmBlockExplorerAndNotEtherscan } from '../block-explorer/utils.js';
import { getExplorerAddressUrl, getExplorerApi, getExplorerApiUrl, getExplorerBaseUrl, getExplorerTxUrl, } from './blockExplorer.js';
import { ChainMetadataSchema, ExplorerFamily, getDomainId, } from './chainMetadataTypes.js';
/**
* A set of utilities to manage chain metadata
* Validates metadata on construction and provides useful methods
* for interacting with the data
*/
export class ChainMetadataManager {
metadata = {};
logger;
static DEFAULT_MAX_BLOCK_RANGE = 1000;
/**
* Create a new ChainMetadataManager with the given chainMetadata,
* or the SDK's default metadata if not provided
*/
constructor(chainMetadata, options = {}) {
Object.entries(chainMetadata).forEach(([key, cm]) => {
if (key !== cm.name)
throw new Error(`Chain name mismatch: Key was ${key}, but name is ${cm.name}`);
this.addChain(cm);
});
this.logger =
options?.logger ||
rootLogger.child({
module: 'MetadataManager',
});
}
/**
* Add a chain to the MultiProvider
* @throws if chain's name or domain ID collide
*/
addChain(metadata) {
ChainMetadataSchema.parse(metadata);
// Ensure no two chains have overlapping names/domainIds
for (const chainMetadata of Object.values(this.metadata)) {
const { name, domainId } = chainMetadata;
if (name == metadata.name)
throw new Error(`Duplicate chain name: ${name}`);
// Domain Ids should be globally unique
const idCollision = metadata.domainId && domainId == metadata.domainId;
if (idCollision)
throw new Error(`Domain id collision: ${name} and ${metadata.name}`);
}
this.metadata[metadata.name] = metadata;
}
/**
* Get the metadata for a given chain name or domain id
* @throws if chain's metadata has not been set
*/
tryGetChainMetadata(chainNameOrId) {
// First check if it's a chain name
if (this.metadata[chainNameOrId])
return this.metadata[chainNameOrId];
// Otherwise search by domain id
const chainMetadata = Object.values(this.metadata).find((m) => m.domainId == chainNameOrId);
return chainMetadata || null;
}
/**
* Get the metadata for a given chain name or domain id
* @throws if chain's metadata has not been set
*/
getChainMetadata(chainNameOrId) {
const chainMetadata = this.tryGetChainMetadata(chainNameOrId);
if (!chainMetadata) {
throw new Error(`No chain metadata set for ${chainNameOrId}`);
}
return chainMetadata;
}
getMaxBlockRange(chainNameOrId) {
const metadata = this.getChainMetadata(chainNameOrId);
return Math.max(...metadata.rpcUrls.map(({ pagination }) => pagination?.maxBlockRange ??
ChainMetadataManager.DEFAULT_MAX_BLOCK_RANGE));
}
/**
* Returns true if the given chain name or domain id is
* included in this manager's metadata, false otherwise
*/
hasChain(chainNameOrId) {
return !!this.tryGetChainMetadata(chainNameOrId);
}
/**
* Get the name for a given chain name or domain id
*/
tryGetChainName(chainNameOrId) {
return this.tryGetChainMetadata(chainNameOrId)?.name ?? null;
}
/**
* Get the name for a given chain name or domain id
* @throws if chain's metadata has not been set
*/
getChainName(chainNameOrId) {
return this.getChainMetadata(chainNameOrId).name;
}
/**
* Get the names for all chains known to this MultiProvider
*/
getKnownChainNames() {
return Object.keys(this.metadata);
}
/**
* Get the id for a given chain name or domain id
*/
tryGetChainId(chainNameOrId) {
return this.tryGetChainMetadata(chainNameOrId)?.chainId ?? null;
}
/**
* Get the id for a given chain name or domain id
* @throws if chain's metadata has not been set
*/
getChainId(chainNameOrId) {
return this.getChainMetadata(chainNameOrId).chainId;
}
/**
* Get the id for a given EVM chain name or domain id
* Returns null if chain's metadata has not been set or is not an EVM chain
*/
tryGetEvmChainId(chainNameOrId) {
const metadata = this.tryGetChainMetadata(chainNameOrId);
if (!metadata)
return null;
if (metadata.protocol !== ProtocolType.Ethereum)
return null;
if (typeof metadata.chainId !== 'number')
return null;
return metadata.chainId;
}
/**
* Get the id for a given EVM chain name or domain id
* @throws if chain's metadata has not been set
*/
getEvmChainId(chainNameOrId) {
const { protocol, chainId } = this.getChainMetadata(chainNameOrId);
if (protocol !== ProtocolType.Ethereum) {
throw new Error(`Chain is not an EVM chain: ${chainNameOrId}`);
}
if (typeof chainId !== 'number') {
throw new Error(`Chain ID is not a number: ${chainId}`);
}
return chainId;
}
/**
* Get the domain id for a given chain name or domain id
*/
tryGetDomainId(chainNameOrId) {
const metadata = this.tryGetChainMetadata(chainNameOrId);
if (!metadata)
return null;
return getDomainId(metadata) ?? null;
}
/**
* Get the domain id for a given chain name or domain id
* @throws if chain's metadata has not been set
*/
getDomainId(chainNameOrId) {
const domainId = this.tryGetDomainId(chainNameOrId);
if (!domainId)
throw new Error(`No domain id set for ${chainNameOrId}`);
return domainId;
}
/**
* Get the protocol type for a given chain name or domain id
*/
tryGetProtocol(chainNameOrId) {
return this.tryGetChainMetadata(chainNameOrId)?.protocol ?? null;
}
/**
* Get the protocol type for a given chain name or domain id
* @throws if chain's metadata or protocol has not been set
*/
getProtocol(chainNameOrId) {
return this.getChainMetadata(chainNameOrId).protocol;
}
/**
* Get the domain ids for a list of chain names or domain ids
* @throws if any chain's metadata has not been set
*/
getDomainIds(chainNamesOrIds) {
return chainNamesOrIds.map((c) => this.getDomainId(c));
}
/**
* Get the ids for all chains known to this MultiProvider
*/
getKnownDomainIds() {
return this.getKnownChainNames().map((chainName) => this.getDomainId(chainName));
}
/**
* Get chain names excluding given chain name
*/
getRemoteChains(name) {
return exclude(name, this.getKnownChainNames());
}
/**
* Run given function on all known chains
*/
mapKnownChains(fn) {
const result = {};
for (const chain of this.getKnownChainNames()) {
result[chain] = fn(chain);
}
return result;
}
/**
* Get the RPC details for a given chain name or domain id.
* Optional index for metadata containing more than one RPC.
* @throws if chain's metadata has not been set
*/
getRpc(chainNameOrId, index = 0) {
const { rpcUrls } = this.getChainMetadata(chainNameOrId);
if (!rpcUrls?.length || !rpcUrls[index])
throw new Error(`No RPC configured at index ${index} for ${chainNameOrId}`);
return rpcUrls[index];
}
/**
* Get an RPC URL for a given chain name or domain id
* @throws if chain's metadata has not been set
*/
getRpcUrl(chainNameOrId, index = 0) {
const { http } = this.getRpc(chainNameOrId, index);
if (!http)
throw new Error(`No RPC URL configured for ${chainNameOrId}`);
return http;
}
/**
* Get an RPC concurrency level for a given chain name or domain id
*/
tryGetRpcConcurrency(chainNameOrId, index = 0) {
const { concurrency } = this.getRpc(chainNameOrId, index);
return concurrency ?? null;
}
/**
* Get a block explorer URL for a given chain name or domain id
*/
tryGetExplorerUrl(chainNameOrId) {
const metadata = this.tryGetChainMetadata(chainNameOrId);
if (!metadata)
return null;
return getExplorerBaseUrl(metadata);
}
/**
* Get a block explorer URL for a given chain name or domain id
* @throws if chain's metadata or block explorer data has no been set
*/
getExplorerUrl(chainNameOrId) {
const url = this.tryGetExplorerUrl(chainNameOrId);
if (!url)
throw new Error(`No explorer url set for ${chainNameOrId}`);
return url;
}
/**
* Get a block explorer's API for a given chain name or domain id
*/
tryGetExplorerApi(chainNameOrId) {
const metadata = this.tryGetChainMetadata(chainNameOrId);
if (!metadata)
return null;
return getExplorerApi(metadata);
}
/**
* Get a block explorer API for a given chain name or domain id
* @throws if chain's metadata or block explorer data has no been set
*/
getExplorerApi(chainNameOrId) {
const explorerApi = this.tryGetExplorerApi(chainNameOrId);
if (!explorerApi)
throw new Error(`No supported explorer api set for ${chainNameOrId}`);
return explorerApi;
}
/**
* Get a block explorer's API URL for a given chain name or domain id
*/
tryGetExplorerApiUrl(chainNameOrId) {
const metadata = this.tryGetChainMetadata(chainNameOrId);
if (!metadata)
return null;
return getExplorerApiUrl(metadata);
}
/**
* Get a block explorer API URL for a given chain name or domain id
* @throws if chain's metadata or block explorer data has no been set
*/
getExplorerApiUrl(chainNameOrId) {
const url = this.tryGetExplorerApiUrl(chainNameOrId);
if (!url)
throw new Error(`No explorer api url set for ${chainNameOrId}`);
return url;
}
/**
* Get a block explorer URL for given chain's tx
*/
tryGetExplorerTxUrl(chainNameOrId, response) {
const metadata = this.tryGetChainMetadata(chainNameOrId);
if (!metadata)
return null;
return getExplorerTxUrl(metadata, response.hash);
}
/**
* Get a block explorer URL for given chain's tx
* @throws if chain's metadata or block explorer data has no been set
*/
getExplorerTxUrl(chainNameOrId, response) {
return `${this.getExplorerUrl(chainNameOrId)}/tx/${response.hash}`;
}
/**
* Get a block explorer URL for given chain's address
*/
async tryGetExplorerAddressUrl(chainNameOrId, address) {
const metadata = this.tryGetChainMetadata(chainNameOrId);
if (!metadata || !address)
return null;
return getExplorerAddressUrl(metadata, address);
}
/**
* Get a block explorer URL for given chain's address
* @throws if address or the chain's block explorer data has no been set
*/
async getExplorerAddressUrl(chainNameOrId, address) {
const url = await this.tryGetExplorerAddressUrl(chainNameOrId, address);
if (!url)
throw new Error(`Missing data for address url for ${chainNameOrId}`);
return url;
}
/**
* Get a block explorer metadata for given chain
* @returns null if there isn't an explorer configured correctly for using the API (missing api key...)
*/
tryGetEvmExplorerMetadata(chainNameOrId) {
const defaultExplorer = this.tryGetExplorerApi(chainNameOrId);
if (!defaultExplorer) {
return null;
}
const chainMetadata = this.getChainMetadata(chainNameOrId);
const [fallBackExplorer] = chainMetadata.blockExplorers?.filter((blockExplorer) => isEvmBlockExplorerAndNotEtherscan(blockExplorer)) ?? [];
// Fallback to use other block explorers if the default block explorer
// is etherscan and an API key is not configured
const isExplorerConfiguredCorrectly = defaultExplorer.family === ExplorerFamily.Etherscan
? !!defaultExplorer.apiKey
: true;
const canUseExplorerApi = defaultExplorer.family !== ExplorerFamily.Other &&
isExplorerConfiguredCorrectly;
const explorer = canUseExplorerApi ? defaultExplorer : fallBackExplorer;
return explorer ?? null;
}
/**
* Get a block explorer metadata for given chain
* @throws if there isn't an explorer configured correctly for using the API (missing api key...)
*/
getEvmExplorerMetadata(chainNameOrId) {
const explorer = this.tryGetEvmExplorerMetadata(chainNameOrId);
assert(explorer, `No explorer was configured correctly to make requests to the API for chain "${chainNameOrId}". Set an API key or configure an explorer API that does not require one`);
return explorer;
}
/**
* Get native token for given chain
* @throws if native token has not been set
*/
async getNativeToken(chainNameOrId) {
const metadata = this.tryGetChainMetadata(chainNameOrId);
if (!metadata || !metadata.nativeToken) {
throw new Error(`Missing data for native token for ${chainNameOrId}`);
}
return metadata.nativeToken;
}
/**
* Creates a new ChainMetadataManager with the extended metadata
* @param additionalMetadata extra fields to add to the metadata for each chain
* @returns a new ChainMetadataManager
*/
extendChainMetadata(additionalMetadata) {
const newMetadata = {};
for (const [name, meta] of Object.entries(this.metadata)) {
newMetadata[name] = { ...meta, ...additionalMetadata[name] };
}
return new ChainMetadataManager(newMetadata);
}
/**
* Create a new instance from the intersection
* of current's chains and the provided chain list
*/
intersect(chains, throwIfNotSubset = false) {
const knownChains = this.getKnownChainNames();
const intersection = [];
for (const chain of chains) {
if (knownChains.includes(chain))
intersection.push(chain);
else if (throwIfNotSubset)
throw new Error(`Known chains does not include ${chain}`);
}
if (!intersection.length) {
throw new Error(`No chains shared between known chains and list (${knownChains} and ${chains})`);
}
const intersectionMetadata = pick(this.metadata, intersection);
const result = new ChainMetadataManager(intersectionMetadata);
return { intersection, result };
}
isLocalRpc(chain) {
const metadata = this.tryGetChainMetadata(chain);
const rpcUrl = metadata?.rpcUrls[0]?.http;
return rpcUrl?.includes('localhost') || rpcUrl?.includes('127.0.0.1');
}
}
//# sourceMappingURL=ChainMetadataManager.js.map