1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00
metamask-extension/app/scripts/controllers/transactions/index.test.js
Elliot Winkler ed3cc404f2
NetworkController: Split network into networkId and networkStatus (#17556)
The `network` store of the network controller crams two types of data
into one place. It roughly tracks whether we have enough information to
make requests to the network and whether the network is capable of
receiving requests, but it also stores the ID of the network (as
obtained via `net_version`).

Generally we shouldn't be using the network ID for anything, as it has
been completely replaced by chain ID, which all custom RPC endpoints
have been required to support for over a year now. However, as the
network ID is used in various places within the extension codebase,
removing it entirely would be a non-trivial effort. So, minimally, this
commit splits `network` into two stores: `networkId` and
`networkStatus`. But it also expands the concept of network status.

Previously, the network was in one of two states: "loading" and
"not-loading". But now it can be in one of four states:

- `available`: The network is able to receive and respond to requests.
- `unavailable`: The network is not able to receive and respond to
  requests for unknown reasons.
- `blocked`: The network is actively blocking requests based on the
  user's geolocation. (This is specific to Infura.)
- `unknown`: We don't know whether the network can receive and respond
  to requests, either because we haven't checked or we tried to check
  and were unsuccessful.

This commit also changes how the network status is determined —
specifically, how many requests are used to determine that status, when
they occur, and whether they are awaited. Previously, the network
controller would make 2 to 3 requests during the course of running
`lookupNetwork`.

* First, if it was an Infura network, it would make a request for
  `eth_blockNumber` to determine whether Infura was blocking requests or
  not, then emit an appropriate event. This operation was not awaited.
* Then, regardless of the network, it would fetch the network ID via
  `net_version`. This operation was awaited.
* Finally, regardless of the network, it would fetch the latest block
  via `eth_getBlockByNumber`, then use the result to determine whether
  the network supported EIP-1559. This operation was awaited.

Now:

* One fewer request is made, specifically `eth_blockNumber`, as we don't
  need to make an extra request to determine whether Infura is blocking
  requests; we can reuse `eth_getBlockByNumber`;
* All requests are awaited, which makes `lookupNetwork` run fully
  in-band instead of partially out-of-band; and
* Both requests for `net_version` and `eth_getBlockByNumber` are
  performed in parallel to make `lookupNetwork` run slightly faster.
2023-03-30 16:49:12 -06:00

2887 lines
91 KiB
JavaScript

import { strict as assert } from 'assert';
import EventEmitter from 'events';
import { toBuffer } from 'ethereumjs-util';
import { TransactionFactory } from '@ethereumjs/tx';
import { ObservableStore } from '@metamask/obs-store';
import sinon from 'sinon';
import {
createTestProviderTools,
getTestAccounts,
} from '../../../../test/stub/provider';
import mockEstimates from '../../../../test/data/mock-estimates.json';
import { EVENT } from '../../../../shared/constants/metametrics';
import {
TransactionStatus,
TransactionType,
TransactionEnvelopeType,
TransactionMetaMetricsEvent,
AssetType,
TokenStandard,
} from '../../../../shared/constants/transaction';
import { SECOND } from '../../../../shared/constants/time';
import {
GasEstimateTypes,
GasRecommendations,
} from '../../../../shared/constants/gas';
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
import { ORIGIN_METAMASK } from '../../../../shared/constants/app';
import { NetworkStatus } from '../../../../shared/constants/network';
import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../shared/lib/transactions-controller-utils';
import TransactionController from '.';
const noop = () => true;
const currentNetworkId = '5';
const currentChainId = '0x5';
const currentNetworkStatus = NetworkStatus.Available;
const providerConfig = {
type: 'goerli',
};
const actionId = 'DUMMY_ACTION_ID';
const VALID_ADDRESS = '0x0000000000000000000000000000000000000000';
const VALID_ADDRESS_TWO = '0x0000000000000000000000000000000000000001';
describe('Transaction Controller', function () {
let txController,
provider,
providerResultStub,
fromAccount,
fragmentExists,
networkStatusStore,
getCurrentChainId;
beforeEach(function () {
fragmentExists = false;
providerResultStub = {
// 1 gwei
eth_gasPrice: '0x0de0b6b3a7640000',
// by default, all accounts are external accounts (not contracts)
eth_getCode: '0x',
};
provider = createTestProviderTools({
scaffold: providerResultStub,
networkId: currentNetworkId,
chainId: parseInt(currentChainId, 16),
}).provider;
networkStatusStore = new ObservableStore(currentNetworkStatus);
fromAccount = getTestAccounts()[0];
const blockTrackerStub = new EventEmitter();
blockTrackerStub.getCurrentBlock = noop;
blockTrackerStub.getLatestBlock = noop;
getCurrentChainId = sinon.stub().callsFake(() => currentChainId);
txController = new TransactionController({
provider,
getGasPrice() {
return '0xee6b2800';
},
getNetworkId: () => currentNetworkId,
getNetworkStatus: () => networkStatusStore.getState(),
onNetworkStateChange: (listener) =>
networkStatusStore.subscribe(listener),
getCurrentNetworkEIP1559Compatibility: () => Promise.resolve(false),
getCurrentAccountEIP1559Compatibility: () => false,
txHistoryLimit: 10,
blockTracker: blockTrackerStub,
signTransaction: (ethTx) =>
new Promise((resolve) => {
resolve(ethTx.sign(fromAccount.key));
}),
getProviderConfig: () => providerConfig,
getPermittedAccounts: () => undefined,
getCurrentChainId,
getParticipateInMetrics: () => false,
trackMetaMetricsEvent: () => undefined,
createEventFragment: () => undefined,
updateEventFragment: () => undefined,
finalizeEventFragment: () => undefined,
getEventFragmentById: () =>
fragmentExists === false ? undefined : { id: 0 },
getEIP1559GasFeeEstimates: () => undefined,
getAccountType: () => 'MetaMask',
getDeviceModel: () => 'N/A',
securityProviderRequest: () => undefined,
});
txController.nonceTracker.getNonceLock = () =>
Promise.resolve({ nextNonce: 0, releaseLock: noop });
});
describe('#getState', function () {
it('should return a state object with the right keys and data types', function () {
const exposedState = txController.getState();
assert.ok(
'unapprovedTxs' in exposedState,
'state should have the key unapprovedTxs',
);
assert.ok(
'currentNetworkTxList' in exposedState,
'state should have the key currentNetworkTxList',
);
assert.ok(
typeof exposedState?.unapprovedTxs === 'object',
'should be an object',
);
assert.ok(
Array.isArray(exposedState.currentNetworkTxList),
'should be an array',
);
});
});
describe('#getUnapprovedTxCount', function () {
it('should return the number of unapproved txs', function () {
txController.txStateManager._addTransactionsToState([
{
id: 1,
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
{
id: 2,
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
{
id: 3,
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
]);
const unapprovedTxCount = txController.getUnapprovedTxCount();
assert.equal(unapprovedTxCount, 3, 'should be 3');
});
});
describe('#getPendingTxCount', function () {
it('should return the number of pending txs', function () {
txController.txStateManager._addTransactionsToState([
{
id: 1,
status: TransactionStatus.submitted,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
{
id: 2,
status: TransactionStatus.submitted,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
{
id: 3,
status: TransactionStatus.submitted,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
]);
const pendingTxCount = txController.getPendingTxCount();
assert.equal(pendingTxCount, 3, 'should be 3');
});
});
describe('#getConfirmedTransactions', function () {
it('should return the number of confirmed txs', function () {
const address = '0xc684832530fcbddae4b4230a47e991ddcec2831d';
const txParams = {
from: address,
to: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
};
txController.txStateManager._addTransactionsToState([
{
id: 0,
status: TransactionStatus.confirmed,
metamaskNetworkId: currentNetworkId,
txParams,
history: [{}],
},
{
id: 1,
status: TransactionStatus.confirmed,
metamaskNetworkId: currentNetworkId,
txParams,
history: [{}],
},
{
id: 2,
status: TransactionStatus.confirmed,
metamaskNetworkId: currentNetworkId,
txParams,
history: [{}],
},
{
id: 3,
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
txParams,
history: [{}],
},
{
id: 4,
status: TransactionStatus.rejected,
metamaskNetworkId: currentNetworkId,
txParams,
history: [{}],
},
{
id: 5,
status: TransactionStatus.approved,
metamaskNetworkId: currentNetworkId,
txParams,
history: [{}],
},
{
id: 6,
status: TransactionStatus.signed,
metamaskNetworkId: currentNetworkId,
txParams,
history: [{}],
},
{
id: 7,
status: TransactionStatus.submitted,
metamaskNetworkId: currentNetworkId,
txParams,
history: [{}],
},
{
id: 8,
status: TransactionStatus.failed,
metamaskNetworkId: currentNetworkId,
txParams,
history: [{}],
},
]);
assert.equal(
txController.nonceTracker.getConfirmedTransactions(address).length,
3,
);
});
});
describe('#newUnapprovedTransaction', function () {
let stub, txMeta, txParams;
beforeEach(function () {
txParams = {
from: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
to: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
};
txMeta = {
status: TransactionStatus.unapproved,
id: 1,
metamaskNetworkId: currentNetworkId,
txParams,
history: [{}],
};
txController.txStateManager._addTransactionsToState([txMeta]);
stub = sinon
.stub(txController, 'addUnapprovedTransaction')
.callsFake(() => {
txController.emit('newUnapprovedTx', txMeta);
return Promise.resolve(
txController.txStateManager.addTransaction(txMeta),
);
});
});
afterEach(function () {
txController.txStateManager._addTransactionsToState([]);
stub.restore();
});
it('should resolve when finished and status is submitted and resolve with the hash', async function () {
txController.once('newUnapprovedTx', (txMetaFromEmit) => {
setTimeout(() => {
txController.setTxHash(txMetaFromEmit.id, '0x0');
txController.txStateManager.setTxStatusSubmitted(txMetaFromEmit.id);
});
});
const hash = await txController.newUnapprovedTransaction(txParams);
assert.ok(hash, 'newUnapprovedTransaction needs to return the hash');
});
it('should reject when finished and status is rejected', async function () {
txController.once('newUnapprovedTx', (txMetaFromEmit) => {
setTimeout(() => {
txController.txStateManager.setTxStatusRejected(txMetaFromEmit.id);
});
});
await assert.rejects(
() => txController.newUnapprovedTransaction(txParams),
{
message: 'MetaMask Tx Signature: User denied transaction signature.',
},
);
});
});
describe('#addUnapprovedTransaction', function () {
const selectedAddress = '0x1678a085c290ebd122dc42cba69373b5953b831d';
const recipientAddress = '0xc42edfcc21ed14dda456aa0756c153f7985d8813';
let getSelectedAddress, getPermittedAccounts, getDefaultGasFees;
beforeEach(function () {
getSelectedAddress = sinon
.stub(txController, 'getSelectedAddress')
.returns(selectedAddress);
getDefaultGasFees = sinon
.stub(txController, '_getDefaultGasFees')
.returns({});
getPermittedAccounts = sinon
.stub(txController, 'getPermittedAccounts')
.returns([selectedAddress]);
});
afterEach(function () {
getSelectedAddress.restore();
getPermittedAccounts.restore();
getDefaultGasFees.restore();
});
it('should add an unapproved transaction and return a valid txMeta', async function () {
const txMeta = await txController.addUnapprovedTransaction(undefined, {
from: selectedAddress,
to: recipientAddress,
});
assert.ok('id' in txMeta, 'should have a id');
assert.ok('time' in txMeta, 'should have a time stamp');
assert.ok(
'metamaskNetworkId' in txMeta,
'should have a metamaskNetworkId',
);
assert.ok('txParams' in txMeta, 'should have a txParams');
assert.ok('history' in txMeta, 'should have a history');
assert.equal(
txMeta.txParams.value,
'0x0',
'should have added 0x0 as the value',
);
const memTxMeta = txController.txStateManager.getTransaction(txMeta.id);
assert.deepEqual(txMeta, memTxMeta);
});
it('should add only 1 unapproved transaction when called twice with same actionId', async function () {
await txController.addUnapprovedTransaction(
undefined,
{
from: selectedAddress,
to: recipientAddress,
},
undefined,
undefined,
undefined,
'12345',
);
const transactionCount1 =
txController.txStateManager.getTransactions().length;
await txController.addUnapprovedTransaction(
undefined,
{
from: selectedAddress,
to: recipientAddress,
},
undefined,
undefined,
undefined,
'12345',
);
const transactionCount2 =
txController.txStateManager.getTransactions().length;
assert.equal(transactionCount1, transactionCount2);
});
it('should add multiple transactions when called with different actionId', async function () {
await txController.addUnapprovedTransaction(
undefined,
{
from: selectedAddress,
to: recipientAddress,
},
undefined,
undefined,
undefined,
'12345',
);
const transactionCount1 =
txController.txStateManager.getTransactions().length;
await txController.addUnapprovedTransaction(
undefined,
{
from: selectedAddress,
to: recipientAddress,
},
undefined,
undefined,
undefined,
'00000',
);
const transactionCount2 =
txController.txStateManager.getTransactions().length;
assert.equal(transactionCount1 + 1, transactionCount2);
});
it('should emit newUnapprovedTx event and pass txMeta as the first argument', function (done) {
providerResultStub.eth_gasPrice = '4a817c800';
txController.once('newUnapprovedTx', (txMetaFromEmit) => {
assert.ok(txMetaFromEmit, 'txMeta is falsy');
done();
});
txController
.addUnapprovedTransaction(undefined, {
from: selectedAddress,
to: recipientAddress,
})
.catch(done);
});
it("should fail if the from address isn't the selected address", async function () {
await assert.rejects(() =>
txController.addUnapprovedTransaction({
from: '0x0d1d4e623D10F9FBA5Db95830F7d3839406C6AF2',
}),
);
});
it('should fail if the network status is not "available"', async function () {
networkStatusStore.putState(NetworkStatus.Unknown);
await assert.rejects(
() =>
txController.addUnapprovedTransaction(undefined, {
from: selectedAddress,
to: '0x0d1d4e623D10F9FBA5Db95830F7d3839406C6AF2',
}),
{ message: 'MetaMask is having trouble connecting to the network' },
);
});
});
describe('#createCancelTransaction', function () {
const selectedAddress = '0x1678a085c290ebd122dc42cba69373b5953b831d';
const recipientAddress = '0xc42edfcc21ed14dda456aa0756c153f7985d8813';
let getSelectedAddress,
getPermittedAccounts,
getDefaultGasFees,
getDefaultGasLimit;
beforeEach(function () {
const hash =
'0x2a5523c6fa98b47b7d9b6c8320179785150b42a16bcff36b398c5062b65657e8';
providerResultStub.eth_sendRawTransaction = hash;
getSelectedAddress = sinon
.stub(txController, 'getSelectedAddress')
.returns(selectedAddress);
getDefaultGasFees = sinon
.stub(txController, '_getDefaultGasFees')
.returns({});
getDefaultGasLimit = sinon
.stub(txController, '_getDefaultGasLimit')
.returns({});
getPermittedAccounts = sinon
.stub(txController, 'getPermittedAccounts')
.returns([selectedAddress]);
});
afterEach(function () {
getSelectedAddress.restore();
getPermittedAccounts.restore();
getDefaultGasFees.restore();
getDefaultGasLimit.restore();
});
it('should add an cancel transaction and return a valid txMeta', async function () {
const txMeta = await txController.addUnapprovedTransaction(undefined, {
from: selectedAddress,
to: recipientAddress,
});
await txController.approveTransaction(txMeta.id);
const cancelTxMeta = await txController.createCancelTransaction(
txMeta.id,
{},
{ actionId: 12345 },
);
assert.equal(cancelTxMeta.type, TransactionType.cancel);
const memTxMeta = txController.txStateManager.getTransaction(
cancelTxMeta.id,
);
assert.deepEqual(cancelTxMeta, memTxMeta);
});
it('should add only 1 cancel transaction when called twice with same actionId', async function () {
const txMeta = await txController.addUnapprovedTransaction(undefined, {
from: selectedAddress,
to: recipientAddress,
});
await txController.approveTransaction(txMeta.id);
await txController.createCancelTransaction(
txMeta.id,
{},
{ actionId: 12345 },
);
const transactionCount1 =
txController.txStateManager.getTransactions().length;
await txController.createCancelTransaction(
txMeta.id,
{},
{ actionId: 12345 },
);
const transactionCount2 =
txController.txStateManager.getTransactions().length;
assert.equal(transactionCount1, transactionCount2);
});
it('should add multiple transactions when called with different actionId', async function () {
const txMeta = await txController.addUnapprovedTransaction(undefined, {
from: selectedAddress,
to: recipientAddress,
});
await txController.approveTransaction(txMeta.id);
await txController.createCancelTransaction(
txMeta.id,
{},
{ actionId: 12345 },
);
const transactionCount1 =
txController.txStateManager.getTransactions().length;
await txController.createCancelTransaction(
txMeta.id,
{},
{ actionId: 11111 },
);
const transactionCount2 =
txController.txStateManager.getTransactions().length;
assert.equal(transactionCount1 + 1, transactionCount2);
});
});
describe('#addTxGasDefaults', function () {
it('should add the tx defaults if their are none', async function () {
txController.txStateManager._addTransactionsToState([
{
id: 1,
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
]);
const txMeta = {
id: 1,
txParams: {
from: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
to: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
},
history: [{}],
};
providerResultStub.eth_gasPrice = '4a817c800';
providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' };
providerResultStub.eth_estimateGas = '5209';
const txMetaWithDefaults = await txController.addTxGasDefaults(txMeta);
assert.ok(
txMetaWithDefaults.txParams.gasPrice,
'should have added the gas price',
);
assert.ok(
txMetaWithDefaults.txParams.gas,
'should have added the gas field',
);
});
it('should add EIP1559 tx defaults', async function () {
const TEST_MAX_FEE_PER_GAS = '0x12a05f200';
const TEST_MAX_PRIORITY_FEE_PER_GAS = '0x77359400';
const stub1 = sinon
.stub(txController, 'getEIP1559Compatibility')
.returns(true);
const stub2 = sinon
.stub(txController, '_getDefaultGasFees')
.callsFake(() => ({
maxFeePerGas: TEST_MAX_FEE_PER_GAS,
maxPriorityFeePerGas: TEST_MAX_PRIORITY_FEE_PER_GAS,
}));
txController.txStateManager._addTransactionsToState([
{
id: 1,
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
]);
const txMeta = {
id: 1,
txParams: {
from: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
to: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
},
history: [{}],
};
providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' };
providerResultStub.eth_estimateGas = '5209';
const txMetaWithDefaults = await txController.addTxGasDefaults(txMeta);
assert.equal(
txMetaWithDefaults.txParams.maxFeePerGas,
TEST_MAX_FEE_PER_GAS,
'should have added the correct max fee per gas',
);
assert.equal(
txMetaWithDefaults.txParams.maxPriorityFeePerGas,
TEST_MAX_PRIORITY_FEE_PER_GAS,
'should have added the correct max priority fee per gas',
);
stub1.restore();
stub2.restore();
});
it('should add gasPrice as maxFeePerGas and maxPriorityFeePerGas if there are no sources of other fee data available', async function () {
const TEST_GASPRICE = '0x12a05f200';
const stub1 = sinon
.stub(txController, 'getEIP1559Compatibility')
.returns(true);
const stub2 = sinon
.stub(txController, '_getDefaultGasFees')
.callsFake(() => ({ gasPrice: TEST_GASPRICE }));
txController.txStateManager._addTransactionsToState([
{
id: 1,
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
]);
const txMeta = {
id: 1,
txParams: {
from: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
to: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
},
history: [{}],
};
providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' };
providerResultStub.eth_estimateGas = '5209';
const txMetaWithDefaults = await txController.addTxGasDefaults(txMeta);
assert.equal(
txMetaWithDefaults.txParams.maxFeePerGas,
TEST_GASPRICE,
'should have added the correct max fee per gas',
);
assert.equal(
txMetaWithDefaults.txParams.maxPriorityFeePerGas,
TEST_GASPRICE,
'should have added the correct max priority fee per gas',
);
stub1.restore();
stub2.restore();
});
it('should not add maxFeePerGas and maxPriorityFeePerGas to type-0 transactions', async function () {
const TEST_GASPRICE = '0x12a05f200';
const stub1 = sinon
.stub(txController, 'getEIP1559Compatibility')
.returns(true);
const stub2 = sinon
.stub(txController, '_getDefaultGasFees')
.callsFake(() => ({ gasPrice: TEST_GASPRICE }));
txController.txStateManager._addTransactionsToState([
{
id: 1,
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
type: TransactionEnvelopeType.legacy,
},
history: [{}],
},
]);
const txMeta = {
id: 1,
txParams: {
from: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
to: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
type: TransactionEnvelopeType.legacy,
},
history: [{}],
};
providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' };
providerResultStub.eth_estimateGas = '5209';
const txMetaWithDefaults = await txController.addTxGasDefaults(txMeta);
assert.equal(
txMetaWithDefaults.txParams.maxFeePerGas,
undefined,
'should not have maxFeePerGas',
);
assert.equal(
txMetaWithDefaults.txParams.maxPriorityFeePerGas,
undefined,
'should not have max priority fee per gas',
);
stub1.restore();
stub2.restore();
});
it('should not add gasPrice if the fee data is available from the dapp', async function () {
const TEST_GASPRICE = '0x12a05f200';
const TEST_MAX_FEE_PER_GAS = '0x12a05f200';
const TEST_MAX_PRIORITY_FEE_PER_GAS = '0x77359400';
const stub1 = sinon
.stub(txController, 'getEIP1559Compatibility')
.returns(true);
const stub2 = sinon
.stub(txController, '_getDefaultGasFees')
.callsFake(() => ({ gasPrice: TEST_GASPRICE }));
txController.txStateManager._addTransactionsToState([
{
id: 1,
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
maxFeePerGas: TEST_MAX_FEE_PER_GAS,
maxPriorityFeePerGas: TEST_MAX_PRIORITY_FEE_PER_GAS,
},
history: [{}],
},
]);
const txMeta = {
id: 1,
txParams: {
from: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
to: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
},
history: [{}],
};
providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' };
providerResultStub.eth_estimateGas = '5209';
const txMetaWithDefaults = await txController.addTxGasDefaults(txMeta);
assert.equal(
txMetaWithDefaults.txParams.maxFeePerGas,
TEST_MAX_FEE_PER_GAS,
'should have added the correct max fee per gas',
);
assert.equal(
txMetaWithDefaults.txParams.maxPriorityFeePerGas,
TEST_MAX_PRIORITY_FEE_PER_GAS,
'should have added the correct max priority fee per gas',
);
stub1.restore();
stub2.restore();
});
});
describe('_getDefaultGasFees', function () {
let getGasFeeStub;
beforeEach(function () {
getGasFeeStub = sinon.stub(txController, '_getEIP1559GasFeeEstimates');
});
afterEach(function () {
getGasFeeStub.restore();
});
it('should return the correct fee data when the gas estimate type is FEE_MARKET', async function () {
const EXPECTED_MAX_FEE_PER_GAS = '12a05f200';
const EXPECTED_MAX_PRIORITY_FEE_PER_GAS = '77359400';
getGasFeeStub.callsFake(() => ({
gasFeeEstimates: {
medium: {
suggestedMaxPriorityFeePerGas: '2',
suggestedMaxFeePerGas: '5',
},
},
gasEstimateType: GasEstimateTypes.feeMarket,
}));
const defaultGasFees = await txController._getDefaultGasFees(
{ txParams: {} },
true,
);
assert.deepEqual(defaultGasFees, {
maxPriorityFeePerGas: EXPECTED_MAX_PRIORITY_FEE_PER_GAS,
maxFeePerGas: EXPECTED_MAX_FEE_PER_GAS,
});
});
it('should return the correct fee data when the gas estimate type is LEGACY', async function () {
const EXPECTED_GAS_PRICE = '77359400';
getGasFeeStub.callsFake(() => ({
gasFeeEstimates: { medium: '2' },
gasEstimateType: GasEstimateTypes.legacy,
}));
const defaultGasFees = await txController._getDefaultGasFees(
{ txParams: {} },
false,
);
assert.deepEqual(defaultGasFees, {
gasPrice: EXPECTED_GAS_PRICE,
});
});
it('should return the correct fee data when the gas estimate type is ETH_GASPRICE', async function () {
const EXPECTED_GAS_PRICE = '77359400';
getGasFeeStub.callsFake(() => ({
gasFeeEstimates: { gasPrice: '2' },
gasEstimateType: GasEstimateTypes.ethGasPrice,
}));
const defaultGasFees = await txController._getDefaultGasFees(
{ txParams: {} },
false,
);
assert.deepEqual(defaultGasFees, {
gasPrice: EXPECTED_GAS_PRICE,
});
});
});
describe('#addTransaction', function () {
let trackTransactionMetricsEventSpy;
beforeEach(function () {
trackTransactionMetricsEventSpy = sinon.spy(
txController,
'_trackTransactionMetricsEvent',
);
});
afterEach(function () {
trackTransactionMetricsEventSpy.restore();
});
it('should emit updates', function (done) {
const txMeta = {
id: '1',
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
};
const eventNames = [
METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE,
'1:unapproved',
];
const listeners = [];
eventNames.forEach((eventName) => {
listeners.push(
new Promise((resolve) => {
txController.once(eventName, (arg) => {
resolve(arg);
});
}),
);
});
Promise.all(listeners)
.then((returnValues) => {
assert.deepEqual(
returnValues.pop(),
txMeta,
'last event 1:unapproved should return txMeta',
);
done();
})
.catch(done);
txController.addTransaction(txMeta);
});
it('should call _trackTransactionMetricsEvent with the correct params', function () {
const txMeta = {
id: 1,
status: TransactionStatus.unapproved,
txParams: {
from: fromAccount.address,
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
gasPrice: '0x77359400',
gas: '0x7b0d',
nonce: '0x4b',
},
type: TransactionType.simpleSend,
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
origin: ORIGIN_METAMASK,
chainId: currentChainId,
time: 1624408066355,
metamaskNetworkId: currentNetworkId,
};
txController.addTransaction(txMeta);
assert.equal(trackTransactionMetricsEventSpy.callCount, 1);
assert.deepEqual(
trackTransactionMetricsEventSpy.getCall(0).args[0],
txMeta,
);
assert.equal(
trackTransactionMetricsEventSpy.getCall(0).args[1],
TransactionMetaMetricsEvent.added,
);
});
});
describe('#approveTransaction', function () {
it('does not overwrite set values', async function () {
const originalValue = '0x01';
const txMeta = {
id: '1',
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: originalValue,
gas: originalValue,
gasPrice: originalValue,
},
};
// eslint-disable-next-line @babel/no-invalid-this
this.timeout(SECOND * 15);
const wrongValue = '0x05';
txController.addTransaction(txMeta);
providerResultStub.eth_gasPrice = wrongValue;
providerResultStub.eth_estimateGas = '0x5209';
const signStub = sinon
.stub(txController, 'signTransaction')
.callsFake(() => Promise.resolve());
const pubStub = sinon
.stub(txController, 'publishTransaction')
.callsFake(() => {
txController.setTxHash('1', originalValue);
txController.txStateManager.setTxStatusSubmitted('1');
});
await txController.approveTransaction(txMeta.id);
const result = txController.txStateManager.getTransaction(txMeta.id);
const params = result.txParams;
assert.equal(params.gas, originalValue, 'gas unmodified');
assert.equal(params.gasPrice, originalValue, 'gas price unmodified');
assert.equal(result.hash, originalValue);
assert.equal(
result.status,
TransactionStatus.submitted,
'should have reached the submitted status.',
);
signStub.restore();
pubStub.restore();
});
});
describe('#sign replay-protected tx', function () {
it('prepares a tx with the chainId set', async function () {
txController.addTransaction(
{
id: '1',
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
},
noop,
);
const rawTx = await txController.signTransaction('1');
const ethTx = TransactionFactory.fromSerializedData(toBuffer(rawTx));
assert.equal(ethTx.common.chainIdBN().toNumber(), 5);
});
});
describe('#updateAndApproveTransaction', function () {
it('should update and approve transactions', async function () {
const txMeta = {
id: 1,
status: TransactionStatus.unapproved,
txParams: {
from: fromAccount.address,
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
gasPrice: '0x77359400',
gas: '0x7b0d',
nonce: '0x4b',
},
metamaskNetworkId: currentNetworkId,
};
txController.txStateManager.addTransaction(txMeta);
const approvalPromise = txController.updateAndApproveTransaction(txMeta);
const tx = txController.txStateManager.getTransaction(1);
assert.equal(tx.status, TransactionStatus.approved);
await approvalPromise;
});
});
describe('#getChainId', function () {
it('returns the chain ID of the network when it is available', function () {
networkStatusStore.putState(NetworkStatus.Available);
assert.equal(txController.getChainId(), 5);
});
it('returns 0 when the network is not available', function () {
networkStatusStore.putState('asdflsfadf');
assert.equal(txController.getChainId(), 0);
});
it('returns 0 when the chain ID cannot be parsed as a hex string', function () {
networkStatusStore.putState(NetworkStatus.Available);
getCurrentChainId.returns('$fdsjfldf');
assert.equal(txController.getChainId(), 0);
});
});
describe('#cancelTransaction', function () {
it('should emit a status change to rejected', function (done) {
txController.txStateManager._addTransactionsToState([
{
id: 0,
status: TransactionStatus.unapproved,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId,
history: [{}],
},
{
id: 1,
status: TransactionStatus.rejected,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId,
history: [{}],
},
{
id: 2,
status: TransactionStatus.approved,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId,
history: [{}],
},
{
id: 3,
status: TransactionStatus.signed,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId,
history: [{}],
},
{
id: 4,
status: TransactionStatus.submitted,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId,
history: [{}],
},
{
id: 5,
status: TransactionStatus.confirmed,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId,
history: [{}],
},
{
id: 6,
status: TransactionStatus.failed,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId,
history: [{}],
},
]);
txController.once('tx:status-update', (txId, status) => {
try {
assert.equal(
status,
TransactionStatus.rejected,
'status should be rejected',
);
assert.equal(txId, 0, 'id should e 0');
done();
} catch (e) {
done(e);
}
});
txController.cancelTransaction(0);
});
});
describe('#createSpeedUpTransaction', function () {
let addTransactionSpy;
let approveTransactionSpy;
let txParams;
let expectedTxParams;
const selectedAddress = '0x1678a085c290ebd122dc42cba69373b5953b831d';
const recipientAddress = '0xc42edfcc21ed14dda456aa0756c153f7985d8813';
let getSelectedAddress,
getPermittedAccounts,
getDefaultGasFees,
getDefaultGasLimit;
beforeEach(function () {
addTransactionSpy = sinon.spy(txController, 'addTransaction');
approveTransactionSpy = sinon.spy(txController, 'approveTransaction');
const hash =
'0x2a5523c6fa98b47b7d9b6c8320179785150b42a16bcff36b398c5062b65657e8';
providerResultStub.eth_sendRawTransaction = hash;
getSelectedAddress = sinon
.stub(txController, 'getSelectedAddress')
.returns(selectedAddress);
getDefaultGasFees = sinon
.stub(txController, '_getDefaultGasFees')
.returns({});
getDefaultGasLimit = sinon
.stub(txController, '_getDefaultGasLimit')
.returns({});
getPermittedAccounts = sinon
.stub(txController, 'getPermittedAccounts')
.returns([selectedAddress]);
txParams = {
nonce: '0x00',
from: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4',
to: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4',
gas: '0x5209',
gasPrice: '0xa',
estimateSuggested: GasRecommendations.medium,
estimateUsed: GasRecommendations.high,
};
txController.txStateManager._addTransactionsToState([
{
id: 1,
status: TransactionStatus.submitted,
metamaskNetworkId: currentNetworkId,
txParams,
history: [{}],
},
]);
expectedTxParams = { ...txParams, gasPrice: '0xb' };
});
afterEach(function () {
addTransactionSpy.restore();
approveTransactionSpy.restore();
getSelectedAddress.restore();
getPermittedAccounts.restore();
getDefaultGasFees.restore();
getDefaultGasLimit.restore();
});
it('should call this.addTransaction and this.approveTransaction with the expected args', async function () {
await txController.createSpeedUpTransaction(1);
assert.equal(addTransactionSpy.callCount, 1);
const addTransactionArgs = addTransactionSpy.getCall(0).args[0];
assert.deepEqual(addTransactionArgs.txParams, expectedTxParams);
const { previousGasParams, type } = addTransactionArgs;
assert.deepEqual(
{ gasPrice: previousGasParams.gasPrice, type },
{
gasPrice: '0xa',
type: TransactionType.retry,
},
);
});
it('should call this.approveTransaction with the id of the returned tx', async function () {
const result = await txController.createSpeedUpTransaction(1);
assert.equal(approveTransactionSpy.callCount, 1);
const approveTransactionArg = approveTransactionSpy.getCall(0).args[0];
assert.equal(result.id, approveTransactionArg);
});
it('should return the expected txMeta', async function () {
const result = await txController.createSpeedUpTransaction(1);
assert.deepEqual(result.txParams, expectedTxParams);
const { previousGasParams, type } = result;
assert.deepEqual(
{ gasPrice: previousGasParams.gasPrice, type },
{
gasPrice: '0xa',
type: TransactionType.retry,
},
);
});
it('should add only 1 speedup transaction when called twice with same actionId', async function () {
const txMeta = await txController.addUnapprovedTransaction(undefined, {
from: selectedAddress,
to: recipientAddress,
});
await txController.approveTransaction(txMeta.id);
await txController.createSpeedUpTransaction(
txMeta.id,
{},
{ actionId: 12345 },
);
const transactionCount1 =
txController.txStateManager.getTransactions().length;
await txController.createSpeedUpTransaction(
txMeta.id,
{},
{ actionId: 12345 },
);
const transactionCount2 =
txController.txStateManager.getTransactions().length;
assert.equal(transactionCount1, transactionCount2);
});
it('should add multiple transactions when called with different actionId', async function () {
const txMeta = await txController.addUnapprovedTransaction(undefined, {
from: selectedAddress,
to: recipientAddress,
});
await txController.approveTransaction(txMeta.id);
await txController.createSpeedUpTransaction(
txMeta.id,
{},
{ actionId: 12345 },
);
const transactionCount1 =
txController.txStateManager.getTransactions().length;
await txController.createSpeedUpTransaction(
txMeta.id,
{},
{ actionId: 11111 },
);
const transactionCount2 =
txController.txStateManager.getTransactions().length;
assert.equal(transactionCount1 + 1, transactionCount2);
});
it('should add multiple transactions when called with different actionId and txMethodType defined', async function () {
const txMeta = await txController.addUnapprovedTransaction(
'eth_sendTransaction',
{
from: selectedAddress,
to: recipientAddress,
},
);
await txController.approveTransaction(txMeta.id);
await txController.createSpeedUpTransaction(
txMeta.id,
{},
{ actionId: 12345 },
);
const transactionCount1 =
txController.txStateManager.getTransactions().length;
await txController.createSpeedUpTransaction(
txMeta.id,
{},
{ actionId: 11111 },
);
const transactionCount2 =
txController.txStateManager.getTransactions().length;
assert.equal(transactionCount1 + 1, transactionCount2);
});
it('should call securityProviderRequest and have flagAsDangerous inside txMeta', async function () {
const txMeta = await txController.addUnapprovedTransaction(
'eth_sendTransaction',
{
from: selectedAddress,
to: recipientAddress,
},
);
assert.ok(
'securityProviderResponse' in txMeta,
'should have a securityProviderResponse',
);
});
});
describe('#signTransaction', function () {
let fromTxDataSpy;
beforeEach(function () {
fromTxDataSpy = sinon.spy(TransactionFactory, 'fromTxData');
});
afterEach(function () {
fromTxDataSpy.restore();
});
it('sets txParams.type to 0x0 (non-EIP-1559)', async function () {
txController.txStateManager._addTransactionsToState([
{
status: TransactionStatus.unapproved,
id: 1,
metamaskNetworkId: currentNetworkId,
history: [{}],
txParams: {
from: VALID_ADDRESS_TWO,
to: VALID_ADDRESS,
gasPrice: '0x77359400',
gas: '0x7b0d',
nonce: '0x4b',
},
},
]);
await txController.signTransaction('1');
assert.equal(fromTxDataSpy.getCall(0).args[0].type, '0x0');
});
it('sets txParams.type to 0x2 (EIP-1559)', async function () {
const eip1559CompatibilityStub = sinon
.stub(txController, 'getEIP1559Compatibility')
.returns(true);
txController.txStateManager._addTransactionsToState([
{
status: TransactionStatus.unapproved,
id: 2,
metamaskNetworkId: currentNetworkId,
history: [{}],
txParams: {
from: VALID_ADDRESS_TWO,
to: VALID_ADDRESS,
maxFeePerGas: '0x77359400',
maxPriorityFeePerGas: '0x77359400',
gas: '0x7b0d',
nonce: '0x4b',
},
},
]);
await txController.signTransaction('2');
assert.equal(fromTxDataSpy.getCall(0).args[0].type, '0x2');
eip1559CompatibilityStub.restore();
});
});
describe('#publishTransaction', function () {
let hash, txMeta, trackTransactionMetricsEventSpy;
beforeEach(function () {
hash =
'0x2a5523c6fa98b47b7d9b6c8320179785150b42a16bcff36b398c5062b65657e8';
txMeta = {
id: 1,
status: TransactionStatus.unapproved,
txParams: {
gas: '0x7b0d',
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId,
};
providerResultStub.eth_sendRawTransaction = hash;
trackTransactionMetricsEventSpy = sinon.spy(
txController,
'_trackTransactionMetricsEvent',
);
});
afterEach(function () {
trackTransactionMetricsEventSpy.restore();
});
it('should publish a tx, updates the rawTx when provided a one', async function () {
const rawTx =
'0x477b2e6553c917af0db0388ae3da62965ff1a184558f61b749d1266b2e6d024c';
txController.txStateManager.addTransaction(txMeta);
await txController.publishTransaction(txMeta.id, rawTx);
const publishedTx = txController.txStateManager.getTransaction(1);
assert.equal(publishedTx.hash, hash);
assert.equal(publishedTx.status, TransactionStatus.submitted);
});
it('should ignore the error "Transaction Failed: known transaction" and be as usual', async function () {
providerResultStub.eth_sendRawTransaction = async (_, __, ___, end) => {
end('Transaction Failed: known transaction');
};
const rawTx =
'0xf86204831e848082520894f231d46dd78806e1dd93442cf33c7671f853874880802ca05f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57a00259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a';
txController.txStateManager.addTransaction(txMeta);
await txController.publishTransaction(txMeta.id, rawTx);
const publishedTx = txController.txStateManager.getTransaction(1);
assert.equal(
publishedTx.hash,
'0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09',
);
assert.equal(publishedTx.status, TransactionStatus.submitted);
});
it('should call _trackTransactionMetricsEvent with the correct params', async function () {
const rawTx =
'0x477b2e6553c917af0db0388ae3da62965ff1a184558f61b749d1266b2e6d024c';
txController.txStateManager.addTransaction(txMeta);
await txController.publishTransaction(txMeta.id, rawTx);
assert.equal(trackTransactionMetricsEventSpy.callCount, 1);
assert.deepEqual(
trackTransactionMetricsEventSpy.getCall(0).args[0],
txMeta,
);
assert.equal(
trackTransactionMetricsEventSpy.getCall(0).args[1],
TransactionMetaMetricsEvent.submitted,
);
});
});
describe('#_markNonceDuplicatesDropped', function () {
it('should mark all nonce duplicates as dropped without marking the confirmed transaction as dropped', function () {
txController.txStateManager._addTransactionsToState([
{
id: 1,
status: TransactionStatus.confirmed,
metamaskNetworkId: currentNetworkId,
history: [{}],
txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: '0x01',
},
},
{
id: 2,
status: TransactionStatus.submitted,
metamaskNetworkId: currentNetworkId,
history: [{}],
txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: '0x01',
},
},
{
id: 3,
status: TransactionStatus.submitted,
metamaskNetworkId: currentNetworkId,
history: [{}],
txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: '0x01',
},
},
{
id: 4,
status: TransactionStatus.submitted,
metamaskNetworkId: currentNetworkId,
history: [{}],
txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: '0x01',
},
},
{
id: 5,
status: TransactionStatus.submitted,
metamaskNetworkId: currentNetworkId,
history: [{}],
txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: '0x01',
},
},
{
id: 6,
status: TransactionStatus.submitted,
metamaskNetworkId: currentNetworkId,
history: [{}],
txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: '0x01',
},
},
{
id: 7,
status: TransactionStatus.submitted,
metamaskNetworkId: currentNetworkId,
history: [{}],
txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: '0x01',
},
},
]);
txController._markNonceDuplicatesDropped(1);
const confirmedTx = txController.txStateManager.getTransaction(1);
const droppedTxs = txController.txStateManager.getTransactions({
searchCriteria: {
nonce: '0x01',
status: TransactionStatus.dropped,
},
});
assert.equal(
confirmedTx.status,
TransactionStatus.confirmed,
'the confirmedTx should remain confirmed',
);
assert.equal(droppedTxs.length, 6, 'their should be 6 dropped txs');
});
});
describe('#getPendingTransactions', function () {
it('should show only submitted and approved transactions as pending transaction', function () {
txController.txStateManager._addTransactionsToState([
{
id: 1,
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
},
{
id: 2,
status: TransactionStatus.rejected,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
{
id: 3,
status: TransactionStatus.approved,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
{
id: 4,
status: TransactionStatus.signed,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
{
id: 5,
status: TransactionStatus.submitted,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
{
id: 6,
status: TransactionStatus.confirmed,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
{
id: 7,
status: TransactionStatus.failed,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
]);
assert.equal(
txController.pendingTxTracker.getPendingTransactions().length,
2,
);
const states = txController.pendingTxTracker
.getPendingTransactions()
.map((tx) => tx.status);
assert.ok(
states.includes(TransactionStatus.approved),
'includes approved',
);
assert.ok(
states.includes(TransactionStatus.submitted),
'includes submitted',
);
});
});
describe('#_trackTransactionMetricsEvent', function () {
let trackMetaMetricsEventSpy;
let createEventFragmentSpy;
let finalizeEventFragmentSpy;
beforeEach(function () {
trackMetaMetricsEventSpy = sinon.spy(
txController,
'_trackMetaMetricsEvent',
);
createEventFragmentSpy = sinon.spy(txController, 'createEventFragment');
finalizeEventFragmentSpy = sinon.spy(
txController,
'finalizeEventFragment',
);
sinon
.stub(txController, '_getEIP1559GasFeeEstimates')
.resolves(mockEstimates['fee-market']);
});
afterEach(function () {
trackMetaMetricsEventSpy.restore();
createEventFragmentSpy.restore();
finalizeEventFragmentSpy.restore();
});
describe('On transaction created by the user', function () {
let txMeta;
before(function () {
txMeta = {
id: 1,
status: TransactionStatus.unapproved,
txParams: {
from: fromAccount.address,
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
gasPrice: '0x77359400',
gas: '0x7b0d',
nonce: '0x4b',
},
type: TransactionType.simpleSend,
origin: ORIGIN_METAMASK,
chainId: currentChainId,
time: 1624408066355,
metamaskNetworkId: currentNetworkId,
defaultGasEstimates: {
gas: '0x7b0d',
gasPrice: '0x77359400',
},
securityProviderResponse: {
flagAsDangerous: 0,
},
};
});
it('should create an event fragment when transaction added', async function () {
const expectedPayload = {
actionId,
initialEvent: 'Transaction Added',
successEvent: 'Transaction Approved',
failureEvent: 'Transaction Rejected',
uniqueIdentifier: 'transaction-added-1',
category: EVENT.CATEGORIES.TRANSACTIONS,
persist: true,
properties: {
chain_id: '0x5',
eip_1559_version: '0',
gas_edit_attempted: 'none',
gas_edit_type: 'none',
network: '5',
referrer: ORIGIN_METAMASK,
source: EVENT.SOURCE.TRANSACTION.USER,
transaction_type: TransactionType.simpleSend,
account_type: 'MetaMask',
asset_type: AssetType.native,
token_standard: TokenStandard.none,
device_model: 'N/A',
transaction_speed_up: false,
ui_customizations: null,
},
sensitiveProperties: {
default_gas: '0.000031501',
default_gas_price: '2',
gas_price: '2',
gas_limit: '0x7b0d',
transaction_contract_method: undefined,
transaction_replaced: undefined,
first_seen: 1624408066355,
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
status: 'unapproved',
},
};
await txController._trackTransactionMetricsEvent(
txMeta,
TransactionMetaMetricsEvent.added,
actionId,
);
assert.equal(createEventFragmentSpy.callCount, 1);
assert.equal(finalizeEventFragmentSpy.callCount, 0);
assert.deepEqual(
createEventFragmentSpy.getCall(0).args[0],
expectedPayload,
);
});
it('Should finalize the transaction added fragment as abandoned if user rejects transaction', async function () {
fragmentExists = true;
await txController._trackTransactionMetricsEvent(
txMeta,
TransactionMetaMetricsEvent.rejected,
actionId,
);
assert.equal(createEventFragmentSpy.callCount, 0);
assert.equal(finalizeEventFragmentSpy.callCount, 1);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[0],
'transaction-added-1',
);
assert.deepEqual(finalizeEventFragmentSpy.getCall(0).args[1], {
abandoned: true,
});
});
it('Should finalize the transaction added fragment if user approves transaction', async function () {
fragmentExists = true;
await txController._trackTransactionMetricsEvent(
txMeta,
TransactionMetaMetricsEvent.approved,
actionId,
);
assert.equal(createEventFragmentSpy.callCount, 0);
assert.equal(finalizeEventFragmentSpy.callCount, 1);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[0],
'transaction-added-1',
);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[1],
undefined,
);
});
it('should create an event fragment when transaction is submitted', async function () {
const expectedPayload = {
actionId,
initialEvent: 'Transaction Submitted',
successEvent: 'Transaction Finalized',
uniqueIdentifier: 'transaction-submitted-1',
category: EVENT.CATEGORIES.TRANSACTIONS,
persist: true,
properties: {
chain_id: '0x5',
eip_1559_version: '0',
gas_edit_attempted: 'none',
gas_edit_type: 'none',
network: '5',
referrer: ORIGIN_METAMASK,
source: EVENT.SOURCE.TRANSACTION.USER,
transaction_type: TransactionType.simpleSend,
account_type: 'MetaMask',
asset_type: AssetType.native,
token_standard: TokenStandard.none,
device_model: 'N/A',
transaction_speed_up: false,
ui_customizations: null,
},
sensitiveProperties: {
default_gas: '0.000031501',
default_gas_price: '2',
gas_price: '2',
gas_limit: '0x7b0d',
transaction_contract_method: undefined,
transaction_replaced: undefined,
first_seen: 1624408066355,
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
status: 'unapproved',
},
};
await txController._trackTransactionMetricsEvent(
txMeta,
TransactionMetaMetricsEvent.submitted,
actionId,
);
assert.equal(createEventFragmentSpy.callCount, 1);
assert.equal(finalizeEventFragmentSpy.callCount, 0);
assert.deepEqual(
createEventFragmentSpy.getCall(0).args[0],
expectedPayload,
);
});
it('Should finalize the transaction submitted fragment when transaction finalizes', async function () {
fragmentExists = true;
await txController._trackTransactionMetricsEvent(
txMeta,
TransactionMetaMetricsEvent.finalized,
actionId,
);
assert.equal(createEventFragmentSpy.callCount, 0);
assert.equal(finalizeEventFragmentSpy.callCount, 1);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[0],
'transaction-submitted-1',
);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[1],
undefined,
);
});
});
describe('On transaction suggested by dapp', function () {
let txMeta;
before(function () {
txMeta = {
id: 1,
status: TransactionStatus.unapproved,
txParams: {
from: fromAccount.address,
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
gasPrice: '0x77359400',
gas: '0x7b0d',
nonce: '0x4b',
},
type: TransactionType.simpleSend,
origin: 'other',
chainId: currentChainId,
time: 1624408066355,
metamaskNetworkId: currentNetworkId,
defaultGasEstimates: {
gas: '0x7b0d',
gasPrice: '0x77359400',
},
securityProviderResponse: {
flagAsDangerous: 0,
},
};
});
it('should create an event fragment when transaction added', async function () {
const expectedPayload = {
actionId,
initialEvent: 'Transaction Added',
successEvent: 'Transaction Approved',
failureEvent: 'Transaction Rejected',
uniqueIdentifier: 'transaction-added-1',
category: EVENT.CATEGORIES.TRANSACTIONS,
persist: true,
properties: {
chain_id: '0x5',
eip_1559_version: '0',
gas_edit_attempted: 'none',
gas_edit_type: 'none',
network: '5',
referrer: 'other',
source: EVENT.SOURCE.TRANSACTION.DAPP,
transaction_type: TransactionType.simpleSend,
account_type: 'MetaMask',
asset_type: AssetType.native,
token_standard: TokenStandard.none,
device_model: 'N/A',
transaction_speed_up: false,
ui_customizations: null,
},
sensitiveProperties: {
default_gas: '0.000031501',
default_gas_price: '2',
gas_price: '2',
gas_limit: '0x7b0d',
transaction_contract_method: undefined,
transaction_replaced: undefined,
first_seen: 1624408066355,
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
status: 'unapproved',
},
};
await txController._trackTransactionMetricsEvent(
txMeta,
TransactionMetaMetricsEvent.added,
actionId,
);
assert.equal(createEventFragmentSpy.callCount, 1);
assert.equal(finalizeEventFragmentSpy.callCount, 0);
assert.deepEqual(
createEventFragmentSpy.getCall(0).args[0],
expectedPayload,
);
});
it('Should finalize the transaction added fragment as abandoned if user rejects transaction', async function () {
fragmentExists = true;
await txController._trackTransactionMetricsEvent(
txMeta,
TransactionMetaMetricsEvent.rejected,
actionId,
);
assert.equal(createEventFragmentSpy.callCount, 0);
assert.equal(finalizeEventFragmentSpy.callCount, 1);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[0],
'transaction-added-1',
);
assert.deepEqual(finalizeEventFragmentSpy.getCall(0).args[1], {
abandoned: true,
});
});
it('Should finalize the transaction added fragment if user approves transaction', async function () {
fragmentExists = true;
await txController._trackTransactionMetricsEvent(
txMeta,
TransactionMetaMetricsEvent.approved,
actionId,
);
assert.equal(createEventFragmentSpy.callCount, 0);
assert.equal(finalizeEventFragmentSpy.callCount, 1);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[0],
'transaction-added-1',
);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[1],
undefined,
);
});
it('should create an event fragment when transaction is submitted', async function () {
const expectedPayload = {
actionId,
initialEvent: 'Transaction Submitted',
successEvent: 'Transaction Finalized',
uniqueIdentifier: 'transaction-submitted-1',
category: EVENT.CATEGORIES.TRANSACTIONS,
persist: true,
properties: {
chain_id: '0x5',
eip_1559_version: '0',
gas_edit_attempted: 'none',
gas_edit_type: 'none',
network: '5',
referrer: 'other',
source: EVENT.SOURCE.TRANSACTION.DAPP,
transaction_type: TransactionType.simpleSend,
account_type: 'MetaMask',
asset_type: AssetType.native,
token_standard: TokenStandard.none,
device_model: 'N/A',
transaction_speed_up: false,
ui_customizations: null,
},
sensitiveProperties: {
default_gas: '0.000031501',
default_gas_price: '2',
gas_price: '2',
gas_limit: '0x7b0d',
transaction_contract_method: undefined,
transaction_replaced: undefined,
first_seen: 1624408066355,
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
status: 'unapproved',
},
};
await txController._trackTransactionMetricsEvent(
txMeta,
TransactionMetaMetricsEvent.submitted,
actionId,
);
assert.equal(createEventFragmentSpy.callCount, 1);
assert.equal(finalizeEventFragmentSpy.callCount, 0);
assert.deepEqual(
createEventFragmentSpy.getCall(0).args[0],
expectedPayload,
);
});
it('Should finalize the transaction submitted fragment when transaction finalizes', async function () {
fragmentExists = true;
await txController._trackTransactionMetricsEvent(
txMeta,
TransactionMetaMetricsEvent.finalized,
actionId,
);
assert.equal(createEventFragmentSpy.callCount, 0);
assert.equal(finalizeEventFragmentSpy.callCount, 1);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[0],
'transaction-submitted-1',
);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[1],
undefined,
);
});
});
it('should create missing fragments when events happen out of order or are missing', async function () {
const txMeta = {
id: 1,
status: TransactionStatus.unapproved,
txParams: {
from: fromAccount.address,
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
gasPrice: '0x77359400',
gas: '0x7b0d',
nonce: '0x4b',
},
type: TransactionType.simpleSend,
origin: 'other',
chainId: currentChainId,
time: 1624408066355,
metamaskNetworkId: currentNetworkId,
securityProviderResponse: {
flagAsDangerous: 0,
},
};
const expectedPayload = {
actionId,
successEvent: 'Transaction Approved',
failureEvent: 'Transaction Rejected',
uniqueIdentifier: 'transaction-added-1',
category: EVENT.CATEGORIES.TRANSACTIONS,
persist: true,
properties: {
chain_id: '0x5',
eip_1559_version: '0',
gas_edit_attempted: 'none',
gas_edit_type: 'none',
network: '5',
referrer: 'other',
source: EVENT.SOURCE.TRANSACTION.DAPP,
transaction_type: TransactionType.simpleSend,
account_type: 'MetaMask',
asset_type: AssetType.native,
token_standard: TokenStandard.none,
device_model: 'N/A',
transaction_speed_up: false,
ui_customizations: null,
},
sensitiveProperties: {
gas_price: '2',
gas_limit: '0x7b0d',
transaction_contract_method: undefined,
transaction_replaced: undefined,
first_seen: 1624408066355,
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
status: 'unapproved',
},
};
await txController._trackTransactionMetricsEvent(
txMeta,
TransactionMetaMetricsEvent.approved,
actionId,
);
assert.equal(createEventFragmentSpy.callCount, 1);
assert.deepEqual(
createEventFragmentSpy.getCall(0).args[0],
expectedPayload,
);
assert.equal(finalizeEventFragmentSpy.callCount, 1);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[0],
'transaction-added-1',
);
assert.deepEqual(finalizeEventFragmentSpy.getCall(0).args[1], undefined);
});
it('should call _trackMetaMetricsEvent with the correct payload (extra params)', async function () {
const txMeta = {
id: 1,
status: TransactionStatus.unapproved,
txParams: {
from: fromAccount.address,
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
gasPrice: '0x77359400',
gas: '0x7b0d',
nonce: '0x4b',
},
type: TransactionType.simpleSend,
origin: 'other',
chainId: currentChainId,
time: 1624408066355,
metamaskNetworkId: currentNetworkId,
securityProviderResponse: {
flagAsDangerous: 0,
},
};
const expectedPayload = {
actionId,
initialEvent: 'Transaction Added',
successEvent: 'Transaction Approved',
failureEvent: 'Transaction Rejected',
uniqueIdentifier: 'transaction-added-1',
persist: true,
category: EVENT.CATEGORIES.TRANSACTIONS,
properties: {
network: '5',
referrer: 'other',
source: EVENT.SOURCE.TRANSACTION.DAPP,
transaction_type: TransactionType.simpleSend,
chain_id: '0x5',
eip_1559_version: '0',
gas_edit_attempted: 'none',
gas_edit_type: 'none',
account_type: 'MetaMask',
asset_type: AssetType.native,
token_standard: TokenStandard.none,
device_model: 'N/A',
transaction_speed_up: false,
ui_customizations: null,
},
sensitiveProperties: {
baz: 3.0,
foo: 'bar',
gas_price: '2',
gas_limit: '0x7b0d',
transaction_contract_method: undefined,
transaction_replaced: undefined,
first_seen: 1624408066355,
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
status: 'unapproved',
},
};
await txController._trackTransactionMetricsEvent(
txMeta,
TransactionMetaMetricsEvent.added,
actionId,
{
baz: 3.0,
foo: 'bar',
},
);
assert.equal(createEventFragmentSpy.callCount, 1);
assert.equal(finalizeEventFragmentSpy.callCount, 0);
assert.deepEqual(
createEventFragmentSpy.getCall(0).args[0],
expectedPayload,
);
});
it('should call _trackMetaMetricsEvent with the correct payload (extra params) when flagAsDangerous is malicious', async function () {
const txMeta = {
id: 1,
status: TransactionStatus.unapproved,
txParams: {
from: fromAccount.address,
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
gasPrice: '0x77359400',
gas: '0x7b0d',
nonce: '0x4b',
},
type: TransactionType.simpleSend,
origin: 'other',
chainId: currentChainId,
time: 1624408066355,
metamaskNetworkId: currentNetworkId,
securityProviderResponse: {
flagAsDangerous: 1,
},
};
const expectedPayload = {
actionId,
initialEvent: 'Transaction Added',
successEvent: 'Transaction Approved',
failureEvent: 'Transaction Rejected',
uniqueIdentifier: 'transaction-added-1',
persist: true,
category: EVENT.CATEGORIES.TRANSACTIONS,
properties: {
network: '5',
referrer: 'other',
source: EVENT.SOURCE.TRANSACTION.DAPP,
transaction_type: TransactionType.simpleSend,
chain_id: '0x5',
eip_1559_version: '0',
gas_edit_attempted: 'none',
gas_edit_type: 'none',
account_type: 'MetaMask',
asset_type: AssetType.native,
token_standard: TokenStandard.none,
device_model: 'N/A',
transaction_speed_up: false,
ui_customizations: ['flagged_as_malicious'],
},
sensitiveProperties: {
baz: 3.0,
foo: 'bar',
gas_price: '2',
gas_limit: '0x7b0d',
transaction_contract_method: undefined,
transaction_replaced: undefined,
first_seen: 1624408066355,
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
status: 'unapproved',
},
};
await txController._trackTransactionMetricsEvent(
txMeta,
TransactionMetaMetricsEvent.added,
actionId,
{
baz: 3.0,
foo: 'bar',
},
);
assert.equal(createEventFragmentSpy.callCount, 1);
assert.equal(finalizeEventFragmentSpy.callCount, 0);
assert.deepEqual(
createEventFragmentSpy.getCall(0).args[0],
expectedPayload,
);
});
it('should call _trackMetaMetricsEvent with the correct payload (extra params) when flagAsDangerous is unknown', async function () {
const txMeta = {
id: 1,
status: TransactionStatus.unapproved,
txParams: {
from: fromAccount.address,
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
gasPrice: '0x77359400',
gas: '0x7b0d',
nonce: '0x4b',
},
type: TransactionType.simpleSend,
origin: 'other',
chainId: currentChainId,
time: 1624408066355,
metamaskNetworkId: currentNetworkId,
securityProviderResponse: {
flagAsDangerous: 2,
},
};
const expectedPayload = {
actionId,
initialEvent: 'Transaction Added',
successEvent: 'Transaction Approved',
failureEvent: 'Transaction Rejected',
uniqueIdentifier: 'transaction-added-1',
persist: true,
category: EVENT.CATEGORIES.TRANSACTIONS,
properties: {
network: '5',
referrer: 'other',
source: EVENT.SOURCE.TRANSACTION.DAPP,
transaction_type: TransactionType.simpleSend,
chain_id: '0x5',
eip_1559_version: '0',
gas_edit_attempted: 'none',
gas_edit_type: 'none',
account_type: 'MetaMask',
asset_type: AssetType.native,
token_standard: TokenStandard.none,
device_model: 'N/A',
transaction_speed_up: false,
ui_customizations: ['flagged_as_safety_unknown'],
},
sensitiveProperties: {
baz: 3.0,
foo: 'bar',
gas_price: '2',
gas_limit: '0x7b0d',
transaction_contract_method: undefined,
transaction_replaced: undefined,
first_seen: 1624408066355,
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
status: 'unapproved',
},
};
await txController._trackTransactionMetricsEvent(
txMeta,
TransactionMetaMetricsEvent.added,
actionId,
{
baz: 3.0,
foo: 'bar',
},
);
assert.equal(createEventFragmentSpy.callCount, 1);
assert.equal(finalizeEventFragmentSpy.callCount, 0);
assert.deepEqual(
createEventFragmentSpy.getCall(0).args[0],
expectedPayload,
);
});
it('should call _trackMetaMetricsEvent with the correct payload (EIP-1559)', async function () {
const txMeta = {
id: 1,
status: TransactionStatus.unapproved,
txParams: {
from: fromAccount.address,
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
maxFeePerGas: '0x77359400',
maxPriorityFeePerGas: '0x77359400',
gas: '0x7b0d',
nonce: '0x4b',
estimateSuggested: GasRecommendations.medium,
estimateUsed: GasRecommendations.high,
},
type: TransactionType.simpleSend,
origin: 'other',
chainId: currentChainId,
time: 1624408066355,
metamaskNetworkId: currentNetworkId,
defaultGasEstimates: {
estimateType: 'medium',
maxFeePerGas: '0x77359400',
maxPriorityFeePerGas: '0x77359400',
},
securityProviderResponse: {
flagAsDangerous: 0,
},
};
const expectedPayload = {
actionId,
initialEvent: 'Transaction Added',
successEvent: 'Transaction Approved',
failureEvent: 'Transaction Rejected',
uniqueIdentifier: 'transaction-added-1',
persist: true,
category: EVENT.CATEGORIES.TRANSACTIONS,
properties: {
chain_id: '0x5',
eip_1559_version: '2',
gas_edit_attempted: 'none',
gas_edit_type: 'none',
network: '5',
referrer: 'other',
source: EVENT.SOURCE.TRANSACTION.DAPP,
transaction_type: TransactionType.simpleSend,
account_type: 'MetaMask',
asset_type: AssetType.native,
token_standard: TokenStandard.none,
device_model: 'N/A',
transaction_speed_up: false,
ui_customizations: null,
},
sensitiveProperties: {
baz: 3.0,
foo: 'bar',
max_fee_per_gas: '2',
max_priority_fee_per_gas: '2',
gas_limit: '0x7b0d',
transaction_contract_method: undefined,
transaction_replaced: undefined,
first_seen: 1624408066355,
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.FEE_MARKET,
status: 'unapproved',
estimate_suggested: GasRecommendations.medium,
estimate_used: GasRecommendations.high,
default_estimate: 'medium',
default_max_fee_per_gas: '70',
default_max_priority_fee_per_gas: '7',
},
};
await txController._trackTransactionMetricsEvent(
txMeta,
TransactionMetaMetricsEvent.added,
actionId,
{
baz: 3.0,
foo: 'bar',
},
);
assert.equal(createEventFragmentSpy.callCount, 1);
assert.equal(finalizeEventFragmentSpy.callCount, 0);
assert.deepEqual(
createEventFragmentSpy.getCall(0).args[0],
expectedPayload,
);
});
});
describe('#_getTransactionCompletionTime', function () {
let nowStub;
beforeEach(function () {
nowStub = sinon.stub(Date, 'now').returns(1625782016341);
});
afterEach(function () {
nowStub.restore();
});
it('calculates completion time (one)', function () {
const submittedTime = 1625781997397;
const result = txController._getTransactionCompletionTime(submittedTime);
assert.equal(result, '19');
});
it('calculates completion time (two)', function () {
const submittedTime = 1625781995397;
const result = txController._getTransactionCompletionTime(submittedTime);
assert.equal(result, '21');
});
});
describe('#_getGasValuesInGWEI', function () {
it('converts gas values in hex GWEi to dec GWEI (EIP-1559)', function () {
const params = {
max_fee_per_gas: '0x77359400',
max_priority_fee_per_gas: '0x77359400',
};
const expectedParams = {
max_fee_per_gas: '2',
max_priority_fee_per_gas: '2',
};
const result = txController._getGasValuesInGWEI(params);
assert.deepEqual(result, expectedParams);
});
it('converts gas values in hex GWEi to dec GWEI (non EIP-1559)', function () {
const params = {
gas_price: '0x37e11d600',
};
const expectedParams = {
gas_price: '15',
};
const result = txController._getGasValuesInGWEI(params);
assert.deepEqual(result, expectedParams);
});
it('converts gas values in hex GWEi to dec GWEI, retains estimate fields', function () {
const params = {
max_fee_per_gas: '0x77359400',
max_priority_fee_per_gas: '0x77359400',
estimate_suggested: GasRecommendations.medium,
estimate_used: GasRecommendations.high,
};
const expectedParams = {
max_fee_per_gas: '2',
max_priority_fee_per_gas: '2',
estimate_suggested: GasRecommendations.medium,
estimate_used: GasRecommendations.high,
};
const result = txController._getGasValuesInGWEI(params);
assert.deepEqual(result, expectedParams);
});
});
describe('update transaction methods', function () {
let txStateManager;
beforeEach(function () {
txStateManager = txController.txStateManager;
txStateManager.addTransaction({
id: '1',
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
txParams: {
gasLimit: '0x001',
gasPrice: '0x002',
// max fees can not be mixed with gasPrice
// maxPriorityFeePerGas: '0x003',
// maxFeePerGas: '0x004',
to: VALID_ADDRESS,
from: VALID_ADDRESS,
},
estimateUsed: '0x005',
estimatedBaseFee: '0x006',
decEstimatedBaseFee: '6',
type: 'swap',
sourceTokenSymbol: 'ETH',
destinationTokenSymbol: 'UNI',
destinationTokenDecimals: 16,
destinationTokenAddress: VALID_ADDRESS,
swapMetaData: {},
swapTokenValue: '0x007',
userEditedGasLimit: '0x008',
userFeeLevel: 'medium',
});
});
it('updates transaction gas fees', function () {
// test update gasFees
txController.updateTransactionGasFees('1', {
gasPrice: '0x0022',
gasLimit: '0x0011',
});
let result = txStateManager.getTransaction('1');
assert.equal(result.txParams.gasPrice, '0x0022');
// TODO: weird behavior here...only gasPrice gets returned.
// assert.equal(result.txParams.gasLimit, '0x0011');
// test update maxPriorityFeePerGas
txStateManager.addTransaction({
id: '2',
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
txParams: {
maxPriorityFeePerGas: '0x003',
to: VALID_ADDRESS,
from: VALID_ADDRESS,
},
estimateUsed: '0x005',
});
txController.updateTransactionGasFees('2', {
maxPriorityFeePerGas: '0x0033',
});
result = txStateManager.getTransaction('2');
assert.equal(result.txParams.maxPriorityFeePerGas, '0x0033');
// test update maxFeePerGas
txStateManager.addTransaction({
id: '3',
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
txParams: {
maxPriorityFeePerGas: '0x003',
maxFeePerGas: '0x004',
to: VALID_ADDRESS,
from: VALID_ADDRESS,
},
estimateUsed: '0x005',
});
txController.updateTransactionGasFees('3', { maxFeePerGas: '0x0044' });
result = txStateManager.getTransaction('3');
assert.equal(result.txParams.maxFeePerGas, '0x0044');
// test update estimate used
txController.updateTransactionGasFees('3', { estimateUsed: '0x0055' });
result = txStateManager.getTransaction('3');
assert.equal(result.estimateUsed, '0x0055');
});
it('updates estimated base fee', function () {
txController.updateTransactionEstimatedBaseFee('1', {
estimatedBaseFee: '0x0066',
decEstimatedBaseFee: '66',
});
const result = txStateManager.getTransaction('1');
assert.equal(result.estimatedBaseFee, '0x0066');
assert.equal(result.decEstimatedBaseFee, '66');
});
it('updates swap approval transaction', function () {
txController.updateSwapApprovalTransaction('1', {
type: 'swapApproval',
sourceTokenSymbol: 'XBN',
});
const result = txStateManager.getTransaction('1');
assert.equal(result.type, 'swapApproval');
assert.equal(result.sourceTokenSymbol, 'XBN');
});
it('updates swap transaction', function () {
txController.updateSwapTransaction('1', {
sourceTokenSymbol: 'BTCX',
destinationTokenSymbol: 'ETH',
});
const result = txStateManager.getTransaction('1');
assert.equal(result.sourceTokenSymbol, 'BTCX');
assert.equal(result.destinationTokenSymbol, 'ETH');
assert.equal(result.destinationTokenDecimals, 16);
assert.equal(result.destinationTokenAddress, VALID_ADDRESS);
assert.equal(result.swapTokenValue, '0x007');
txController.updateSwapTransaction('1', {
type: 'swapped',
destinationTokenDecimals: 8,
destinationTokenAddress: VALID_ADDRESS_TWO,
swapTokenValue: '0x0077',
});
assert.equal(result.sourceTokenSymbol, 'BTCX');
assert.equal(result.destinationTokenSymbol, 'ETH');
assert.equal(result.type, 'swapped');
assert.equal(result.destinationTokenDecimals, 8);
assert.equal(result.destinationTokenAddress, VALID_ADDRESS_TWO);
assert.equal(result.swapTokenValue, '0x0077');
});
it('updates transaction user settings', function () {
txController.updateTransactionUserSettings('1', {
userEditedGasLimit: '0x0088',
userFeeLevel: 'high',
});
const result = txStateManager.getTransaction('1');
assert.equal(result.userEditedGasLimit, '0x0088');
assert.equal(result.userFeeLevel, 'high');
});
it('should not update and should throw error if status is not type "unapproved"', function () {
txStateManager.addTransaction({
id: '4',
status: TransactionStatus.dropped,
metamaskNetworkId: currentNetworkId,
txParams: {
maxPriorityFeePerGas: '0x007',
maxFeePerGas: '0x008',
to: VALID_ADDRESS,
from: VALID_ADDRESS,
},
estimateUsed: '0x009',
});
assert.throws(
() =>
txController.updateTransactionGasFees('4', {
maxFeePerGas: '0x0088',
}),
Error,
`TransactionsController: Can only call updateTransactionGasFees on an unapproved transaction.
Current tx status: ${TransactionStatus.dropped}`,
);
const transaction = txStateManager.getTransaction('4');
assert.equal(transaction.txParams.maxFeePerGas, '0x008');
});
it('does not update unknown parameters in update method', function () {
txController.updateSwapTransaction('1', {
type: 'swapped',
destinationTokenDecimals: 8,
destinationTokenAddress: VALID_ADDRESS_TWO,
swapTokenValue: '0x011',
gasPrice: '0x12',
});
let result = txStateManager.getTransaction('1');
assert.equal(result.type, 'swapped');
assert.equal(result.destinationTokenDecimals, 8);
assert.equal(result.destinationTokenAddress, VALID_ADDRESS_TWO);
assert.equal(result.swapTokenValue, '0x011');
assert.equal(result.txParams.gasPrice, '0x002'); // not updated even though it's passed in to update
txController.updateTransactionGasFees('1', {
estimateUsed: '0x13',
gasPrice: '0x14',
destinationTokenAddress: VALID_ADDRESS,
});
result = txStateManager.getTransaction('1');
assert.equal(result.estimateUsed, '0x13');
assert.equal(result.txParams.gasPrice, '0x14');
assert.equal(result.destinationTokenAddress, VALID_ADDRESS_TWO); // not updated even though it's passed in to update
});
});
describe('updateEditableParams', function () {
let txStateManager;
beforeEach(function () {
txStateManager = txController.txStateManager;
txStateManager.addTransaction({
id: '1',
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
txParams: {
gas: '0x001',
gasPrice: '0x002',
// max fees can not be mixed with gasPrice
// maxPriorityFeePerGas: '0x003',
// maxFeePerGas: '0x004',
to: VALID_ADDRESS,
from: VALID_ADDRESS,
},
estimateUsed: '0x005',
estimatedBaseFee: '0x006',
decEstimatedBaseFee: '6',
type: 'simpleSend',
userEditedGasLimit: '0x008',
userFeeLevel: 'medium',
});
});
it('updates editible params when type changes from simple send to token transfer', async function () {
providerResultStub.eth_getCode = '0xab';
// test update gasFees
await txController.updateEditableParams('1', {
data: '0xa9059cbb000000000000000000000000e18035bf8712672935fdb4e5e431b1a0183d2dfc0000000000000000000000000000000000000000000000000de0b6b3a7640000',
});
const result = txStateManager.getTransaction('1');
assert.equal(
result.txParams.data,
'0xa9059cbb000000000000000000000000e18035bf8712672935fdb4e5e431b1a0183d2dfc0000000000000000000000000000000000000000000000000de0b6b3a7640000',
);
assert.equal(result.type, TransactionType.tokenMethodTransfer);
});
it('updates editible params when type changes from token transfer to simple send', async function () {
// test update gasFees
txStateManager.addTransaction({
id: '2',
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
txParams: {
gas: '0x001',
gasPrice: '0x002',
// max fees can not be mixed with gasPrice
// maxPriorityFeePerGas: '0x003',
// maxFeePerGas: '0x004',
to: VALID_ADDRESS,
from: VALID_ADDRESS,
data: '0xa9059cbb000000000000000000000000e18035bf8712672935fdb4e5e431b1a0183d2dfc0000000000000000000000000000000000000000000000000de0b6b3a7640000',
},
estimateUsed: '0x005',
estimatedBaseFee: '0x006',
decEstimatedBaseFee: '6',
type: TransactionType.tokenMethodTransfer,
userEditedGasLimit: '0x008',
userFeeLevel: 'medium',
});
await txController.updateEditableParams('2', {
data: '0x',
});
const result = txStateManager.getTransaction('2');
assert.equal(result.txParams.data, '0x');
assert.equal(result.type, TransactionType.simpleSend);
});
it('updates editible params when type changes from simpleSend to contract interaction', async function () {
// test update gasFees
txStateManager.addTransaction({
id: '3',
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
txParams: {
gas: '0x001',
gasPrice: '0x002',
// max fees can not be mixed with gasPrice
// maxPriorityFeePerGas: '0x003',
// maxFeePerGas: '0x004',
to: VALID_ADDRESS,
from: VALID_ADDRESS,
},
estimateUsed: '0x005',
estimatedBaseFee: '0x006',
decEstimatedBaseFee: '6',
type: TransactionType.tokenMethodTransfer,
userEditedGasLimit: '0x008',
userFeeLevel: 'medium',
});
providerResultStub.eth_getCode = '0x5';
await txController.updateEditableParams('3', {
data: '0x123',
});
const result = txStateManager.getTransaction('3');
assert.equal(result.txParams.data, '0x123');
assert.equal(result.type, TransactionType.contractInteraction);
});
it('updates editible params when type does not change', async function () {
// test update gasFees
await txController.updateEditableParams('1', {
data: '0x123',
gas: '0xabc',
from: VALID_ADDRESS_TWO,
});
const result = txStateManager.getTransaction('1');
assert.equal(result.txParams.data, '0x123');
assert.equal(result.txParams.gas, '0xabc');
assert.equal(result.txParams.from, VALID_ADDRESS_TWO);
assert.equal(result.txParams.to, VALID_ADDRESS);
assert.equal(result.txParams.gasPrice, '0x002');
assert.equal(result.type, TransactionType.simpleSend);
});
});
});