1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00
metamask-extension/app/scripts/controllers/incoming-transactions.test.js

1413 lines
44 KiB
JavaScript

import assert from 'assert';
import sinon from 'sinon';
import proxyquire from 'proxyquire';
import nock from 'nock';
import { cloneDeep } from 'lodash';
import waitUntilCalled from '../../../test/lib/wait-until-called';
import {
GOERLI,
KOVAN,
MAINNET,
MAINNET_CHAIN_ID,
RINKEBY,
ROPSTEN,
ROPSTEN_CHAIN_ID,
ROPSTEN_NETWORK_ID,
} from '../../../shared/constants/network';
import {
TRANSACTION_TYPES,
TRANSACTION_STATUSES,
} from '../../../shared/constants/transaction';
import { NETWORK_EVENTS } from './network';
const IncomingTransactionsController = proxyquire('./incoming-transactions', {
'../../../shared/modules/random-id': { default: () => 54321 },
}).default;
const FAKE_CHAIN_ID = '0x1338';
const MOCK_SELECTED_ADDRESS = '0x0101';
const SET_STATE_TIMEOUT = 10;
function getEmptyInitState() {
return {
incomingTransactions: {},
incomingTxLastFetchedBlocksByNetwork: {
[GOERLI]: null,
[KOVAN]: null,
[MAINNET]: null,
[RINKEBY]: null,
[ROPSTEN]: null,
},
};
}
function getNonEmptyInitState() {
return {
incomingTransactions: {
'0x123456': { id: 777 },
},
incomingTxLastFetchedBlocksByNetwork: {
[GOERLI]: 1,
[KOVAN]: 2,
[MAINNET]: 3,
[RINKEBY]: 5,
[ROPSTEN]: 4,
},
};
}
function getMockNetworkController(chainId = FAKE_CHAIN_ID) {
return {
getCurrentChainId: () => chainId,
on: sinon.spy(),
};
}
function getMockPreferencesController({
showIncomingTransactions = true,
} = {}) {
return {
getSelectedAddress: sinon.stub().returns(MOCK_SELECTED_ADDRESS),
store: {
getState: sinon.stub().returns({
featureFlags: {
showIncomingTransactions,
},
}),
subscribe: sinon.spy(),
},
};
}
function getMockBlockTracker() {
return {
addListener: sinon.stub().callsArgWithAsync(1, '0xa'),
removeListener: sinon.spy(),
testProperty: 'fakeBlockTracker',
getCurrentBlock: () => '0xa',
};
}
/**
* A transaction object in the format returned by the Etherscan API.
*
* Note that this is not an exhaustive type definiton; only the properties we use are defined
*
* @typedef {Object} EtherscanTransaction
* @property {string} blockNumber - The number of the block this transaction was found in, in decimal
* @property {string} from - The hex-prefixed address of the sender
* @property {string} gas - The gas limit, in decimal WEI
* @property {string} gasPrice - The gas price, in decimal WEI
* @property {string} hash - The hex-prefixed transaction hash
* @property {string} isError - Whether the transaction was confirmed or failed (0 for confirmed, 1 for failed)
* @property {string} nonce - The transaction nonce, in decimal
* @property {string} timeStamp - The timestamp for the transaction, in seconds
* @property {string} to - The hex-prefixed address of the recipient
* @property {string} value - The amount of ETH sent in this transaction, in decimal WEI
*/
/**
* Returns a transaction object matching the expected format returned
* by the Etherscan API
*
* @param {string} [toAddress] - The hex-prefixed address of the recipient
* @param {number} [blockNumber] - The block number for the transaction
* @returns {EtherscanTransaction}
*/
const getFakeEtherscanTransaction = (
toAddress = MOCK_SELECTED_ADDRESS,
blockNumber = 10,
) => {
return {
blockNumber: blockNumber.toString(),
from: '0xfake',
gas: '0',
gasPrice: '0',
hash: '0xfake',
isError: '0',
nonce: '100',
timeStamp: '16000000000000',
to: toAddress,
value: '0',
};
};
describe('IncomingTransactionsController', function () {
afterEach(function () {
sinon.restore();
nock.cleanAll();
});
describe('constructor', function () {
it('should set up correct store, listeners and properties in the constructor', function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(),
preferencesController: getMockPreferencesController(),
initState: {},
},
);
sinon.spy(incomingTransactionsController, '_update');
assert.deepEqual(
incomingTransactionsController.store.getState(),
getEmptyInitState(),
);
assert(incomingTransactionsController.networkController.on.calledOnce);
assert.equal(
incomingTransactionsController.networkController.on.getCall(0).args[0],
NETWORK_EVENTS.NETWORK_DID_CHANGE,
);
const networkControllerListenerCallback = incomingTransactionsController.networkController.on.getCall(
0,
).args[1];
assert.equal(incomingTransactionsController._update.callCount, 0);
networkControllerListenerCallback('testNetworkType');
assert.equal(incomingTransactionsController._update.callCount, 1);
assert.deepEqual(
incomingTransactionsController._update.getCall(0).args[0],
{
address: '0x0101',
},
);
incomingTransactionsController._update.resetHistory();
});
it('should set the store to a provided initial state', function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(),
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
assert.deepEqual(
incomingTransactionsController.store.getState(),
getNonEmptyInitState(),
);
});
});
describe('update events', function () {
it('should set up a listener for the latest block', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(),
preferencesController: getMockPreferencesController(),
initState: {},
},
);
incomingTransactionsController.start();
assert(
incomingTransactionsController.blockTracker.addListener.calledOnce,
);
assert.equal(
incomingTransactionsController.blockTracker.addListener.getCall(0)
.args[0],
'latest',
);
});
it('should update upon latest block when started and on supported network', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(ROPSTEN_CHAIN_ID),
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
const startBlock = getNonEmptyInitState()
.incomingTxLastFetchedBlocksByNetwork[ROPSTEN];
nock('https://api-ropsten.etherscan.io')
.get(
`/api?module=account&action=txlist&address=${MOCK_SELECTED_ADDRESS}&tag=latest&page=1&startBlock=${startBlock}`,
)
.reply(
200,
JSON.stringify({
status: '1',
result: [getFakeEtherscanTransaction()],
}),
);
const updateStateStub = sinon.stub(
incomingTransactionsController.store,
'updateState',
);
const updateStateCalled = waitUntilCalled(
updateStateStub,
incomingTransactionsController.store,
);
incomingTransactionsController.start();
await updateStateCalled();
const actualState = incomingTransactionsController.store.getState();
const generatedTxId = actualState?.incomingTransactions?.['0xfake']?.id;
const actualStateWithoutGenerated = cloneDeep(actualState);
delete actualStateWithoutGenerated?.incomingTransactions?.['0xfake']?.id;
assert.ok(
typeof generatedTxId === 'number' && generatedTxId > 0,
'Generated transaction ID should be a positive number',
);
assert.deepStrictEqual(
actualStateWithoutGenerated,
{
incomingTransactions: {
...getNonEmptyInitState().incomingTransactions,
'0xfake': {
blockNumber: '10',
hash: '0xfake',
metamaskNetworkId: ROPSTEN_NETWORK_ID,
chainId: ROPSTEN_CHAIN_ID,
status: TRANSACTION_STATUSES.CONFIRMED,
time: 16000000000000000,
type: TRANSACTION_TYPES.INCOMING,
txParams: {
from: '0xfake',
gas: '0x0',
gasPrice: '0x0',
nonce: '0x64',
to: '0x0101',
value: '0x0',
},
},
},
incomingTxLastFetchedBlocksByNetwork: {
...getNonEmptyInitState().incomingTxLastFetchedBlocksByNetwork,
[ROPSTEN]: 11,
},
},
'State should have been updated after first block was received',
);
});
it('should not update upon latest block when started and not on supported network', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(),
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
// reply with a valid request for any supported network, so that this test has every opportunity to fail
for (const network of [
GOERLI,
KOVAN,
MAINNET,
RINKEBY,
ROPSTEN,
'undefined',
]) {
nock(
`https://api${
network === MAINNET ? '' : `-${network.toLowerCase()}`
}.etherscan.io`,
)
.get(/api.+/u)
.reply(
200,
JSON.stringify({
status: '1',
result: [getFakeEtherscanTransaction()],
}),
);
}
const updateStateStub = sinon.stub(
incomingTransactionsController.store,
'updateState',
);
const updateStateCalled = waitUntilCalled(
updateStateStub,
incomingTransactionsController.store,
);
const putStateStub = sinon.stub(
incomingTransactionsController.store,
'putState',
);
const putStateCalled = waitUntilCalled(
putStateStub,
incomingTransactionsController.store,
);
incomingTransactionsController.start();
try {
await Promise.race([
updateStateCalled(),
putStateCalled(),
new Promise((_, reject) => {
setTimeout(() => reject(new Error('TIMEOUT')), SET_STATE_TIMEOUT);
}),
]);
assert.fail('Update state should not have been called');
} catch (error) {
assert(error.message === 'TIMEOUT', 'TIMEOUT error should be thrown');
}
});
it('should not update upon latest block when started and incoming transactions disabled', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(),
preferencesController: getMockPreferencesController({
showIncomingTransactions: false,
}),
initState: getNonEmptyInitState(),
},
);
// reply with a valid request for any supported network, so that this test has every opportunity to fail
for (const network of [
GOERLI,
KOVAN,
MAINNET,
RINKEBY,
ROPSTEN,
'undefined',
]) {
nock(
`https://api${
network === MAINNET ? '' : `-${network.toLowerCase()}`
}.etherscan.io`,
)
.get(/api.+/u)
.reply(
200,
JSON.stringify({
status: '1',
result: [getFakeEtherscanTransaction()],
}),
);
}
const updateStateStub = sinon.stub(
incomingTransactionsController.store,
'updateState',
);
const updateStateCalled = waitUntilCalled(
updateStateStub,
incomingTransactionsController.store,
);
const putStateStub = sinon.stub(
incomingTransactionsController.store,
'putState',
);
const putStateCalled = waitUntilCalled(
putStateStub,
incomingTransactionsController.store,
);
incomingTransactionsController.start();
try {
await Promise.race([
updateStateCalled(),
putStateCalled(),
new Promise((_, reject) => {
setTimeout(() => reject(new Error('TIMEOUT')), SET_STATE_TIMEOUT);
}),
]);
assert.fail('Update state should not have been called');
} catch (error) {
assert(error.message === 'TIMEOUT', 'TIMEOUT error should be thrown');
}
});
it('should not update upon latest block when not started', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(ROPSTEN_CHAIN_ID),
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
// reply with a valid request for any supported network, so that this test has every opportunity to fail
for (const network of [
GOERLI,
KOVAN,
MAINNET,
RINKEBY,
ROPSTEN,
'undefined',
]) {
nock(
`https://api${
network === MAINNET ? '' : `-${network.toLowerCase()}`
}.etherscan.io`,
)
.get(/api.+/u)
.reply(
200,
JSON.stringify({
status: '1',
result: [getFakeEtherscanTransaction()],
}),
);
}
const updateStateStub = sinon.stub(
incomingTransactionsController.store,
'updateState',
);
const updateStateCalled = waitUntilCalled(
updateStateStub,
incomingTransactionsController.store,
);
const putStateStub = sinon.stub(
incomingTransactionsController.store,
'putState',
);
const putStateCalled = waitUntilCalled(
putStateStub,
incomingTransactionsController.store,
);
try {
await Promise.race([
updateStateCalled(),
putStateCalled(),
new Promise((_, reject) => {
setTimeout(() => reject(new Error('TIMEOUT')), SET_STATE_TIMEOUT);
}),
]);
assert.fail('Update state should not have been called');
} catch (error) {
assert(error.message === 'TIMEOUT', 'TIMEOUT error should be thrown');
}
});
it('should not update upon latest block when stopped', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(ROPSTEN_CHAIN_ID),
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
// reply with a valid request for any supported network, so that this test has every opportunity to fail
for (const network of [
GOERLI,
KOVAN,
MAINNET,
RINKEBY,
ROPSTEN,
'undefined',
]) {
nock(
`https://api${
network === MAINNET ? '' : `-${network.toLowerCase()}`
}.etherscan.io`,
)
.get(/api.+/u)
.reply(
200,
JSON.stringify({
status: '1',
result: [getFakeEtherscanTransaction()],
}),
);
}
const updateStateStub = sinon.stub(
incomingTransactionsController.store,
'updateState',
);
const updateStateCalled = waitUntilCalled(
updateStateStub,
incomingTransactionsController.store,
);
const putStateStub = sinon.stub(
incomingTransactionsController.store,
'putState',
);
const putStateCalled = waitUntilCalled(
putStateStub,
incomingTransactionsController.store,
);
incomingTransactionsController.stop();
try {
await Promise.race([
updateStateCalled(),
putStateCalled(),
new Promise((_, reject) => {
setTimeout(() => reject(new Error('TIMEOUT')), SET_STATE_TIMEOUT);
}),
]);
assert.fail('Update state should not have been called');
} catch (error) {
assert(error.message === 'TIMEOUT', 'TIMEOUT error should be thrown');
}
});
it('should update when the selected address changes and on supported network', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(ROPSTEN_CHAIN_ID),
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
const NEW_MOCK_SELECTED_ADDRESS = `${MOCK_SELECTED_ADDRESS}9`;
const startBlock = getNonEmptyInitState()
.incomingTxLastFetchedBlocksByNetwork[ROPSTEN];
nock('https://api-ropsten.etherscan.io')
.get(
`/api?module=account&action=txlist&address=${NEW_MOCK_SELECTED_ADDRESS}&tag=latest&page=1&startBlock=${startBlock}`,
)
.reply(
200,
JSON.stringify({
status: '1',
result: [getFakeEtherscanTransaction(NEW_MOCK_SELECTED_ADDRESS)],
}),
);
const updateStateStub = sinon.stub(
incomingTransactionsController.store,
'updateState',
);
const updateStateCalled = waitUntilCalled(
updateStateStub,
incomingTransactionsController.store,
);
const subscription = incomingTransactionsController.preferencesController.store.subscribe.getCall(
1,
).args[0];
// The incoming transactions controller will always skip the first event
// We need to call subscription twice to test the event handling
// TODO: stop skipping the first event
await subscription({ selectedAddress: MOCK_SELECTED_ADDRESS });
await subscription({ selectedAddress: NEW_MOCK_SELECTED_ADDRESS });
await updateStateCalled();
const actualState = incomingTransactionsController.store.getState();
const generatedTxId = actualState?.incomingTransactions?.['0xfake']?.id;
const actualStateWithoutGenerated = cloneDeep(actualState);
delete actualStateWithoutGenerated?.incomingTransactions?.['0xfake']?.id;
assert.ok(
typeof generatedTxId === 'number' && generatedTxId > 0,
'Generated transaction ID should be a positive number',
);
assert.deepStrictEqual(
actualStateWithoutGenerated,
{
incomingTransactions: {
...getNonEmptyInitState().incomingTransactions,
'0xfake': {
blockNumber: '10',
hash: '0xfake',
metamaskNetworkId: ROPSTEN_NETWORK_ID,
chainId: ROPSTEN_CHAIN_ID,
status: TRANSACTION_STATUSES.CONFIRMED,
time: 16000000000000000,
type: TRANSACTION_TYPES.INCOMING,
txParams: {
from: '0xfake',
gas: '0x0',
gasPrice: '0x0',
nonce: '0x64',
to: '0x01019',
value: '0x0',
},
},
},
incomingTxLastFetchedBlocksByNetwork: {
...getNonEmptyInitState().incomingTxLastFetchedBlocksByNetwork,
[ROPSTEN]: 11,
},
},
'State should have been updated after first block was received',
);
});
it('should not update when the selected address changes and not on supported network', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: { ...getMockBlockTracker() },
networkController: getMockNetworkController(),
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
const NEW_MOCK_SELECTED_ADDRESS = `${MOCK_SELECTED_ADDRESS}9`;
// reply with a valid request for any supported network, so that this test has every opportunity to fail
for (const network of [
GOERLI,
KOVAN,
MAINNET,
RINKEBY,
ROPSTEN,
'undefined',
]) {
nock(
`https://api${
network === MAINNET ? '' : `-${network.toLowerCase()}`
}.etherscan.io`,
)
.get(/api.+/u)
.reply(
200,
JSON.stringify({
status: '1',
result: [getFakeEtherscanTransaction(NEW_MOCK_SELECTED_ADDRESS)],
}),
);
}
const updateStateStub = sinon.stub(
incomingTransactionsController.store,
'updateState',
);
const updateStateCalled = waitUntilCalled(
updateStateStub,
incomingTransactionsController.store,
);
const putStateStub = sinon.stub(
incomingTransactionsController.store,
'putState',
);
const putStateCalled = waitUntilCalled(
putStateStub,
incomingTransactionsController.store,
);
const subscription = incomingTransactionsController.preferencesController.store.subscribe.getCall(
1,
).args[0];
// The incoming transactions controller will always skip the first event
// We need to call subscription twice to test the event handling
// TODO: stop skipping the first event
await subscription({ selectedAddress: MOCK_SELECTED_ADDRESS });
await subscription({ selectedAddress: NEW_MOCK_SELECTED_ADDRESS });
try {
await Promise.race([
updateStateCalled(),
putStateCalled(),
new Promise((_, reject) => {
setTimeout(() => reject(new Error('TIMEOUT')), SET_STATE_TIMEOUT);
}),
]);
assert.fail('Update state should not have been called');
} catch (error) {
assert(error.message === 'TIMEOUT', 'TIMEOUT error should be thrown');
}
});
it('should update when switching to a supported network', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(ROPSTEN_CHAIN_ID),
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
const startBlock = getNonEmptyInitState()
.incomingTxLastFetchedBlocksByNetwork[ROPSTEN];
nock('https://api-ropsten.etherscan.io')
.get(
`/api?module=account&action=txlist&address=${MOCK_SELECTED_ADDRESS}&tag=latest&page=1&startBlock=${startBlock}`,
)
.reply(
200,
JSON.stringify({
status: '1',
result: [getFakeEtherscanTransaction()],
}),
);
const updateStateStub = sinon.stub(
incomingTransactionsController.store,
'updateState',
);
const updateStateCalled = waitUntilCalled(
updateStateStub,
incomingTransactionsController.store,
);
const subscription = incomingTransactionsController.networkController.on.getCall(
0,
).args[1];
incomingTransactionsController.networkController = getMockNetworkController(
ROPSTEN_CHAIN_ID,
);
await subscription(ROPSTEN);
await updateStateCalled();
const actualState = incomingTransactionsController.store.getState();
const generatedTxId = actualState?.incomingTransactions?.['0xfake']?.id;
const actualStateWithoutGenerated = cloneDeep(actualState);
delete actualStateWithoutGenerated?.incomingTransactions?.['0xfake']?.id;
assert.ok(
typeof generatedTxId === 'number' && generatedTxId > 0,
'Generated transaction ID should be a positive number',
);
assert.deepStrictEqual(
actualStateWithoutGenerated,
{
incomingTransactions: {
...getNonEmptyInitState().incomingTransactions,
'0xfake': {
blockNumber: '10',
hash: '0xfake',
metamaskNetworkId: ROPSTEN_NETWORK_ID,
chainId: ROPSTEN_CHAIN_ID,
status: TRANSACTION_STATUSES.CONFIRMED,
time: 16000000000000000,
type: TRANSACTION_TYPES.INCOMING,
txParams: {
from: '0xfake',
gas: '0x0',
gasPrice: '0x0',
nonce: '0x64',
to: '0x0101',
value: '0x0',
},
},
},
incomingTxLastFetchedBlocksByNetwork: {
...getNonEmptyInitState().incomingTxLastFetchedBlocksByNetwork,
[ROPSTEN]: 11,
},
},
'State should have been updated after first block was received',
);
});
it('should not update when switching to an unsupported network', async function () {
const networkController = getMockNetworkController(ROPSTEN_CHAIN_ID);
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController,
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
// reply with a valid request for any supported network, so that this test has every opportunity to fail
for (const network of [
GOERLI,
KOVAN,
MAINNET,
RINKEBY,
ROPSTEN,
'undefined',
]) {
nock(
`https://api${
network === MAINNET ? '' : `-${network.toLowerCase()}`
}.etherscan.io`,
)
.get(/api.+/u)
.reply(
200,
JSON.stringify({
status: '1',
result: [getFakeEtherscanTransaction()],
}),
);
}
const updateStateStub = sinon.stub(
incomingTransactionsController.store,
'updateState',
);
const updateStateCalled = waitUntilCalled(
updateStateStub,
incomingTransactionsController.store,
);
const putStateStub = sinon.stub(
incomingTransactionsController.store,
'putState',
);
const putStateCalled = waitUntilCalled(
putStateStub,
incomingTransactionsController.store,
);
const subscription = incomingTransactionsController.networkController.on.getCall(
0,
).args[1];
networkController.getCurrentChainId = () => FAKE_CHAIN_ID;
await subscription();
try {
await Promise.race([
updateStateCalled(),
putStateCalled(),
new Promise((_, reject) => {
setTimeout(() => reject(new Error('TIMEOUT')), SET_STATE_TIMEOUT);
}),
]);
assert.fail('Update state should not have been called');
} catch (error) {
assert(error.message === 'TIMEOUT', 'TIMEOUT error should be thrown');
}
});
});
describe('_getDataForUpdate', function () {
it('should call fetchAll with the correct params when passed a new block number and the current network has no stored block', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(ROPSTEN_CHAIN_ID),
preferencesController: getMockPreferencesController(),
initState: getEmptyInitState(),
},
);
incomingTransactionsController._fetchAll = sinon.stub().returns({});
await incomingTransactionsController._getDataForUpdate({
address: 'fakeAddress',
chainId: ROPSTEN_CHAIN_ID,
newBlockNumberDec: 999,
});
assert(incomingTransactionsController._fetchAll.calledOnce);
assert.deepEqual(
incomingTransactionsController._fetchAll.getCall(0).args,
['fakeAddress', 999, ROPSTEN_CHAIN_ID],
);
});
it('should call fetchAll with the correct params when passed a new block number but the current network has a stored block', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(ROPSTEN_CHAIN_ID),
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
incomingTransactionsController._fetchAll = sinon.stub().returns({});
await incomingTransactionsController._getDataForUpdate({
address: 'fakeAddress',
chainId: ROPSTEN_CHAIN_ID,
newBlockNumberDec: 999,
});
assert(incomingTransactionsController._fetchAll.calledOnce);
assert.deepEqual(
incomingTransactionsController._fetchAll.getCall(0).args,
['fakeAddress', 4, ROPSTEN_CHAIN_ID],
);
});
it('should return the expected data', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(ROPSTEN_CHAIN_ID),
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
incomingTransactionsController._fetchAll = sinon.stub().returns({
latestIncomingTxBlockNumber: 444,
txs: [{ id: 555 }],
});
const result = await incomingTransactionsController._getDataForUpdate({
address: 'fakeAddress',
chainId: ROPSTEN_CHAIN_ID,
});
assert.deepEqual(result, {
latestIncomingTxBlockNumber: 444,
newTxs: [{ id: 555 }],
currentIncomingTxs: {
'0x123456': { id: 777 },
},
currentBlocksByNetwork: {
[GOERLI]: 1,
[KOVAN]: 2,
[MAINNET]: 3,
[RINKEBY]: 5,
[ROPSTEN]: 4,
},
fetchedBlockNumber: 4,
chainId: ROPSTEN_CHAIN_ID,
});
});
});
describe('_updateStateWithNewTxData', function () {
const MOCK_INPUT_WITHOUT_LASTEST = {
newTxs: [{ id: 555, hash: '0xfff' }],
currentIncomingTxs: {
'0x123456': { id: 777, hash: '0x123456' },
},
currentBlocksByNetwork: {
[GOERLI]: 1,
[KOVAN]: 2,
[MAINNET]: 3,
[RINKEBY]: 5,
[ROPSTEN]: 4,
},
fetchedBlockNumber: 1111,
chainId: ROPSTEN_CHAIN_ID,
};
const MOCK_INPUT_WITH_LASTEST = {
...MOCK_INPUT_WITHOUT_LASTEST,
latestIncomingTxBlockNumber: 444,
};
it('should update state with correct blockhash and transactions when passed a truthy latestIncomingTxBlockNumber', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(ROPSTEN_CHAIN_ID),
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
sinon.spy(incomingTransactionsController.store, 'updateState');
await incomingTransactionsController._updateStateWithNewTxData(
MOCK_INPUT_WITH_LASTEST,
);
assert(incomingTransactionsController.store.updateState.calledOnce);
assert.deepEqual(
incomingTransactionsController.store.updateState.getCall(0).args[0],
{
incomingTxLastFetchedBlocksByNetwork: {
...MOCK_INPUT_WITH_LASTEST.currentBlocksByNetwork,
[ROPSTEN]: 445,
},
incomingTransactions: {
'0x123456': { id: 777, hash: '0x123456' },
'0xfff': { id: 555, hash: '0xfff' },
},
},
);
});
it('should update state with correct blockhash and transactions when passed a falsy latestIncomingTxBlockNumber', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(ROPSTEN_CHAIN_ID),
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
sinon.spy(incomingTransactionsController.store, 'updateState');
await incomingTransactionsController._updateStateWithNewTxData(
MOCK_INPUT_WITHOUT_LASTEST,
);
assert(incomingTransactionsController.store.updateState.calledOnce);
assert.deepEqual(
incomingTransactionsController.store.updateState.getCall(0).args[0],
{
incomingTxLastFetchedBlocksByNetwork: {
...MOCK_INPUT_WITH_LASTEST.currentBlocksByNetwork,
[ROPSTEN]: 1112,
},
incomingTransactions: {
'0x123456': { id: 777, hash: '0x123456' },
'0xfff': { id: 555, hash: '0xfff' },
},
},
);
});
});
describe('_fetchTxs', function () {
const mockFetch = sinon.stub().returns(
Promise.resolve({
json: () => Promise.resolve({ someKey: 'someValue' }),
}),
);
let tempFetch;
beforeEach(function () {
tempFetch = window.fetch;
window.fetch = mockFetch;
});
afterEach(function () {
window.fetch = tempFetch;
mockFetch.resetHistory();
});
it('should call fetch with the expected url when passed an address, block number and supported network', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(ROPSTEN_CHAIN_ID),
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
await incomingTransactionsController._fetchTxs(
'0xfakeaddress',
'789',
ROPSTEN_CHAIN_ID,
);
assert(mockFetch.calledOnce);
assert.equal(
mockFetch.getCall(0).args[0],
`https://api-${ROPSTEN}.etherscan.io/api?module=account&action=txlist&address=0xfakeaddress&tag=latest&page=1&startBlock=789`,
);
});
it('should call fetch with the expected url when passed an address, block number and MAINNET', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(MAINNET_CHAIN_ID),
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
await incomingTransactionsController._fetchTxs(
'0xfakeaddress',
'789',
MAINNET_CHAIN_ID,
);
assert(mockFetch.calledOnce);
assert.equal(
mockFetch.getCall(0).args[0],
`https://api.etherscan.io/api?module=account&action=txlist&address=0xfakeaddress&tag=latest&page=1&startBlock=789`,
);
});
it('should call fetch with the expected url when passed an address and supported network, but a falsy block number', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(ROPSTEN_CHAIN_ID),
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
await incomingTransactionsController._fetchTxs(
'0xfakeaddress',
null,
ROPSTEN_CHAIN_ID,
);
assert(mockFetch.calledOnce);
assert.equal(
mockFetch.getCall(0).args[0],
`https://api-${ROPSTEN}.etherscan.io/api?module=account&action=txlist&address=0xfakeaddress&tag=latest&page=1`,
);
});
it('should return the results from the fetch call, plus the address and currentNetworkID, when passed an address, block number and supported network', async function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(ROPSTEN_CHAIN_ID),
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
const result = await incomingTransactionsController._fetchTxs(
'0xfakeaddress',
'789',
ROPSTEN_CHAIN_ID,
);
assert(mockFetch.calledOnce);
assert.deepEqual(result, {
someKey: 'someValue',
address: '0xfakeaddress',
chainId: ROPSTEN_CHAIN_ID,
});
});
});
describe('_processTxFetchResponse', function () {
it('should return a null block number and empty tx array if status is 0', function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(ROPSTEN_CHAIN_ID),
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
const result = incomingTransactionsController._processTxFetchResponse({
status: '0',
result: [{ id: 1 }],
address: '0xfakeaddress',
});
assert.deepEqual(result, {
latestIncomingTxBlockNumber: null,
txs: [],
});
});
it('should return a null block number and empty tx array if the passed result array is empty', function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(ROPSTEN_CHAIN_ID),
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
const result = incomingTransactionsController._processTxFetchResponse({
status: '1',
result: [],
address: '0xfakeaddress',
});
assert.deepEqual(result, {
latestIncomingTxBlockNumber: null,
txs: [],
});
});
it('should return the expected block number and tx list when passed data from a successful fetch', function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(ROPSTEN_CHAIN_ID),
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
incomingTransactionsController._normalizeTxFromEtherscan = (tx) => ({
...tx,
currentNetworkID: ROPSTEN_NETWORK_ID,
normalized: true,
});
const result = incomingTransactionsController._processTxFetchResponse({
status: '1',
address: '0xfakeaddress',
chainId: ROPSTEN_CHAIN_ID,
result: [
{
hash: '0xabc123',
txParams: {
to: '0xfakeaddress',
},
blockNumber: 5000,
time: 10,
},
{
hash: '0xabc123',
txParams: {
to: '0xfakeaddress',
},
blockNumber: 5000,
time: 10,
},
{
hash: '0xabc1234',
txParams: {
to: '0xfakeaddress',
},
blockNumber: 5000,
time: 9,
},
{
hash: '0xabc12345',
txParams: {
to: '0xfakeaddress',
},
blockNumber: 5001,
time: 11,
},
{
hash: '0xabc123456',
txParams: {
to: '0xfakeaddress',
},
blockNumber: 5001,
time: 12,
},
{
hash: '0xabc1234567',
txParams: {
to: '0xanotherFakeaddress',
},
blockNumber: 5002,
time: 13,
},
],
});
assert.deepEqual(result, {
latestIncomingTxBlockNumber: 5001,
txs: [
{
hash: '0xabc1234',
txParams: {
to: '0xfakeaddress',
},
blockNumber: 5000,
time: 9,
normalized: true,
currentNetworkID: ROPSTEN_NETWORK_ID,
},
{
hash: '0xabc123',
txParams: {
to: '0xfakeaddress',
},
blockNumber: 5000,
time: 10,
normalized: true,
currentNetworkID: ROPSTEN_NETWORK_ID,
},
{
hash: '0xabc12345',
txParams: {
to: '0xfakeaddress',
},
blockNumber: 5001,
time: 11,
normalized: true,
currentNetworkID: ROPSTEN_NETWORK_ID,
},
{
hash: '0xabc123456',
txParams: {
to: '0xfakeaddress',
},
blockNumber: 5001,
time: 12,
normalized: true,
currentNetworkID: ROPSTEN_NETWORK_ID,
},
],
});
});
});
describe('_normalizeTxFromEtherscan', function () {
it('should return the expected data when the tx is in error', function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(ROPSTEN_CHAIN_ID),
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
const result = incomingTransactionsController._normalizeTxFromEtherscan(
{
timeStamp: '4444',
isError: '1',
blockNumber: 333,
from: '0xa',
gas: '11',
gasPrice: '12',
nonce: '13',
to: '0xe',
value: '15',
hash: '0xg',
},
ROPSTEN_CHAIN_ID,
);
assert.deepEqual(result, {
blockNumber: 333,
id: 54321,
metamaskNetworkId: ROPSTEN_NETWORK_ID,
chainId: ROPSTEN_CHAIN_ID,
status: TRANSACTION_STATUSES.FAILED,
time: 4444000,
txParams: {
from: '0xa',
gas: '0xb',
gasPrice: '0xc',
nonce: '0xd',
to: '0xe',
value: '0xf',
},
hash: '0xg',
type: TRANSACTION_TYPES.INCOMING,
});
});
it('should return the expected data when the tx is not in error', function () {
const incomingTransactionsController = new IncomingTransactionsController(
{
blockTracker: getMockBlockTracker(),
networkController: getMockNetworkController(ROPSTEN_CHAIN_ID),
preferencesController: getMockPreferencesController(),
initState: getNonEmptyInitState(),
},
);
const result = incomingTransactionsController._normalizeTxFromEtherscan(
{
timeStamp: '4444',
isError: '0',
blockNumber: 333,
from: '0xa',
gas: '11',
gasPrice: '12',
nonce: '13',
to: '0xe',
value: '15',
hash: '0xg',
},
ROPSTEN_CHAIN_ID,
);
assert.deepEqual(result, {
blockNumber: 333,
id: 54321,
metamaskNetworkId: ROPSTEN_NETWORK_ID,
chainId: ROPSTEN_CHAIN_ID,
status: TRANSACTION_STATUSES.CONFIRMED,
time: 4444000,
txParams: {
from: '0xa',
gas: '0xb',
gasPrice: '0xc',
nonce: '0xd',
to: '0xe',
value: '0xf',
},
hash: '0xg',
type: TRANSACTION_TYPES.INCOMING,
});
});
});
});