Powerful, type-safe alternative to Multicall for off-chain smart contract query
This is a library that help you to perform off-chain query to smart contracts by batching multiple calls together into one call. But unlike the well done muitlcall3 that try to do everything in a smart contract, this library will generate new smart contracts bytecode for you each time you call the library.
Benefit from the raw power of the bytecode generation, Rawr-multicall provides more powerful features than the on-chain counter part
- âś… gas efficient,
- No ABI encode/decode.
- No looping through calls.
- Values (address, gas limit, call value) are inlined.
- âś… small payload, for both request and response.
- Input calldata compression where possible.
- Custom output format.
- âś… ability to set custom gas and value limit for individual call.
- âś… custom call logic with custom contract deployment before calls.
With builtin plugins come more features:
- âś… type-safe wrapper for [Ether.js] (v5 and v6) that generated with Typechain,
- Easy to extend for the other libraries too!
- âś… Make call and get the result without side effect (revert after call).
Core package installation
npm install @rawr-multicall/core
# or
yarn add @rawr-multicall/core
Plugin installation
npm install @rawr-multicall/ethers-v5-helper @rawr-multicall/ethers-v6-helper @rawr-multicall/no-side-effect-call
# or
yarn add @rawr-multicall/ethers-v5-helper @rawr-multicall/ethers-v6-helper @rawr-multicall/no-side-effect-call
Note that ethers-v5-helper
or ethers-v6-helper
should be installed based on your requirement, as they used
different Ethers.js version.
Using pure Rawr-multicall
To use pure Rawr-multicall, you need external too to generate your call data, as well as to decode the result.
In this example, Ethers.js v5/v6 is used.
import {
createCall,
buildRawrMulticallContract,
decodeResult,
unwrap,
} from "@rawr-multicall/core";
import { Interface, JsonRpcProvider } from "ethers"; // ethers v6
import { abi as ERC20Abi } from "@rawr-multicall/test-helper/artifacts/test-contracts/ERC20.sol/ERC20.json";
const ERC20Iface = new Interface(ERC20Abi);
const provider = new JsonRpcProvider("https://eth.llamarpc.com");
const usdcAddress = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
const userAddress = "0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503";
async function main() {
// Step 1. Create calls
const calls = [
createCall(usdcAddress, ERC20Iface.encodeFunctionData("name")),
createCall(usdcAddress, ERC20Iface.encodeFunctionData("symbol")),
createCall(usdcAddress, ERC20Iface.encodeFunctionData("decimals")),
createCall(usdcAddress, ERC20Iface.encodeFunctionData("totalSupply")),
createCall(
usdcAddress,
ERC20Iface.encodeFunctionData("balanceOf", [userAddress]),
),
];
// Step 2. Build contract
const contract = buildRawrMulticallContract(calls);
// Step 3. Simulate
const rawResult = await provider.call({ data: contract.byteCode });
// Step 4. Decode result
const results = decodeResult(calls, rawResult);
// Step 5. Extract individual result
console.log(
"name",
ERC20Iface.decodeFunctionResult("name", unwrap(results[0])),
);
console.log(
"symbol",
ERC20Iface.decodeFunctionResult("symbol", unwrap(results[1])),
);
console.log(
"decimals",
ERC20Iface.decodeFunctionResult("decimals", unwrap(results[2])),
);
console.log(
"totalSupply",
ERC20Iface.decodeFunctionResult("totalSupply", unwrap(results[3])),
);
console.log(
"balanceOf",
ERC20Iface.decodeFunctionResult("balanceOf", unwrap(results[4])),
);
}
main()
.then(() => process.exit(0))
.catch((e) => {
console.error(e);
process.exit(1);
});
To create a single call, createCalls(address, calldata)
is used.
In the above example, Ethers.js v6's Interface is used to generate the byte code.
const contract = buildRawrMulticallContract(calls);
That is, just use buildRawrMulticallContract
to generate the contract.
const rawResult = await provider.call({ data: contract.byteCode });
Here provider.call
is used, which is essentially make an eth_call
to
the provider.
Note that there is no receiver address (the to
property), meaning this is
a contract deployment simulation call! We can receive the result because the
contract initialization code contains the RETURN
instruction, and
Rawr-multicall makes use of that!
const results = decodeResult(calls, rawResult);
The constant results
will be an array of the following type:
type Result =
| { success: true; result: Uint8Array }
| { success: false; error: Uint8Array };
So in case of failed call, the error data is also returned.
To get the meaningful result, again Ethers.js v6's Interface is used again to decode the result.
Rawr-multicall provides a function unwrap
which is similar to Result::unwrap
function of Rust: it will return the result bytes in the successful case,
and will throw the error bytes otherwise.
Using Rawr-multicall with ethers-v5-helpers
and ethers-v6-helpers
If you think the above way to use Rawr-multicall is a bit raw
and tedious, @rawr-multicall/ethers-v5-helper
and ethers-v6-helper
will
provide you a type-safe, easy-to-use wrapper for createCall
.
import {
buildRawrMulticallContract,
decodeResult,
unwrap,
} from "@rawr-multicall/core";
import { createEthersV5Call } from "@rawr-multicall/ethers-v5-helper";
import { abi as ERC20Abi } from "@rawr-multicall/test-helper/artifacts/test-contracts/ERC20.sol/ERC20.json";
import { ERC20 } from "@rawr-multicall/ethers-v5-helper/ethers-v5-contracts/typechain-types/ERC20";
import { ethers, providers } from "ethers-v5"; // package alias
const provider = new providers.JsonRpcProvider("https://eth.llamarpc.com");
const usdcAddress = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
const userAddress = "0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503";
const usdcContract = new ethers.Contract(usdcAddress, ERC20Abi) as ERC20;
async function main() {
// Step 1. Create calls
const calls = [
createEthersV5Call(usdcContract, "name", []),
createEthersV5Call(usdcContract, "symbol", []),
createEthersV5Call(usdcContract, "decimals", []),
createEthersV5Call(usdcContract, "totalSupply", []),
createEthersV5Call(usdcContract, "balanceOf", [userAddress]),
] as const; // add this for type-safe
// Step 2. Build contract
const contract = buildRawrMulticallContract(calls);
// Step 3. Simulate
const rawResult = await provider.call({ data: contract.byteCode });
// Step 4. Decode result
const results = decodeResult(calls, rawResult);
// Step 5. Print result
// No extraction required!
console.log("name", unwrap(results[0]));
console.log("symbol", unwrap(results[1]));
console.log("decimals", unwrap(results[2]));
console.log("totalSupply", unwrap(results[3]).toString());
console.log("balanceOf", unwrap(results[4]).toString());
}
main()
.then(() => process.exit(0))
.catch((e) => {
console.error(e);
process.exit(1);
});
This example is very similar to the pure example, except, everything will be decoded for you!
The parameters methodName
and methodParams
of createEthersV5Call(contract, methodName, methodParams, params?)
will be typed, meaning if you pass in the typed contract
(that generated via Typechain),
then your methodName
will only be the name of contract
's method. Then methodParams
will tie to methodName
.
The result is also typed. Thanks to as const
in the calls
creation:
unwrap(results[0])
andunwrap(results[1])
will bestring
,unwrap(results[2])
will benumber
(as the contract type isuint8
),unwrap(result[3])
andunwrap(results[4])
will beBigNumber
.
import {
buildRawrMulticallContract,
decodeResult,
unwrap,
} from "@rawr-multicall/core";
import { createEthersV6Call } from "@rawr-multicall/ethers-v6-helper";
import { abi as ERC20Abi } from "@rawr-multicall/test-helper/artifacts/test-contracts/ERC20.sol/ERC20.json";
import { ethers } from "ethers";
import { ERC20 } from "@rawr-multicall/test-helper/ethers-v6-contracts/typechain-types/ERC20";
const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
const usdcAddress = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
const userAddress = "0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503";
const usdcContract = new ethers.Contract(
usdcAddress,
ERC20Abi,
) as unknown as ERC20;
async function main() {
// Step 1. Create calls
const calls = [
await createEthersV6Call(usdcContract, "name", []),
await createEthersV6Call(usdcContract, "symbol", []),
await createEthersV6Call(usdcContract, "decimals", []),
await createEthersV6Call(usdcContract, "totalSupply", []),
await createEthersV6Call(usdcContract, "balanceOf", [userAddress]),
] as const; // add this for type-safe
// Step 2. Build contract
const contract = buildRawrMulticallContract(calls);
// Step 3. Simulate
const rawResult = await provider.call({ data: contract.byteCode });
// Step 4. Decode result
const results = decodeResult(calls, rawResult);
// Step 5. Print result
// No extraction required!
console.log("name", unwrap(results[0]));
console.log("symbol", unwrap(results[1]));
console.log("decimals", unwrap(results[2]));
console.log("totalSupply", unwrap(results[3]));
console.log("balanceOf", unwrap(results[4]));
}
main()
.then(() => process.exit(0))
.catch((e) => {
console.error(e);
process.exit(1);
});
This is almost the same as ethers-v5-helpers
. Everything will be typed as you expected.
The only different here is createEthersV6Call
is async. The reason is that Ethers.js v6
does not have a way to get the contract address in a sync
way. So we need to await
for createEthersV6Call
.
All three functions createCall
, createEthersV5Call
and createEthersV6Call
accept an
optional params, allowing you to set custom parameters.
createCall(<address>, <calldata>, {
gasLimit: /* Custom gas limit. Optional */,
value: /* Custom value. Optional */,
});
createEthersV5Call(<contract>, <methodName>, <methodParams>, {
gasLimit: /* Custom gas limit. Optional */,
value: /* Custom value. Optional */,
});
createEthersV6Call(<contract>, <methodName, <methodParams>, {
gasLimit: /* Custom gas limit. Optional */,
value: /* Custom value. Optional */,
});
Suppose that you have this Counter contract. You don't want to actually deploy it, but still want to interact with it somehow. Rawr-multicall got your cover!
import {
buildRawrMulticallContract,
decodeResult,
unwrap,
labeledAddress,
} from "@rawr-multicall/core";
import { createEthersV6Call } from "@rawr-multicall/ethers-v6-helper";
import { abi as CounterABI } from "@rawr-multicall/test-helper/artifacts/test-contracts/Counter.sol/Counter.json";
import { ethers } from "ethers";
import {
Counter,
Counter__factory,
} from "@rawr-multicall/test-helper/ethers-v6-contracts/typechain-types";
const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
const usdcContract = new ethers.Contract(
ethers.ZeroAddress,
CounterABI,
) as unknown as Counter;
async function main() {
// Step 1. Create calls
const calls = [
await createEthersV6Call(usdcContract, "counter", [], {
withAddress: labeledAddress("test-counter"),
}),
await createEthersV6Call(usdcContract, "inc", [], {
withAddress: labeledAddress("test-counter"),
}),
await createEthersV6Call(usdcContract, "inc", [], {
withAddress: labeledAddress("test-counter"),
}),
await createEthersV6Call(usdcContract, "counter", [], {
withAddress: labeledAddress("test-counter"),
}),
] as const; // add this for type-safe
// Step 2. Build contract
const contract = buildRawrMulticallContract(calls, {
predeployContracts: {
"test-counter": (await new Counter__factory().getDeployTransaction())
.data,
},
});
// Step 3. Simulate
const rawResult = await provider.call({ data: contract.byteCode });
// Step 4. Decode result
const results = decodeResult(calls, rawResult);
// Step 5. Print result
// No extraction required!
console.log(unwrap(results[0]));
console.log(unwrap(results[1]));
console.log(unwrap(results[2]));
console.log(unwrap(results[3]));
}
main()
.then(() => process.exit(0))
.catch((e) => {
console.error(e);
process.exit(1);
});
Here we name our contract test-counter
. Rawr-multicall when received the byte code
of test-counter
passed in buildRawrMulticallContract
, the generated byte code will
deploy the contract first. The calls can then access this contract with labeledAddress
.
For createCall
, you can pass labeledAddress
as the first parameters.
For createEthersV5Call
and createEthersV6Call
, you pass this in the optional parameters
as above.
Under the hood, Rawr-multicall uses CREATE2
for deterministic addresses.
Rawr-multicall also provides a way to register predeploy contract globally.
import { registerPredeployContract } from '@rawr-multicall/core';
registerPredeployContract(label: string, initCode: string | Uint8Array);
Sometimes you just want to get the function result, but you don't want that function
to modify the storage. @rawr-multicall/no-side-effect-call
will help you!
import {
buildRawrMulticallContract,
decodeResult,
unwrap,
labeledAddress,
} from "@rawr-multicall/core";
import { createEthersV6Call } from "@rawr-multicall/ethers-v6-helper";
import { abi as CounterABI } from "@rawr-multicall/test-helper/artifacts/test-contracts/Counter.sol/Counter.json";
import { ethers } from "ethers";
import {
Counter,
Counter__factory,
} from "@rawr-multicall/test-helper/ethers-v6-contracts/typechain-types";
import { wrapNoSideEffectCall } from "@rawr-multicall/no-side-effect-call";
const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
const usdcContract = new ethers.Contract(
ethers.ZeroAddress,
CounterABI,
) as unknown as Counter;
async function main() {
// Step 1. Create calls
const calls = [
await createEthersV6Call(usdcContract, "counter", [], {
withAddress: labeledAddress("test-counter"),
}),
await createEthersV6Call(usdcContract, "inc", [], {
withAddress: labeledAddress("test-counter"),
}),
wrapNoSideEffectCall(
await createEthersV6Call(usdcContract, "inc", [], {
withAddress: labeledAddress("test-counter"),
}),
),
await createEthersV6Call(usdcContract, "counter", [], {
withAddress: labeledAddress("test-counter"),
}),
] as const; // add this for type-safe
// Step 2. Build contract
const contract = buildRawrMulticallContract(calls, {
predeployContracts: {
"test-counter": (await new Counter__factory().getDeployTransaction())
.data,
},
});
// Step 3. Simulate
const rawResult = await provider.call({ data: contract.byteCode });
// Step 4. Decode result
const results = decodeResult(calls, rawResult);
// Step 5. Print result
// No extraction required!
console.log(unwrap(results[0]));
console.log(unwrap(results[1]));
console.log(unwrap(results[2]));
console.log(unwrap(results[3]));
}
main()
.then(() => process.exit(0))
.catch((e) => {
console.error(e);
process.exit(1);
});
With wrapNoSideEffectCall
, the second inc
effect was nullified, but the
result is still calculated and return to us. So the output will be 0 1 2 1
in
order.
Also wrapNoSideEffectCall
is type-safe, meaning its result and error will have
the same type as the inner call.
Under the hood, wrapNoSideEffectCall
also relied on a predeploy contract, but
the contract was written in Yul, with custom output
format. The make sure that the byte code, as well as the output is small.
For createCall
, the error will be bytes
. You need to decode the error yourself.
For createEthersV5Call
, the error sighash is looked up in the contract. If the
corresponding error fragment is found, the error will contain that fragment.
Please refer to
ethers-v5-helper/src/errors.ts to
see the list of errors.
The same is for createEthersV6
, but we won't need to decode Error(message)
and Panic(code)
individually, since Ethers.js will do that for us. Please
refer to
ethers-v6-helper/src/errors.ts.
wrapNoSideEffectCall
will have the same error type as the inner
call.
buildRawrMulticallContract
also accept an optional parameters.
buildRawrMulticallContract(calls, params: {
/**
* Dictionary of predeploy contracts init code
*/
predeployContract?: Record<string, Bytes | string>;
/**
* Turn it on to enable the usage of `PUSH0` instruction.
*
* Default to false, as not all chain support this instruction yet.
*/
allowPUSH0?: boolean;
/**
* Set the sender from and nonce to calculate the
* rawr-multicall contract address (to calculate the other CREATE2 contract)
*/
sender?: {
from?: RawAddressString; // default to 0x000...0
nonce?: number; // default to 0
};
/**
* A prefix to add before the predeploy contract label.
* The salt will be calculated as `keccak25(create2SaltPrefix + contractLabel).
*
* Default to `rawr-multicall:`
*/
create2SaltPrefix?: string;
});
Originally I named it raw-multicall
. But then I remembered this
meme.