@arbitrum/sdk
Version:
Typescript library client-side interactions with Arbitrum
226 lines (225 loc) • 11.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ParentToChildMessageGasEstimator = void 0;
const bignumber_1 = require("@ethersproject/bignumber");
const ethers_1 = require("ethers");
const Inbox__factory_1 = require("../abi/factories/Inbox__factory");
const NodeInterface__factory_1 = require("../abi/factories/NodeInterface__factory");
const constants_1 = require("../dataEntities/constants");
const errors_1 = require("../dataEntities/errors");
const networks_1 = require("../dataEntities/networks");
const retryableData_1 = require("../dataEntities/retryableData");
const lib_1 = require("../utils/lib");
/**
* The default amount to increase the maximum submission cost. Submission cost is calculated
* from (call data size * some const * parent chain base fee). So we need to provide some leeway for
* base fee increase. Since submission fee is a small amount it isn't too bas for UX to increase
* it by a large amount, and provide better safety.
*/
const DEFAULT_SUBMISSION_FEE_PERCENT_INCREASE = bignumber_1.BigNumber.from(300);
/**
* When submitting a retryable we need to estimate what the gas price for it will be when we actually come
* to execute it. Since the l2 price can move due to congestion we should provide some padding here
*/
const DEFAULT_GAS_PRICE_PERCENT_INCREASE = bignumber_1.BigNumber.from(500);
const defaultParentToChildMessageEstimateOptions = {
maxSubmissionFeePercentIncrease: DEFAULT_SUBMISSION_FEE_PERCENT_INCREASE,
// gas limit for Parent->Child messages should be predictable. If it isn't due to the nature
// of the specific transaction, then the caller should provide a 'min' override
gasLimitPercentIncrease: ethers_1.constants.Zero,
maxFeePerGasPercentIncrease: DEFAULT_GAS_PRICE_PERCENT_INCREASE,
};
class ParentToChildMessageGasEstimator {
constructor(childProvider) {
this.childProvider = childProvider;
}
percentIncrease(num, increase) {
return num.add(num.mul(increase).div(100));
}
applySubmissionPriceDefaults(maxSubmissionFeeOptions) {
return {
base: maxSubmissionFeeOptions === null || maxSubmissionFeeOptions === void 0 ? void 0 : maxSubmissionFeeOptions.base,
percentIncrease: (maxSubmissionFeeOptions === null || maxSubmissionFeeOptions === void 0 ? void 0 : maxSubmissionFeeOptions.percentIncrease) ||
defaultParentToChildMessageEstimateOptions.maxSubmissionFeePercentIncrease,
};
}
applyMaxFeePerGasDefaults(maxFeePerGasOptions) {
return {
base: maxFeePerGasOptions === null || maxFeePerGasOptions === void 0 ? void 0 : maxFeePerGasOptions.base,
percentIncrease: (maxFeePerGasOptions === null || maxFeePerGasOptions === void 0 ? void 0 : maxFeePerGasOptions.percentIncrease) ||
defaultParentToChildMessageEstimateOptions.maxFeePerGasPercentIncrease,
};
}
applyGasLimitDefaults(gasLimitDefaults) {
return {
base: gasLimitDefaults === null || gasLimitDefaults === void 0 ? void 0 : gasLimitDefaults.base,
percentIncrease: (gasLimitDefaults === null || gasLimitDefaults === void 0 ? void 0 : gasLimitDefaults.percentIncrease) ||
defaultParentToChildMessageEstimateOptions.gasLimitPercentIncrease,
min: (gasLimitDefaults === null || gasLimitDefaults === void 0 ? void 0 : gasLimitDefaults.min) || ethers_1.constants.Zero,
};
}
/**
* Return the fee, in wei, of submitting a new retryable tx with a given calldata size.
* @param parentProvider
* @param parentBaseFee
* @param callDataSize
* @param options
* @returns
*/
async estimateSubmissionFee(parentProvider, parentBaseFee, callDataSize, options) {
const defaultedOptions = this.applySubmissionPriceDefaults(options);
const network = await (0, networks_1.getArbitrumNetwork)(this.childProvider);
const inbox = Inbox__factory_1.Inbox__factory.connect(network.ethBridge.inbox, parentProvider);
return this.percentIncrease(defaultedOptions.base ||
(await inbox.calculateRetryableSubmissionFee(callDataSize, parentBaseFee)), defaultedOptions.percentIncrease);
}
/**
* Estimate the amount of child chain gas required for putting the transaction in the L2 inbox, and executing it.
* @param retryableData object containing retryable ticket data
* @param senderDeposit we dont know how much gas the transaction will use when executing
* so by default we supply a dummy amount of call value that will definately be more than we need
* @returns
*/
async estimateRetryableTicketGasLimit({ from, to, l2CallValue: l2CallValue, excessFeeRefundAddress, callValueRefundAddress, data, }, senderDeposit = ethers_1.utils.parseEther('1').add(l2CallValue)) {
const nodeInterface = NodeInterface__factory_1.NodeInterface__factory.connect(constants_1.NODE_INTERFACE_ADDRESS, this.childProvider);
return await nodeInterface.estimateGas.estimateRetryableTicket(from, senderDeposit, to, l2CallValue, excessFeeRefundAddress, callValueRefundAddress, data);
}
/**
* Provides an estimate for the child chain maxFeePerGas, adding some margin to allow for gas price variation
* @param options
* @returns
*/
async estimateMaxFeePerGas(options) {
const maxFeePerGasDefaults = this.applyMaxFeePerGasDefaults(options);
// estimate the child gas price
return this.percentIncrease(maxFeePerGasDefaults.base || (await this.childProvider.getGasPrice()), maxFeePerGasDefaults.percentIncrease);
}
/**
* Checks if the estimate is valid when compared with a new one
* @param estimates Original estimate
* @param reEstimates Estimate to compare against
* @returns
*/
static async isValid(estimates, reEstimates) {
// L2 base fee and minimum submission cost which affect the success of the tx
return (estimates.maxFeePerGas.gte(reEstimates.maxFeePerGas) &&
estimates.maxSubmissionCost.gte(reEstimates.maxSubmissionCost));
}
/**
* Get gas limit, gas price and submission price estimates for sending a Parent->Child message
* @param retryableData Data of retryable ticket transaction
* @param parentBaseFee Current parent chain base fee
* @param parentProvider
* @param options
* @returns
*/
async estimateAll(retryableEstimateData, parentBaseFee, parentProvider, options) {
var _a, _b;
const { data } = retryableEstimateData;
const gasLimitDefaults = this.applyGasLimitDefaults(options === null || options === void 0 ? void 0 : options.gasLimit);
const childNetwork = await (0, networks_1.getArbitrumNetwork)(this.childProvider);
const decimals = await (0, lib_1.getNativeTokenDecimals)({
parentProvider,
childNetwork,
});
// estimate the child gas price
const maxFeePerGasPromise = this.estimateMaxFeePerGas(options === null || options === void 0 ? void 0 : options.maxFeePerGas);
// estimate the submission fee
const maxSubmissionFeePromise = this.estimateSubmissionFee(parentProvider, parentBaseFee, ethers_1.utils.hexDataLength(data), options === null || options === void 0 ? void 0 : options.maxSubmissionFee);
// estimate the gas limit
const calculatedGasLimit = this.percentIncrease(gasLimitDefaults.base ||
(await this.estimateRetryableTicketGasLimit(retryableEstimateData, (_a = options === null || options === void 0 ? void 0 : options.deposit) === null || _a === void 0 ? void 0 : _a.base)), gasLimitDefaults.percentIncrease);
const [maxFeePerGas, maxSubmissionFee] = await Promise.all([
maxFeePerGasPromise,
maxSubmissionFeePromise,
]);
// always ensure the max gas is greater than the min - this can be useful if we know that
// gas estimation is bad for the provided transaction
const gasLimit = calculatedGasLimit.gt(gasLimitDefaults.min)
? calculatedGasLimit
: gasLimitDefaults.min;
const deposit = ((_b = options === null || options === void 0 ? void 0 : options.deposit) === null || _b === void 0 ? void 0 : _b.base) ||
(0, lib_1.scaleFrom18DecimalsToNativeTokenDecimals)({
amount: gasLimit
.mul(maxFeePerGas)
.add(maxSubmissionFee)
.add(retryableEstimateData.l2CallValue),
decimals,
});
return {
gasLimit,
maxSubmissionCost: maxSubmissionFee,
maxFeePerGas,
deposit,
};
}
/**
* Transactions that make a Parent->Child message need to estimate L2 gas parameters
* This function does that, and populates those parameters into a transaction request
* @param dataFunc
* @param parentProvider
* @param gasOverrides
* @returns
*/
async populateFunctionParams(
/**
* Function that will internally make a Parent->Child transaction
* Will initially be called with dummy values to trigger a special revert containing
* the real params. Then called again with the real params to form the final data to be submitted
*/
dataFunc, parentProvider, gasOverrides) {
// get function data that should trigger a retryable data error
const { data: nullData, to, value, from, } = dataFunc({
gasLimit: retryableData_1.RetryableDataTools.ErrorTriggeringParams.gasLimit,
maxFeePerGas: retryableData_1.RetryableDataTools.ErrorTriggeringParams.maxFeePerGas,
maxSubmissionCost: bignumber_1.BigNumber.from(1),
});
let retryable;
try {
// get retryable data from the null call
const res = await parentProvider.call({
to: to,
data: nullData,
value: value,
from: from,
});
retryable = retryableData_1.RetryableDataTools.tryParseError(res);
if (!(0, lib_1.isDefined)(retryable)) {
throw new errors_1.ArbSdkError(`No retryable data found in error: ${res}`);
}
}
catch (err) {
// ethersjs currently doesnt throw for custom solidity errors, so we shouldn't end up here
// however we try to catch and parse the error anyway in case ethersjs changes
// behaviour and we dont pick up on it
retryable = retryableData_1.RetryableDataTools.tryParseError(err);
if (!(0, lib_1.isDefined)(retryable)) {
throw new errors_1.ArbSdkError('No retryable data found in error', err);
}
}
// use retryable data to get gas estimates
const baseFee = await (0, lib_1.getBaseFee)(parentProvider);
const estimates = await this.estimateAll({
from: retryable.from,
to: retryable.to,
data: retryable.data,
l2CallValue: retryable.l2CallValue,
excessFeeRefundAddress: retryable.excessFeeRefundAddress,
callValueRefundAddress: retryable.callValueRefundAddress,
}, baseFee, parentProvider, gasOverrides);
// form the real data for the transaction
const { data: realData, to: realTo, value: realValue, } = dataFunc({
gasLimit: estimates.gasLimit,
maxFeePerGas: estimates.maxFeePerGas,
maxSubmissionCost: estimates.maxSubmissionCost,
});
return {
estimates,
retryable,
data: realData,
to: realTo,
value: realValue,
};
}
}
exports.ParentToChildMessageGasEstimator = ParentToChildMessageGasEstimator;