/* eslint-disable prefer-promise-reject-errors */
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 { ApprovalType } from '@metamask/controller-utils';
import sinon from 'sinon';

import { errorCodes, ethErrors } from 'eth-rpc-errors';
import {
  createTestProviderTools,
  getTestAccounts,
} from '../../../../test/stub/provider';
import mockEstimates from '../../../../test/data/mock-estimates.json';
import {
  MetaMetricsEventCategory,
  MetaMetricsTransactionEventSource,
} from '../../../../shared/constants/metametrics';
import {
  TransactionStatus,
  TransactionType,
  TransactionEnvelopeType,
  TransactionMetaMetricsEvent,
  AssetType,
  TokenStandard,
} from '../../../../shared/constants/transaction';

import {
  GasEstimateTypes,
  GasRecommendations,
} from '../../../../shared/constants/gas';
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 TxGasUtil from './tx-gas-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';

async function flushPromises() {
  await new Promise((resolve) => setImmediate(resolve));
}

describe('Transaction Controller', function () {
  let txController,
    provider,
    providerResultStub,
    fromAccount,
    fragmentExists,
    networkStatusStore,
    getCurrentChainId,
    messengerMock,
    resultCallbacksMock,
    updateSpy;

  beforeEach(function () {
    fragmentExists = false;
    providerResultStub = {
      // 1 gwei
      eth_gasPrice: '0x0de0b6b3a7640000',
      // by default, all accounts are external accounts (not contracts)
      eth_getCode: '0x',
      eth_sendRawTransaction:
        '0x2a5523c6fa98b47b7d9b6c8320179785150b42a16bcff36b398c5062b65657e8',
    };
    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);

    resultCallbacksMock = {
      success: sinon.spy(),
      error: sinon.spy(),
    };

    messengerMock = {
      call: sinon.stub(),
    };

    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,
      messenger: messengerMock,
    });

    txController.nonceTracker.getNonceLock = () =>
      Promise.resolve({ nextNonce: 0, releaseLock: noop });

    updateSpy = sinon.spy(txController.txStateManager, 'updateTransaction');

    messengerMock.call.callsFake((_) =>
      Promise.resolve({
        value: { txMeta: getLastTxMeta() },
        resultCallbacks: resultCallbacksMock,
      }),
    );
  });

  function getLastTxMeta() {
    return updateSpy.lastCall.args[0];
  }

  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('#addTransaction', function () {
    const selectedAddress = '0xc684832530fcbddae4b4230a47e991ddcec2831d';
    const recipientAddress = '0xc684832530fcbddae4b4230a47e991ddcec2831d';

    let txMeta,
      txParams,
      getPermittedAccounts,
      signStub,
      getSelectedAddress,
      getDefaultGasFees;

    beforeEach(function () {
      txParams = {
        from: selectedAddress,
        to: recipientAddress,
      };

      txMeta = {
        status: TransactionStatus.unapproved,
        id: 1,
        metamaskNetworkId: currentNetworkId,
        txParams,
        history: [{}],
      };

      txController.txStateManager._addTransactionsToState([txMeta]);

      getPermittedAccounts = sinon
        .stub(txController, 'getPermittedAccounts')
        .returns([txParams.from]);

      getSelectedAddress = sinon
        .stub(txController, 'getSelectedAddress')
        .returns(selectedAddress);

      getDefaultGasFees = sinon
        .stub(txController, '_getDefaultGasFees')
        .returns({});
    });

    afterEach(function () {
      txController.txStateManager._addTransactionsToState([]);
      getPermittedAccounts.restore();
      signStub?.restore();
      getSelectedAddress.restore();
      getDefaultGasFees.restore();
    });

    it('adds an unapproved transaction and returns transaction metadata', async function () {
      ({ transactionMeta: txMeta } = await txController.addTransaction({
        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('creates an approval request', async function () {
      await txController.addTransaction(txParams);

      const txId = getLastTxMeta().id;

      assert.equal(messengerMock.call.callCount, 1);
      assert.deepEqual(messengerMock.call.getCall(0).args, [
        'ApprovalController:addRequest',
        {
          id: String(txId),
          origin: undefined,
          requestData: { txId },
          type: ApprovalType.Transaction,
          expectsResult: true,
        },
        true, // Show popup
      ]);
    });

    it('throws if the from address is not the selected address', async function () {
      await assert.rejects(() =>
        txController.addTransaction({
          from: '0x0d1d4e623D10F9FBA5Db95830F7d3839406C6AF2',
        }),
      );
    });

    it('throws if the network status is not available', async function () {
      networkStatusStore.putState(NetworkStatus.Unknown);
      await assert.rejects(
        () =>
          txController.addTransaction({
            from: selectedAddress,
            to: '0x0d1d4e623D10F9FBA5Db95830F7d3839406C6AF2',
          }),
        { message: 'MetaMask is having trouble connecting to the network' },
      );
    });

    it('updates meta if type is swap approval', async function () {
      await txController.addTransaction(
        {
          from: selectedAddress,
          to: recipientAddress,
        },
        {
          origin: ORIGIN_METAMASK,
          type: TransactionType.swapApproval,
          actionId: '12345',
          swaps: { meta: { type: 'swapApproval', sourceTokenSymbol: 'XBN' } },
        },
      );

      const transaction = txController.getTransactions({
        searchCriteria: { id: getLastTxMeta().id },
      })[0];

      assert.equal(transaction.type, 'swapApproval');
      assert.equal(transaction.sourceTokenSymbol, 'XBN');
    });

    it('updates meta if type is swap', async function () {
      await txController.addTransaction(
        {
          from: selectedAddress,
          to: recipientAddress,
        },
        {
          origin: ORIGIN_METAMASK,
          type: TransactionType.swap,
          actionId: '12345',
          swaps: {
            meta: {
              sourceTokenSymbol: 'BTCX',
              destinationTokenSymbol: 'ETH',
              type: 'swapped',
              destinationTokenDecimals: 8,
              destinationTokenAddress: VALID_ADDRESS_TWO,
              swapTokenValue: '0x0077',
            },
          },
        },
      );

      const transaction = txController.getTransactions({
        searchCriteria: { id: getLastTxMeta().id },
      })[0];

      assert.equal(transaction.sourceTokenSymbol, 'BTCX');
      assert.equal(transaction.destinationTokenSymbol, 'ETH');
      assert.equal(transaction.type, 'swapped');
      assert.equal(transaction.destinationTokenDecimals, 8);
      assert.equal(transaction.destinationTokenAddress, VALID_ADDRESS_TWO);
      assert.equal(transaction.swapTokenValue, '0x0077');
    });

    describe('if swaps trade with no approval transaction and simulation fails', function () {
      let analyzeGasUsageOriginal;

      beforeEach(function () {
        analyzeGasUsageOriginal = TxGasUtil.prototype.analyzeGasUsage;

        sinon.stub(TxGasUtil.prototype, 'analyzeGasUsage').returns({
          simulationFails: true,
        });
      });

      afterEach(function () {
        // Sinon restore didn't work
        TxGasUtil.prototype.analyzeGasUsage = analyzeGasUsageOriginal;
      });

      it('throws error', async function () {
        await assert.rejects(
          txController.addTransaction(
            {
              from: selectedAddress,
              to: recipientAddress,
            },
            {
              origin: ORIGIN_METAMASK,
              type: TransactionType.swap,
              actionId: '12345',
              swaps: {
                hasApproveTx: false,
              },
            },
          ),
          new Error('Simulation failed'),
        );
      });

      it('cancels transaction', async function () {
        const listener = sinon.spy();

        txController.on('tx:status-update', listener);

        try {
          await txController.addTransaction(
            {
              from: selectedAddress,
              to: recipientAddress,
            },
            {
              origin: ORIGIN_METAMASK,
              type: TransactionType.swap,
              actionId: '12345',
              swaps: {
                hasApproveTx: false,
              },
            },
          );
        } catch (error) {
          // Expected error
        }

        assert.equal(listener.callCount, 1, listener.args);

        const [txId, status] = listener.args[0];

        assert.equal(status, TransactionStatus.rejected);
        assert.equal(txId, getLastTxMeta().id);
      });
    });

    describe('with actionId', function () {
      it('adds single unapproved transaction when called twice with same actionId', async function () {
        await txController.addTransaction(
          {
            from: selectedAddress,
            to: recipientAddress,
          },
          { actionId: '12345' },
        );
        const transactionCount1 =
          txController.txStateManager.getTransactions().length;
        await txController.addTransaction(
          {
            from: selectedAddress,
            to: recipientAddress,
          },
          { actionId: '12345' },
        );
        const transactionCount2 =
          txController.txStateManager.getTransactions().length;
        assert.equal(transactionCount1, transactionCount2);
      });

      it('adds single approval request when called twice with same actionId', async function () {
        await txController.addTransaction(txParams, { actionId: '12345' });
        await txController.addTransaction(txParams, { actionId: '12345' });

        const txId = getLastTxMeta().id;

        assert.equal(messengerMock.call.callCount, 1);
        assert.deepEqual(messengerMock.call.getCall(0).args, [
          'ApprovalController:addRequest',
          {
            id: String(txId),
            origin: undefined,
            requestData: { txId },
            type: ApprovalType.Transaction,
            expectsResult: true,
          },
          true, // Show popup
        ]);
      });

      it('adds multiple transactions when called with different actionId', async function () {
        await txController.addTransaction(
          {
            from: selectedAddress,
            to: recipientAddress,
          },
          { actionId: '12345' },
        );
        const transactionCount1 =
          txController.txStateManager.getTransactions().length;
        await txController.addTransaction(
          {
            from: selectedAddress,
            to: recipientAddress,
          },
          { actionId: '00000' },
        );
        const transactionCount2 =
          txController.txStateManager.getTransactions().length;
        assert.equal(transactionCount1 + 1, transactionCount2);
      });

      it('resolves second result when first transaction is finished', async function () {
        let firstTransactionResolve;
        let firstTransactionCompleted = false;
        let secondTransactionCompleted = false;

        messengerMock.call.returns(
          new Promise((resolve) => {
            firstTransactionResolve = resolve;
          }),
        );

        const { result: firstResult } = await txController.addTransaction(
          txParams,
          { actionId: '12345' },
        );

        firstResult.then(() => {
          firstTransactionCompleted = true;
        });

        const { result: secondResult } = await txController.addTransaction(
          txParams,
          { actionId: '12345' },
        );

        secondResult.then(() => {
          secondTransactionCompleted = true;
        });

        await flushPromises();

        assert.equal(firstTransactionCompleted, false);
        assert.equal(secondTransactionCompleted, false);

        firstTransactionResolve({ value: { txMeta: getLastTxMeta() } });

        await flushPromises();
        await firstResult;
        await secondResult;

        assert.equal(firstTransactionCompleted, true);
        assert.equal(secondTransactionCompleted, true);
      });
    });

    describe('on success', function () {
      it('resolves result with the transaction hash', async function () {
        const { result } = await txController.addTransaction(txParams);
        const hash = await result;
        assert.ok(hash, 'addTransaction needs to return the hash');
      });

      it('changes status to submitted', async function () {
        const { result } = await txController.addTransaction(
          {
            from: selectedAddress,
            to: recipientAddress,
          },
          { origin: ORIGIN_METAMASK, actionId: '12345' },
        );

        await result;

        const transaction = txController.getTransactions({
          searchCriteria: { id: getLastTxMeta().id },
        })[0];

        assert.equal(transaction.status, TransactionStatus.submitted);
      });

      it('emits approved, signed, and submitted status events', async function () {
        const listener = sinon.spy();

        txController.on('tx:status-update', listener);

        const { result } = await txController.addTransaction(
          {
            from: selectedAddress,
            to: recipientAddress,
          },
          { origin: ORIGIN_METAMASK, actionId: '12345' },
        );

        await result;

        const txId = getLastTxMeta().id;

        assert.equal(listener.callCount, 3);
        assert.equal(listener.args[0][0], txId);
        assert.equal(listener.args[0][1], TransactionStatus.approved);
        assert.equal(listener.args[1][0], txId);
        assert.equal(listener.args[1][1], TransactionStatus.signed);
        assert.equal(listener.args[2][0], txId);
        assert.equal(listener.args[2][1], TransactionStatus.submitted);
      });

      it('reports success to approval request acceptor', async function () {
        const { result } = await txController.addTransaction(
          {
            from: selectedAddress,
            to: recipientAddress,
          },
          { origin: ORIGIN_METAMASK, actionId: '12345' },
        );

        await result;

        assert.equal(resultCallbacksMock.success.callCount, 1);
      });

      it('does not overwrite set values', async function () {
        const originalValue = '0x01';
        const wrongValue = '0x05';

        providerResultStub.eth_gasPrice = wrongValue;
        providerResultStub.eth_estimateGas = '0x5209';

        signStub = sinon
          .stub(txController, 'signTransaction')
          .callsFake(() => Promise.resolve());

        const pubStub = sinon
          .stub(txController, 'publishTransaction')
          .callsFake(() => {
            const txId = getLastTxMeta().id;
            txController.setTxHash(txId, originalValue);
            txController.txStateManager.setTxStatusSubmitted(txId);
          });

        const { result } = await txController.addTransaction(
          {
            from: selectedAddress,
            to: recipientAddress,
            nonce: originalValue,
            gas: originalValue,
            gasPrice: originalValue,
          },
          { origin: ORIGIN_METAMASK, actionId: '12345' },
        );

        await result;

        const txId = getLastTxMeta().id;
        const finalMeta = txController.txStateManager.getTransaction(txId);
        const params = finalMeta.txParams;

        assert.equal(params.gas, originalValue, 'gas unmodified');
        assert.equal(params.gasPrice, originalValue, 'gas price unmodified');
        assert.equal(finalMeta.hash, originalValue);
        assert.equal(
          finalMeta.status,
          TransactionStatus.submitted,
          'should have reached the submitted status.',
        );

        signStub.restore();
        pubStub.restore();
      });
    });

    describe('on cancel', function () {
      beforeEach(async function () {
        messengerMock.call.returns(
          Promise.reject({ code: errorCodes.provider.userRejectedRequest }),
        );
      });

      it('rejects result', async function () {
        const { result } = await txController.addTransaction(
          {
            from: selectedAddress,
            to: recipientAddress,
          },
          { origin: ORIGIN_METAMASK, actionId: '12345' },
        );

        await assert.rejects(result, {
          code: ethErrors.provider.userRejectedRequest().code,
          message: 'MetaMask Tx Signature: User denied transaction signature.',
        });
      });

      it('emits rejected status event', async function () {
        const listener = sinon.spy();

        txController.on('tx:status-update', listener);

        const { result } = await txController.addTransaction(
          {
            from: selectedAddress,
            to: recipientAddress,
          },
          { origin: ORIGIN_METAMASK, actionid: '12345' },
        );

        try {
          await result;
        } catch (error) {
          // Expected error
        }

        assert.equal(listener.callCount, 1);

        const [txId, status] = listener.args[0];

        assert.equal(status, TransactionStatus.rejected);
        assert.equal(txId, getLastTxMeta().id);
      });
    });

    describe('on signing error', function () {
      const signError = new Error('TestSignError');

      beforeEach(async function () {
        signStub = sinon.stub(txController, 'signEthTx').throws(signError);
      });

      afterEach(function () {
        signStub.restore();
      });

      it('changes status to failed', async function () {
        const { result } = await txController.addTransaction(
          {
            from: selectedAddress,
            to: recipientAddress,
          },
          { origin: ORIGIN_METAMASK, actionId: '12345' },
        );

        try {
          await result;
        } catch {
          // Expected error
        }

        const transaction = txController.getTransactions({
          searchCriteria: { id: getLastTxMeta().id },
        })[0];

        assert.equal(transaction.status, TransactionStatus.failed);
      });

      it('rejects result', async function () {
        const { result } = await txController.addTransaction(
          {
            from: selectedAddress,
            to: recipientAddress,
          },
          { origin: ORIGIN_METAMASK, actionId: '12345' },
        );

        await assert.rejects(result, signError);
      });

      it('emits approved and failed status events', async function () {
        const listener = sinon.spy();

        txController.on('tx:status-update', listener);

        const { result } = await txController.addTransaction(
          {
            from: selectedAddress,
            to: recipientAddress,
          },
          { origin: ORIGIN_METAMASK, actionId: '12345' },
        );

        try {
          await result;
        } catch (error) {
          // Expected error
        }

        const txId = getLastTxMeta().id;

        assert.equal(listener.callCount, 2);
        assert.equal(listener.args[0][0], txId);
        assert.equal(listener.args[0][1], TransactionStatus.approved);
        assert.equal(listener.args[1][0], txId);
        assert.equal(listener.args[1][1], TransactionStatus.failed);
      });

      it('reports error to approval request acceptor', async function () {
        const { result } = await txController.addTransaction(
          {
            from: selectedAddress,
            to: recipientAddress,
          },
          { origin: ORIGIN_METAMASK, actionId: '12345' },
        );

        try {
          await result;
        } catch {
          // Expected error
        }

        assert.equal(resultCallbacksMock.error.callCount, 1);
        assert.strictEqual(
          resultCallbacksMock.error.getCall(0).args[0].message,
          signError.message,
        );
      });
    });

    describe('on publish error', function () {
      const publishError = new Error('TestPublishError');
      let originalQuery;

      beforeEach(async function () {
        originalQuery = txController.query;

        txController.query = {
          sendRawTransaction: sinon.stub().throws(publishError),
        };
      });

      afterEach(function () {
        txController.query = originalQuery;
      });

      it('changes status to failed', async function () {
        const { result } = await txController.addTransaction(
          {
            from: selectedAddress,
            to: recipientAddress,
          },
          { origin: ORIGIN_METAMASK, actionId: '12345' },
        );

        try {
          await result;
        } catch {
          // Expected error
        }

        const transaction = txController.getTransactions({
          searchCriteria: { id: getLastTxMeta().id },
        })[0];

        assert.equal(transaction.status, TransactionStatus.failed);
      });

      it('rejects result', async function () {
        const { result } = await txController.addTransaction(
          {
            from: selectedAddress,
            to: recipientAddress,
          },
          { origin: ORIGIN_METAMASK, actionId: '12345' },
        );

        await assert.rejects(result, publishError);
      });

      it('emits approved, signed, and failed status events', async function () {
        const listener = sinon.spy();

        txController.on('tx:status-update', listener);

        const { result } = await txController.addTransaction(
          {
            from: selectedAddress,
            to: recipientAddress,
          },
          { origin: ORIGIN_METAMASK, actionId: '12345' },
        );

        try {
          await result;
        } catch (error) {
          // Expected error
        }

        const txId = getLastTxMeta().id;

        assert.equal(listener.callCount, 3);
        assert.equal(listener.args[0][0], txId);
        assert.equal(listener.args[0][1], TransactionStatus.approved);
        assert.equal(listener.args[1][0], txId);
        assert.equal(listener.args[1][1], TransactionStatus.signed);
        assert.equal(listener.args[2][0], txId);
        assert.equal(listener.args[2][1], TransactionStatus.failed);
      });

      it('reports error to approval request acceptor', async function () {
        const { result } = await txController.addTransaction(
          {
            from: selectedAddress,
            to: recipientAddress,
          },
          { origin: ORIGIN_METAMASK, actionId: '12345' },
        );

        try {
          await result;
        } catch {
          // Expected error
        }

        assert.equal(resultCallbacksMock.error.callCount, 1);
        assert.strictEqual(
          resultCallbacksMock.error.getCall(0).args[0].message,
          publishError.message,
        );
      });
    });

    describe('with require approval set to false', function () {
      beforeEach(function () {
        // Ensure that the default approval mock is not being used
        messengerMock.call.callsFake(() => Promise.reject());
      });

      it('resolves result with the transaction hash', async function () {
        const { result } = await txController.addTransaction(txParams, {
          requireApproval: false,
        });
        const hash = await result;
        assert.ok(hash, 'addTransaction needs to return the hash');
      });

      it('changes status to submitted', async function () {
        const { result } = await txController.addTransaction(
          {
            from: selectedAddress,
            to: recipientAddress,
          },
          {
            origin: ORIGIN_METAMASK,
            actionid: '12345',
            requireApproval: false,
          },
        );

        await result;

        const transaction = txController.getTransactions({
          searchCriteria: { id: getLastTxMeta().id },
        })[0];

        assert.equal(transaction.status, TransactionStatus.submitted);
      });

      it('emits approved, signed, and submitted status events', async function () {
        const listener = sinon.spy();

        txController.on('tx:status-update', listener);

        const { result } = await txController.addTransaction(
          {
            from: selectedAddress,
            to: recipientAddress,
          },
          {
            origin: ORIGIN_METAMASK,
            actionId: '12345',
            requireApproval: false,
          },
        );

        await result;

        const txId = getLastTxMeta().id;

        assert.equal(listener.callCount, 3);
        assert.equal(listener.args[0][0], txId);
        assert.equal(listener.args[0][1], TransactionStatus.approved);
        assert.equal(listener.args[1][0], txId);
        assert.equal(listener.args[1][1], TransactionStatus.signed);
        assert.equal(listener.args[2][0], txId);
        assert.equal(listener.args[2][1], TransactionStatus.submitted);
      });
    });
  });

  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 a cancel transaction and return a valid txMeta', async function () {
      const { transactionMeta: txMeta, result } =
        await txController.addTransaction({
          from: selectedAddress,
          to: recipientAddress,
        });
      await result;
      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);
      assert.equal(messengerMock.call.callCount, 1);
    });

    it('should add only 1 cancel transaction when called twice with same actionId', async function () {
      const { transactionMeta: txMeta, result } =
        await txController.addTransaction({
          from: selectedAddress,
          to: recipientAddress,
        });
      await result;
      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 { transactionMeta: txMeta, result } =
        await txController.addTransaction({
          from: selectedAddress,
          to: recipientAddress,
        });
      await result;
      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('#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(Number(ethTx.common.chainId()), 5);
    });
  });

  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('#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,
        },
      );
      assert.equal(messengerMock.call.callCount, 0);
    });

    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 { transactionMeta: txMeta, result } =
        await txController.addTransaction({
          from: selectedAddress,
          to: recipientAddress,
        });
      await result;
      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 { transactionMeta: txMeta, result } =
        await txController.addTransaction({
          from: selectedAddress,
          to: recipientAddress,
        });
      await result;
      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 { transactionMeta: txMeta, result } =
        await txController.addTransaction(
          {
            from: selectedAddress,
            to: recipientAddress,
          },
          { method: 'eth_sendTransaction' },
        );
      await result;
      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 { transactionMeta: txMeta } = await txController.addTransaction(
        {
          from: selectedAddress,
          to: recipientAddress,
        },
        { method: 'eth_sendTransaction' },
      );

      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: MetaMetricsEventCategory.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: MetaMetricsTransactionEventSource.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,
            status: 'unapproved',
          },
          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,
          },
        };

        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: MetaMetricsEventCategory.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: MetaMetricsTransactionEventSource.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,
            status: 'unapproved',
          },
          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,
          },
        };

        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: MetaMetricsEventCategory.Transactions,
          persist: true,
          properties: {
            chain_id: '0x5',
            eip_1559_version: '0',
            gas_edit_attempted: 'none',
            gas_edit_type: 'none',
            network: '5',
            referrer: 'other',
            source: MetaMetricsTransactionEventSource.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,
            status: 'unapproved',
          },
          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,
          },
        };

        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: MetaMetricsEventCategory.Transactions,
          persist: true,
          properties: {
            chain_id: '0x5',
            eip_1559_version: '0',
            gas_edit_attempted: 'none',
            gas_edit_type: 'none',
            network: '5',
            referrer: 'other',
            source: MetaMetricsTransactionEventSource.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,
            status: 'unapproved',
          },
          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,
          },
        };

        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: MetaMetricsEventCategory.Transactions,
        persist: true,
        properties: {
          chain_id: '0x5',
          eip_1559_version: '0',
          gas_edit_attempted: 'none',
          gas_edit_type: 'none',
          network: '5',
          referrer: 'other',
          source: MetaMetricsTransactionEventSource.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,
          status: 'unapproved',
        },
        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,
        },
      };
      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: MetaMetricsEventCategory.Transactions,
        properties: {
          network: '5',
          referrer: 'other',
          source: MetaMetricsTransactionEventSource.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,
          status: 'unapproved',
        },
        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,
        },
      };

      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: MetaMetricsEventCategory.Transactions,
        properties: {
          network: '5',
          referrer: 'other',
          source: MetaMetricsTransactionEventSource.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'],
          status: 'unapproved',
        },
        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,
        },
      };

      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: MetaMetricsEventCategory.Transactions,
        properties: {
          network: '5',
          referrer: 'other',
          source: MetaMetricsTransactionEventSource.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'],
          status: 'unapproved',
        },
        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,
        },
      };

      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: MetaMetricsEventCategory.Transactions,
        properties: {
          chain_id: '0x5',
          eip_1559_version: '2',
          gas_edit_attempted: 'none',
          gas_edit_type: 'none',
          network: '5',
          referrer: 'other',
          source: MetaMetricsTransactionEventSource.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,
          status: 'unapproved',
        },
        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,
          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('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.updateTransactionGasFees('1', {
        estimateUsed: '0x13',
        gasPrice: '0x14',
        destinationTokenAddress: VALID_ADDRESS,
      });

      const result = txStateManager.getTransaction('1');
      assert.equal(result.estimateUsed, '0x13');
      assert.equal(result.txParams.gasPrice, '0x14');
      assert.equal(result.destinationTokenAddress, VALID_ADDRESS); // 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);
    });
  });

  describe('initApprovals', function () {
    it('adds unapprovedTxs as approvals', async function () {
      const firstTxId = '1';
      const firstTxMeta = {
        id: firstTxId,
        origin: ORIGIN_METAMASK,
        status: TransactionStatus.unapproved,
        metamaskNetworkId: currentNetworkId,
        txParams: {
          to: VALID_ADDRESS,
          from: VALID_ADDRESS_TWO,
        },
      };

      const secondTxId = '2';
      const secondTxMeta = {
        id: secondTxId,
        origin: ORIGIN_METAMASK,
        status: TransactionStatus.unapproved,
        metamaskNetworkId: currentNetworkId,
        txParams: {
          to: VALID_ADDRESS,
          from: VALID_ADDRESS_TWO,
        },
      };

      txController._addTransaction(firstTxMeta);
      txController._addTransaction(secondTxMeta);

      await txController.initApprovals();

      assert.deepEqual(messengerMock.call.getCall(0).args, [
        'ApprovalController:addRequest',
        {
          id: firstTxId,
          origin: ORIGIN_METAMASK,
          requestData: { txId: firstTxId },
          type: ApprovalType.Transaction,
          expectsResult: true,
        },
        false,
      ]);
      assert.deepEqual(messengerMock.call.getCall(1).args, [
        'ApprovalController:addRequest',
        {
          id: secondTxId,
          origin: ORIGIN_METAMASK,
          requestData: { txId: secondTxId },
          type: ApprovalType.Transaction,
          expectsResult: true,
        },
        false,
      ]);
    });
  });

  describe('#updateTransactionSendFlowHistory', function () {
    it('returns same result after two sequential calls with same history', async function () {
      const txId = 1;

      const txMeta = {
        id: txId,
        origin: ORIGIN_METAMASK,
        status: TransactionStatus.unapproved,
        metamaskNetworkId: currentNetworkId,
        txParams: {
          to: VALID_ADDRESS,
          from: VALID_ADDRESS_TWO,
        },
      };

      txController._addTransaction(txMeta);

      const transaction1 = txController.updateTransactionSendFlowHistory(
        txId,
        2,
        ['foo1', 'foo2'],
      );

      const transaction2 = txController.updateTransactionSendFlowHistory(
        txId,
        2,
        ['foo1', 'foo2'],
      );

      assert.deepEqual(transaction1, transaction2);
    });
  });
});
