@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
364 lines • 14.3 kB
JavaScript
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