@arbitrum/sdk
Version:
Typescript library client-side interactions with Arbitrum
277 lines (276 loc) • 14.6 kB
JavaScript
/*
* Copyright 2021, Offchain Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env node */
'use strict';
Object.defineProperty(exports, "__esModule", { value: true });
exports.InboxTools = void 0;
const ethers_1 = require("ethers");
const Bridge__factory_1 = require("../abi/factories/Bridge__factory");
const SequencerInbox__factory_1 = require("../abi/factories/SequencerInbox__factory");
const IInbox__factory_1 = require("../abi/factories/IInbox__factory");
const signerOrProvider_1 = require("../dataEntities/signerOrProvider");
const eventFetcher_1 = require("../utils/eventFetcher");
const multicall_1 = require("../utils/multicall");
const errors_1 = require("../dataEntities/errors");
const NodeInterface__factory_1 = require("../abi/factories/NodeInterface__factory");
const constants_1 = require("../dataEntities/constants");
const message_1 = require("../dataEntities/message");
const lib_1 = require("../utils/lib");
const arbProvider_1 = require("../utils/arbProvider");
/**
* Tools for interacting with the inbox and bridge contracts
*/
class InboxTools {
constructor(parentSigner, childChain) {
this.parentSigner = parentSigner;
this.childChain = childChain;
this.parentProvider = signerOrProvider_1.SignerProviderUtils.getProviderOrThrow(this.parentSigner);
}
/**
* Find the first (or close to first) block whose number
* is below the provided number, and whose timestamp is below
* the provided timestamp
* @param blockNumber
* @param blockTimestamp
* @returns
*/
async findFirstBlockBelow(blockNumber, blockTimestamp) {
const isParentChainArbitrum = await (0, lib_1.isArbitrumChain)(this.parentProvider);
if (isParentChainArbitrum) {
const nodeInterface = NodeInterface__factory_1.NodeInterface__factory.connect(constants_1.NODE_INTERFACE_ADDRESS, this.parentProvider);
try {
blockNumber = (await nodeInterface.l2BlockRangeForL1(blockNumber - 1)).firstBlock.toNumber();
}
catch (e) {
// l2BlockRangeForL1 reverts if no L2 block exist with the given L1 block number,
// since l1 block is updated in batch sometimes block can be skipped even when there are activities
// alternatively we use binary search to get the nearest block
const _blockNum = (await (0, lib_1.getBlockRangesForL1Block)({
arbitrumProvider: this.parentProvider,
forL1Block: blockNumber - 1,
allowGreater: true,
}))[0];
if (!_blockNum) {
throw e;
}
blockNumber = _blockNum;
}
}
const block = await this.parentProvider.getBlock(blockNumber);
const diff = block.timestamp - blockTimestamp;
if (diff < 0)
return block;
// we take a long average block time of 12s
// and always move at least 10 blocks
const diffBlocks = Math.max(Math.ceil(diff / 12), 10);
return await this.findFirstBlockBelow(blockNumber - diffBlocks, blockTimestamp);
}
// Check if this request is contract creation or not.
isContractCreation(childTransactionRequest) {
if (childTransactionRequest.to === '0x' ||
!(0, lib_1.isDefined)(childTransactionRequest.to) ||
childTransactionRequest.to === ethers_1.ethers.constants.AddressZero) {
return true;
}
return false;
}
/**
* We should use nodeInterface to get the gas estimate is because we
* are making a delayed inbox message which doesn't need parent calldata
* gas fee part.
*/
async estimateArbitrumGas(childTransactionRequest, childProvider) {
const nodeInterface = NodeInterface__factory_1.NodeInterface__factory.connect(constants_1.NODE_INTERFACE_ADDRESS, childProvider);
const contractCreation = this.isContractCreation(childTransactionRequest);
const gasComponents = await nodeInterface.callStatic.gasEstimateComponents(childTransactionRequest.to || ethers_1.ethers.constants.AddressZero, contractCreation, childTransactionRequest.data, {
from: childTransactionRequest.from,
value: childTransactionRequest.value,
});
const gasEstimateForChild = gasComponents.gasEstimate.sub(gasComponents.gasEstimateForL1);
return Object.assign(Object.assign({}, gasComponents), { gasEstimateForChild });
}
/**
* Get a range of blocks within messages eligible for force inclusion emitted events
* @param blockNumberRangeSize
* @returns
*/
async getForceIncludableBlockRange(blockNumberRangeSize) {
let currentL1BlockNumber;
const sequencerInbox = SequencerInbox__factory_1.SequencerInbox__factory.connect(this.childChain.ethBridge.sequencerInbox, this.parentProvider);
const isParentChainArbitrum = await (0, lib_1.isArbitrumChain)(this.parentProvider);
if (isParentChainArbitrum) {
const arbProvider = new arbProvider_1.ArbitrumProvider(this.parentProvider);
const currentArbBlock = await arbProvider.getBlock('latest');
currentL1BlockNumber = currentArbBlock.l1BlockNumber;
}
const multicall = await multicall_1.MultiCaller.fromProvider(this.parentProvider);
const multicallInput = [
{
targetAddr: sequencerInbox.address,
encoder: () => sequencerInbox.interface.encodeFunctionData('maxTimeVariation'),
decoder: (returnData) => sequencerInbox.interface.decodeFunctionResult('maxTimeVariation', returnData)[0],
},
multicall.getBlockNumberInput(),
multicall.getCurrentBlockTimestampInput(),
];
const [maxTimeVariation, currentBlockNumber, currentBlockTimestamp] = await multicall.multiCall(multicallInput, true);
const blockNumber = isParentChainArbitrum
? currentL1BlockNumber
: currentBlockNumber.toNumber();
const firstEligibleBlockNumber = blockNumber - maxTimeVariation.delayBlocks.toNumber();
const firstEligibleTimestamp = currentBlockTimestamp.toNumber() -
maxTimeVariation.delaySeconds.toNumber();
const firstEligibleBlock = await this.findFirstBlockBelow(firstEligibleBlockNumber, firstEligibleTimestamp);
return {
endBlock: firstEligibleBlock.number,
startBlock: firstEligibleBlock.number - blockNumberRangeSize,
};
}
/**
* Look for force includable events in the search range blocks, if no events are found the search range is
* increased incrementally up to the max search range blocks.
* @param bridge
* @param searchRangeBlocks
* @param maxSearchRangeBlocks
* @returns
*/
async getEventsAndIncreaseRange(bridge, searchRangeBlocks, maxSearchRangeBlocks, rangeMultiplier) {
const eFetcher = new eventFetcher_1.EventFetcher(this.parentProvider);
// events don't become eligible until they pass a delay
// find a block range which will emit eligible events
const cappedSearchRangeBlocks = Math.min(searchRangeBlocks, maxSearchRangeBlocks);
const blockRange = await this.getForceIncludableBlockRange(cappedSearchRangeBlocks);
// get all the events in this range
const events = await eFetcher.getEvents(Bridge__factory_1.Bridge__factory, b => b.filters.MessageDelivered(), {
fromBlock: blockRange.startBlock,
toBlock: blockRange.endBlock,
address: bridge.address,
});
if (events.length !== 0)
return events;
else if (cappedSearchRangeBlocks === maxSearchRangeBlocks)
return [];
else {
return await this.getEventsAndIncreaseRange(bridge, searchRangeBlocks * rangeMultiplier, maxSearchRangeBlocks, rangeMultiplier);
}
}
/**
* Find the event of the latest message that can be force include
* @param maxSearchRangeBlocks The max range of blocks to search in.
* Defaults to 3 * 6545 ( = ~3 days) prior to the first eligible block
* @param startSearchRangeBlocks The start range of block to search in.
* Moves incrementally up to the maxSearchRangeBlocks. Defaults to 100;
* @param rangeMultiplier The multiplier to use when increasing the block range
* Defaults to 2.
* @returns Null if non can be found.
*/
async getForceIncludableEvent(maxSearchRangeBlocks = 3 * 6545, startSearchRangeBlocks = 100, rangeMultiplier = 2) {
const bridge = Bridge__factory_1.Bridge__factory.connect(this.childChain.ethBridge.bridge, this.parentProvider);
// events dont become eligible until they pass a delay
// find a block range which will emit eligible events
const events = await this.getEventsAndIncreaseRange(bridge, startSearchRangeBlocks, maxSearchRangeBlocks, rangeMultiplier);
// no events appeared within that time period
if (events.length === 0)
return null;
// take the last event - as including this one will include all previous events
const eventInfo = events[events.length - 1];
const sequencerInbox = SequencerInbox__factory_1.SequencerInbox__factory.connect(this.childChain.ethBridge.sequencerInbox, this.parentProvider);
// has the sequencer inbox already read this latest message
const totalDelayedRead = await sequencerInbox.totalDelayedMessagesRead();
if (totalDelayedRead.gt(eventInfo.event.messageIndex)) {
// nothing to read - more delayed messages have been read than this current index
return null;
}
const delayedAcc = await bridge.delayedInboxAccs(eventInfo.event.messageIndex);
return Object.assign(Object.assign({}, eventInfo), { delayedAcc: delayedAcc });
}
async forceInclude(messageDeliveredEvent, overrides) {
const sequencerInbox = SequencerInbox__factory_1.SequencerInbox__factory.connect(this.childChain.ethBridge.sequencerInbox, this.parentSigner);
const eventInfo = messageDeliveredEvent || (await this.getForceIncludableEvent());
if (!eventInfo)
return null;
const block = await this.parentProvider.getBlock(eventInfo.blockHash);
return await sequencerInbox.functions.forceInclusion(eventInfo.event.messageIndex.add(1), eventInfo.event.kind, [eventInfo.blockNumber, block.timestamp], eventInfo.event.baseFeeL1, eventInfo.event.sender, eventInfo.event.messageDataHash,
// we need to pass in {} because if overrides is undefined it thinks we've provided too many params
overrides || {});
}
/**
* Send Child Chain signed tx using delayed inbox, which won't alias the sender's address
* It will be automatically included by the sequencer on Chain, if it isn't included
* within 24 hours, you can force include it
* @param signedTx A signed transaction which can be sent directly to chain,
* you can call inboxTools.signChainMessage to get.
* @returns The parent delayed inbox's transaction itself.
*/
async sendChildSignedTx(signedTx) {
const delayedInbox = IInbox__factory_1.IInbox__factory.connect(this.childChain.ethBridge.inbox, this.parentSigner);
const sendData = ethers_1.ethers.utils.solidityPack(['uint8', 'bytes'], [ethers_1.ethers.utils.hexlify(message_1.InboxMessageKind.L2MessageType_signedTx), signedTx]);
return await delayedInbox.functions.sendL2Message(sendData);
}
/**
* Sign a transaction with msg.to, msg.value and msg.data.
* You can use this as a helper to call inboxTools.sendChainSignedMessage
* above.
* @param txRequest A signed transaction which can be sent directly to chain,
* tx.to, tx.data, tx.value must be provided when not contract creation, if
* contractCreation is true, no need provide tx.to. tx.gasPrice and tx.nonce
* can be overrided. (You can also send contract creation transaction by set tx.to
* to zero address or null)
* @param childSigner ethers Signer type, used to sign Chain transaction
* @returns The parent delayed inbox's transaction signed data.
*/
async signChildTx(txRequest, childSigner) {
const tx = Object.assign({}, txRequest);
const contractCreation = this.isContractCreation(tx);
if (!(0, lib_1.isDefined)(tx.nonce)) {
tx.nonce = await childSigner.getTransactionCount();
}
//check transaction type (if no transaction type or gasPrice provided, use eip1559 type)
if (tx.type === 1 || tx.gasPrice) {
if (tx.gasPrice) {
tx.gasPrice = await childSigner.getGasPrice();
}
}
else {
if (!(0, lib_1.isDefined)(tx.maxFeePerGas)) {
const feeData = await childSigner.getFeeData();
tx.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas;
tx.maxFeePerGas = feeData.maxFeePerGas;
}
tx.type = 2;
}
tx.from = await childSigner.getAddress();
tx.chainId = await childSigner.getChainId();
// if this is contract creation, user might not input the to address,
// however, it is needed when we call to estimateArbitrumGas, so
// we add a zero address here.
if (!(0, lib_1.isDefined)(tx.to)) {
tx.to = ethers_1.ethers.constants.AddressZero;
}
//estimate gas on child chain
try {
tx.gasLimit = (await this.estimateArbitrumGas(tx, childSigner.provider)).gasEstimateForChild;
}
catch (error) {
throw new errors_1.ArbSdkError('execution failed (estimate gas failed)');
}
if (contractCreation) {
delete tx.to;
}
return await childSigner.signTransaction(tx);
}
}
exports.InboxTools = InboxTools;