MyCrypto/spec/sagas/config.spec.ts
HenryNguyen5 08d4ccbdae Productionize Transaction Stack (#456)
* export conditional input and hoc

* Move typings and fields out of send transaction

* Move fields into their own component for decoupled handling, use conditional inputs to simplify disabled components

* Handle hex and non hex strings automatically in BN conversion

* Fix handling of strings and numbers for BN

* add web3 fixes & comments

* Display short balances on deterministic modals

* add more tests, fix rounding

* Add spacer to balance sidebar network name

* Fix tsc error

* Add offline render CB

* Make more render callbacks

* Transform NonceField into its self contained component

* Remove styling from nonce field

* Better network handling in nonce cb

* Move network nonce initialization to componentDidMount

* Remove unessesary conditional input

* Make nonce component return a BN

* Simplify Query render cb

* Add gas query and token query render cbs

* Re-write address field component, strip out ENS name functionality for now

* Add address and data as unit types

* Cleanup Address Field component

* Export gas query

* Re-write gas field component

* Cleanup gas estimation check

* Re-write Data field

* Transaction field skeleton

* Export transaction field actions

* Rename fields to map to ethtx fields

* Make reducers for fields

* Fix reducer module exports

* Export reducer

* Formatting fix

* Type return of GasQuery

* Add transaction field getter / setter component

* Make transaction fields more flexible

* Formatting fix

* Split transaction fields component into two

* Remove erroneous prop

* Fix field naming to follow ethereum transaction fields

* Merge valid prop into componeent

* Change address field to be redux based

* Convert nonce field to redux based

* Make component for passing in current transaction

* Re-write Gas component to use redux state

* Reduxify data field component

* seperate transaction fields redux state into field data and meta data

* Rename SetTransactionFields to be singular

* Make render callback components for getting/setting meta fields

* Add non-zero option prop for token balance render cb

* re-write unit dropdown component to be redux based

* Make ether the first option

* Fix tsc error on tokenquery

* Handle query string default values in unit drop down

* Add thunks to package

* Add helper function for encoding transfer data

* Handle co-dependencies between fields via thunks, seperate value fields into ether and token based

* Fix wrong typing

* Add token metafield as export

* Start scaffolding out amount field component

* Make render cbs for conditional selection of value and balance

* Make render callbacks nullable

* Progress commit -- get dynaming swapping between tokens and ether working

* Get gas estimation working between ether and tokens

* Remove nonce from breaking gas estimation

* Add better validation for amount field

* Add 500ms debounce to gas saga

* Self contain custom message component

* Add web3 awareness to wallet render cb

* Add render cb for checking if  wallet is unlocked

* Cleanup inline typing

* export available params

* Add render cb to render component when a query string exists

* Add boolean callback param that check that the transaction is filled by user

* Remove uneeded typings from send transaction

* Fix misnomer

* Self contain generate transaction button

* Compartmentalize more send transaction components

* Add query string warning, custom message and generate tx button to fields

* Cleanup send tx component with new components

* export render callbacks

* saga transaction scaffolding

* make gas saga fully declarative

* transaction lib renaming

* Seperate gasprice into its own generator

* Make action creators for tx sign actions

* Clean up signing saga, introduce reducer for signing, make HW wallet libs compatible with new tx format, fix some typing with ethereumjs-tx

* Add TransactionComparison component

* Add pushTx

* Progress commit --  Streamline web3 and local signing / broadcasting flows. Need to still implement reducers for broadcasting and notifications

* Get local transaction broadcasting working

* re-write confirmation modal to be redux based

* Fix spacing and import

* Move confirmation modal to be attached to send button, create send button

* Properly handle broadcasting for conf modal

* Handle gas cost > balance for send everything

* Add signing status as its own component (#454)

* Fix ledger errors not showing on notifs

* Make dedicated actions for swapping from tokens to ether and ether to tokens

* Split actionTypes file

* Cleanup comments

* Cleanup comments

* Fix various tsc errors

* Lay down infrastructure for saving configurations per-wallet.

* Add pending and rejected states properly to token values.

* Add custom token form improvements.

* Fix metamask transaction errors

* Fix send entire balance estimation

* Fix add token form from never being enabled.

* Initial pass at account tab with send and view wallet tabs.

* Fix inactive tab.

* Hide private key, toggling

* Progress commit -- Replacing render callbacks with selectors, put  validation logic in sagas

* Moved the restore keystore functionality to view wallet info, and put it in a modal / util file.

* Fix navigation link active

* Force read only wallets to info tab.

* Remove commented code

* Saga-ify send everything

* Scan for new tokens, track saved tokens, only request tracked tokens on initial load.

* Add custom token to current wallets tracked tokens.

* Rework remove token icon.

* Adjust button margin

* Remove the rest of the needless render callbacks for selectors, sagaify nonce

* Bug fix send transaction

* remove unused redux-thunk

* Move fields to general components

* Clean up saga structure

* Refactor broadcast tx

* Implement better validation logic, get contract deploy working

* PR feedback.

* Convert tokenbalances component to connected redux component.

* Addressed feedback from Henry.

* Progress commit -- Implement Interact logic, needs manual testing

* Get rid of commented code

* move exports after declarations

* add tests, rough draft

* Get contract method calls working

* Bugfix contracts

* Cleanup hex prefixing

* Reset transaction state on wallet change

* Get rid of old send transaction component

* Disable sign transaction button when network request is underway

* Flatten send button tree, make nonce human readable in confirmation modal

* Add ghetto cost breakdown component, fix token field validation

* Create Generic SubTab and use in Send

* MVP of mnemonics with sub-tabs in Create Wallet view.

* Do dynamic revalidation

* move exports after declarations

* add forgotten signing tests

* update token spec

* update currentValue spec

* update validationHelpers spec

* Address TODO - use injected history to push navigation state instead of hardcoding window.location

* Use SubTabs in Contracts

* Fix revertPath prop for AcceptOrRedirectModal

* Use subtabs in SignAndVerifyMessage

* Routing for subtabs

* Fix routes, adjust sizing.

* Remove unused import

* Request nonce in base 10

* Add offline override to unit display

* Make cost breakdown less buggy

* Add non standard transaction warning

* Fix amount validity

* Cleanup datafield validity

* Display notif on gas estimation failure

* Add post-signing verification against fields, clean up gas price

* Fix tsc errors

* Code cleanup

* add exports to functions

* add specs for sendEverything and reset sagas

* delete duplicate files

* make tslint happy

* Merge develop

* Fix develop regressions

* Delegate nonce pulling  to wallet being set

* Clarify non standard transaction

* Make address a buffer to avoid leading 0's bug

* Clarify validation helper comment

* Increase debounce time, add console error

* Better validation for non-standard transactions

* Add verification skipping for broadcasting txs

* Fix state and wallet resetting for contract tabs

* Fix some spec files, remove contract.spec

* Remove broadcasting specs from wallet

* Close DeterministicWalletModal on confirm

* Revert "Close DeterministicWalletModal on confirm"

This reverts commit 16c860e854ca29e9de754164d8be5e24f722cbad.

* Reset hardware wallet state on unlocking. Dont render walletdecrypt content when its hidden.

* Fix client side broadcast checking

* Add more state resetters in error scenarios

* Fix gas estimation

* Add validation for value transactions to contract creation

* Add transaction comparaision differentiation depending on wallet type

* Fix token row display balance showing twice

* Properly handle failed transactions

* Handle bad error messages

* fix broken tests

* fix broken test

* Progress commit -- Implement generic subtab types

* Remove react router v3

* Remove unused routes

* Clean up Tabbing code, add onTabChange handler

* Fix tests

* Add nav fix

* revert opinionated sub-tab implementation

* additional reverts

* Add decimal validation

* Make gas price single source of truth, dont save any transaction state other than gas price

* Get rid of old wallet.spec reducer tests

* Add decimal validation when re-validating gasCost

* remove utilities view

* Remove cost breakdown

* Remove local gas estimation warning

* Create getShownTokenBalances selector; use in UnitDropDown and Equivalent Values

* Convert reducers to switch case

* Clean tsc errors

* Fix failing test

* fix tscheck error

* Add number validation to gas field

* Fix misaligned input dropdown

* Revert "Fix misaligned input dropdown"

This reverts commit a40a4c0e8d52471dea01e6727f741a737b798695.

* Set window timeout long enough for node switch to be persisted to state

* Transaction Refactor Style Fixes (#615)

* Fix unit dropdown alignment by rendering it in AmountField, and fixing a missed bootstrap case.

* Fix modal amount and gas text.

* Fix misaligned dropdown

* Update conditions for NavLink is-active class
2017-12-18 15:23:31 -06:00

421 lines
12 KiB
TypeScript

import { configuredStore } from 'store';
import { delay } from 'redux-saga';
import { call, cancel, fork, put, take, select } from 'redux-saga/effects';
import { cloneableGenerator, createMockTask } from 'redux-saga/utils';
import { toggleOfflineConfig, changeNode, changeNodeIntent, setLatestBlock } from 'actions/config';
import {
pollOfflineStatus,
handlePollOfflineStatus,
handleNodeChangeIntent,
handleTogglePollOfflineStatus,
reload,
unsetWeb3Node,
unsetWeb3NodeOnWalletEvent,
equivalentNodeOrDefault
} from 'sagas/config';
import { NODES } from 'config/data';
import {
getNode,
getNodeConfig,
getOffline,
getForceOffline,
getCustomNodeConfigs
} from 'selectors/config';
import { INITIAL_STATE as configInitialState } from 'reducers/config';
import { getWalletInst } from 'selectors/wallet';
import { Web3Wallet } from 'libs/wallet';
import { RPCNode } from 'libs/nodes';
import { showNotification } from 'actions/notifications';
import translate from 'translations';
// init module
configuredStore.getState();
describe('pollOfflineStatus*', () => {
const nav = navigator as any;
const doc = document as any;
const data = {} as any;
data.gen = cloneableGenerator(pollOfflineStatus)();
const node = {
lib: {
ping: jest.fn()
}
};
const isOffline = true;
const isForcedOffline = true;
const raceSuccess = {
pingSucceeded: true
};
const raceFailure = {
pingSucceeded: false
};
let originalHidden;
let originalOnLine;
let originalRandom;
beforeAll(() => {
// backup global config
originalHidden = document.hidden;
originalOnLine = navigator.onLine;
originalRandom = Math.random;
// mock config
Object.defineProperty(document, 'hidden', { value: false, writable: true });
Object.defineProperty(navigator, 'onLine', { value: true, writable: true });
Math.random = () => 0.001;
});
afterAll(() => {
// restore global config
Object.defineProperty(document, 'hidden', {
value: originalHidden,
writable: false
});
Object.defineProperty(navigator, 'onLine', {
value: originalOnLine,
writable: false
});
Math.random = originalRandom;
});
it('should select getNodeConfig', () => {
expect(data.gen.next().value).toEqual(select(getNodeConfig));
});
it('should select getOffline', () => {
expect(data.gen.next(node).value).toEqual(select(getOffline));
});
it('should select getForceOffline', () => {
data.isOfflineClone = data.gen.clone();
expect(data.gen.next(isOffline).value).toEqual(select(getForceOffline));
});
it('should be done if isForcedOffline', () => {
data.clone1 = data.gen.clone();
expect(data.clone1.next(isForcedOffline).done).toEqual(true);
});
it('should call delay if document is hidden', () => {
data.clone2 = data.gen.clone();
doc.hidden = true;
expect(data.clone2.next(!isForcedOffline).value).toEqual(call(delay, 1000));
});
it('should race pingSucceeded and timeout', () => {
doc.hidden = false;
expect(data.gen.next(!isForcedOffline).value).toMatchSnapshot();
});
it('should put showNotification and put toggleOfflineConfig if pingSucceeded && isOffline', () => {
expect(data.gen.next(raceSuccess).value).toEqual(
put(showNotification('success', 'Your connection to the network has been restored!', 3000))
);
expect(data.gen.next().value).toEqual(put(toggleOfflineConfig()));
});
it('should put showNotification and put toggleOfflineConfig if !pingSucceeded && !isOffline', () => {
nav.onLine = !isOffline;
data.isOfflineClone.next(!isOffline);
data.isOfflineClone.next(!isForcedOffline);
data.clone3 = data.isOfflineClone.clone();
expect(data.isOfflineClone.next(raceFailure).value).toMatchSnapshot();
expect(data.isOfflineClone.next().value).toEqual(put(toggleOfflineConfig()));
});
it('should call delay when neither case is true', () => {
expect(data.clone3.next(raceSuccess).value).toEqual(call(delay, 5000));
});
});
describe('handlePollOfflineStatus*', () => {
const gen = handlePollOfflineStatus();
const mockTask = createMockTask();
it('should fork pollOffineStatus', () => {
const expectedForkYield = fork(pollOfflineStatus);
expect(gen.next().value).toEqual(expectedForkYield);
});
it('should take CONFIG_STOP_POLL_OFFLINE_STATE', () => {
expect(gen.next(mockTask).value).toEqual(take('CONFIG_STOP_POLL_OFFLINE_STATE'));
});
it('should cancel pollOfflineStatus', () => {
expect(gen.next().value).toEqual(cancel(mockTask));
});
});
describe('handleTogglePollOfflineStatus*', () => {
const data = {} as any;
data.gen = cloneableGenerator(handleTogglePollOfflineStatus)();
const isForcedOffline = true;
it('should select getForceOffline', () => {
expect(data.gen.next().value).toEqual(select(getForceOffline));
});
it('should fork handlePollOfflineStatus when isForcedOffline', () => {
data.clone = data.gen.clone();
expect(data.gen.next(isForcedOffline).value).toEqual(fork(handlePollOfflineStatus));
});
it('should call handlePollOfflineStatus when !isForcedOffline', () => {
expect(data.clone.next(!isForcedOffline).value).toEqual(call(handlePollOfflineStatus));
});
it('should be done', () => {
expect(data.gen.next().done).toEqual(true);
expect(data.clone.next().done).toEqual(true);
});
});
describe('handleNodeChangeIntent*', () => {
let originalRandom;
// normal operation variables
const defaultNode = configInitialState.nodeSelection;
const defaultNodeConfig = NODES[defaultNode];
const newNode = Object.keys(NODES).reduce(
(acc, cur) => (NODES[acc].network === defaultNodeConfig.network ? cur : acc)
);
const newNodeConfig = NODES[newNode];
const changeNodeIntentAction = changeNodeIntent(newNode);
const truthyWallet = true;
const latestBlock = '0xa';
const raceSuccess = {
lb: latestBlock
};
const raceFailure = {
to: true
};
const data = {} as any;
data.gen = cloneableGenerator(handleNodeChangeIntent)(changeNodeIntentAction);
beforeAll(() => {
originalRandom = Math.random;
Math.random = () => 0.001;
});
afterAll(() => {
Math.random = originalRandom;
});
it('should select getNode', () => {
expect(data.gen.next().value).toEqual(select(getNode));
});
it('should select nodeConfig', () => {
expect(data.gen.next(defaultNode).value).toEqual(select(getNodeConfig));
});
it('should race getCurrentBlock and delay', () => {
expect(data.gen.next(defaultNodeConfig).value).toMatchSnapshot();
});
it('should put showNotification and put changeNode if timeout', () => {
data.clone1 = data.gen.clone();
expect(data.clone1.next(raceFailure).value).toEqual(
put(showNotification('danger', translate('ERROR_32'), 5000))
);
expect(data.clone1.next().value).toEqual(put(changeNode(defaultNode, defaultNodeConfig)));
expect(data.clone1.next().done).toEqual(true);
});
it('should put setLatestBlock', () => {
expect(data.gen.next(raceSuccess).value).toEqual(put(setLatestBlock(latestBlock)));
});
it('should put changeNode', () => {
expect(data.gen.next().value).toEqual(
put(changeNode(changeNodeIntentAction.payload, newNodeConfig))
);
});
it('should select getWalletInst', () => {
expect(data.gen.next().value).toEqual(select(getWalletInst));
});
it('should call reload if wallet exists and network is new', () => {
data.clone2 = data.gen.clone();
expect(data.clone2.next(truthyWallet).value).toEqual(call(reload));
expect(data.clone2.next().done).toEqual(true);
});
it('should be done', () => {
expect(data.gen.next().done).toEqual(true);
});
// custom node variables
const customNodeConfigs = [
{
name: 'name',
url: 'url',
port: 443,
network: 'network'
}
];
const customNodeIdFound = 'url:443';
const customNodeIdNotFound = 'notFound';
const customNodeAction = changeNodeIntent(customNodeIdFound);
const customNodeNotFoundAction = changeNodeIntent(customNodeIdNotFound);
data.customNode = handleNodeChangeIntent(customNodeAction);
data.customNodeNotFound = handleNodeChangeIntent(customNodeNotFoundAction);
// test custom node
it('should select getCustomNodeConfig and match race snapshot', () => {
data.customNode.next();
data.customNode.next(defaultNode);
expect(data.customNode.next(defaultNodeConfig).value).toEqual(select(getCustomNodeConfigs));
expect(data.customNode.next(customNodeConfigs).value).toMatchSnapshot();
});
// test custom node not found
it('should select getCustomNodeConfig, put showNotification, put changeNode', () => {
data.customNodeNotFound.next();
data.customNodeNotFound.next(defaultNode);
expect(data.customNodeNotFound.next(defaultNodeConfig).value).toEqual(
select(getCustomNodeConfigs)
);
expect(data.customNodeNotFound.next(customNodeConfigs).value).toEqual(
put(
showNotification(
'danger',
`Attempted to switch to unknown node '${customNodeNotFoundAction.payload}'`,
5000
)
)
);
expect(data.customNodeNotFound.next().value).toEqual(
put(changeNode(defaultNode, defaultNodeConfig))
);
expect(data.customNodeNotFound.next().done).toEqual(true);
});
});
describe('unsetWeb3Node*', () => {
const node = 'web3';
const mockNodeConfig = { network: 'ETH' };
const newNode = equivalentNodeOrDefault(mockNodeConfig);
const gen = unsetWeb3Node();
it('should select getNode', () => {
expect(gen.next().value).toEqual(select(getNode));
});
it('should select getNodeConfig', () => {
expect(gen.next(node).value).toEqual(select(getNodeConfig));
});
it('should put changeNodeIntent', () => {
expect(gen.next(mockNodeConfig).value).toEqual(put(changeNodeIntent(newNode)));
});
it('should be done', () => {
expect(gen.next().done).toEqual(true);
});
it('should return early if node type is not web3', () => {
const gen1 = unsetWeb3Node();
gen1.next();
gen1.next('notWeb3');
expect(gen1.next().done).toEqual(true);
});
});
describe('unsetWeb3NodeOnWalletEvent*', () => {
const fakeAction = {};
const mockNode = 'web3';
const mockNodeConfig = { network: 'ETH' };
const gen = unsetWeb3NodeOnWalletEvent(fakeAction);
it('should select getNode', () => {
expect(gen.next().value).toEqual(select(getNode));
});
it('should select getNodeConfig', () => {
expect(gen.next(mockNode).value).toEqual(select(getNodeConfig));
});
it('should put changeNodeIntent', () => {
expect(gen.next(mockNodeConfig).value).toEqual(
put(changeNodeIntent(equivalentNodeOrDefault(mockNodeConfig)))
);
});
it('should be done', () => {
expect(gen.next().done).toEqual(true);
});
it('should return early if node type is not web3', () => {
const gen1 = unsetWeb3NodeOnWalletEvent({ payload: false });
gen1.next(); //getNode
gen1.next('notWeb3'); //getNodeConfig
expect(gen1.next().done).toEqual(true);
});
it('should return early if wallet type is web3', () => {
const mockAddress = '0x0';
const mockNetwork = 'ETH';
const mockWeb3Wallet = new Web3Wallet(mockAddress, mockNetwork);
const gen2 = unsetWeb3NodeOnWalletEvent({ payload: mockWeb3Wallet });
gen2.next(); //getNode
gen2.next('web3'); //getNodeConfig
expect(gen2.next().done).toEqual(true);
});
});
describe('equivalentNodeOrDefault', () => {
const originalNodeList = Object.keys(NODES);
const appDefaultNode = configInitialState.nodeSelection;
const mockNodeConfig = {
network: 'ETH',
service: 'fakeService',
lib: new RPCNode('fakeEndpoint'),
estimateGas: false
};
afterEach(() => {
Object.keys(NODES).forEach(node => {
if (originalNodeList.indexOf(node) === -1) {
delete NODES[node];
}
});
});
it('should return node with equivalent network', () => {
const node = equivalentNodeOrDefault({
...mockNodeConfig,
network: 'Kovan'
});
expect(NODES[node].network).toEqual('Kovan');
});
it('should return app default if no eqivalent is found', () => {
const node = equivalentNodeOrDefault({
...mockNodeConfig,
network: 'noEqivalentExists'
});
expect(node).toEqual(appDefaultNode);
});
it('should ignore web3 from node list', () => {
NODES.web3 = {
...mockNodeConfig,
network: 'uniqueToWeb3'
};
const node = equivalentNodeOrDefault({
...mockNodeConfig,
network: 'uniqueToWeb3'
});
expect(node).toEqual(appDefaultNode);
});
});