Skip to content

Commit

Permalink
Add EIP1559 support including speedUpTransaction and `stopTransacti…
Browse files Browse the repository at this point in the history
…on` (MetaMask#521)

Co-authored-by: andrepimenta <andrepimenta7@gmail.com>
Co-authored-by: Alex Donesky <adonesky@gmail.com>
  • Loading branch information
3 people committed Jul 23, 2021
1 parent 6f7e951 commit 7fa3e23
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 51 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"single-call-balance-checker-abi": "^1.0.0",
"uuid": "^8.3.2",
"web3": "^0.20.7",
"web3-provider-engine": "^16.0.1"
"web3-provider-engine": "^16.0.3"
},
"devDependencies": {
"@lavamoat/allow-scripts": "^1.0.6",
Expand Down
2 changes: 1 addition & 1 deletion src/network/NetworkController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export class NetworkController extends BaseController<
}

private refreshNetwork() {
this.update({ network: 'loading' });
this.update({ network: 'loading', properties: {} });
const { rpcTarget, type, chainId, ticker } = this.state.provider;
this.initializeProvider(type, rpcTarget, chainId, ticker);
this.lookupNetwork();
Expand Down
153 changes: 110 additions & 43 deletions src/transaction/TransactionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ import {
isSmartContractCode,
handleTransactionFetch,
query,
getIncreasedPriceFromExisting,
isEIP1559Transaction,
} from '../util';
import { MAINNET, RPC } from '../constants';

const HARDFORK = 'berlin';
const HARDFORK = 'london';

/**
* @type Result
Expand Down Expand Up @@ -72,6 +74,9 @@ export interface Transaction {
nonce?: string;
to?: string;
value?: string;
maxFeePerGas?: string;
maxPriorityFeePerGas?: string;
estimatedBaseFee?: string;
}

/**
Expand Down Expand Up @@ -495,9 +500,8 @@ export class TransactionController extends BaseController<
};

try {
const { gas, gasPrice } = await this.estimateGas(transaction);
const { gas } = await this.estimateGas(transaction);
transaction.gas = gas;
transaction.gasPrice = gasPrice;
} catch (error) {
this.failTransaction(transactionMeta, error);
return Promise.reject(error);
Expand Down Expand Up @@ -621,16 +625,34 @@ export class TransactionController extends BaseController<
transactionMeta.transaction.nonce = txNonce;
transactionMeta.transaction.chainId = chainId;

const txParams = {
const baseTxParams = {
...transactionMeta.transaction,
gasLimit: transactionMeta.transaction.gas,
chainId,
nonce: txNonce,
status,
};

const unsignedEthTx = this.prepareUnsignedEthTx(txParams);
const isEIP1559 = isEIP1559Transaction(transactionMeta.transaction);

const txParams = isEIP1559
? {
...baseTxParams,
maxFeePerGas: transactionMeta.transaction.maxFeePerGas,
maxPriorityFeePerGas:
transactionMeta.transaction.maxPriorityFeePerGas,
estimatedBaseFee: transactionMeta.transaction.estimatedBaseFee,
// specify type 2 if maxFeePerGas and maxPriorityFeePerGas are set
type: 2,
}
: baseTxParams;

// delete gasPrice if maxFeePerGas and maxPriorityFeePerGas are set
if (isEIP1559) {
delete txParams.gasPrice;
}

const unsignedEthTx = this.prepareUnsignedEthTx(txParams);
const signedTx = await this.sign(unsignedEthTx, from);
transactionMeta.status = TransactionStatus.signed;
this.updateTransaction(transactionMeta);
Expand Down Expand Up @@ -691,26 +713,42 @@ export class TransactionController extends BaseController<
throw new Error('No sign method defined.');
}

const existingGasPrice = transactionMeta.transaction.gasPrice;
/* istanbul ignore next */
const existingGasPriceDecimal = parseInt(
existingGasPrice === undefined ? '0x0' : existingGasPrice,
16,
);
const gasPrice = addHexPrefix(
`${parseInt(`${existingGasPriceDecimal * CANCEL_RATE}`, 10).toString(
16,
)}`,
const gasPrice = getIncreasedPriceFromExisting(
transactionMeta.transaction.gasPrice,
CANCEL_RATE,
);

const txParams = {
from: transactionMeta.transaction.from,
gasLimit: transactionMeta.transaction.gas,
gasPrice,
nonce: transactionMeta.transaction.nonce,
to: transactionMeta.transaction.from,
value: '0x0',
};
const existingMaxFeePerGas = transactionMeta.transaction?.maxFeePerGas;
const existingMaxPriorityFeePerGas =
transactionMeta.transaction?.maxPriorityFeePerGas;

const newMaxFeePerGas =
existingMaxFeePerGas &&
getIncreasedPriceFromExisting(existingMaxFeePerGas, CANCEL_RATE);
const newMaxPriorityFeePerGas =
existingMaxPriorityFeePerGas &&
getIncreasedPriceFromExisting(existingMaxPriorityFeePerGas, CANCEL_RATE);

const txParams =
newMaxFeePerGas && newMaxPriorityFeePerGas
? {
from: transactionMeta.transaction.from,
gasLimit: transactionMeta.transaction.gas,
maxFeePerGas: newMaxFeePerGas,
maxPriorityFeePerGas: newMaxPriorityFeePerGas,
type: 2,
nonce: transactionMeta.transaction.nonce,
to: transactionMeta.transaction.from,
value: '0x0',
}
: {
from: transactionMeta.transaction.from,
gasLimit: transactionMeta.transaction.gas,
gasPrice,
nonce: transactionMeta.transaction.nonce,
to: transactionMeta.transaction.from,
value: '0x0',
};

const unsignedEthTx = this.prepareUnsignedEthTx(txParams);

Expand Down Expand Up @@ -744,23 +782,39 @@ export class TransactionController extends BaseController<
}

const { transactions } = this.state;
const existingGasPrice = transactionMeta.transaction.gasPrice;
/* istanbul ignore next */
const existingGasPriceDecimal = parseInt(
existingGasPrice === undefined ? '0x0' : existingGasPrice,
16,
);
const gasPrice = addHexPrefix(
`${parseInt(`${existingGasPriceDecimal * SPEED_UP_RATE}`, 10).toString(
16,
)}`,
const gasPrice = getIncreasedPriceFromExisting(
transactionMeta.transaction.gasPrice,
SPEED_UP_RATE,
);

const txParams = {
...transactionMeta.transaction,
gasLimit: transactionMeta.transaction.gas,
gasPrice,
};
const existingMaxFeePerGas = transactionMeta.transaction?.maxFeePerGas;
const existingMaxPriorityFeePerGas =
transactionMeta.transaction?.maxPriorityFeePerGas;

const newMaxFeePerGas =
existingMaxFeePerGas &&
getIncreasedPriceFromExisting(existingMaxFeePerGas, SPEED_UP_RATE);
const newMaxPriorityFeePerGas =
existingMaxPriorityFeePerGas &&
getIncreasedPriceFromExisting(
existingMaxPriorityFeePerGas,
SPEED_UP_RATE,
);

const txParams =
newMaxFeePerGas && newMaxPriorityFeePerGas
? {
...transactionMeta.transaction,
gasLimit: transactionMeta.transaction.gas,
maxFeePerGas: newMaxFeePerGas,
maxPriorityFeePerGas: newMaxPriorityFeePerGas,
type: 2,
}
: {
...transactionMeta.transaction,
gasLimit: transactionMeta.transaction.gas,
gasPrice,
};

const unsignedEthTx = this.prepareUnsignedEthTx(txParams);

Expand All @@ -772,16 +826,29 @@ export class TransactionController extends BaseController<
const transactionHash = await query(this.ethQuery, 'sendRawTransaction', [
rawTransaction,
]);
const newTransactionMeta = {
const baseTransactionMeta = {
...transactionMeta,
id: random(),
time: Date.now(),
transaction: {
...transactionMeta.transaction,
gasPrice,
},
transactionHash,
};
const newTransactionMeta =
newMaxFeePerGas && newMaxPriorityFeePerGas
? {
...baseTransactionMeta,
transaction: {
...transactionMeta.transaction,
maxFeePerGas: newMaxFeePerGas,
maxPriorityFeePerGas: newMaxPriorityFeePerGas,
},
}
: {
...baseTransactionMeta,
transaction: {
...transactionMeta.transaction,
gasPrice,
},
};
transactions.push(newTransactionMeta);
this.update({ transactions: [...transactions] });
this.hub.emit(`${transactionMeta.id}:speedup`, newTransactionMeta);
Expand Down
47 changes: 47 additions & 0 deletions src/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import nock from 'nock';
import HttpProvider from 'ethjs-provider-http';
import EthQuery from 'eth-query';
import * as util from './util';
import { Transaction } from './transaction/TransactionController';

const VALID = '4e1fF7229BDdAf0A73DF183a88d9c3a04cc975e0';
const SOME_API = 'https://someapi.com';
Expand Down Expand Up @@ -95,6 +96,9 @@ describe('util', () => {
nonce: 'nonce',
to: 'TO',
value: 'value',
maxFeePerGas: 'maxFeePerGas',
maxPriorityFeePerGas: 'maxPriorityFeePerGas',
estimatedBaseFee: 'estimatedBaseFee',
});
expect(normalized).toStrictEqual({
data: '0xdata',
Expand All @@ -104,6 +108,9 @@ describe('util', () => {
nonce: '0xnonce',
to: '0xto',
value: '0xvalue',
maxFeePerGas: '0xmaxFeePerGas',
maxPriorityFeePerGas: '0xmaxPriorityFeePerGas',
estimatedBaseFee: '0xestimatedBaseFee',
});
});

Expand Down Expand Up @@ -876,4 +883,44 @@ describe('util', () => {
);
});
});

describe('convertPriceToDecimal', () => {
it('should convert hex price to decimal', () => {
expect(util.convertPriceToDecimal('0x50fd51da')).toStrictEqual(
1358778842,
);
});
it('should return zero when undefined', () => {
expect(util.convertPriceToDecimal(undefined)).toStrictEqual(0);
});
});

describe('getIncreasedPriceHex', () => {
it('should get increased price from number as hex', () => {
expect(util.getIncreasedPriceHex(1358778842, 1.1)).toStrictEqual(
'0x5916a6d6',
);
});
});

describe('getIncreasedPriceFromExisting', () => {
it('should get increased price from hex as hex', () => {
expect(
util.getIncreasedPriceFromExisting('0x50fd51da', 1.1),
).toStrictEqual('0x5916a6d6');
});
});

describe('isEIP1559Transaction', () => {
it('should detect EIP1559 transaction', () => {
const tx: Transaction = { from: '' };
const eip1559tx: Transaction = {
...tx,
maxFeePerGas: '2',
maxPriorityFeePerGas: '3',
};
expect(util.isEIP1559Transaction(eip1559tx)).toBe(true);
expect(util.isEIP1559Transaction(tx)).toBe(false);
});
});
});
34 changes: 34 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ const NORMALIZERS: { [param in keyof Transaction]: any } = {
nonce: (nonce: string) => addHexPrefix(nonce),
to: (to: string) => addHexPrefix(to).toLowerCase(),
value: (value: string) => addHexPrefix(value),
maxFeePerGas: (maxFeePerGas: string) => addHexPrefix(maxFeePerGas),
maxPriorityFeePerGas: (maxPriorityFeePerGas: string) =>
addHexPrefix(maxPriorityFeePerGas),
estimatedBaseFee: (maxPriorityFeePerGas: string) =>
addHexPrefix(maxPriorityFeePerGas),
};

/**
Expand Down Expand Up @@ -654,6 +659,35 @@ export function query(
});
}

/**
* Checks if a transaction is EIP-1559 by checking for the existence of
* maxFeePerGas and maxPriorityFeePerGas within its parameters
*
* @param transaction - Transaction object to add
* @returns - Boolean that is true if the transaction is EIP-1559 (has maxFeePerGas and maxPriorityFeePerGas), otherwise returns false
*/
export const isEIP1559Transaction = (transaction: Transaction): boolean => {
const hasOwnProp = (obj: Transaction, key: string) =>
Object.prototype.hasOwnProperty.call(obj, key);
return (
hasOwnProp(transaction, 'maxFeePerGas') &&
hasOwnProp(transaction, 'maxPriorityFeePerGas')
);
};

export const convertPriceToDecimal = (value: string | undefined): number =>
parseInt(value === undefined ? '0x0' : value, 16);

export const getIncreasedPriceHex = (value: number, rate: number): string =>
addHexPrefix(`${parseInt(`${value * rate}`, 10).toString(16)}`);

export const getIncreasedPriceFromExisting = (
value: string | undefined,
rate: number,
): string => {
return getIncreasedPriceHex(convertPriceToDecimal(value), rate);
};

export default {
BNToHex,
fractionBN,
Expand Down
Loading

0 comments on commit 7fa3e23

Please sign in to comment.