MyCrypto/common/features/transaction/network/sagas.spec.ts

464 lines
15 KiB
TypeScript
Raw Normal View History

Refactor redux pattern from top-level to ducks (#1936) * Add a new route for AddressBook * Further templating of the AddressBook view * Add initial functionality to handle a table of existing address labels * Make the linter happy * Adjust paths * Factor out TableRow and add common functionality * Add initial Redux boilerplate for addressBook | fix minor linting issues * Swap out terminology and types * Connect up to Redux * Connect data for AddressBookTable to Redux * Use temporary fields for addition * Remove alignment and index column * Stopping point * Adjust the sizing of rows to be consistent * Initial implementation of a dropdown for the address field * Minor styling to dropdown * Stopping point * Apply a focus concept onto the factory * Add keyboard controls for the address field dropdown * Adjust label of address field when it matches an addressBook entry * Properly handle attempting to blur a non-existent component * Minor styling changes on dropdown box * Standardize address casing, add accessibility to dropdown * Create an addressLabel component * Pass refs correctly and fix some typings * Exact version * Add module name mapping for shared/keycodes * addressBook reducer tests * Add functionality to DeterministicModal * Minor changes / Add test for addressBook selectors * Move out AddressBookTable to a component * Typing, translation and restructuring * More typing and translation fixes * More linting fixes * More type changes * Variable name for dropdown background * Fix TS type errors, lint errors, remove unused props * Used a different selector and removed method: AddressBookTable * Linter was mad * Linter mad again :( * Add a translation and adjust styling of AddressBookTable * Move the onBlur to a class method * Prevent the default behavior of up/down/enter for dropdown * Let's do it this way instead * Adjust the styling on DeterministicWalletModal labels * Change `AddressBookTable` into a pseudo-table using section and div * Use readable keys vs. keycodes * Put the dropdown in InputFactory and position it correctly * Sanitation of label adding and changing * Prevent duplicate labels in AddressBook and Row * Add a box shadow and use `invalid` class insted of custom * Use emphasis vs strong for address in dropdown * Display the label undernearth the input vs. changing it * Isolate AccountAddress into its own component * Introduce interactivity to AccountAddress * Fully incorporate with Redux * Validation for AccountAddress * Add validation notifications for address field on AddressBookTable * Minor formatting * Adjust wrappage for optimal flexboxxing * Make AddressBookTable responsive * Show an invalid input in "real time" instead of only on submit * Real time input validation for AddressBookTableRow * Responsive-ize the To address dropdown * Hide identicons instead at small enough screen sizes * Fix repsonsiveness of dropdown further * Fix responsiveness of table rows and inputs * Truncate account info and switch identicons to the right for consistency * Use classnames instead of targetting element directly for DWM * Display a notice if the entered query doesnt match a label and isnt an addr * Don't show an error on the To address if its a label entry * Display an error under AddressBookTableRow in real time * Display errors in real time for AddressBookTable temp inputs * Add realtime validation to AccountAddress * Ensure toChecksumAddress is used when entering labels to address manager * Show errors even after blurring. * Create a ducks/ implementation for addressBook * Duck-ize notifications * Duck-ize customTokens * Duck-ize deterministicWallets * Only show errors on address/label entry if they have been blurred * On certain inputs, show an invalid input immediately * spec files in same directory * Rename top-level redux directory * Duck-ize gas * Add displayed errors for labels with 0x and labels containing ens * Move ENS checking validation out * Add a saga for addLabelForAddress * Completely revamp the redux side of Address Manager and test it all * Adjust components to use new redux addressBook * Incorporate new redux into AddressBookTableRow and clean up for linter * Make linter and tests happy * Another reduxy overhaul * Still fixing it * More redux updates * Finalize redux stuff. * Incorporate new reduxy way into AddressBookTable & Row * Incorporate redux changes into Account Address * Small tests fix * Add and fix some selector tests * Addressing Will's comments * Shortened visibility class for line length reasons. * Incorporate ducks pattern on updates addressBook * Fix typeerror * Migrate messages to ducks * For Henry * Duckify onboardStatus * Duckify paritySigner * Duckify rates * Duckify transactions * Duckerize wallet * Reduckerate config * Adjust exports and tests of every duck so far * Duckify ENS * Duckerificate schedule * Duckificate swap * Actually use the new sagas; fix a circular dependency problem. * Duckify transaction (phew) * Add basics to redux/ directory * Remove non-ducked redux stuff * First sweep of redux/ directory * Combine redundant imports * Fix more linting stuff. * A few more type fixes * Welp... now I know not to use index. * Sweep components/ * Sweep through containers/ * Im really starting to get frustrated * The dawn of a new age. * Linter fixes. * De-flatten config/ reducers * Do my thang on config selectors * Adjust all references to config * Split up ens reducers * Wrap up de-nesting ENS * Big boy refactor * Split transaction into its reducers * Fix reducers in transaction/ * Stopping point * Adjust references to transaction from components * Fix references to selectors * Nest broadcast actions * Nest field actions * Nest meta actions * Nest network actions * Nest sign actions * Nest broadcast types * Nested fields types * Nest meta types * Nested network types * Nested sign types * Implement transaction saga changes * Huh? No prepush problems? * Update snappshot * Reintroduce deleted tests * A few missing tests found * Found three missing transaction tests * Found more tests * Found the rest of the tests, woohoo. * Renamed TypeKeys in TRANSACTION * Specify TRANSACTION_BROADCAST * Pretty up these imports * Specify TRANSACTION_FIELDS * Specify TRANSACTION_META * Specify TRANSACTION_NETWORK * Specify TRANSACTION_SIGN * Adjust imports and add translations * Update config snapshot * Post-merge * Temporary fix for DW/Config sagas so Daniel can continue smoke testing * Remove first circulat dependency * Fix more circular dependencies * Properly structure config indices * Further restructure config * Prepare for idea * Target directly from within features/ * Remove that circular dependency -- woohoo * Remove the circular dependency from Web3Wallet, temporarily comment some tests pending assistance * Un-comment the component-in-redux phenomenon * Move onLoad to the store file * Adjust addressBook imports/exports * Adjusted imports/exports for customTokens * Adjust imports/exports of deterministicWallets * Adjust imports/exports of ens * Restructure imports/exports of gas * Restructure imports/exports for message * Adjust imports/exports of notifications * Restructure onboardStatus imports/exports * Restructure paritySigner imports/exports * Restructure rates imports/exports * Restructure schedule imports/exports * Fix broadcastweb3handler test * Restructure swap imports/exports/ * Restructure transactionS imports/exports * Restructured wallet imports and exports * Hoist all necessary selectors aside from config/**/* and transaction/**/* * Hoist all top-level selectors from transaction * [Fix] Estimate Gas on Value Field Change (#1942) @ skubakdj * Implement right-click context menu (#1780) @ bryanwb * No Private Keys Online (#1466) @ wbobeirne * Fix Stuck Node on Metamask Logout (#1951) @ wbobeirne * [Fix] Make ENS Value Consistent (#1956) @ skubakdj * Auto token add (#1808) @ HenryNguyen5 * Electron Ledger + Trezor Support (#1836) @ wbobeirne * Fix Context Menu Popup Parameters (#1957) * Add RSK network w/ network agnostic refactors (#1939) @ wbobeirne * Change displayed notification back in helpers.tsx * Remove newline on shell files * Re-add newlines * Remove newling on .travis.yml * Prettier two files * Re-add index.scss import in OnboardModal * Restructure transaction subdirectories * Everything in transaction/ except for sagas * Restructure transaction imports/exports * Nest broadcast sagas * Nest fields * Nest meta * Nest network * Nest sign * Use generic names for reduxy stuff in the same directory to save space * Do everything every in the whole wide world
2018-06-18 01:53:00 +00:00
import BN from 'bn.js';
import { buffers, delay } from 'redux-saga';
import {
call,
select,
put,
take,
apply,
actionChannel,
race,
cancel,
fork
} from 'redux-saga/effects';
import { cloneableGenerator, SagaIteratorClone, createMockTask } from 'redux-saga/utils';
import { Wei, Nonce } from 'libs/units';
import { makeTransaction, getTransactionFields } from 'libs/transaction';
import * as derivedSelectors from 'features/selectors';
import * as configMetaTypes from 'features/config/meta/types';
import * as configMetaSelectors from 'features/config/meta/selectors';
import * as configNodesSelectors from 'features/config/nodes/selectors';
import { walletTypes, walletSelectors } from 'features/wallet';
import { scheduleActions, scheduleSelectors } from 'features/schedule';
import { notificationsActions } from 'features/notifications';
import { transactionFieldsTypes, transactionFieldsActions } from '../fields';
import * as transactionTypes from '../types';
import * as types from './types';
import * as actions from './actions';
import * as sagas from './sagas';
describe('Network Sagas', () => {
describe('From', () => {
describe('handleFromRequest*', () => {
const walletInst: any = {
getAddressString: jest.fn()
};
const fromAddress = '0xa';
const gens: any = {};
gens.gen = cloneableGenerator(sagas.handleFromRequest)();
let random: () => number;
beforeAll(() => {
random = Math.random;
Math.random = () => 0.001;
});
afterAll(() => {
Math.random = random;
});
it('should select getWalletInst', () => {
expect(gens.gen.next().value).toEqual(select(walletSelectors.getWalletInst));
});
it('should handle errors as expected', () => {
gens.clone = gens.gen.clone();
expect(gens.clone.next(false).value).toEqual(
put(
notificationsActions.showNotification(
'warning',
'Your wallets address could not be fetched'
)
)
);
expect(gens.clone.next().value).toEqual(put(actions.getFromFailed()));
expect(gens.clone.next().done).toEqual(true);
});
it('should apply walletInst.getAddress', () => {
expect(gens.gen.next(walletInst).value).toEqual(
apply(walletInst, walletInst.getAddressString)
);
});
it('should put getFromSucceeded', () => {
expect(gens.gen.next(fromAddress).value).toEqual(
put(actions.getFromSucceeded(fromAddress))
);
});
});
});
describe('Gas', () => {
describe('shouldEstimateGas*', () => {
const offline = false;
const autoGasLimitEnabled = true;
const addressMessage = undefined;
const transaction: any = 'transaction';
const tx = { transaction };
const rest: any = {
mock1: 'mock1',
mock2: 'mock2'
};
const transactionFields = {
gasLimit: 'gasLimit',
gasPrice: 'gasPrice',
nonce: 'nonce',
chainId: 'chainId',
...rest
};
const action: any = {
type: transactionFieldsTypes.TransactionFieldsActions.TO_FIELD_SET,
payload: {
value: 'value',
raw: 'raw'
}
};
const gen = sagas.shouldEstimateGas();
it('should take expected types', () => {
expect(gen.next().value).toEqual(
take([
transactionFieldsTypes.TransactionFieldsActions.TO_FIELD_SET,
transactionFieldsTypes.TransactionFieldsActions.VALUE_FIELD_SET,
transactionFieldsTypes.TransactionFieldsActions.DATA_FIELD_SET,
transactionTypes.TransactionActions.ETHER_TO_TOKEN_SWAP,
transactionTypes.TransactionActions.TOKEN_TO_TOKEN_SWAP,
transactionTypes.TransactionActions.TOKEN_TO_ETHER_SWAP,
configMetaTypes.CONFIG_META.TOGGLE_AUTO_GAS_LIMIT
])
);
});
it('should select getOffline', () => {
expect(gen.next(action).value).toEqual(select(configMetaSelectors.getOffline));
});
it('should select autoGasLimitEnabled', () => {
expect(gen.next(offline).value).toEqual(select(configMetaSelectors.getAutoGasLimitEnabled));
});
it('should select getCurrentToAddressMessage', () => {
expect(gen.next(autoGasLimitEnabled).value).toEqual(
select(derivedSelectors.getCurrentToAddressMessage)
);
});
it('should select getTransaction', () => {
expect(gen.next(addressMessage).value).toEqual(select(derivedSelectors.getTransaction));
});
it('should call getTransactionFields with transaction', () => {
expect(gen.next(tx).value).toEqual(call(getTransactionFields, transaction));
});
it('should put estimatedGasRequested with rest', () => {
expect(gen.next(transactionFields).value).toEqual(put(actions.estimateGasRequested(rest)));
});
});
describe('estimateGas*', () => {
const offline = false;
const autoGasLimitEnabled = true;
const requestChan = 'requestChan';
const payload: any = {
mock1: 'mock1',
mock2: 'mock2'
};
const action = { payload };
const node: any = {
estimateGas: jest.fn()
};
const walletInst: any = {
getAddressString: jest.fn()
};
const from = '0xa';
const txObj = { ...payload, from };
const gasLimit = Wei('100');
const successfulGasEstimationResult = {
gasLimit
};
const unsuccessfulGasEstimationResult = {
gasLimit: null
};
const gasSetOptions = {
raw: gasLimit.toString(),
value: gasLimit
};
const gens: { [name: string]: any } = {};
gens.successCase = cloneableGenerator(sagas.estimateGas)();
let random: () => number;
beforeAll(() => {
random = Math.random;
Math.random = () => 0.001;
});
afterAll(() => {
Math.random = random;
});
it('should yield actionChannel', () => {
const expected = JSON.stringify(
actionChannel(types.TransactionNetworkActions.ESTIMATE_GAS_REQUESTED, buffers.sliding(1))
);
const result = JSON.stringify(gens.successCase.next().value);
expect(expected).toEqual(result);
});
it('should select autoGasLimit', () => {
expect(gens.successCase.next(requestChan).value).toEqual(
select(configMetaSelectors.getAutoGasLimitEnabled)
);
});
it('should select getOffline', () => {
expect(gens.successCase.next(autoGasLimitEnabled).value).toEqual(
select(configMetaSelectors.getOffline)
);
});
it('should take requestChan', () => {
expect(gens.successCase.next(offline).value).toEqual(take(requestChan));
});
it('should call delay', () => {
expect(gens.successCase.next(action).value).toEqual(call(delay, 250));
});
it('should select getNodeLib', () => {
expect(gens.successCase.next().value).toEqual(select(configNodesSelectors.getNodeLib));
});
it('should select getWalletInst', () => {
expect(gens.successCase.next(node).value).toEqual(select(walletSelectors.getWalletInst));
});
it('should apply walletInst', () => {
expect(gens.successCase.next(walletInst).value).toEqual(
apply(walletInst, walletInst.getAddressString)
);
});
it('should race between node.estimate gas and a 10 second timeout', () => {
gens.failCase = gens.successCase.clone();
expect(gens.successCase.next(from).value).toEqual(
race({
gasLimit: apply(node, node.estimateGas, [txObj]),
timeout: call(delay, 10000)
})
);
});
it('should select isSchedulingEnabled', () => {
gens.timeOutCase = gens.successCase.clone();
expect(gens.successCase.next(successfulGasEstimationResult).value).toEqual(
select(scheduleSelectors.isSchedulingEnabled)
);
});
it('should put setGasLimitField', () => {
gens.scheduleCase = gens.successCase.clone();
const notScheduling = null as any;
expect(gens.successCase.next(notScheduling).value).toEqual(
put(transactionFieldsActions.setGasLimitField(gasSetOptions))
);
});
it('should put setScheduleGasLimitField', () => {
const scheduling = { value: true } as any;
expect(gens.scheduleCase.next(scheduling).value).toEqual(
put(scheduleActions.setScheduleGasLimitField(gasSetOptions))
);
});
it('should put estimateGasSucceeded', () => {
expect(gens.successCase.next().value).toEqual(put(actions.estimateGasSucceeded()));
});
describe('when it times out', () => {
it('should put estimateGasTimedout ', () => {
expect(gens.timeOutCase.next(unsuccessfulGasEstimationResult).value).toEqual(
put(actions.estimateGasTimedout())
);
});
it('should call localGasEstimation', () => {
expect(gens.timeOutCase.next(actions.estimateGasFailed()).value).toEqual(
call(sagas.localGasEstimation, payload)
);
});
});
describe('when it throws', () => {
it('should catch and put estimateGasFailed', () => {
expect(gens.failCase.throw().value).toEqual(put(actions.estimateGasFailed()));
});
it('should call localGasEstimation', () => {
expect(gens.failCase.next(actions.estimateGasFailed()).value).toEqual(
call(sagas.localGasEstimation, payload)
);
});
});
});
describe('localGasEstimation', () => {
const payload: any = {
mock1: 'mock1',
mock2: 'mock2'
};
const tx = {
getBaseFee: jest.fn()
};
const gasLimit = Wei('100');
const gen = sagas.localGasEstimation(payload);
it('should call makeTransaction with payload', () => {
expect(gen.next().value).toEqual(call(makeTransaction, payload));
});
it('should apply tx.getBaseFee', () => {
expect(gen.next(tx).value).toEqual(apply(tx, tx.getBaseFee));
});
it('should put setGasLimitField', () => {
expect(gen.next(gasLimit).value).toEqual(
put(
transactionFieldsActions.setGasLimitField({
raw: gasLimit.toString(),
value: gasLimit
})
)
);
});
});
describe('setAddressMessageGasLimit*', () => {
const gens = cloneableGenerator(sagas.setAddressMessageGasLimit)();
const gen = gens.clone();
let noAutoGen: SagaIteratorClone;
let noMessageGen: SagaIteratorClone;
const addressMessage = {
gasLimit: 123456,
msg: 'Thanks for donating, er, investing in SCAM'
};
it('should select getAutoGasLimitEnabled', () => {
expect(gen.next().value).toEqual(select(configMetaSelectors.getAutoGasLimitEnabled));
});
it('should select getCurrentToAddressMessage', () => {
noAutoGen = gen.clone();
expect(gen.next(true).value).toEqual(select(derivedSelectors.getCurrentToAddressMessage));
});
it('should put setGasLimitField', () => {
noMessageGen = gen.clone();
expect(gen.next(addressMessage).value).toEqual(
put(
transactionFieldsActions.setGasLimitField({
raw: addressMessage.gasLimit.toString(),
value: new BN(addressMessage.gasLimit)
})
)
);
});
it('should do nothing if getAutoGasLimitEnabled is false', () => {
noAutoGen.next(false);
expect(noAutoGen.next(addressMessage).done).toBeTruthy();
});
it('should do nothing if getCurrentToAddressMessage is undefined', () => {
expect(noMessageGen.next(undefined).done).toBeTruthy();
});
});
});
describe('Nonce', () => {
describe('handleNonceRequest*', () => {
const nodeLib = {
getTransactionCount: jest.fn()
};
const walletInst = {
getAddressString: jest.fn()
};
const offline = false;
const fromAddress = 'fromAddress';
const retrievedNonce = '0xa';
const base10Nonce = Nonce(retrievedNonce);
const gens: any = {};
gens.gen = cloneableGenerator(sagas.handleNonceRequest)();
let random: () => number;
beforeAll(() => {
random = Math.random;
Math.random = () => 0.001;
});
afterAll(() => {
Math.random = random;
});
it('should select getNodeLib', () => {
expect(gens.gen.next().value).toEqual(select(configNodesSelectors.getNodeLib));
});
it('should select getWalletInstance', () => {
expect(gens.gen.next(nodeLib).value).toEqual(select(walletSelectors.getWalletInst));
});
it('should exit if being called without a wallet inst', () => {
gens.noWallet = gens.gen.clone();
gens.noWallet.next(null); // No wallet inst
expect(gens.noWallet.next(offline).done).toEqual(true);
});
it('should select getOffline', () => {
expect(gens.gen.next(walletInst).value).toEqual(select(configMetaSelectors.getOffline));
});
it('should exit if being called while offline', () => {
gens.offline = gens.gen.clone();
expect(gens.offline.next(true).done).toEqual(true);
});
it('should apply walletInst.getAddressString', () => {
expect(gens.gen.next(offline).value).toEqual(
apply(walletInst, walletInst.getAddressString)
);
});
it('should apply nodeLib.getTransactionCount', () => {
expect(gens.gen.next(fromAddress).value).toEqual(
apply(nodeLib, nodeLib.getTransactionCount, [fromAddress])
);
});
it('should put inputNonce', () => {
expect(gens.gen.next(retrievedNonce).value).toEqual(
put(transactionFieldsActions.inputNonce(base10Nonce.toString()))
);
});
it('should put getNonceSucceeded', () => {
expect(gens.gen.next().value).toEqual(put(actions.getNonceSucceeded(retrievedNonce)));
});
});
describe('handleNonceRequestWrapper*', () => {
const gen = sagas.handleNonceRequestWrapper();
const nonceRequest = createMockTask();
it('should fork handleNonceRequest', () => {
expect(gen.next().value).toEqual(fork(sagas.handleNonceRequest));
});
it('should take on WALLET_SET', () => {
expect(gen.next(nonceRequest).value).toEqual(take(walletTypes.WalletActions.SET));
});
it('should cancel nonceRequest', () => {
expect(gen.next().value).toEqual(cancel(nonceRequest));
});
it('should be done', () => {
expect(gen.next().done).toEqual(true);
});
});
});
});