The multichain module is a utility for allowing cross-network requests to be made to several chains at once. The module handles:
- Fetching data across the different networks
- Graceful error handling in the case of a network issue
- Returning data in a standardised interface
- A simplified API to capture instances where network differences are material
The simplest way to use multichain is to setup a multichain wrapper to pass common multichain config to all your contracts:
// create a new contract wrapper
const multichain = new MultiChainContractWrapper(config);
// generate a multichain contract by wrapping an existing ethers contract
// and adding per-contract overrides
const multichainContract = multichain.wrap(contract, overrides);
// call the contract using `.multichain` or `.mc`:
const res = await multichainContract.multichain.allowance('0x...');
// is equal to
const res = await multichainContract.mc.allowance('0x...');
// get the data:
console.log(res)
>>> data: {
>>> 1: { status: 'fulfilled', value: BigNumber },
>>> 137: { status: 'rejected', reason: Error }
>>> },
>>> meta: { results: 2, ok: 1, err: 1 }
There are 2 ways to configure multichain
- Configuration shared by all contracts
- Configuration specific to a single contract
When setting up the wrapper, config settings will be shared between all wrapped contracts.
This is useful when you want to setup something like a set of providers, that aren't likely to change every time you want to add a new contract.
The wrapper config object is a key value pairing of chain ids to config settings. Supported settings include:
provider
: an ethers Provider
. This can include custom providers that inherit from the abstract provider class.
exclude
: a boolean value, indicating whether or not to make calls to a particular network by default (this can be overriden on a per-contract basis).
As an example:
const config: MultiChainWrapperConfig = {
[1]: {
provider: new ethers.providers.JsonRpcProvider('https://rpc.ankr.com/eth'),
},
[42161]: {
provider: new ethers.providers.JsonRpcProvider(
'https://rpc.ankr.com/arbitrum',
),
// by default, calls will not be make to Arbitrum
exclude: true,
},
};
const multichain = new MultiChainContractWrapper(config);
While settings like Providers might be shared between contract instances, there are some properties that absolutely will need to be changed on a per contract and per-chain basis.
The most common of these is address
. Let's say we want to call the USDC balance of the zero address on Ethereum and Arbitrum.
Ethereum USDC Address: 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48
Arbitrum One USDC Address: 0xff970a61a04b1ca14834a43f5de4533ebddb5cc8
We need a way to tell the multichain wrapper to send the respective calls to different locations. This can be done when we either wrap or create the contact, using overrides
Overrides have all the same configuration options as the MultichainWrapperConfig
but also allow you to pass an address, for each chain:
const overrides: MultiChainConfigOverrides = {
[1]: {
address: `0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48`,
},
[42161]: {
address: `0xff970a61a04b1ca14834a43f5de4533ebddb5cc8`,
},
};
// take an existing ethers ERC20 contract instance and pass the overrides
const multichainErc20Contract = multichain.wrap(baseErc20Contract, overrides);
// create a new contract with multichain enabled
const multichainErc20Contract = multichain.create(
address,
ABI,
signerOrProvider,
overrides,
);
You can think of the global contract wrapper as primarily a way to share settings across contract instances. To actually make contract calls we still need the ethers contract instance, but first we need to wrap the contract.
We can either wrap an existing ethers contract, or we can create one.
You can use the wrapper to create a new ethers contract and it will return a new multichain enabled contract, simply provide the following parameters:
address
: The address for the contract you want to connect to.*
ABI
: The JSON ABI for the contract. All contracts in a multichain call must use the same ABI
provider
: (optional) the provider for the contract to use in the default case.
overrides
: (optional) Any configuration (including addresses) that need to be changed from the wrapper.
// create a new contract
const newMultichainContract = multichain.create<Erc20>('0x....', ABI, provider);
// this will also work, assuming a provider for the chain you need was passed to the multichain wrapper
const newMultichainContract = multichain.create<Erc20>('0x....', ABI);
*Note: The address must be a valid ethereum address on a chain where you have passed a provider. This provider can be passed to the contract when you create it OR can be one of the providers passed when the wrapper was initialised.
The reason we need to pass this information for a single chain is because the multicall wrapper returns a decorated contract: specifically, it will work as normal like a standard ethers contract if you don't make a multichain call:
// this will just return a single result as normal
newMultichainContract.balanceOf(ethers.constants.zeroAddress);
Assuming you already have a contract setup, you can just wrap
the existing contract - internally this is the same function that is called during the create process. Wrapping is even simpler:
const unwrappedcontract = new ethers.Contract('0x...', ABI, providerOrSigner);
const wrapped = multicall.wrap(unwrappedContract);
Under the hood, multichain uses the ES20 Promise.allSettled
to serialise multiple simulataneous calls into a series of responses. The calls are returned collectively in the data
property, each with a status
that is either 'fulfilled'
or 'rejected'
.
- A
'fulfilled'
response has avalue
property that contains the multichain response. - A
'rejected'
response has areason
property containing the error.
There is also a meta
property containing the total number of results, errors and successes.
The SDK is fully written in typescript. Contract types are inferred automatically. You can also pass a generic typechain type during .wrap
or .create
and types will be added to multichain automatically.