From 9a6c22263298f02120e47fa6347cf872408e4659 Mon Sep 17 00:00:00 2001 From: Brad Decker Date: Wed, 7 Jul 2021 11:13:40 -0500 Subject: [PATCH] Add gas utils shared module (#11452) --- shared/modules/gas.utils.js | 129 ++++++++++++++++++++++++++++++ shared/modules/gas.utils.test.js | 131 +++++++++++++++++++++++++++++++ 2 files changed, 260 insertions(+) create mode 100644 shared/modules/gas.utils.js create mode 100644 shared/modules/gas.utils.test.js diff --git a/shared/modules/gas.utils.js b/shared/modules/gas.utils.js new file mode 100644 index 000000000..ab8c44b36 --- /dev/null +++ b/shared/modules/gas.utils.js @@ -0,0 +1,129 @@ +import { addHexPrefix } from 'ethereumjs-util'; +import { + addCurrencies, + conversionGreaterThan, + multiplyCurrencies, +} from './conversion.utils'; + +/** + * Accepts an options bag containing gas fee parameters in hex format and + * returns a gasTotal parameter representing the maximum amount of wei the + * transaction will cost. + * + * @param {object} options - gas fee parameters object + * @param {string} [options.gasLimit] - the maximum amount of gas to allow this + * transaction to consume. Value is a hex string + * @param {string} [options.gasPrice] - The fee in wei to pay per gas used. + * gasPrice is only set on Legacy type transactions. Value is hex string + * @param {string} [options.maxFeePerGas] - The maximum fee in wei to pay per + * gas used. maxFeePerGas is introduced in EIP 1559 and represents the max + * total a user will pay per gas. Actual cost is determined by baseFeePerGas + * on the block + maxPriorityFeePerGas. Value is hex string + * @returns {string} - The maximum total cost of transaction in hex wei string + */ +export function getMaximumGasTotalInHexWei({ + gasLimit = '0x0', + gasPrice, + maxFeePerGas, +} = {}) { + if (maxFeePerGas) { + return addHexPrefix( + multiplyCurrencies(gasLimit, maxFeePerGas, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 16, + }), + ); + } + if (!gasPrice) { + throw new Error( + 'getMaximumGasTotalInHexWei requires gasPrice be provided to calculate legacy gas total', + ); + } + return addHexPrefix( + multiplyCurrencies(gasLimit, gasPrice, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 16, + }), + ); +} + +/** + * Accepts an options bag containing gas fee parameters in hex format and + * returns a gasTotal parameter representing the minimum amount of wei the + * transaction will cost. For gasPrice types this is the same as max. + * + * @param {object} options - gas fee parameters object + * @param {string} [options.gasLimit] - the maximum amount of gas to allow this + * transaction to consume. Value is a hex string + * @param {string} [options.gasPrice] - The fee in wei to pay per gas used. + * gasPrice is only set on Legacy type transactions. Value is hex string + * @param {string} [options.maxFeePerGas] - The maximum fee in wei to pay per + * gas used. maxFeePerGas is introduced in EIP 1559 and represents the max + * total a user will pay per gas. Actual cost is determined by baseFeePerGas + * on the block + maxPriorityFeePerGas. Value is hex string + * @param {string} [options.maxPriorityFeePerGas] - The maximum fee in wei to + * pay a miner to include this transaction. + * @param {string} [options.baseFeePerGas] - The estimated block baseFeePerGas + * that will be burned. Introduced in EIP 1559. Value in hex wei. + * @returns {string} - The minimum total cost of transaction in hex wei string + */ +export function getMinimumGasTotalInHexWei({ + gasLimit = '0x0', + gasPrice, + maxPriorityFeePerGas, + maxFeePerGas, + baseFeePerGas, +} = {}) { + const isEIP1559Estimate = Boolean( + maxFeePerGas || maxPriorityFeePerGas || baseFeePerGas, + ); + if (isEIP1559Estimate && gasPrice) { + throw new Error( + `getMinimumGasTotalInHexWei expects either gasPrice OR the EIP-1559 gas fields, but both were provided`, + ); + } + + if (isEIP1559Estimate === false && !gasPrice) { + throw new Error( + `getMinimumGasTotalInHexWei expects either gasPrice OR the EIP-1559 gas fields, but neither were provided`, + ); + } + + if (isEIP1559Estimate && !baseFeePerGas) { + throw new Error( + `getMinimumGasTotalInHexWei requires baseFeePerGas be provided when calculating EIP-1559 totals`, + ); + } + + if (isEIP1559Estimate && (!maxFeePerGas || !maxPriorityFeePerGas)) { + throw new Error( + `getMinimumGasTotalInHexWei requires maxFeePerGas and maxPriorityFeePerGas be provided when calculating EIP-1559 totals`, + ); + } + if (isEIP1559Estimate === false) { + return getMaximumGasTotalInHexWei({ gasLimit, gasPrice }); + } + const minimumFeePerGas = addCurrencies(baseFeePerGas, maxPriorityFeePerGas, { + toNumericBase: 'hex', + aBase: 16, + bBase: 16, + }); + + if ( + conversionGreaterThan( + { value: minimumFeePerGas, fromNumericBase: 'hex' }, + { value: maxFeePerGas, fromNumericBase: 'hex' }, + ) + ) { + return getMaximumGasTotalInHexWei({ gasLimit, maxFeePerGas }); + } + return addHexPrefix( + multiplyCurrencies(gasLimit, minimumFeePerGas, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 16, + }), + ); +} diff --git a/shared/modules/gas.utils.test.js b/shared/modules/gas.utils.test.js new file mode 100644 index 000000000..8c2b9a359 --- /dev/null +++ b/shared/modules/gas.utils.test.js @@ -0,0 +1,131 @@ +const { addHexPrefix } = require('ethereumjs-util'); +const { conversionUtil } = require('./conversion.utils'); +const { + getMaximumGasTotalInHexWei, + getMinimumGasTotalInHexWei, +} = require('./gas.utils'); + +const feesToTest = [10, 24, 90]; +const tipsToTest = [2, 10, 50]; +const baseFeesToTest = [8, 12, 24]; +const gasLimitsToTest = [21000, 100000]; + +describe('gas utils', () => { + describe('when using EIP 1559 fields', () => { + describe('getMaximumGasTotalInHexWei', () => { + feesToTest.forEach((maxFeePerGas) => { + describe(`when maxFeePerGas is ${maxFeePerGas}`, () => { + gasLimitsToTest.forEach((gasLimit) => { + const expectedResult = (gasLimit * maxFeePerGas).toString(); + const gasLimitHex = addHexPrefix(gasLimit.toString(16)); + const result = conversionUtil( + getMaximumGasTotalInHexWei({ + gasLimit: gasLimitHex, + maxFeePerGas: addHexPrefix(maxFeePerGas.toString(16)), + }), + { fromNumericBase: 'hex', toNumericBase: 'dec' }, + ); + it(`returns ${expectedResult} when provided gasLimit: ${gasLimit}`, () => { + expect(result).toStrictEqual(expectedResult); + }); + }); + }); + }); + }); + + describe('getMinimumGasTotalInHexWei', () => { + feesToTest.forEach((maxFeePerGas) => { + tipsToTest.forEach((maxPriorityFeePerGas) => { + baseFeesToTest.forEach((baseFeePerGas) => { + describe(`when baseFee is ${baseFeePerGas}, maxFeePerGas is ${maxFeePerGas} and tip is ${maxPriorityFeePerGas}`, () => { + const maximum = maxFeePerGas; + const minimum = baseFeePerGas + maxPriorityFeePerGas; + const expectedEffectiveGasPrice = + minimum < maximum ? minimum : maximum; + const results = gasLimitsToTest.map((gasLimit) => { + const gasLimitHex = addHexPrefix(gasLimit.toString(16)); + const result = conversionUtil( + getMinimumGasTotalInHexWei({ + gasLimit: gasLimitHex, + maxFeePerGas: addHexPrefix(maxFeePerGas.toString(16)), + maxPriorityFeePerGas: addHexPrefix( + maxPriorityFeePerGas.toString(16), + ), + baseFeePerGas: addHexPrefix(baseFeePerGas.toString(16)), + }), + { fromNumericBase: 'hex', toNumericBase: 'dec' }, + ); + return { result, gasLimit }; + }); + it(`should use an effective gasPrice of ${expectedEffectiveGasPrice}`, () => { + expect( + results.every(({ result, gasLimit }) => { + const effectiveGasPrice = Number(result) / gasLimit; + return effectiveGasPrice === expectedEffectiveGasPrice; + }), + ).toBe(true); + }); + results.forEach(({ result, gasLimit }) => { + const expectedResult = ( + expectedEffectiveGasPrice * gasLimit + ).toString(); + it(`returns ${expectedResult} when provided gasLimit: ${gasLimit}`, () => { + expect(result).toStrictEqual(expectedResult); + }); + }); + }); + }); + }); + }); + }); + }); + + describe('when using legacy fields', () => { + describe('getMaximumGasTotalInHexWei', () => { + feesToTest.forEach((gasPrice) => { + describe(`when gasPrice is ${gasPrice}`, () => { + gasLimitsToTest.forEach((gasLimit) => { + const expectedResult = (gasLimit * gasPrice).toString(); + const gasLimitHex = addHexPrefix(gasLimit.toString(16)); + it(`returns ${expectedResult} when provided gasLimit of ${gasLimit}`, () => { + expect( + conversionUtil( + getMaximumGasTotalInHexWei({ + gasLimit: gasLimitHex, + gasPrice: addHexPrefix(gasPrice.toString(16)), + }), + { fromNumericBase: 'hex', toNumericBase: 'dec' }, + ), + ).toStrictEqual(expectedResult); + }); + }); + }); + }); + }); + + describe('getMinimumGasTotalInHexWei', () => { + feesToTest.forEach((gasPrice) => { + describe(`when gasPrice is ${gasPrice}`, () => { + gasLimitsToTest.forEach((gasLimit) => { + const expectedResult = (gasLimit * gasPrice).toString(); + const gasLimitHex = addHexPrefix(gasLimit.toString(16)); + it(`returns ${expectedResult} when provided gasLimit of ${gasLimit}`, () => { + expect( + conversionUtil( + getMinimumGasTotalInHexWei({ + gasLimit: gasLimitHex, + gasPrice: addHexPrefix(gasPrice.toString(16)), + }), + { + fromNumericBase: 'hex', + toNumericBase: 'dec', + }, + ), + ).toStrictEqual(expectedResult); + }); + }); + }); + }); + }); + }); +});