Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handling the error when eth_estimateGas fails #920

Merged
merged 12 commits into from
Oct 11, 2022
4 changes: 3 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export const ERC1155_TOKEN_RECEIVER_INTERFACE_ID = '0x4e2312e0';
// UNITS
export const GWEI = 'gwei';

// TRANSACTION CONTROLLER ERRORS
export const ESTIMATE_GAS_ERROR = 'eth_estimateGas rpc method error';

// ASSET TYPES
export const ASSET_TYPES = {
NATIVE: 'NATIVE',
Expand All @@ -40,7 +43,6 @@ export const TESTNET_TICKER_SYMBOLS = {
ROPSTEN: 'RopstenETH',
KOVAN: 'KovanETH',
};

// TYPED NetworkType TICKER SYMBOLS
export const TESTNET_NETWORK_TYPE_TO_TICKER_SYMBOL: {
[K in NetworkType]: string;
Expand Down
182 changes: 168 additions & 14 deletions src/transaction/TransactionController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
NetworkType,
NetworkState,
} from '../network/NetworkController';
import { ESTIMATE_GAS_ERROR } from '../constants';
import {
TransactionController,
TransactionStatus,
Expand All @@ -22,15 +23,22 @@ import {
const globalAny: any = global;

const mockFlags: { [key: string]: any } = {
estimateGas: null,
estimateGasError: null,
estimateGasValue: null,
getBlockByNumberValue: null,
};

jest.mock('eth-query', () =>
jest.fn().mockImplementation(() => {
return {
estimateGas: (_transaction: any, callback: any) => {
if (mockFlags.estimateGas) {
callback(new Error(mockFlags.estimateGas));
if (mockFlags.estimateGasError) {
callback(new Error(mockFlags.estimateGasError));
return;
}

if (mockFlags.estimateGasValue) {
callback(undefined, mockFlags.estimateGasValue);
return;
}
callback(undefined, '0x0');
Expand All @@ -43,6 +51,10 @@ jest.mock('eth-query', () =>
_fetchTxs: boolean,
callback: any,
) => {
if (mockFlags.getBlockByNumberValue) {
callback(undefined, { gasLimit: '0x12a05f200' });
return;
}
callback(undefined, { gasLimit: '0x0' });
},
getCode: (_to: any, callback: any) => {
Expand Down Expand Up @@ -354,6 +366,118 @@ describe('TransactionController', () => {
});
});

it('should on eth_estimateGas succeed when gasBn it is greater than maxGasBN', async () => {
const controller = new TransactionController({
getNetworkState: () => MOCK_NETWORK.state,
onNetworkStateChange: MOCK_NETWORK.subscribe,
getProvider: MOCK_NETWORK.getProvider,
});
mockFlags.estimateGasValue = '0x12a05f200';
const from = '0x4579d0ad79bfbdf4539a1ddf5f10b378d724a34c';
const result = await controller.estimateGas({ from, to: from });

expect(result.estimateGasError).toBeUndefined();
});

it('should on Mainnet eth_estimateGas succeed when gasBn it is higher than maxGasBN', async () => {
const controller = new TransactionController({
getNetworkState: () => MOCK_NETWORK.state,
onNetworkStateChange: MOCK_NETWORK.subscribe,
getProvider: MOCK_NETWORK.getProvider,
});
mockFlags.estimateGasValue = '0x12a05f200';
mockFlags.estimateGasError = ESTIMATE_GAS_ERROR;

const from = '0x4579d0ad79bfbdf4539a1ddf5f10b378d724a34c';
const result = await controller.estimateGas({ from, to: from });

expect(result.estimateGasError).toBeUndefined();
});

it('should on Custom Network eth_estimateGas succeed when gasBN is equal to maxGasBN', async () => {
const controller = new TransactionController({
getNetworkState: () => MOCK_CUSTOM_NETWORK.state,
onNetworkStateChange: MOCK_CUSTOM_NETWORK.subscribe,
getProvider: MOCK_CUSTOM_NETWORK.getProvider,
});
const from = '0x4579d0ad79bfbdf4539a1ddf5f10b378d724a34c';
const result = await controller.estimateGas({ from, to: from });
expect(result.estimateGasError).toBeUndefined();
});

it('should on Custom Network eth_estimateGas fail when gasBN is equal to maxGasBN', async () => {
const controller = new TransactionController({
getNetworkState: () => MOCK_CUSTOM_NETWORK.state,
onNetworkStateChange: MOCK_CUSTOM_NETWORK.subscribe,
getProvider: MOCK_CUSTOM_NETWORK.getProvider,
});
mockFlags.estimateGasError = ESTIMATE_GAS_ERROR;
const from = '0x4579d0ad79bfbdf4539a1ddf5f10b378d724a34c';
const result = await controller.estimateGas({ from, to: from });
expect(result.estimateGasError).toBe(ESTIMATE_GAS_ERROR);
});

it('should on Custom Network eth_estimateGas succeed when gasBN is less than maxGasBN', async () => {
const controller = new TransactionController({
getNetworkState: () => MOCK_CUSTOM_NETWORK.state,
onNetworkStateChange: MOCK_CUSTOM_NETWORK.subscribe,
getProvider: MOCK_CUSTOM_NETWORK.getProvider,
});

mockFlags.getBlockByNumberValue = '0x12a05f200';

const from = '0x4579d0ad79bfbdf4539a1ddf5f10b378d724a34c';
const result = await controller.estimateGas({ from, to: from });
expect(result.estimateGasError).toBeUndefined();
});

it('should on Custom Network eth_estimateGas fail when gasBN is less than maxGasBN', async () => {
const controller = new TransactionController({
getNetworkState: () => MOCK_CUSTOM_NETWORK.state,
onNetworkStateChange: MOCK_CUSTOM_NETWORK.subscribe,
getProvider: MOCK_CUSTOM_NETWORK.getProvider,
});

mockFlags.getBlockByNumberValue = '0x12a05f200';

mockFlags.estimateGasError = ESTIMATE_GAS_ERROR;
const from = '0x4579d0ad79bfbdf4539a1ddf5f10b378d724a34c';
const result = await controller.estimateGas({ from, to: from });

expect(result.estimateGasError).toBe(ESTIMATE_GAS_ERROR);
});

it('should eth_estimateGas succeed when gasBN is less than maxGasBN and paddedGasBN is less than MaxGasBN', async () => {
const controller = new TransactionController({
getNetworkState: () => MOCK_NETWORK.state,
onNetworkStateChange: MOCK_NETWORK.subscribe,
getProvider: MOCK_NETWORK.getProvider,
});

mockFlags.getBlockByNumberValue = '0x12a05f200';

const from = '0x4579d0ad79bfbdf4539a1ddf5f10b378d724a34c';
const result = await controller.estimateGas({ from, to: from });

expect(result.estimateGasError).toBeUndefined();
});

it('should eth_estimateGas fail when gasBN is less than maxGasBN and paddedGasBN is less than MaxGasBN', async () => {
const controller = new TransactionController({
getNetworkState: () => MOCK_NETWORK.state,
onNetworkStateChange: MOCK_NETWORK.subscribe,
getProvider: MOCK_NETWORK.getProvider,
});

mockFlags.getBlockByNumberValue = '0x12a05f200';
mockFlags.estimateGasError = ESTIMATE_GAS_ERROR;

const from = '0x4579d0ad79bfbdf4539a1ddf5f10b378d724a34c';
const result = await controller.estimateGas({ from, to: from });

expect(result.estimateGasError).toBe(ESTIMATE_GAS_ERROR);
});

it('should throw when adding invalid transaction', async () => {
const controller = new TransactionController({
getNetworkState: () => MOCK_NETWORK.state,
Expand Down Expand Up @@ -590,20 +714,50 @@ describe('TransactionController', () => {
await expect(result).rejects.toThrow('foo');
});

it('should fail transaction if gas calculation fails', async () => {
it('should have gasEstimatedError variable on transaction object if gas calculation fails', async () => {
const controller = new TransactionController({
getNetworkState: () => MOCK_NETWORK.state,
onNetworkStateChange: MOCK_NETWORK.subscribe,
getProvider: MOCK_NETWORK.getProvider,
getNetworkState: () => MOCK_MAINNET_NETWORK.state,
onNetworkStateChange: MOCK_MAINNET_NETWORK.subscribe,
getProvider: MOCK_MAINNET_NETWORK.getProvider,
});
mockFlags.estimateGasError = ESTIMATE_GAS_ERROR;
const from = '0xc38bf1ad06ef69f0c04e29dbeb4152b4175f0a8d';
mockFlags.estimateGas = 'Uh oh';
await expect(
controller.addTransaction({
from,
to: from,
}),
).rejects.toThrow('Uh oh');
await controller.addTransaction({
from,
to: from,
});

controller.hub.once(
`${controller.state.transactions[0].id}:finished`,
() => {
const { transaction, status } = controller.state.transactions[0];
expect(transaction.estimateGasError).toBe(ESTIMATE_GAS_ERROR);
expect(status).toBe(TransactionStatus.submitted);
},
);
});

it('should have gasEstimatedError variable on transaction object on custom network if gas calculation fails', async () => {
const controller = new TransactionController({
getNetworkState: () => MOCK_CUSTOM_NETWORK.state,
onNetworkStateChange: MOCK_CUSTOM_NETWORK.subscribe,
getProvider: MOCK_CUSTOM_NETWORK.getProvider,
});
mockFlags.estimateGasError = ESTIMATE_GAS_ERROR;
const from = '0xc38bf1ad06ef69f0c04e29dbeb4152b4175f0a8d';
await controller.addTransaction({
from,
to: from,
});

controller.hub.once(
`${controller.state.transactions[0].id}:finished`,
() => {
const { transaction, status } = controller.state.transactions[0];
expect(transaction.estimateGasError).toBe(ESTIMATE_GAS_ERROR);
expect(status).toBe(TransactionStatus.submitted);
},
);
});

it('should fail if no sign method defined', async () => {
Expand Down
26 changes: 19 additions & 7 deletions src/transaction/TransactionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
validateGasValues,
validateMinimumIncrease,
} from '../util';
import { MAINNET, RPC } from '../constants';
import { ESTIMATE_GAS_ERROR, MAINNET, RPC } from '../constants';

const HARDFORK = 'london';

Expand Down Expand Up @@ -80,6 +80,7 @@ export interface Transaction {
maxFeePerGas?: string;
maxPriorityFeePerGas?: string;
estimatedBaseFee?: string;
estimateGasError?: string;
}

export interface GasPriceValue {
Expand Down Expand Up @@ -517,8 +518,9 @@ export class TransactionController extends BaseController<
};

try {
const { gas } = await this.estimateGas(transaction);
const { gas, estimateGasError } = await this.estimateGas(transaction);
tommasini marked this conversation as resolved.
Show resolved Hide resolved
transaction.gas = gas;
transaction.estimateGasError = estimateGasError;
} catch (error: any) {
this.failTransaction(transactionMeta, error);
return Promise.reject(error);
Expand Down Expand Up @@ -988,23 +990,33 @@ export class TransactionController extends BaseController<
typeof value === 'undefined' ? '0x0' : /* istanbul ignore next */ value;
const gasLimitBN = hexToBN(gasLimit);
estimatedTransaction.gas = BNToHex(fractionBN(gasLimitBN, 19, 20));
const gasHex = await query(this.ethQuery, 'estimateGas', [
estimatedTransaction,
]);

let gasHex;
let estimateGasError;
try {
gasHex = await query(this.ethQuery, 'estimateGas', [
estimatedTransaction,
]);
} catch (error) {
estimateGasError = ESTIMATE_GAS_ERROR;
}
// 4. Pad estimated gas without exceeding the most recent block gasLimit. If the network is a
// a custom network then return the eth_estimateGas value.
const gasBN = hexToBN(gasHex);
const maxGasBN = gasLimitBN.muln(0.9);
const paddedGasBN = gasBN.muln(1.5);
/* istanbul ignore next */
if (gasBN.gt(maxGasBN) || isCustomNetwork) {
return { gas: addHexPrefix(gasHex), gasPrice };
return { gas: addHexPrefix(gasHex), gasPrice, estimateGasError };
}

/* istanbul ignore next */
if (paddedGasBN.lt(maxGasBN)) {
return { gas: addHexPrefix(BNToHex(paddedGasBN)), gasPrice };
return {
gas: addHexPrefix(BNToHex(paddedGasBN)),
gasPrice,
estimateGasError,
};
}
return { gas: addHexPrefix(BNToHex(maxGasBN)), gasPrice };
}
Expand Down