UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

364 lines 14.3 kB
import { providers, } from 'ethers'; import { Provider as ZKSyncProvider, } from 'zksync-ethers'; import { addBufferToGasLimit, pick, rootLogger, } from '@hyperlane-xyz/utils'; import { testChainMetadata, testChains } from '../consts/testChains.js'; import { ChainMetadataManager } from '../metadata/ChainMetadataManager.js'; import { ChainTechnicalStack, } from '../metadata/chainMetadataTypes.js'; import { ZKSyncDeployer } from '../zksync/ZKSyncDeployer.js'; import { defaultProviderBuilder, defaultZKProviderBuilder, } from './providerBuilders.js'; /** * A utility class to create and manage providers and signers for multiple chains * @typeParam MetaExt - Extra metadata fields for chains (such as contract addresses) */ export class MultiProvider extends ChainMetadataManager { options; providers; providerBuilder; signers; useSharedSigner = false; // A single signer to be used for all chains logger; /** * Create a new MultiProvider with the given chainMetadata, * or the SDK's default metadata if not provided */ constructor(chainMetadata, options = {}) { super(chainMetadata, options); this.options = options; this.logger = options?.logger || rootLogger.child({ module: 'MultiProvider', }); this.providers = options?.providers || {}; this.providerBuilder = options?.providerBuilder || defaultProviderBuilder; this.signers = options?.signers || {}; } addChain(metadata) { super.addChain(metadata); if (this.useSharedSigner) { const signers = Object.values(this.signers); if (signers.length > 0) { this.setSharedSigner(signers[0]); } } } extendChainMetadata(additionalMetadata) { const newMetadata = super.extendChainMetadata(additionalMetadata).metadata; return new MultiProvider(newMetadata, this.options); } /** * Get an Ethers provider for a given chain name or domain id */ tryGetProvider(chainNameOrId) { const metadata = this.tryGetChainMetadata(chainNameOrId); if (!metadata) return null; const { name, chainId, rpcUrls, technicalStack } = metadata; if (this.providers[name]) return this.providers[name]; if (testChains.includes(name)) { if (technicalStack === ChainTechnicalStack.ZkSync) { this.providers[name] = new ZKSyncProvider('http://127.0.0.1:8011', 260); } else { this.providers[name] = new providers.JsonRpcProvider('http://127.0.0.1:8545', 31337); } } else if (rpcUrls.length) { if (technicalStack === ChainTechnicalStack.ZkSync) { this.providers[name] = defaultZKProviderBuilder(rpcUrls, chainId); } else { this.providers[name] = this.providerBuilder(rpcUrls, chainId); } } else { return null; } return this.providers[name]; } /** * Get an Ethers provider for a given chain name or domain id * @throws if chain's metadata has not been set */ getProvider(chainNameOrId) { const provider = this.tryGetProvider(chainNameOrId); if (!provider) throw new Error(`No chain metadata set for ${chainNameOrId}`); return provider; } /** * Sets an Ethers provider for a given chain name or domain id * @throws if chain's metadata has not been set */ setProvider(chainNameOrId, provider) { const chainName = this.getChainName(chainNameOrId); this.providers[chainName] = provider; const signer = this.signers[chainName]; if (signer && signer.provider) { this.setSigner(chainName, signer.connect(provider)); } return provider; } /** * Sets Ethers providers for a set of chains * @throws if chain's metadata has not been set */ setProviders(providers) { for (const chain of Object.keys(providers)) { const chainName = this.getChainName(chain); this.providers[chainName] = providers[chain]; } } /** * Get an Ethers signer for a given chain name or domain id * If signer is not yet connected, it will be connected */ tryGetSigner(chainNameOrId) { const chainName = this.tryGetChainName(chainNameOrId); if (!chainName) return null; const signer = this.signers[chainName]; if (!signer) return null; if (signer.provider) return signer; // Auto-connect the signer for convenience const provider = this.tryGetProvider(chainName); if (!provider) return signer; return signer.connect(provider); } /** * Get an Ethers signer for a given chain name or domain id * If signer is not yet connected, it will be connected * @throws if chain's metadata or signer has not been set */ getSigner(chainNameOrId) { const signer = this.tryGetSigner(chainNameOrId); if (!signer) throw new Error(`No chain signer set for ${chainNameOrId}`); return signer; } /** * Get an Ethers signer for a given chain name or domain id * @throws if chain's metadata or signer has not been set */ async getSignerAddress(chainNameOrId) { const signer = this.getSigner(chainNameOrId); const address = await signer.getAddress(); return address; } /** * Sets an Ethers Signer for a given chain name or domain id * @throws if chain's metadata has not been set or shared signer has already been set */ setSigner(chainNameOrId, signer) { if (this.useSharedSigner) { throw new Error('MultiProvider already set to use a shared signer'); } const chainName = this.getChainName(chainNameOrId); this.signers[chainName] = signer; if (signer.provider && !this.providers[chainName]) { this.providers[chainName] = signer.provider; } return signer; } /** * Sets Ethers Signers for a set of chains * @throws if chain's metadata has not been set or shared signer has already been set */ setSigners(signers) { if (this.useSharedSigner) { throw new Error('MultiProvider already set to use a shared signer'); } for (const chain of Object.keys(signers)) { const chainName = this.getChainName(chain); this.signers[chainName] = signers[chain]; } } /** * Gets the Signer if it's been set, otherwise the provider */ tryGetSignerOrProvider(chainNameOrId) { return (this.tryGetSigner(chainNameOrId) || this.tryGetProvider(chainNameOrId)); } /** * Gets the Signer if it's been set, otherwise the provider * @throws if chain metadata has not been set */ getSignerOrProvider(chainNameOrId) { return this.tryGetSigner(chainNameOrId) || this.getProvider(chainNameOrId); } /** * Sets Ethers Signers to be used for all chains * Any subsequent calls to getSigner will return given signer * Setting sharedSigner to null clears all signers */ setSharedSigner(sharedSigner) { if (!sharedSigner) { this.useSharedSigner = false; this.signers = {}; return null; } this.useSharedSigner = true; for (const chain of this.getKnownChainNames()) { this.signers[chain] = sharedSigner; } return sharedSigner; } /** * Create a new MultiProvider from the intersection * of current's chains and the provided chain list */ intersect(chains, throwIfNotSubset = false) { const { intersection, result } = super.intersect(chains, throwIfNotSubset); const multiProvider = new MultiProvider(result.metadata, { ...this.options, providers: pick(this.providers, intersection), signers: pick(this.signers, intersection), }); return { intersection, result: multiProvider }; } /** * Get a block explorer URL for given chain's address */ async tryGetExplorerAddressUrl(chainNameOrId, address) { if (address) return super.tryGetExplorerAddressUrl(chainNameOrId, address); const signer = this.tryGetSigner(chainNameOrId); if (signer) { const signerAddr = await signer.getAddress(); return super.tryGetExplorerAddressUrl(chainNameOrId, signerAddr); } return null; } /** * Get the latest block range for a given chain's RPC provider */ async getLatestBlockRange(chainNameOrId, rangeSize = this.getMaxBlockRange(chainNameOrId)) { const toBlock = await this.getProvider(chainNameOrId).getBlock('latest'); const fromBlock = Math.max(toBlock.number - rangeSize, 0); return { fromBlock, toBlock: toBlock.number }; } /** * Get the transaction overrides for a given chain name or domain id * @throws if chain's metadata has not been set */ getTransactionOverrides(chainNameOrId) { return this.getChainMetadata(chainNameOrId)?.transactionOverrides ?? {}; } /** * Wait for deploy tx to be confirmed * @throws if chain's metadata or signer has not been set or tx fails */ async handleDeploy(chainNameOrId, factory, params, artifact) { const overrides = this.getTransactionOverrides(chainNameOrId); const signer = this.getSigner(chainNameOrId); const metadata = this.getChainMetadata(chainNameOrId); const { technicalStack } = metadata; let contract; let estimatedGas; // estimate gas for deploy // deploy with buffer on gas limit if (technicalStack === ChainTechnicalStack.ZkSync) { if (!artifact) throw new Error(`No ZkSync contract artifact provided!`); const deployer = new ZKSyncDeployer(signer); estimatedGas = await deployer.estimateDeployGas(artifact, params); contract = await deployer.deploy(artifact, params, { gasLimit: addBufferToGasLimit(estimatedGas), ...overrides, }); // no need to `handleTx` for zkSync as the zksync deployer itself // will wait for the deploy tx to be confirmed before returning } else { const contractFactory = factory.connect(signer); const deployTx = contractFactory.getDeployTransaction(...params); estimatedGas = await signer.estimateGas(deployTx); contract = await contractFactory.deploy(...params, { gasLimit: addBufferToGasLimit(estimatedGas), ...overrides, }); // manually wait for deploy tx to be confirmed for non-zksync chains await this.handleTx(chainNameOrId, contract.deployTransaction); } this.logger.trace(`Contract deployed at ${contract.address} on ${chainNameOrId}:`, { transaction: contract.deployTransaction }); // return deployed contract return contract; } /** * Wait for given tx to be confirmed * @throws if chain's metadata or signer has not been set or tx fails */ async handleTx(chainNameOrId, tx) { const confirmations = this.getChainMetadata(chainNameOrId).blocks?.confirmations ?? 1; const response = await tx; const txUrl = this.tryGetExplorerTxUrl(chainNameOrId, response); this.logger.info(`Pending ${txUrl || response.hash} (waiting ${confirmations} blocks for confirmation)`); return response.wait(confirmations); } /** * Populate a transaction's fields using signer address and overrides * @throws if chain's metadata has not been set or tx fails */ async prepareTx(chainNameOrId, tx, from) { const txFrom = from ?? (await this.getSignerAddress(chainNameOrId)); const overrides = this.getTransactionOverrides(chainNameOrId); return { ...tx, from: txFrom, ...overrides, }; } /** * Estimate gas for given tx * @throws if chain's metadata has not been set or tx fails */ async estimateGas(chainNameOrId, tx, from) { const txReq = { ...(await this.prepareTx(chainNameOrId, tx, from)), // Reset any tx request params that may have an unintended effect on gas estimation gasLimit: undefined, gasPrice: undefined, maxPriorityFeePerGas: undefined, maxFeePerGas: undefined, }; const provider = this.getProvider(chainNameOrId); return provider.estimateGas(txReq); } /** * Send a transaction and wait for confirmation * @throws if chain's metadata or signer has not been set or tx fails */ async sendTransaction(chainNameOrId, txProm) { const { annotation, ...tx } = await txProm; if (annotation) { this.logger.info(annotation); } const txReq = await this.prepareTx(chainNameOrId, tx); const signer = this.getSigner(chainNameOrId); const response = await signer.sendTransaction(txReq); this.logger.info(`Sent tx ${response.hash}`); return this.handleTx(chainNameOrId, response); } /** * Creates a MultiProvider using the given signer for all test networks */ static createTestMultiProvider(params = {}, chains = testChains) { const { signer, provider } = params; const mp = new MultiProvider(testChainMetadata); if (signer) { mp.setSharedSigner(signer); } const _provider = provider || signer?.provider; if (_provider) { const providerMap = {}; chains.forEach((t) => (providerMap[t] = _provider)); mp.setProviders(providerMap); } return mp; } } //# sourceMappingURL=MultiProvider.js.map