import assert from 'assert'; import sinon from 'sinon'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import EthQuery from 'eth-query'; import Eth from 'ethjs'; import { createTestProviderTools } from '../../../test/stub/provider'; import enLocale from '../../../app/_locales/en/messages.json'; import MetaMaskController from '../../../app/scripts/metamask-controller'; import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction'; import * as actions from './actions'; const { provider } = createTestProviderTools({ scaffold: {} }); const middleware = [thunk]; const defaultState = { metamask: { currentLocale: 'test', selectedAddress: '0xFirstAddress', provider: { chainId: '0x1' }, }, }; const mockStore = (state = defaultState) => configureStore(middleware)(state); describe('Actions', function () { let background; const currentNetworkId = '42'; beforeEach(async function () { background = sinon.createStubInstance(MetaMaskController, { getState: sinon.stub().callsFake((cb) => cb(null, { currentLocale: 'test', selectedAddress: '0xFirstAddress', }), ), }); global.ethQuery = new EthQuery(provider); }); describe('#tryUnlockMetamask', function () { afterEach(function () { sinon.restore(); }); it('calls submitPassword and verifySeedPhrase', async function () { const store = mockStore(); const submitPassword = background.submitPassword.callsFake((_, cb) => cb(), ); const verifySeedPhrase = background.verifySeedPhrase.callsFake((cb) => cb(), ); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'UNLOCK_IN_PROGRESS' }, { type: 'UNLOCK_SUCCEEDED', value: undefined }, { type: 'UPDATE_METAMASK_STATE', value: { currentLocale: 'test', selectedAddress: '0xFirstAddress', }, }, { type: 'HIDE_LOADING_INDICATION' }, ]; await store.dispatch(actions.tryUnlockMetamask()); assert(submitPassword.calledOnce); assert(verifySeedPhrase.calledOnce); assert.deepStrictEqual(store.getActions(), expectedActions); }); it('errors on submitPassword will fail', async function () { const store = mockStore(); background.submitPassword.callsFake((_, cb) => cb(new Error('error'))); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'UNLOCK_IN_PROGRESS' }, { type: 'UNLOCK_FAILED', value: 'error' }, { type: 'HIDE_LOADING_INDICATION' }, ]; try { await store.dispatch(actions.tryUnlockMetamask('test')); assert.fail('Should have thrown error'); } catch (_) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); it('displays warning error and unlock failed when verifySeed fails', async function () { const store = mockStore(); background.submitPassword.callsFake((_, cb) => cb()); background.verifySeedPhrase.callsFake((cb) => { cb(new Error('error')); }); actions._setBackgroundConnection(background); const displayWarningError = [{ type: 'DISPLAY_WARNING', value: 'error' }]; const unlockFailedError = [{ type: 'UNLOCK_FAILED', value: 'error' }]; try { await store.dispatch(actions.tryUnlockMetamask('test')); assert.fail('Should have thrown error'); } catch (_) { const actions1 = store.getActions(); const warning = actions1.filter( (action) => action.type === 'DISPLAY_WARNING', ); const unlockFailed = actions1.filter( (action) => action.type === 'UNLOCK_FAILED', ); assert.deepStrictEqual(warning, displayWarningError); assert.deepStrictEqual(unlockFailed, unlockFailedError); } }); }); describe('#createNewVaultAndRestore', function () { afterEach(function () { sinon.restore(); }); it('calls createNewVaultAndRestore', async function () { const store = mockStore(); const createNewVaultAndRestore = background.createNewVaultAndRestore.callsFake( (_, __, cb) => cb(), ); background.unMarkPasswordForgotten.callsFake((cb) => cb()); actions._setBackgroundConnection(background); await store.dispatch(actions.createNewVaultAndRestore()); assert(createNewVaultAndRestore.calledOnce); }); it('calls the expected actions', async function () { const store = mockStore(); background.createNewVaultAndRestore.callsFake((_, __, cb) => cb()); background.unMarkPasswordForgotten.callsFake((cb) => cb()); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'FORGOT_PASSWORD', value: false }, { type: 'UPDATE_METAMASK_STATE', value: { currentLocale: 'test', selectedAddress: '0xFirstAddress', }, }, { type: 'SHOW_ACCOUNTS_PAGE' }, { type: 'HIDE_LOADING_INDICATION' }, ]; await store.dispatch(actions.createNewVaultAndRestore()); assert.deepStrictEqual(store.getActions(), expectedActions); }); it('errors when callback in createNewVaultAndRestore throws', async function () { const store = mockStore(); background.createNewVaultAndRestore.callsFake((_, __, cb) => cb(new Error('error')), ); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'DISPLAY_WARNING', value: 'error' }, { type: 'HIDE_LOADING_INDICATION' }, ]; try { await store.dispatch(actions.createNewVaultAndRestore()); assert.fail('Should have thrown error'); } catch (_) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); }); describe('#requestRevealSeedWords', function () { afterEach(function () { sinon.restore(); }); it('calls verifyPassword in background', async function () { const store = mockStore(); const verifyPassword = background.verifyPassword.callsFake((_, cb) => cb(), ); const verifySeedPhrase = background.verifySeedPhrase.callsFake((cb) => cb(), ); actions._setBackgroundConnection(background); await store.dispatch(actions.requestRevealSeedWords()); assert(verifyPassword.calledOnce); assert(verifySeedPhrase.calledOnce); }); it('displays warning error message then callback in background errors', async function () { const store = mockStore(); background.verifyPassword.callsFake((_, cb) => cb()); background.verifySeedPhrase.callsFake((cb) => { cb(new Error('error')); }); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'DISPLAY_WARNING', value: 'error' }, { type: 'HIDE_LOADING_INDICATION' }, ]; try { await store.dispatch(actions.requestRevealSeedWords()); assert.fail('Should have thrown error'); } catch (_) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); }); describe('#removeAccount', function () { afterEach(function () { sinon.restore(); }); it('calls removeAccount in background and expect actions to show account', async function () { const store = mockStore(); background.getState.callsFake((cb) => cb(null, { currentLocale: 'test', selectedAddress: '0xAnotherAddress', }), ); const removeAccount = background.removeAccount.callsFake((_, cb) => cb()); actions._setBackgroundConnection(background); const expectedActions = [ 'SHOW_LOADING_INDICATION', 'SELECTED_ADDRESS_CHANGED', 'UPDATE_METAMASK_STATE', 'HIDE_LOADING_INDICATION', 'SHOW_ACCOUNTS_PAGE', ]; await store.dispatch( actions.removeAccount('0xe18035bf8712672935fdb4e5e431b1a0183d2dfc'), ); assert(removeAccount.calledOnce); const actionTypes = store.getActions().map((action) => action.type); assert.deepStrictEqual(actionTypes, expectedActions); }); it('displays warning error message when removeAccount callback errors', async function () { const store = mockStore(); background.removeAccount.callsFake((_, cb) => { cb(new Error('error')); }); actions._setBackgroundConnection(background); const expectedActions = [ 'SHOW_LOADING_INDICATION', 'DISPLAY_WARNING', 'HIDE_LOADING_INDICATION', ]; try { await store.dispatch( actions.removeAccount('0xe18035bf8712672935fdb4e5e431b1a0183d2dfc'), ); assert.fail('Should have thrown error'); } catch (_) { const actionTypes = store.getActions().map((action) => action.type); assert.deepStrictEqual(actionTypes, expectedActions); } }); }); describe('#resetAccount', function () { afterEach(function () { sinon.restore(); }); it('resets account', async function () { const store = mockStore(); const resetAccount = background.resetAccount.callsFake((cb) => cb()); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'HIDE_LOADING_INDICATION' }, { type: 'SHOW_ACCOUNTS_PAGE' }, ]; await store.dispatch(actions.resetAccount()); assert(resetAccount.calledOnce); assert.deepStrictEqual(store.getActions(), expectedActions); }); it('throws if resetAccount throws', async function () { const store = mockStore(); background.resetAccount.callsFake((cb) => { cb(new Error('error')); }); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'error' }, ]; try { await store.dispatch(actions.resetAccount()); assert.fail('Should have thrown error'); } catch (_) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); }); describe('#importNewAccount', function () { afterEach(function () { sinon.restore(); }); it('calls importAccountWithStrategies in background', async function () { const store = mockStore(); const importAccountWithStrategy = background.importAccountWithStrategy.callsFake( (_, __, cb) => { cb(); }, ); actions._setBackgroundConnection(background); await store.dispatch( actions.importNewAccount('Private Key', [ 'c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3', ]), ); assert(importAccountWithStrategy.calledOnce); }); it('displays warning error message when importAccount in background callback errors', async function () { const store = mockStore(); background.importAccountWithStrategy.callsFake((_, __, cb) => cb(new Error('error')), ); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: 'This may take a while, please be patient.', }, { type: 'DISPLAY_WARNING', value: 'error' }, { type: 'HIDE_LOADING_INDICATION' }, ]; try { await store.dispatch(actions.importNewAccount()); assert.fail('Should have thrown error'); } catch (_) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); }); describe('#addNewAccount', function () { it('Adds a new account', async function () { const store = mockStore({ metamask: { identities: {} } }); const addNewAccount = background.addNewAccount.callsFake((cb) => cb(null, { identities: {}, }), ); actions._setBackgroundConnection(background); await store.dispatch(actions.addNewAccount()); assert(addNewAccount.calledOnce); }); it('displays warning error message when addNewAccount in background callback errors', async function () { const store = mockStore(); background.addNewAccount.callsFake((cb) => { cb(new Error('error')); }); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'DISPLAY_WARNING', value: 'error' }, { type: 'HIDE_LOADING_INDICATION' }, ]; try { await store.dispatch(actions.addNewAccount()); assert.fail('Should have thrown error'); } catch (_) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); }); describe('#checkHardwareStatus', function () { afterEach(function () { sinon.restore(); }); it('calls checkHardwareStatus in background', async function () { const store = mockStore(); const checkHardwareStatus = background.checkHardwareStatus.callsFake( (_, __, cb) => { cb(); }, ); actions._setBackgroundConnection(background); await store.dispatch( actions.checkHardwareStatus('ledger', `m/44'/60'/0'/0`), ); assert.strictEqual(checkHardwareStatus.calledOnce, true); }); it('shows loading indicator and displays error', async function () { const store = mockStore(); background.checkHardwareStatus.callsFake((_, __, cb) => cb(new Error('error')), ); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'DISPLAY_WARNING', value: 'error' }, { type: 'HIDE_LOADING_INDICATION' }, ]; try { await store.dispatch(actions.checkHardwareStatus()); assert.fail('Should have thrown error'); } catch (_) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); }); describe('#forgetDevice', function () { afterEach(function () { sinon.restore(); }); it('calls forgetDevice in background', async function () { const store = mockStore(); const forgetDevice = background.forgetDevice.callsFake((_, cb) => cb()); actions._setBackgroundConnection(background); await store.dispatch(actions.forgetDevice('ledger')); assert(forgetDevice.calledOnce); }); it('shows loading indicator and displays error', async function () { const store = mockStore(); background.forgetDevice.callsFake((_, cb) => cb(new Error('error'))); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'DISPLAY_WARNING', value: 'error' }, { type: 'HIDE_LOADING_INDICATION' }, ]; try { await store.dispatch(actions.forgetDevice()); assert.fail('Should have thrown error'); } catch (_) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); }); describe('#connectHardware', function () { afterEach(function () { sinon.restore(); }); it('calls connectHardware in background', async function () { const store = mockStore(); const connectHardware = background.connectHardware.callsFake( (_, __, ___, cb) => cb(), ); actions._setBackgroundConnection(background); await store.dispatch( actions.connectHardware('ledger', 0, `m/44'/60'/0'/0`), ); assert(connectHardware.calledOnce); }); it('shows loading indicator and displays error', async function () { const store = mockStore(); background.connectHardware.callsFake((_, __, ___, cb) => cb(new Error('error')), ); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: 'Looking for your Ledger...', }, { type: 'DISPLAY_WARNING', value: 'error' }, { type: 'HIDE_LOADING_INDICATION' }, ]; try { await store.dispatch(actions.connectHardware('ledger')); assert.fail('Should have thrown error'); } catch (_) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); }); describe('#unlockHardwareWalletAccount', function () { afterEach(function () { sinon.restore(); }); it('calls unlockHardwareWalletAccount in background', async function () { const store = mockStore(); const unlockHardwareWalletAccount = background.unlockHardwareWalletAccount.callsFake( (_, __, ___, ____, cb) => cb(), ); actions._setBackgroundConnection(background); await store.dispatch( actions.unlockHardwareWalletAccounts( [0], 'ledger', `m/44'/60'/0'/0`, '', ), ); assert(unlockHardwareWalletAccount.calledOnce); }); it('shows loading indicator and displays error', async function () { const store = mockStore(); background.unlockHardwareWalletAccount.callsFake((_, __, ___, ____, cb) => cb(new Error('error')), ); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'DISPLAY_WARNING', value: 'error' }, { type: 'HIDE_LOADING_INDICATION' }, ]; try { await store.dispatch(actions.unlockHardwareWalletAccounts([null])); assert.fail('Should have thrown error'); } catch (error) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); }); describe('#setCurrentCurrency', function () { afterEach(function () { sinon.restore(); }); it('calls setCurrentCurrency', async function () { const store = mockStore(); const setCurrentCurrency = background.setCurrentCurrency.callsFake( (_, cb) => cb(null, { currentCurrency: 'currency', conversionRate: 100, conversionDate: 1611839083653, }), ); actions._setBackgroundConnection(background); await store.dispatch(actions.setCurrentCurrency('jpy')); assert(setCurrentCurrency.calledOnce); }); it('throws if setCurrentCurrency throws', async function () { const store = mockStore(); background.setCurrentCurrency.callsFake((_, cb) => cb(new Error('error')), ); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'DISPLAY_WARNING', value: 'error' }, { type: 'HIDE_LOADING_INDICATION' }, ]; await store.dispatch(actions.setCurrentCurrency()); assert.deepStrictEqual(store.getActions(), expectedActions); }); }); describe('#signMsg', function () { const msgParams = { metamaskId: 123, from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', data: '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0', }; afterEach(function () { sinon.restore(); }); it('calls signMsg in background', async function () { const store = mockStore(); const signMessage = background.signMessage.callsFake((_, cb) => cb(null, defaultState), ); actions._setBackgroundConnection(background); await store.dispatch(actions.signMsg(msgParams)); assert(signMessage.calledOnce); }); it('errors when signMessage in background throws', async function () { const store = mockStore(); background.signMessage.callsFake((_, cb) => cb(new Error('error'))); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'DISPLAY_WARNING', value: 'error' }, { type: 'HIDE_LOADING_INDICATION' }, ]; try { await store.dispatch(actions.signMsg(msgParams)); assert.fail('Should have thrown error'); } catch (_) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); }); describe('#signPersonalMsg', function () { const msgParams = { from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', data: '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0', }; afterEach(function () { sinon.restore(); }); it('calls signPersonalMessage', async function () { const store = mockStore(); const signPersonalMessage = background.signPersonalMessage.callsFake( (_, cb) => cb(null, defaultState), ); actions._setBackgroundConnection(background); await store.dispatch(actions.signPersonalMsg(msgParams)); assert(signPersonalMessage.calledOnce); }); it('throws if signPersonalMessage throws', async function () { const store = mockStore(); background.signPersonalMessage.callsFake((_, cb) => { cb(new Error('error')); }); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'DISPLAY_WARNING', value: 'error' }, { type: 'HIDE_LOADING_INDICATION' }, ]; try { await store.dispatch(actions.signPersonalMsg(msgParams)); assert.fail('Should have thrown error'); } catch (_) { assert.deepEqual(store.getActions(), expectedActions); } }); }); describe('#signTypedMsg', function () { const msgParamsV3 = { from: '0x0DCD5D886577d5081B0c52e242Ef29E70Be3E7bc', data: JSON.stringify({ types: { EIP712Domain: [ { name: 'name', type: 'string' }, { name: 'version', type: 'string' }, { name: 'chainId', type: 'uint256' }, { name: 'verifyingContract', type: 'address' }, ], Person: [ { name: 'name', type: 'string' }, { name: 'wallet', type: 'address' }, ], Mail: [ { name: 'from', type: 'Person' }, { name: 'to', type: 'Person' }, { name: 'contents', type: 'string' }, ], }, primaryType: 'Mail', domain: { name: 'Ether Mainl', version: '1', verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', }, message: { from: { name: 'Cow', wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', }, to: { name: 'Bob', wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', }, contents: 'Hello, Bob!', }, }), }; afterEach(function () { sinon.restore(); }); it('calls signTypedMsg in background with no error', async function () { const store = mockStore(); const signTypedMsg = background.signTypedMessage.callsFake((_, cb) => cb(null, defaultState), ); actions._setBackgroundConnection(background); await store.dispatch(actions.signTypedMsg(msgParamsV3)); assert(signTypedMsg.calledOnce); }); it('returns expected actions with error', async function () { const store = mockStore(); background.signTypedMessage.callsFake((_, cb) => cb(new Error('error'))); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'DISPLAY_WARNING', value: 'error' }, { type: 'HIDE_LOADING_INDICATION' }, ]; try { await store.dispatch(actions.signTypedMsg()); assert.fail('Should have thrown error'); } catch (_) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); }); describe('#signTx', function () { let sendTransaction; beforeEach(function () { sendTransaction = sinon.stub(global.ethQuery, 'sendTransaction'); }); afterEach(function () { sendTransaction.restore(); }); it('calls sendTransaction in global ethQuery', function () { const store = mockStore(); store.dispatch(actions.signTx()); assert(sendTransaction.calledOnce); }); it('errors in when sendTransaction throws', function () { const store = mockStore(); const expectedActions = [ { type: 'DISPLAY_WARNING', value: 'error' }, { type: 'SHOW_CONF_TX_PAGE', id: undefined }, ]; sendTransaction.callsFake((_, callback) => { callback(new Error('error')); }); store.dispatch(actions.signTx()); assert.deepStrictEqual(store.getActions(), expectedActions); }); }); describe('#updatedGasData', function () { it('errors when get code does not return', async function () { const store = mockStore(); const expectedActions = [ { type: 'GAS_LOADING_STARTED' }, { type: 'UPDATE_SEND_ERRORS', value: { gasLoadingError: 'gasLoadingError' }, }, { type: 'GAS_LOADING_FINISHED' }, ]; const mockData = { gasPrice: '0x3b9aca00', // blockGasLimit: '0x6ad79a', // 7002010 selectedAddress: '0x0DCD5D886577d5081B0c52e242Ef29E70Be3E7bc', to: '0xEC1Adf982415D2Ef5ec55899b9Bfb8BC0f29251B', value: '0xde0b6b3a7640000', // 1000000000000000000 }; try { await store.dispatch(actions.updateGasData(mockData)); assert.fail('Should have thrown error'); } catch (error) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); it('returns default gas limit for basic eth transaction', async function () { const mockData = { gasPrice: '0x3b9aca00', blockGasLimit: '0x6ad79a', // 7002010 selectedAddress: '0x0DCD5D886577d5081B0c52e242Ef29E70Be3E7bc', to: '0xEC1Adf982415D2Ef5ec55899b9Bfb8BC0f29251B', value: '0xde0b6b3a7640000', // 1000000000000000000 }; global.eth = { getCode: sinon.stub().returns('0x'), }; const store = mockStore(); const expectedActions = [ { type: 'GAS_LOADING_STARTED' }, { type: 'UPDATE_GAS_LIMIT', value: '0x5208' }, { type: 'metamask/gas/SET_CUSTOM_GAS_LIMIT', value: '0x5208' }, { type: 'UPDATE_SEND_ERRORS', value: { gasLoadingError: null } }, { type: 'GAS_LOADING_FINISHED' }, ]; await store.dispatch(actions.updateGasData(mockData)); assert.deepStrictEqual(store.getActions(), expectedActions); global.eth.getCode.reset(); }); }); describe('#signTokenTx', function () { it('calls eth.contract', function () { global.eth = new Eth(provider); const tokenSpy = sinon.spy(global.eth, 'contract'); const store = mockStore(); store.dispatch(actions.signTokenTx()); assert(tokenSpy.calledOnce); tokenSpy.restore(); }); }); describe('#updateTransaction', function () { const txParams = { from: '0x1', gas: '0x5208', gasPrice: '0x3b9aca00', to: '0x2', value: '0x0', }; const txData = { id: '1', status: TRANSACTION_STATUSES.UNAPPROVED, metamaskNetworkId: currentNetworkId, txParams, }; afterEach(function () { sinon.restore(); }); it('updates transaction', async function () { const store = mockStore(); const updateTransactionStub = sinon.stub().callsFake((_, cb) => cb()); background.getApi.returns({ updateTransaction: updateTransactionStub, getState: sinon.stub().callsFake((cb) => cb(null, { currentLocale: 'test', selectedAddress: '0xFirstAddress', }), ), }); actions._setBackgroundConnection(background.getApi()); await store.dispatch(actions.updateTransaction(txData)); const resultantActions = store.getActions(); assert(updateTransactionStub.calledOnce); assert.deepStrictEqual(resultantActions[1], { type: 'UPDATE_TRANSACTION_PARAMS', id: txData.id, value: txParams, }); }); it('rejects with error message', async function () { const store = mockStore(); background.getApi.returns({ updateTransaction: (_, callback) => { callback(new Error('error')); }, getState: sinon.stub().callsFake((cb) => cb(null, { currentLocale: 'test', selectedAddress: '0xFirstAddress', }), ), }); actions._setBackgroundConnection(background.getApi()); try { await store.dispatch(actions.updateTransaction(txData)); assert.fail('Should have thrown error'); } catch (error) { assert.strictEqual(error.message, 'error'); } }); }); describe('#lockMetamask', function () { afterEach(function () { sinon.restore(); }); it('calls setLocked', async function () { const store = mockStore(); const backgroundSetLocked = background.setLocked.callsFake((cb) => cb()); actions._setBackgroundConnection(background); await store.dispatch(actions.lockMetamask()); assert(backgroundSetLocked.calledOnce); }); it('returns display warning error with value when setLocked in background callback errors', async function () { const store = mockStore(); background.setLocked.callsFake((cb) => { cb(new Error('error')); }); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'DISPLAY_WARNING', value: 'error' }, { type: 'HIDE_LOADING_INDICATION' }, { type: 'LOCK_METAMASK' }, ]; try { await store.dispatch(actions.lockMetamask()); assert.fail('Should have thrown error'); } catch (error) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); }); describe('#setSelectedAddress', function () { afterEach(function () { sinon.restore(); }); it('calls setSelectedAddress in background', async function () { const store = mockStore(); const setSelectedAddressSpy = sinon.stub().callsFake((_, cb) => cb()); background.getApi.returns({ setSelectedAddress: setSelectedAddressSpy, }); actions._setBackgroundConnection(background.getApi()); await store.dispatch( actions.setSelectedAddress( '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', ), ); assert(setSelectedAddressSpy.calledOnce); }); it('errors when setSelectedAddress throws', async function () { const store = mockStore(); const setSelectedAddressSpy = sinon .stub() .callsFake((_, cb) => cb(new Error('error'))); background.getApi.returns({ setSelectedAddress: setSelectedAddressSpy, }); actions._setBackgroundConnection(background.getApi()); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'DISPLAY_WARNING', value: 'error' }, { type: 'HIDE_LOADING_INDICATION' }, ]; await store.dispatch(actions.setSelectedAddress()); assert.deepStrictEqual(store.getActions(), expectedActions); }); }); describe('#showAccountDetail', function () { afterEach(function () { sinon.restore(); }); it('#showAccountDetail', async function () { const store = mockStore({ activeTab: {}, metamask: { alertEnabledness: {}, selectedAddress: '0x123' }, }); const setSelectedAddressSpy = sinon.stub().callsFake((_, cb) => cb()); background.getApi.returns({ setSelectedAddress: setSelectedAddressSpy, }); actions._setBackgroundConnection(background.getApi()); await store.dispatch(actions.showAccountDetail()); assert(setSelectedAddressSpy.calledOnce); }); it('displays warning if setSelectedAddress throws', async function () { const store = mockStore({ activeTab: {}, metamask: { alertEnabledness: {}, selectedAddress: '0x123' }, }); const setSelectedAddressSpy = sinon .stub() .callsFake((_, cb) => cb(new Error('error'))); background.getApi.returns({ setSelectedAddress: setSelectedAddressSpy, }); actions._setBackgroundConnection(background.getApi()); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'DISPLAY_WARNING', value: 'error' }, { type: 'HIDE_LOADING_INDICATION' }, ]; await store.dispatch(actions.showAccountDetail()); assert.deepStrictEqual(store.getActions(), expectedActions); }); }); describe('#addToken', function () { afterEach(function () { sinon.restore(); }); it('calls addToken in background', async function () { const store = mockStore(); const addTokenStub = sinon .stub() .callsFake((_, __, ___, ____, cb) => cb()); background.getApi.returns({ addToken: addTokenStub, }); actions._setBackgroundConnection(background.getApi()); await store.dispatch( actions.addToken({ address: '0x514910771af9ca656af840dff83e8264ecf986ca', symbol: 'LINK', decimals: 18, }), ); assert(addTokenStub.calledOnce); }); it('expected actions', async function () { const store = mockStore(); const tokenDetails = { address: 'tokenAddress', symbol: 'token', decimal: 18, }; const addTokenStub = sinon .stub() .callsFake((_, __, ___, ____, cb) => cb(null, tokenDetails)); background.getApi.returns({ addToken: addTokenStub, }); actions._setBackgroundConnection(background.getApi()); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'HIDE_LOADING_INDICATION' }, { type: 'UPDATE_TOKENS', newTokens: tokenDetails, }, ]; await store.dispatch( actions.addToken({ address: '0x514910771af9ca656af840dff83e8264ecf986ca', symbol: 'LINK', decimals: 18, }), ); assert.deepStrictEqual(store.getActions(), expectedActions); }); it('errors when addToken in background throws', async function () { const store = mockStore(); const addTokenStub = sinon .stub() .callsFake((_, __, ___, ____, cb) => cb(new Error('error'))); background.getApi.returns({ addToken: addTokenStub, }); actions._setBackgroundConnection(background.getApi()); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'error' }, ]; try { await store.dispatch( actions.addToken({ address: '_', symbol: '', decimals: 0, }), ); assert.fail('Should have thrown error'); } catch (_) { assert.deepEqual(store.getActions(), expectedActions); } }); }); describe('#removeToken', function () { afterEach(function () { sinon.restore(); }); it('calls removeToken in background', async function () { const store = mockStore(); const removeTokenStub = sinon.stub().callsFake((_, cb) => cb()); background.getApi.returns({ removeToken: removeTokenStub, }); actions._setBackgroundConnection(background.getApi()); await store.dispatch(actions.removeToken()); assert(removeTokenStub.calledOnce); }); it('errors when removeToken in background fails', async function () { const store = mockStore(); background.getApi.returns({ removeToken: sinon.stub().callsFake((_, cb) => cb(new Error('error'))), }); actions._setBackgroundConnection(background.getApi()); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'error' }, ]; try { await store.dispatch(actions.removeToken()); assert.fail('Should have thrown error'); } catch (_) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); }); describe('#setProviderType', function () { afterEach(function () { sinon.restore(); }); it('calls setProviderType', async function () { const store = mockStore(); const setProviderTypeStub = sinon.stub().callsFake((_, cb) => cb()); background.getApi.returns({ setProviderType: setProviderTypeStub, }); actions._setBackgroundConnection(background.getApi()); await store.dispatch(actions.setProviderType()); assert(setProviderTypeStub.calledOnce); }); it('displays warning when setProviderType throws', async function () { const store = mockStore(); background.getApi.returns({ setProviderType: sinon .stub() .callsFake((_, cb) => cb(new Error('error'))), }); actions._setBackgroundConnection(background.getApi()); const expectedActions = [ { type: 'DISPLAY_WARNING', value: 'Had a problem changing networks!' }, ]; await store.dispatch(actions.setProviderType()); assert.deepStrictEqual(store.getActions(), expectedActions); }); }); describe('#setRpcTarget', function () { afterEach(function () { sinon.restore(); }); it('calls setRpcTarget', async function () { const store = mockStore(); background.setCustomRpc.callsFake((_, __, ___, ____, cb) => cb()); actions._setBackgroundConnection(background); await store.dispatch(actions.setRpcTarget('http://localhost:8545')); assert(background.setCustomRpc.calledOnce); }); it('displays warning when setRpcTarget throws', async function () { const store = mockStore(); background.setCustomRpc.callsFake((_, __, ___, ____, cb) => cb(new Error('error')), ); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'DISPLAY_WARNING', value: 'Had a problem changing networks!' }, ]; await store.dispatch(actions.setRpcTarget()); assert.deepStrictEqual(store.getActions(), expectedActions); }); }); describe('#addToAddressBook', function () { it('calls setAddressBook', async function () { const store = mockStore(); const setAddressBookStub = sinon .stub() .callsFake((_, __, ___, ____, cb) => cb()); background.getApi.returns({ setAddressBook: setAddressBookStub, }); actions._setBackgroundConnection(background.getApi()); await store.dispatch(actions.addToAddressBook('test')); assert(setAddressBookStub.calledOnce); sinon.restore(); }); }); describe('#exportAccount', function () { afterEach(function () { sinon.restore(); }); it('returns expected actions for successful action', async function () { const store = mockStore(); const testPrivKey = 'a-test-priv-key'; const verifyPasswordStub = sinon.stub().callsFake((_, cb) => cb()); const exportAccountStub = sinon .stub() .callsFake((_, cb) => cb(null, testPrivKey)); background.getApi.returns({ verifyPassword: verifyPasswordStub, exportAccount: exportAccountStub, }); actions._setBackgroundConnection(background.getApi()); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'HIDE_LOADING_INDICATION' }, { type: 'SHOW_PRIVATE_KEY', value: testPrivKey, }, ]; await store.dispatch( actions.exportAccount('a-test-password', '0xAddress'), ); assert(verifyPasswordStub.calledOnce); assert(exportAccountStub.calledOnce); assert.deepStrictEqual(store.getActions(), expectedActions); }); it('returns action errors when first func callback errors', async function () { const store = mockStore(); const verifyPasswordStub = sinon .stub() .callsFake((_, cb) => cb(new Error('error'))); background.getApi.returns({ verifyPassword: verifyPasswordStub, }); actions._setBackgroundConnection(background.getApi()); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'Incorrect Password.' }, ]; try { await store.dispatch( actions.exportAccount('a-test-password', '0xAddress'), ); assert.fail('Should have thrown error'); } catch (_) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); it('returns action errors when second func callback errors', async function () { const store = mockStore(); const verifyPasswordStub = sinon.stub().callsFake((_, cb) => cb()); const exportAccountStub = sinon .stub() .callsFake((_, cb) => cb(new Error('error'))); background.getApi.returns({ verifyPassword: verifyPasswordStub, exportAccount: exportAccountStub, }); actions._setBackgroundConnection(background.getApi()); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'Had a problem exporting the account.', }, ]; try { await store.dispatch( actions.exportAccount('a-test-password', '0xAddress'), ); assert.fail('Should have thrown error'); } catch (_) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); }); describe('#setAccountLabel', function () { afterEach(function () { sinon.restore(); }); it('calls setAccountLabel', async function () { const store = mockStore(); const setAccountLabelStub = sinon.stub().callsFake((_, __, cb) => cb()); background.getApi.returns({ setAccountLabel: setAccountLabelStub, }); actions._setBackgroundConnection(background.getApi()); await store.dispatch( actions.setAccountLabel( '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', 'test', ), ); assert(setAccountLabelStub.calledOnce); }); it('returns action errors when func callback errors', async function () { const store = mockStore(); background.getApi.returns({ setAccountLabel: sinon .stub() .callsFake((_, __, cb) => cb(new Error('error'))), }); actions._setBackgroundConnection(background.getApi()); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'error' }, ]; try { await store.dispatch( actions.setAccountLabel( '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', 'test', ), ); assert.fail('Should have thrown error'); } catch (error) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); }); describe('#setFeatureFlag', function () { afterEach(function () { sinon.restore(); }); it('calls setFeatureFlag in the background', async function () { const store = mockStore(); const setFeatureFlagStub = sinon.stub().callsFake((_, __, cb) => cb()); background.getApi.returns({ setFeatureFlag: setFeatureFlagStub, }); actions._setBackgroundConnection(background.getApi()); await store.dispatch(actions.setFeatureFlag()); assert(setFeatureFlagStub.calledOnce); }); it('errors when setFeatureFlag in background throws', async function () { const store = mockStore(); background.getApi.returns({ setFeatureFlag: sinon .stub() .callsFake((_, __, cb) => cb(new Error('error'))), }); actions._setBackgroundConnection(background.getApi()); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'error' }, ]; try { await store.dispatch(actions.setFeatureFlag()); assert.fail('Should have thrown error'); } catch (_) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); }); describe('#setCompletedOnboarding', function () { afterEach(function () { sinon.restore(); }); it('completes onboarding', async function () { const store = mockStore(); const completeOnboardingStub = sinon.stub().callsFake((cb) => cb()); background.getApi.returns({ completeOnboarding: completeOnboardingStub, }); actions._setBackgroundConnection(background.getApi()); await store.dispatch(actions.setCompletedOnboarding()); assert(completeOnboardingStub.calledOnce); }); it('errors when setCompletedOnboarding in background throws', async function () { const store = mockStore(); background.getApi.returns({ completeOnboarding: sinon .stub() .callsFake((cb) => cb(new Error('error'))), }); actions._setBackgroundConnection(background.getApi()); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'DISPLAY_WARNING', value: 'error' }, { type: 'HIDE_LOADING_INDICATION' }, ]; try { await store.dispatch(actions.setCompletedOnboarding()); assert.fail('Should have thrown error'); } catch (_) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); }); describe('#setUseBlockie', function () { afterEach(function () { sinon.restore(); }); it('calls setUseBlockie in background', async function () { const store = mockStore(); const setUseBlockStub = background.setUseBlockie.callsFake((_, cb) => cb(), ); actions._setBackgroundConnection(background); await store.dispatch(actions.setUseBlockie()); assert(setUseBlockStub.calledOnce); }); it('errors when setUseBlockie in background throws', async function () { const store = mockStore(); background.setUseBlockie.callsFake((_, cb) => cb(new Error('error'))); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'error' }, { type: 'SET_USE_BLOCKIE', value: undefined }, ]; await store.dispatch(actions.setUseBlockie()); assert.deepStrictEqual(store.getActions(), expectedActions); }); }); describe('#updateCurrentLocale', function () { beforeEach(function () { sinon.stub(window, 'fetch').resolves({ json: async () => enLocale, }); }); afterEach(function () { sinon.restore(); }); it('calls expected actions', async function () { const store = mockStore(); background.setCurrentLocale.callsFake((_, cb) => cb()); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'SET_CURRENT_LOCALE', value: { locale: 'test', messages: enLocale }, }, { type: 'HIDE_LOADING_INDICATION' }, ]; await store.dispatch(actions.updateCurrentLocale('test')); assert(background.setCurrentLocale.calledOnce); assert.deepStrictEqual(store.getActions(), expectedActions); }); it('errors when setCurrentLocale throws', async function () { const store = mockStore(); background.setCurrentLocale.callsFake((_, cb) => cb(new Error('error'))); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'DISPLAY_WARNING', value: 'error' }, { type: 'HIDE_LOADING_INDICATION' }, ]; try { await store.dispatch(actions.updateCurrentLocale('test')); assert.fail('Should have thrown error'); } catch (_) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); }); describe('#markPasswordForgotten', function () { afterEach(function () { sinon.restore(); }); it('calls markPasswordForgotten', async function () { const store = mockStore(); background.markPasswordForgotten.callsFake((cb) => cb()); actions._setBackgroundConnection(background); await store.dispatch(actions.markPasswordForgotten()); const resultantActions = store.getActions(); assert.deepStrictEqual(resultantActions[1], { type: 'FORGOT_PASSWORD', value: true, }); assert(background.markPasswordForgotten.calledOnce); }); it('errors when markPasswordForgotten throws', async function () { const store = mockStore(); background.markPasswordForgotten.callsFake((cb) => cb(new Error('error')), ); actions._setBackgroundConnection(background); const expectedActions = [ { type: 'HIDE_LOADING_INDICATION' }, { type: 'FORGOT_PASSWORD', value: true }, { type: 'UPDATE_METAMASK_STATE', value: { currentLocale: 'test', selectedAddress: '0xFirstAddress', }, }, ]; try { await store.dispatch(actions.markPasswordForgotten('test')); assert.fail('Should have thrown error'); } catch (_) { assert.deepStrictEqual(store.getActions(), expectedActions); } }); }); describe('#unMarkPasswordForgotten', function () { it('calls unMarkPasswordForgotten', async function () { const store = mockStore(); background.unMarkPasswordForgotten.callsFake((cb) => cb()); actions._setBackgroundConnection(background); await store.dispatch(actions.unMarkPasswordForgotten()); const resultantActions = store.getActions(); assert.deepStrictEqual(resultantActions[0], { type: 'FORGOT_PASSWORD', value: false, }); assert(background.unMarkPasswordForgotten.calledOnce); }); }); describe('#displayWarning', function () { it('sets appState.warning to provided value', async function () { const store = mockStore(); const warningText = 'This is a sample warning message'; await store.dispatch(actions.displayWarning(warningText)); const resultantActions = store.getActions(); assert.deepStrictEqual(resultantActions[0], { type: 'DISPLAY_WARNING', value: warningText, }); }); }); describe('#cancelTx', function () { it('creates COMPLETED_TX with the cancelled transaction ID', async function () { const store = mockStore(); background.getApi.returns({ cancelTransaction: sinon.stub().callsFake((_, cb) => { cb(); }), getState: sinon.stub().callsFake((cb) => cb(null, { currentLocale: 'test', selectedAddress: '0xFirstAddress', }), ), }); actions._setBackgroundConnection(background.getApi()); const txId = 1457634084250832; await store.dispatch(actions.cancelTx({ id: txId })); const resultantActions = store.getActions(); const expectedAction = resultantActions.find( (action) => action.type === 'COMPLETED_TX', ); assert.ok(expectedAction, 'expected action not found'); assert.strictEqual(expectedAction.value.id, txId); }); }); });