A Metamask fork with Infura removed and default networks editable
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
ciphermask/app/scripts/metamask-controller.test.js

1288 lines
40 KiB

import { strict as assert } from 'assert';
import sinon from 'sinon';
import { cloneDeep } from 'lodash';
import nock from 'nock';
import { pubToAddress, bufferToHex } from 'ethereumjs-util';
import { obj as createThoughStream } from 'through2';
import EthQuery from 'eth-query';
import proxyquire from 'proxyquire';
import browser from 'webextension-polyfill';
import { TRANSACTION_STATUSES } from '../../shared/constants/transaction';
import createTxMeta from '../../test/lib/createTxMeta';
import { NETWORK_TYPE_RPC } from '../../shared/constants/network';
import {
KEYRING_TYPES,
DEVICE_NAMES,
} from '../../shared/constants/hardware-wallets';
import { addHexPrefix } from './lib/util';
const Ganache = require('../../test/e2e/ganache');
const NOTIFICATION_ID = 'NHL8f2eSSTn9TKBamRLiU';
const firstTimeState = {
config: {},
NetworkController: {
provider: {
type: NETWORK_TYPE_RPC,
rpcUrl: 'http://localhost:8545',
chainId: '0x539',
},
networkDetails: {
EIPS: {
1559: false,
},
},
},
NotificationController: {
notifications: {
[NOTIFICATION_ID]: {
id: NOTIFICATION_ID,
origin: 'local:http://localhost:8086/',
createdDate: 1652967897732,
readDate: null,
message: 'Hello, http://localhost:8086!',
},
},
},
};
const ganacheServer = new Ganache();
const threeBoxSpies = {
_registerUpdates: sinon.spy(),
init: sinon.stub(),
getLastUpdated: sinon.stub(),
getThreeBoxSyncingState: sinon.stub().returns(true),
restoreFromThreeBox: sinon.stub(),
setShowRestorePromptToFalse: sinon.stub(),
setThreeBoxSyncingPermission: sinon.stub(),
turnThreeBoxSyncingOn: sinon.stub(),
};
class ThreeBoxControllerMock {
constructor() {
this._registerUpdates = threeBoxSpies._registerUpdates;
this.init = threeBoxSpies.init;
this.getLastUpdated = threeBoxSpies.getLastUpdated;
this.getThreeBoxSyncingState = threeBoxSpies.getThreeBoxSyncingState;
this.restoreFromThreeBox = threeBoxSpies.restoreFromThreeBox;
this.setShowRestorePromptToFalse =
threeBoxSpies.setShowRestorePromptToFalse;
this.setThreeBoxSyncingPermission =
threeBoxSpies.setThreeBoxSyncingPermission;
this.store = {
subscribe: () => undefined,
getState: () => ({}),
};
this.turnThreeBoxSyncingOn = threeBoxSpies.turnThreeBoxSyncingOn;
}
}
const browserPolyfillMock = {
runtime: {
id: 'fake-extension-id',
onInstalled: {
addListener: () => undefined,
},
onMessageExternal: {
addListener: () => undefined,
},
getPlatformInfo: async () => 'mac',
},
};
let loggerMiddlewareMock;
const initializeMockMiddlewareLog = () => {
loggerMiddlewareMock = {
requests: [],
responses: [],
};
};
const tearDownMockMiddlewareLog = () => {
loggerMiddlewareMock = undefined;
};
const createLoggerMiddlewareMock = () => (req, res, next) => {
if (loggerMiddlewareMock) {
loggerMiddlewareMock.requests.push(req);
next((cb) => {
loggerMiddlewareMock.responses.push(res);
cb();
});
return;
}
next();
};
const MetaMaskController = proxyquire('./metamask-controller', {
'./controllers/threebox': { default: ThreeBoxControllerMock },
'./lib/createLoggerMiddleware': { default: createLoggerMiddlewareMock },
}).default;
const currentNetworkId = '42';
const DEFAULT_LABEL = 'Account 1';
const TEST_SEED =
'debris dizzy just program just float decrease vacant alarm reduce speak stadium';
const TEST_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc';
const TEST_ADDRESS_2 = '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b';
const TEST_ADDRESS_3 = '0xeb9e64b93097bc15f01f13eae97015c57ab64823';
const TEST_SEED_ALT =
'setup olympic issue mobile velvet surge alcohol burger horse view reopen gentle';
const TEST_ADDRESS_ALT = '0xc42edfcc21ed14dda456aa0756c153f7985d8813';
const CUSTOM_RPC_URL = 'http://localhost:8545';
const CUSTOM_RPC_CHAIN_ID = '0x539';
describe('MetaMaskController', function () {
let metamaskController;
const sandbox = sinon.createSandbox();
const noop = () => undefined;
before(async function () {
await ganacheServer.start();
});
beforeEach(function () {
nock('https://min-api.cryptocompare.com')
.persist()
.get(/.*/u)
.reply(200, '{"JPY":12415.9}');
sandbox.replace(browser, 'runtime', {
sendMessage: sandbox.stub().rejects(),
});
metamaskController = new MetaMaskController({
showUserConfirmation: noop,
encryptor: {
encrypt(_, object) {
this.object = object;
return Promise.resolve('mock-encrypted');
},
decrypt() {
return Promise.resolve(this.object);
},
},
initState: cloneDeep(firstTimeState),
initLangCode: 'en_US',
platform: {
showTransactionNotification: () => undefined,
getVersion: () => 'foo',
},
browser: browserPolyfillMock,
infuraProjectId: 'foo',
});
// add sinon method spies
sandbox.spy(
metamaskController.keyringController,
'createNewVaultAndKeychain',
);
sandbox.spy(
metamaskController.keyringController,
'createNewVaultAndRestore',
);
});
afterEach(function () {
nock.cleanAll();
sandbox.restore();
});
after(async function () {
await ganacheServer.quit();
});
describe('#getAccounts', function () {
it('returns first address when dapp calls web3.eth.getAccounts', async function () {
const password = 'a-fake-password';
await metamaskController.createNewVaultAndRestore(password, TEST_SEED);
metamaskController.networkController._baseProviderParams.getAccounts(
(err, res) => {
assert.ifError(err);
assert.equal(res.length, 1);
assert.equal(res[0], '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc');
},
);
});
});
describe('#importAccountWithStrategy', function () {
const importPrivkey =
'4cfd3e90fc78b0f86bf7524722150bb8da9c60cd532564d7ff43f5716514f553';
beforeEach(async function () {
const password = 'a-fake-password';
await metamaskController.createNewVaultAndRestore(password, TEST_SEED);
await metamaskController.importAccountWithStrategy('Private Key', [
importPrivkey,
]);
});
it('adds private key to keyrings in KeyringController', async function () {
const simpleKeyrings =
metamaskController.keyringController.getKeyringsByType(
'Simple Key Pair',
);
const privKeyBuffer = simpleKeyrings[0].wallets[0].privateKey;
const pubKeyBuffer = simpleKeyrings[0].wallets[0].publicKey;
const addressBuffer = pubToAddress(pubKeyBuffer);
const privKey = bufferToHex(privKeyBuffer);
const pubKey = bufferToHex(addressBuffer);
assert.equal(privKey, addHexPrefix(importPrivkey));
assert.equal(pubKey, '0xe18035bf8712672935fdb4e5e431b1a0183d2dfc');
});
it('adds 1 account', async function () {
const keyringAccounts =
await metamaskController.keyringController.getAccounts();
assert.equal(
keyringAccounts[keyringAccounts.length - 1],
'0xe18035bf8712672935fdb4e5e431b1a0183d2dfc',
);
});
});
describe('submitPassword', function () {
const password = 'password';
beforeEach(async function () {
await metamaskController.createNewVaultAndKeychain(password);
threeBoxSpies.init.reset();
threeBoxSpies.turnThreeBoxSyncingOn.reset();
});
it('removes any identities that do not correspond to known accounts.', async function () {
const fakeAddress = '0xbad0';
metamaskController.preferencesController.addAddresses([fakeAddress]);
await metamaskController.submitPassword(password);
const identities = Object.keys(
metamaskController.preferencesController.store.getState().identities,
);
const addresses =
await metamaskController.keyringController.getAccounts();
identities.forEach((identity) => {
assert.ok(
addresses.includes(identity),
`addresses should include all IDs: ${identity}`,
);
});
addresses.forEach((address) => {
assert.ok(
identities.includes(address),
`identities should include all Addresses: ${address}`,
);
});
});
it('gets the address from threebox and creates a new 3box instance', async function () {
await metamaskController.submitPassword(password);
assert(threeBoxSpies.init.calledOnce);
assert(threeBoxSpies.turnThreeBoxSyncingOn.calledOnce);
});
it('succeeds even if blockTracker or threeBoxController throw', async function () {
const throwErr = sinon.fake.throws('foo');
metamaskController.blockTracker.checkForLatestBlock = throwErr;
metamaskController.threeBoxController.getThreeBoxSyncingState = throwErr;
await metamaskController.submitPassword(password);
assert.ok(
throwErr.calledTwice,
'should have called checkForLatestBlock and getThreeBoxSyncingState',
);
});
});
describe('#createNewVaultAndKeychain', function () {
it('can only create new vault on keyringController once', async function () {
const selectStub = sandbox.stub(
metamaskController,
'selectFirstIdentity',
);
const password = 'a-fake-password';
await metamaskController.createNewVaultAndKeychain(password);
await metamaskController.createNewVaultAndKeychain(password);
assert(
metamaskController.keyringController.createNewVaultAndKeychain
.calledOnce,
);
selectStub.reset();
});
});
describe('#createNewVaultAndRestore', function () {
it('should be able to call newVaultAndRestore despite a mistake.', async function () {
const password = 'what-what-what';
sandbox.stub(metamaskController, 'getBalance');
metamaskController.getBalance.callsFake(() => {
return Promise.resolve('0x0');
});
await metamaskController
.createNewVaultAndRestore(password, TEST_SEED.slice(0, -1))
.catch(() => null);
await metamaskController.createNewVaultAndRestore(password, TEST_SEED);
assert(
metamaskController.keyringController.createNewVaultAndRestore
.calledTwice,
);
});
it('should clear previous identities after vault restoration', async function () {
sandbox.stub(metamaskController, 'getBalance');
metamaskController.getBalance.callsFake(() => {
return Promise.resolve('0x0');
});
let startTime = Date.now();
await metamaskController.createNewVaultAndRestore(
'foobar1337',
TEST_SEED,
);
let endTime = Date.now();
const firstVaultIdentities = cloneDeep(
metamaskController.getState().identities,
);
assert.ok(
firstVaultIdentities[TEST_ADDRESS].lastSelected >= startTime &&
firstVaultIdentities[TEST_ADDRESS].lastSelected <= endTime,
`'${firstVaultIdentities[TEST_ADDRESS].lastSelected}' expected to be between '${startTime}' and '${endTime}'`,
);
delete firstVaultIdentities[TEST_ADDRESS].lastSelected;
assert.deepEqual(firstVaultIdentities, {
[TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL },
});
await metamaskController.preferencesController.setAccountLabel(
TEST_ADDRESS,
'Account Foo',
);
const labelledFirstVaultIdentities = cloneDeep(
metamaskController.getState().identities,
);
delete labelledFirstVaultIdentities[TEST_ADDRESS].lastSelected;
assert.deepEqual(labelledFirstVaultIdentities, {
[TEST_ADDRESS]: { address: TEST_ADDRESS, name: 'Account Foo' },
});
startTime = Date.now();
await metamaskController.createNewVaultAndRestore(
'foobar1337',
TEST_SEED_ALT,
);
endTime = Date.now();
const secondVaultIdentities = cloneDeep(
metamaskController.getState().identities,
);
assert.ok(
secondVaultIdentities[TEST_ADDRESS_ALT].lastSelected >= startTime &&
secondVaultIdentities[TEST_ADDRESS_ALT].lastSelected <= endTime,
`'${secondVaultIdentities[TEST_ADDRESS_ALT].lastSelected}' expected to be between '${startTime}' and '${endTime}'`,
);
delete secondVaultIdentities[TEST_ADDRESS_ALT].lastSelected;
assert.deepEqual(secondVaultIdentities, {
[TEST_ADDRESS_ALT]: { address: TEST_ADDRESS_ALT, name: DEFAULT_LABEL },
});
});
it('should restore any consecutive accounts with balances without extra zero balance accounts', async function () {
sandbox.stub(metamaskController, 'getBalance');
metamaskController.getBalance.withArgs(TEST_ADDRESS).callsFake(() => {
return Promise.resolve('0x14ced5122ce0a000');
});
metamaskController.getBalance.withArgs(TEST_ADDRESS_2).callsFake(() => {
return Promise.resolve('0x0');
});
metamaskController.getBalance.withArgs(TEST_ADDRESS_3).callsFake(() => {
return Promise.resolve('0x14ced5122ce0a000');
});
const startTime = Date.now();
await metamaskController.createNewVaultAndRestore(
'foobar1337',
TEST_SEED,
);
const identities = cloneDeep(metamaskController.getState().identities);
assert.ok(
identities[TEST_ADDRESS].lastSelected >= startTime &&
identities[TEST_ADDRESS].lastSelected <= Date.now(),
);
delete identities[TEST_ADDRESS].lastSelected;
assert.deepEqual(identities, {
[TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL },
});
});
});
describe('#getBalance', function () {
it('should return the balance known by accountTracker', async function () {
const accounts = {};
const balance = '0x14ced5122ce0a000';
accounts[TEST_ADDRESS] = { balance };
metamaskController.accountTracker.store.putState({ accounts });
const gotten = await metamaskController.getBalance(TEST_ADDRESS);
assert.equal(balance, gotten);
});
it('should ask the network for a balance when not known by accountTracker', async function () {
const accounts = {};
const balance = '0x14ced5122ce0a000';
const ethQuery = new EthQuery();
sinon.stub(ethQuery, 'getBalance').callsFake((_, callback) => {
callback(undefined, balance);
});
metamaskController.accountTracker.store.putState({ accounts });
const gotten = await metamaskController.getBalance(
TEST_ADDRESS,
ethQuery,
);
assert.equal(balance, gotten);
});
});
describe('#getApi', function () {
it('getState', function () {
const getApi = metamaskController.getApi();
const state = getApi.getState();
assert.deepEqual(state, metamaskController.getState());
});
});
describe('#selectFirstIdentity', function () {
let identities, address;
beforeEach(function () {
address = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc';
identities = {
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': {
address,
name: 'Account 1',
},
'0xc42edfcc21ed14dda456aa0756c153f7985d8813': {
address: '0xc42edfcc21ed14dda456aa0756c153f7985d8813',
name: 'Account 2',
},
};
metamaskController.preferencesController.store.updateState({
identities,
});
metamaskController.selectFirstIdentity();
});
it('changes preferences controller select address', function () {
const preferenceControllerState =
metamaskController.preferencesController.store.getState();
assert.equal(preferenceControllerState.selectedAddress, address);
});
it('changes metamask controller selected address', function () {
const metamaskState = metamaskController.getState();
assert.equal(metamaskState.selectedAddress, address);
});
});
describe('connectHardware', function () {
it('should throw if it receives an unknown device name', async function () {
try {
await metamaskController.connectHardware(
'Some random device name',
0,
`m/44/0'/0'`,
);
} catch (e) {
assert.equal(
e.message,
'MetamaskController:getKeyringForDevice - Unknown device',
);
}
});
it('should add the Trezor Hardware keyring', async function () {
sinon.spy(metamaskController.keyringController, 'addNewKeyring');
await metamaskController
.connectHardware(DEVICE_NAMES.TREZOR, 0)
.catch(() => null);
const keyrings =
await metamaskController.keyringController.getKeyringsByType(
KEYRING_TYPES.TREZOR,
);
assert.deepEqual(
metamaskController.keyringController.addNewKeyring.getCall(0).args,
[KEYRING_TYPES.TREZOR],
);
assert.equal(keyrings.length, 1);
});
it('should add the Ledger Hardware keyring', async function () {
sinon.spy(metamaskController.keyringController, 'addNewKeyring');
await metamaskController
.connectHardware(DEVICE_NAMES.LEDGER, 0)
.catch(() => null);
const keyrings =
await metamaskController.keyringController.getKeyringsByType(
KEYRING_TYPES.LEDGER,
);
assert.deepEqual(
metamaskController.keyringController.addNewKeyring.getCall(0).args,
[KEYRING_TYPES.LEDGER],
);
assert.equal(keyrings.length, 1);
});
});
describe('checkHardwareStatus', function () {
it('should throw if it receives an unknown device name', async function () {
try {
await metamaskController.checkHardwareStatus(
'Some random device name',
`m/44/0'/0'`,
);
} catch (e) {
assert.equal(
e.message,
'MetamaskController:getKeyringForDevice - Unknown device',
);
}
});
it('should be locked by default', async function () {
await metamaskController
.connectHardware(DEVICE_NAMES.TREZOR, 0)
.catch(() => null);
const status = await metamaskController.checkHardwareStatus(
DEVICE_NAMES.TREZOR,
);
assert.equal(status, false);
});
});
describe('forgetDevice', function () {
it('should throw if it receives an unknown device name', async function () {
try {
await metamaskController.forgetDevice('Some random device name');
} catch (e) {
assert.equal(
e.message,
'MetamaskController:getKeyringForDevice - Unknown device',
);
}
});
it('should wipe all the keyring info', async function () {
await metamaskController
.connectHardware(DEVICE_NAMES.TREZOR, 0)
.catch(() => null);
await metamaskController.forgetDevice(DEVICE_NAMES.TREZOR);
const keyrings =
await metamaskController.keyringController.getKeyringsByType(
KEYRING_TYPES.TREZOR,
);
assert.deepEqual(keyrings[0].accounts, []);
assert.deepEqual(keyrings[0].page, 0);
assert.deepEqual(keyrings[0].isUnlocked(), false);
});
});
describe('unlockHardwareWalletAccount', function () {
let accountToUnlock;
let windowOpenStub;
let addNewAccountStub;
let getAccountsStub;
beforeEach(async function () {
accountToUnlock = 10;
windowOpenStub = sinon.stub(window, 'open');
windowOpenStub.returns(noop);
addNewAccountStub = sinon.stub(
metamaskController.keyringController,
'addNewAccount',
);
addNewAccountStub.returns({});
getAccountsStub = sinon.stub(
metamaskController.keyringController,
'getAccounts',
);
// Need to return different address to mock the behavior of
// adding a new account from the keyring
getAccountsStub.onCall(0).returns(Promise.resolve(['0x1']));
getAccountsStub.onCall(1).returns(Promise.resolve(['0x2']));
getAccountsStub.onCall(2).returns(Promise.resolve(['0x3']));
getAccountsStub.onCall(3).returns(Promise.resolve(['0x4']));
sinon.spy(metamaskController.preferencesController, 'setAddresses');
sinon.spy(metamaskController.preferencesController, 'setSelectedAddress');
sinon.spy(metamaskController.preferencesController, 'setAccountLabel');
await metamaskController
.connectHardware(DEVICE_NAMES.TREZOR, 0, `m/44'/1'/0'/0`)
.catch(() => null);
await metamaskController.unlockHardwareWalletAccount(
accountToUnlock,
DEVICE_NAMES.TREZOR,
`m/44'/1'/0'/0`,
);
});
afterEach(function () {
window.open.restore();
metamaskController.keyringController.addNewAccount.restore();
metamaskController.keyringController.getAccounts.restore();
metamaskController.preferencesController.setAddresses.restore();
metamaskController.preferencesController.setSelectedAddress.restore();
metamaskController.preferencesController.setAccountLabel.restore();
});
it('should set unlockedAccount in the keyring', async function () {
const keyrings =
await metamaskController.keyringController.getKeyringsByType(
KEYRING_TYPES.TREZOR,
);
assert.equal(keyrings[0].unlockedAccount, accountToUnlock);
});
it('should call keyringController.addNewAccount', async function () {
assert(metamaskController.keyringController.addNewAccount.calledOnce);
});
it('should call keyringController.getAccounts ', async function () {
assert(metamaskController.keyringController.getAccounts.called);
});
it('should call preferencesController.setAddresses', async function () {
assert(metamaskController.preferencesController.setAddresses.calledOnce);
});
it('should call preferencesController.setSelectedAddress', async function () {
assert(
metamaskController.preferencesController.setSelectedAddress.calledOnce,
);
});
it('should call preferencesController.setAccountLabel', async function () {
assert(
metamaskController.preferencesController.setAccountLabel.calledOnce,
);
});
});
describe('#setCustomRpc', function () {
it('returns custom RPC that when called', async function () {
const rpcUrl = await metamaskController.setCustomRpc(
CUSTOM_RPC_URL,
CUSTOM_RPC_CHAIN_ID,
);
assert.equal(rpcUrl, CUSTOM_RPC_URL);
});
it('changes the network controller rpc', async function () {
await metamaskController.setCustomRpc(
CUSTOM_RPC_URL,
CUSTOM_RPC_CHAIN_ID,
);
const networkControllerState =
metamaskController.networkController.store.getState();
assert.equal(networkControllerState.provider.rpcUrl, CUSTOM_RPC_URL);
});
});
describe('#addNewAccount', function () {
it('errors when an primary keyring is does not exist', async function () {
const addNewAccount = metamaskController.addNewAccount();
try {
await addNewAccount;
assert.fail('should throw');
} catch (e) {
assert.equal(e.message, 'MetamaskController - No HD Key Tree found');
}
});
});
describe('#verifyseedPhrase', function () {
it('errors when no keying is provided', async function () {
try {
await metamaskController.verifySeedPhrase();
} catch (error) {
assert.equal(
error.message,
'MetamaskController - No HD Key Tree found',
);
}
});
beforeEach(async function () {
await metamaskController.createNewVaultAndKeychain('password');
});
it('#addNewAccount', async function () {
await metamaskController.addNewAccount();
const getAccounts =
await metamaskController.keyringController.getAccounts();
assert.equal(getAccounts.length, 2);
});
});
describe('#resetAccount', function () {
it('wipes transactions from only the correct network id and with the selected address', async function () {
const selectedAddressStub = sinon.stub(
metamaskController.preferencesController,
'getSelectedAddress',
);
const getNetworkstub = sinon.stub(
metamaskController.txController.txStateManager,
'getNetwork',
);
selectedAddressStub.returns('0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc');
getNetworkstub.returns(42);
metamaskController.txController.txStateManager._addTransactionsToState([
createTxMeta({
id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: currentNetworkId,
txParams: { from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' },
}),
createTxMeta({
id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: currentNetworkId,
txParams: { from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' },
}),
createTxMeta({
id: 2,
status: TRANSACTION_STATUSES.REJECTED,
metamaskNetworkId: '32',
}),
createTxMeta({
id: 3,
status: TRANSACTION_STATUSES.SUBMITTED,
metamaskNetworkId: currentNetworkId,
txParams: { from: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4' },
}),
]);
await metamaskController.resetAccount();
assert.equal(
metamaskController.txController.txStateManager.getTransaction(1),
undefined,
);
});
});
describe('#removeAccount', function () {
let ret;
const addressToRemove = '0x1';
let mockKeyring;
beforeEach(async function () {
mockKeyring = {
getAccounts: sinon.stub().returns(Promise.resolve([])),
destroy: sinon.stub(),
};
sinon.stub(metamaskController.preferencesController, 'removeAddress');
sinon.stub(metamaskController.accountTracker, 'removeAccount');
sinon.stub(metamaskController.keyringController, 'removeAccount');
sinon.stub(metamaskController, 'removeAllAccountPermissions');
sinon
.stub(metamaskController.keyringController, 'getKeyringForAccount')
.returns(Promise.resolve(mockKeyring));
ret = await metamaskController.removeAccount(addressToRemove);
});
afterEach(function () {
metamaskController.keyringController.removeAccount.restore();
metamaskController.accountTracker.removeAccount.restore();
metamaskController.preferencesController.removeAddress.restore();
metamaskController.removeAllAccountPermissions.restore();
mockKeyring.getAccounts.resetHistory();
mockKeyring.destroy.resetHistory();
});
it('should call preferencesController.removeAddress', async function () {
assert(
metamaskController.preferencesController.removeAddress.calledWith(
addressToRemove,
),
);
});
it('should call accountTracker.removeAccount', async function () {
assert(
metamaskController.accountTracker.removeAccount.calledWith([
addressToRemove,
]),
);
});
it('should call keyringController.removeAccount', async function () {
assert(
metamaskController.keyringController.removeAccount.calledWith(
addressToRemove,
),
);
});
it('should call metamaskController.removeAllAccountPermissions', async function () {
assert(
metamaskController.removeAllAccountPermissions.calledWith(
addressToRemove,
),
);
});
it('should return address', async function () {
assert.equal(ret, '0x1');
});
it('should call keyringController.getKeyringForAccount', async function () {
assert(
metamaskController.keyringController.getKeyringForAccount.calledWith(
addressToRemove,
),
);
});
it('should call keyring.destroy', async function () {
assert(mockKeyring.destroy.calledOnce);
});
});
describe('#newUnsignedMessage', function () {
let msgParams, metamaskMsgs, messages, msgId;
const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813';
const data =
'0x0000000000000000000000000000000000000043727970746f6b697474696573';
beforeEach(async function () {
sandbox.stub(metamaskController, 'getBalance');
metamaskController.getBalance.callsFake(() => {
return Promise.resolve('0x0');
});
await metamaskController.createNewVaultAndRestore(
'foobar1337',
TEST_SEED_ALT,
);
msgParams = {
from: address,
data,
};
const promise = metamaskController.newUnsignedMessage(msgParams);
// handle the promise so it doesn't throw an unhandledRejection
promise.then(noop).catch(noop);
metamaskMsgs = metamaskController.messageManager.getUnapprovedMsgs();
messages = metamaskController.messageManager.messages;
msgId = Object.keys(metamaskMsgs)[0];
messages[0].msgParams.metamaskId = parseInt(msgId, 10);
});
it('persists address from msg params', function () {
assert.equal(metamaskMsgs[msgId].msgParams.from, address);
});
it('persists data from msg params', function () {
assert.equal(metamaskMsgs[msgId].msgParams.data, data);
});
it('sets the status to unapproved', function () {
assert.equal(metamaskMsgs[msgId].status, TRANSACTION_STATUSES.UNAPPROVED);
});
it('sets the type to eth_sign', function () {
assert.equal(metamaskMsgs[msgId].type, 'eth_sign');
});
it('rejects the message', function () {
const msgIdInt = parseInt(msgId, 10);
metamaskController.cancelMessage(msgIdInt, noop);
assert.equal(messages[0].status, TRANSACTION_STATUSES.REJECTED);
});
it('checks message length', async function () {
msgParams = {
from: address,
data: '0xDEADBEEF',
};
try {
await metamaskController.newUnsignedMessage(msgParams);
} catch (error) {
assert.equal(error.message, 'eth_sign requires 32 byte message hash');
}
});
it('errors when signing a message', async function () {
try {
await metamaskController.signMessage(messages[0].msgParams);
} catch (error) {
assert.equal(
error.message,
'Expected message to be an Uint8Array with length 32',
);
}
});
});
describe('#newUnsignedPersonalMessage', function () {
let msgParams, metamaskPersonalMsgs, personalMessages, msgId;
const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813';
const data = '0x43727970746f6b697474696573';
beforeEach(async function () {
sandbox.stub(metamaskController, 'getBalance');
metamaskController.getBalance.callsFake(() => {
return Promise.resolve('0x0');
});
await metamaskController.createNewVaultAndRestore(
'foobar1337',
TEST_SEED_ALT,
);
msgParams = {
from: address,
data,
};
const promise = metamaskController.newUnsignedPersonalMessage(msgParams);
// handle the promise so it doesn't throw an unhandledRejection
promise.then(noop).catch(noop);
metamaskPersonalMsgs =
metamaskController.personalMessageManager.getUnapprovedMsgs();
personalMessages = metamaskController.personalMessageManager.messages;
msgId = Object.keys(metamaskPersonalMsgs)[0];
personalMessages[0].msgParams.metamaskId = parseInt(msgId, 10);
});
it('errors with no from in msgParams', async function () {
try {
await metamaskController.newUnsignedPersonalMessage({
data,
});
assert.fail('should have thrown');
} catch (error) {
assert.equal(
error.message,
'MetaMask Message Signature: from field is required.',
);
}
});
it('persists address from msg params', function () {
assert.equal(metamaskPersonalMsgs[msgId].msgParams.from, address);
});
it('persists data from msg params', function () {
assert.equal(metamaskPersonalMsgs[msgId].msgParams.data, data);
});
it('sets the status to unapproved', function () {
assert.equal(
metamaskPersonalMsgs[msgId].status,
TRANSACTION_STATUSES.UNAPPROVED,
);
});
it('sets the type to personal_sign', function () {
assert.equal(metamaskPersonalMsgs[msgId].type, 'personal_sign');
});
it('rejects the message', function () {
const msgIdInt = parseInt(msgId, 10);
metamaskController.cancelPersonalMessage(msgIdInt, noop);
assert.equal(personalMessages[0].status, TRANSACTION_STATUSES.REJECTED);
});
it('errors when signing a message', async function () {
await metamaskController.signPersonalMessage(
personalMessages[0].msgParams,
);
assert.equal(
metamaskPersonalMsgs[msgId].status,
TRANSACTION_STATUSES.SIGNED,
);
assert.equal(
metamaskPersonalMsgs[msgId].rawSig,
'0x6a1b65e2b8ed53cf398a769fad24738f9fbe29841fe6854e226953542c4b6a173473cb152b6b1ae5f06d601d45dd699a129b0a8ca84e78b423031db5baa734741b',
);
});
});
describe('#setupUntrustedCommunication', function () {
const mockTxParams = { from: TEST_ADDRESS };
beforeEach(function () {
initializeMockMiddlewareLog();
});
after(function () {
tearDownMockMiddlewareLog();
});
it('sets up phishing stream for untrusted communication', async function () {
const phishingMessageSender = {
url: 'http://myethereumwalletntw.com',
tab: {},
};
const { promise, resolve } = deferredPromise();
const streamTest = createThoughStream((chunk, _, cb) => {
if (chunk.name !== 'phishing') {
cb();
return;
}
assert.equal(
chunk.data.hostname,
new URL(phishingMessageSender.url).hostname,
);
resolve();
cb();
});
metamaskController.setupUntrustedCommunication({
connectionStream: streamTest,
sender: phishingMessageSender,
});
await promise;
streamTest.end();
});
it('adds a tabId and origin to requests', function (done) {
const messageSender = {
url: 'http://mycrypto.com',
tab: { id: 456 },
};
const streamTest = createThoughStream((chunk, _, cb) => {
if (chunk.data && chunk.data.method) {
cb(null, chunk);
return;
}
cb();
});
metamaskController.setupUntrustedCommunication({
connectionStream: streamTest,
sender: messageSender,
});
const message = {
id: 1999133338649204,
jsonrpc: '2.0',
params: [{ ...mockTxParams }],
method: 'eth_sendTransaction',
};
streamTest.write(
{
name: 'metamask-provider',
data: message,
},
null,
() => {
setTimeout(() => {
assert.deepStrictEqual(loggerMiddlewareMock.requests[0], {
...message,
origin: 'http://mycrypto.com',
tabId: 456,
});
done();
});
},
);
});
it('should add only origin to request if tabId not provided', function (done) {
const messageSender = {
url: 'http://mycrypto.com',
};
const streamTest = createThoughStream((chunk, _, cb) => {
if (chunk.data && chunk.data.method) {
cb(null, chunk);
return;
}
cb();
});
metamaskController.setupUntrustedCommunication({
connectionStream: streamTest,
sender: messageSender,
});
const message = {
id: 1999133338649204,
jsonrpc: '2.0',
params: [{ ...mockTxParams }],
method: 'eth_sendTransaction',
};
streamTest.write(
{
name: 'metamask-provider',
data: message,
},
null,
() => {
setTimeout(() => {
assert.deepStrictEqual(loggerMiddlewareMock.requests[0], {
...message,
origin: 'http://mycrypto.com',
});
done();
});
},
);
});
});
describe('#setupTrustedCommunication', function () {
it('sets up controller JSON-RPC api for trusted communication', async function () {
const messageSender = {
url: 'http://mycrypto.com',
tab: {},
};
const { promise, resolve } = deferredPromise();
const streamTest = createThoughStream((chunk, _, cb) => {
assert.equal(chunk.name, 'controller');
resolve();
cb();
});
metamaskController.setupTrustedCommunication(streamTest, messageSender);
await promise;
streamTest.end();
});
});
describe('#markPasswordForgotten', function () {
it('adds and sets forgottenPassword to config data to true', function () {
metamaskController.markPasswordForgotten(noop);
const state = metamaskController.getState();
assert.equal(state.forgottenPassword, true);
});
});
describe('#unMarkPasswordForgotten', function () {
it('adds and sets forgottenPassword to config data to false', function () {
metamaskController.unMarkPasswordForgotten(noop);
const state = metamaskController.getState();
assert.equal(state.forgottenPassword, false);
});
});
describe('#_onKeyringControllerUpdate', function () {
it('should do nothing if there are no keyrings in state', async function () {
const syncAddresses = sinon.fake();
const syncWithAddresses = sinon.fake();
sandbox.replace(metamaskController, 'preferencesController', {
syncAddresses,
});
sandbox.replace(metamaskController, 'accountTracker', {
syncWithAddresses,
});
const oldState = metamaskController.getState();
await metamaskController._onKeyringControllerUpdate({ keyrings: [] });
assert.ok(syncAddresses.notCalled);
assert.ok(syncWithAddresses.notCalled);
assert.deepEqual(metamaskController.getState(), oldState);
});
it('should sync addresses if there are keyrings in state', async function () {
const syncAddresses = sinon.fake();
const syncWithAddresses = sinon.fake();
sandbox.replace(metamaskController, 'preferencesController', {
syncAddresses,
});
sandbox.replace(metamaskController, 'accountTracker', {
syncWithAddresses,
});
const oldState = metamaskController.getState();
await metamaskController._onKeyringControllerUpdate({
keyrings: [
{
accounts: ['0x1', '0x2'],
},
],
});
assert.deepEqual(syncAddresses.args, [[['0x1', '0x2']]]);
assert.deepEqual(syncWithAddresses.args, [[['0x1', '0x2']]]);
assert.deepEqual(metamaskController.getState(), oldState);
});
it('should NOT update selected address if already unlocked', async function () {
const syncAddresses = sinon.fake();
const syncWithAddresses = sinon.fake();
sandbox.replace(metamaskController, 'preferencesController', {
syncAddresses,
});
sandbox.replace(metamaskController, 'accountTracker', {
syncWithAddresses,
});
const oldState = metamaskController.getState();
await metamaskController._onKeyringControllerUpdate({
isUnlocked: true,
keyrings: [
{
accounts: ['0x1', '0x2'],
},
],
});
assert.deepEqual(syncAddresses.args, [[['0x1', '0x2']]]);
assert.deepEqual(syncWithAddresses.args, [[['0x1', '0x2']]]);
assert.deepEqual(metamaskController.getState(), oldState);
});
});
describe('markNotificationsAsRead', function () {
it('marks the notification as read', function () {
metamaskController.markNotificationsAsRead([NOTIFICATION_ID]);
const readNotification =
metamaskController.getState().notifications[NOTIFICATION_ID];
assert.notEqual(readNotification.readDate, null);
});
});
describe('dismissNotifications', function () {
it('deletes the notification from state', function () {
metamaskController.dismissNotifications([NOTIFICATION_ID]);
const state = metamaskController.getState().notifications;
assert.ok(
!Object.values(state).includes(NOTIFICATION_ID),
'Object should not include the deleted notification',
);
});
});
});
function deferredPromise() {
let resolve;
const promise = new Promise((_resolve) => {
resolve = _resolve;
});
return { promise, resolve };
}