Contracts Tab Scaffolding (#70)

* Empty component, routes setup.

* Shared components for all Contracts inputs. Dont do anything yet.

* Check in reducer work so far. Still WIP.

* Header styling

* Check in input work so far, splitting to new branch.

* Strip down contracts inputs. Split out into form and explorer

* Contract selector

* Constantized config actions to use in contract saga.

* Interact explorer UI, no functionality

* Convert to constants, hook up errors

* Deploy and style cleanup.

* Remove unnecessary class.

* Fix flow errors with css modules

* Attempt at fixing all newly introduced flow errors in the contracts branch.

* Removed unused validator.

* Remove action constants, fix flow specificity in reducers

* Fix unit tests

* Move network contracts out of redux / sagas, and read directly from state with a selector in mapStateToProps.

* Fix initialState -> INITIAL_STATE rename

* foreach push -> concat
This commit is contained in:
William O'Beirne 2017-07-27 20:31:59 -04:00 committed by Daniel Ternyak
parent 96405157f0
commit e3505fd958
25 changed files with 909 additions and 20 deletions

View File

@ -0,0 +1,47 @@
// @flow
/***** Access Contract *****/
export type AccessContractAction = {
type: 'ACCESS_CONTRACT',
address: string,
abiJson: string
};
export function accessContract(
address: string,
abiJson: string
): AccessContractAction {
return {
type: 'ACCESS_CONTRACT',
address,
abiJson
};
}
/***** Set Interactive Contract *****/
export type ABIFunctionField = {
name: string,
type: string
};
export type ABIFunction = {
name: string,
type: string,
constant: boolean,
inputs: Array<ABIFunctionField>,
outputs: Array<ABIFunctionField>
};
export type SetInteractiveContractAction = {
type: 'SET_INTERACTIVE_CONTRACT',
functions: Array<ABIFunction>
};
export function setInteractiveContract(
functions: Array<ABIFunction>
): SetInteractiveContractAction {
return {
type: 'SET_INTERACTIVE_CONTRACT',
functions
};
}

View File

@ -22,7 +22,8 @@ const tabs = [
name: 'NAV_Offline'
},
{
name: 'NAV_Contracts'
name: 'NAV_Contracts',
link: 'contracts'
},
{
name: 'NAV_ViewWallet',

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,11 @@
import ETC from './ETC.json';
import ETH from './ETH.json';
import Rinkeby from './Rinkeby.json';
import Ropsten from './Ropsten.json';
export default {
ETC,
ETH,
Rinkeby,
Ropsten
};

View File

@ -0,0 +1,12 @@
[
{
"name": "ENS - Eth Registrar (Auction)",
"address": "0x21397c1a1f4acd9132fe36df011610564b87e24b",
"abi": "[{\"constant\":true,\"inputs\":[],\"name\":\"ens\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"expiryTimes\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"subnode\",\"type\":\"bytes32\"},{\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"register\",\"outputs\":[],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"rootNode\",\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\"}],\"payable\":false,\"type\":\"function\"},{\"inputs\":[{\"name\":\"ensAddr\",\"type\":\"address\"},{\"name\":\"node\",\"type\":\"bytes32\"}],\"type\":\"constructor\"}]"
},
{
"name": "ENS - Registry",
"address": "0xe7410170f87102df0055eb195163a03b7f2bff4a",
"abi": "[{\"constant\":true,\"inputs\":[{\"name\":\"node\",\"type\":\"bytes32\"}],\"name\":\"resolver\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"node\",\"type\":\"bytes32\"}],\"name\":\"owner\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"node\",\"type\":\"bytes32\"},{\"name\":\"label\",\"type\":\"bytes32\"},{\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"setSubnodeOwner\",\"outputs\":[],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"node\",\"type\":\"bytes32\"},{\"name\":\"ttl\",\"type\":\"uint64\"}],\"name\":\"setTTL\",\"outputs\":[],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"node\",\"type\":\"bytes32\"}],\"name\":\"ttl\",\"outputs\":[{\"name\":\"\",\"type\":\"uint64\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"node\",\"type\":\"bytes32\"},{\"name\":\"resolver\",\"type\":\"address\"}],\"name\":\"setResolver\",\"outputs\":[],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"node\",\"type\":\"bytes32\"},{\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"setOwner\",\"outputs\":[],\"payable\":false,\"type\":\"function\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"node\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"node\",\"type\":\"bytes32\"},{\"indexed\":true,\"name\":\"label\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"NewOwner\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"node\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"resolver\",\"type\":\"address\"}],\"name\":\"NewResolver\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"node\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"ttl\",\"type\":\"uint64\"}],\"name\":\"NewTTL\",\"type\":\"event\"}]"
}
]

File diff suppressed because one or more lines are too long

View File

@ -117,6 +117,12 @@ export type Token = {
decimal: number
};
export type NetworkContract = {
name: string,
address: string,
abi: string
};
export type NetworkConfig = {
name: string,
// unit: string,
@ -130,7 +136,8 @@ export type NetworkConfig = {
address: string
},
chainId: number,
tokens: Token[]
tokens: Token[],
contracts: ?Array<NetworkContract>
};
export const NETWORKS: { [key: string]: NetworkConfig } = {
@ -147,8 +154,8 @@ export const NETWORKS: { [key: string]: NetworkConfig } = {
name: 'Ethplorer.io',
address: 'https://ethplorer.io/address/[[address]]'
},
tokens: require('./tokens/eth').default
// 'abiList': require('./abiDefinitions/ethAbi.json'),
tokens: require('./tokens/eth').default,
contracts: require('./contracts/eth.json')
}
};

View File

@ -0,0 +1,55 @@
// @flow
import React, { Component } from 'react';
import translate from 'translations';
type Props = {};
export default class Deploy extends Component {
props: Props;
state = {
byteCode: '',
gasLimit: ''
};
_handleInput = (ev: SyntheticInputEvent) => {
this.setState({ [ev.target.name]: ev.target.value });
};
render() {
const { byteCode, gasLimit } = this.state;
// TODO: Use common components for byte code / gas price
return (
<div className="Deploy">
<label className="Deploy-field form-group">
<h4 className="Deploy-field-label">
{translate('CONTRACT_ByteCode')}
</h4>
<textarea
name="byteCode"
placeholder="0x8f87a973e..."
rows={6}
onChange={this._handleInput}
className="Deploy-field-input form-control"
value={byteCode}
/>
</label>
<label className="Deploy-field form-group">
<h4 className="Deploy-field-label">
{translate('CONTRACT_ByteCode')}
</h4>
<input
name="gasLimit"
value={gasLimit}
onChange={this._handleInput}
className="Deploy-field-input form-control"
/>
</label>
<button className="Deploy-submit btn btn-primary">Implement Me</button>
</div>
);
}
}

View File

@ -0,0 +1,45 @@
// @flow
import React, { Component } from 'react';
import InteractForm from './InteractForm';
import InteractExplorer from './InteractExplorer';
import type { ABIFunction } from 'actions/contracts';
import type { NetworkContract } from 'config/data';
type Props = {
NetworkContracts: Array<NetworkContract>,
selectedAddress: ?string,
selectedABIJson: ?string,
selectedABIFunctions: ?Array<ABIFunction>,
accessContract: Function
};
export default class Interact extends Component {
props: Props;
render() {
const {
NetworkContracts,
selectedAddress,
selectedABIJson,
selectedABIFunctions,
accessContract
} = this.props;
// TODO: Use common components for address, abi json
return (
<div className="Interact">
<InteractForm
contracts={NetworkContracts}
address={selectedAddress}
abiJson={selectedABIJson}
accessContract={accessContract}
/>
<hr />
<InteractExplorer
address={selectedAddress}
functions={selectedABIFunctions}
/>
</div>
);
}
}

View File

@ -0,0 +1,140 @@
// @flow
import React, { Component } from 'react';
import translate from 'translations';
import type { ABIFunction } from 'actions/contracts';
import './InteractExplorer.scss';
type Props = {
address: ?string,
functions: ?Array<ABIFunction>
};
type State = {
selectedFunction: ?ABIFunction,
inputs: Object,
outputs: Object
};
export default class InteractExplorer extends Component {
props: Props;
state: State = {
selectedFunction: null,
inputs: {},
outputs: {}
};
_handleFunctionSelect = (ev: SyntheticInputEvent) => {
const { functions } = this.props;
if (!functions) {
return;
}
const selectedFunction = functions.reduce((prev, fn) => {
return ev.target.value === fn.name ? fn : prev;
});
this.setState({
selectedFunction,
inputs: {}
});
};
_handleInputChange = (ev: SyntheticInputEvent) => {
this.setState({
inputs: {
...this.state.inputs,
[ev.target.name]: ev.target.value
}
});
};
render() {
const { address, functions } = this.props;
const { selectedFunction, inputs, outputs } = this.state;
if (!functions) {
return null;
}
return (
<div className="InteractExplorer">
<h3 className="InteractExplorer-title">
{translate('CONTRACT_Interact_Title')}
<span className="InteractExplorer-title-address">
{address}
</span>
</h3>
<select
value={selectedFunction ? selectedFunction.name : ''}
className="InteractExplorer-fnselect form-control"
onChange={this._handleFunctionSelect}
>
<option>
{translate('CONTRACT_Interact_CTA')}
</option>
{functions.map(fn =>
<option key={fn.name} value={fn.name}>
{fn.name}
</option>
)}
</select>
{selectedFunction &&
<div key={selectedFunction.name} className="InteractExplorer-func">
{/* TODO: Use reusable components with validation */}
{selectedFunction.inputs.map(input =>
<label
key={input.name}
className="InteractExplorer-func-in form-group"
>
<h4 className="InteractExplorer-func-in-label">
{input.name}
<span className="InteractExplorer-func-in-label-type">
{input.type}
</span>
</h4>
<input
className="InteractExplorer-func-in-input form-control"
name={input.name}
value={inputs[input.name]}
onChange={this._handleInputChange}
/>
</label>
)}
{selectedFunction.outputs.map(output =>
<label
key={output.name}
className="InteractExplorer-func-out form-group"
>
<h4 className="InteractExplorer-func-out-label">
{output.name}
<span className="InteractExplorer-func-out-label-type">
{output.type}
</span>
</h4>
<input
className="InteractExplorer-func-out-input form-control"
value={outputs[output.name]}
disabled
/>
</label>
)}
{selectedFunction.constant
? <button className="InteractExplorer-func-submit btn btn-primary">
{/* translate('CONTRACT_Read') */}
Implement Me
</button>
: <button className="InteractExplorer-func-submit btn btn-primary">
{/* translate('CONTRACT_Write') */}
Implement Me
</button>}
</div>}
</div>
);
}
}

View File

@ -0,0 +1,32 @@
@import "common/sass/variables";
.InteractExplorer {
&-title {
&-address {
margin-left: 6px;
font-weight: 300;
opacity: 0.6;
}
}
&-func {
&-in,
&-out {
&-label {
&-type {
margin-left: 5px;
font-weight: 300;
opacity: 0.6;
}
}
}
&-in {
margin-right: 2rem;
}
&-out {
margin-left: 2rem;
}
}
}

View File

@ -0,0 +1,130 @@
// @flow
import React, { Component } from 'react';
import translate from 'translations';
import './InteractForm.scss';
import type { NetworkContract } from 'config/data';
type Props = {
contracts: Array<NetworkContract>,
address: ?string,
abiJson: ?string,
accessContract: Function
};
export default class InteractForm extends Component {
props: Props;
state = {
address: '',
abiJson: ''
};
_handleInput = (ev: SyntheticInputEvent) => {
this.setState({ [ev.target.name]: ev.target.value });
};
_handleSelectContract = (ev: SyntheticInputEvent) => {
const addr = ev.target.value;
const contract = this.props.contracts.reduce((prev, contract) => {
return contract.address === addr ? contract : prev;
});
this.setState({
address: contract.address,
abiJson: contract.abi
});
};
_accessContract = () => {
this.props.accessContract(this.state.address, this.state.abiJson);
};
render() {
const { contracts } = this.props;
const { address, abiJson } = this.state;
let contractOptions;
if (contracts && contracts.length) {
contractOptions = [
{
name: 'Select a contract...',
value: null
}
];
contractOptions = contractOptions.concat(
contracts.map(contract => {
return {
name: `${contract.name} (${contract.address.substr(0, 10)}...)`,
value: contract.address
};
})
);
} else {
contractOptions = [
{
name: 'No contracts available',
value: null
}
];
}
// TODO: Use common components for address, abi json
return (
<div className="InteractForm">
<div className="InteractForm-address">
<label className="InteractForm-address-field form-group">
<h4>
{translate('CONTRACT_Title_2')}
</h4>
<input
name="address"
value={address}
className="InteractForm-address-field-input form-control"
onChange={this._handleInput}
/>
</label>
<label className="InteractForm-address-contract form-group">
<h4>
{translate('CONTRACT_Title_2')}
</h4>
<select
className="InteractForm-address-field-input form-control"
onChange={this._handleSelectContract}
disabled={!contracts || !contracts.length}
>
{contractOptions.map(opt =>
<option key={opt.value} value={opt.value}>
{opt.name}
</option>
)}
</select>
</label>
</div>
<div className="InteractForm-interface">
<label className="InteractForm-interface-field form-group">
<h4 className="InteractForm-interface-field-label">
{translate('CONTRACT_Json')}
</h4>
<textarea
name="abiJson"
className="InteractForm-interface-field-input form-control"
onChange={this._handleInput}
value={abiJson}
rows={6}
/>
</label>
</div>
<button
className="InteractForm-submit btn btn-primary"
onClick={this._accessContract}
>
{translate('x_Access')}
</button>
</div>
);
}
}

View File

@ -0,0 +1,14 @@
.InteractForm {
&-address {
display: flex;
> * {
flex: 1;
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
}
}

View File

@ -0,0 +1,103 @@
// @flow
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { accessContract } from 'actions/contracts';
import type { ABIFunction } from 'actions/contracts';
import { getNetworkContracts } from 'selectors/config';
import type { NetworkContract } from 'config/data';
import { State } from 'reducers/contracts';
import translate from 'translations';
import Interact from './components/Interact';
import Deploy from './components/Deploy';
import './index.scss';
type Props = {
NetworkContracts: Array<NetworkContract>,
selectedAddress: ?string,
selectedABIJson: ?string,
selectedABIFunctions: ?Array<ABIFunction>,
accessContract: Function
};
class Contracts extends Component {
props: Props;
state = {
activeTab: 'interact'
};
changeTab(activeTab) {
this.setState({ activeTab });
}
render() {
const {
NetworkContracts,
selectedAddress,
selectedABIJson,
selectedABIFunctions,
accessContract
} = this.props;
const { activeTab } = this.state;
let content = '';
let interactActive = '';
let deployActive = '';
if (activeTab === 'interact') {
content = (
<Interact
NetworkContracts={NetworkContracts}
selectedAddress={selectedAddress}
selectedABIJson={selectedABIJson}
selectedABIFunctions={selectedABIFunctions}
accessContract={accessContract}
/>
);
interactActive = 'is-active';
} else {
content = <Deploy />;
deployActive = 'is-active';
}
return (
<section className="container" style={{ minHeight: '50%' }}>
<div className="tab-content">
<main className="tab-pane active" role="main">
<div className="Contracts">
<h1 className="Contracts-header">
<button
className={`Contracts-header-tab ${interactActive}`}
onClick={this.changeTab.bind(this, 'interact')}
>
{translate('NAV_InteractContract')}
</button>{' '}
<span>or</span>{' '}
<button
className={`Contracts-header-tab ${deployActive}`}
onClick={this.changeTab.bind(this, 'deploy')}
>
{translate('NAV_DeployContract')}
</button>
</h1>
<div className="Contracts-content">
{content}
</div>
</div>
</main>
</div>
</section>
);
}
}
function mapStateToProps(state: State) {
return {
NetworkContracts: getNetworkContracts(state),
selectedAddress: state.contracts.selectedAddress,
selectedABIJson: state.contracts.selectedABIJson,
selectedABIFunctions: state.contracts.selectedABIFunctions
};
}
export default connect(mapStateToProps, { accessContract })(Contracts);

View File

@ -0,0 +1,30 @@
@import "common/sass/variables";
@import "common/sass/mixins";
.Contracts {
&-header {
text-align: center;
margin: 2rem 0;
&-tab {
@include reset-button;
color: $ether-blue;
&:hover,
&:active {
opacity: 0.8;
}
&.is-active {
&,
&:hover,
&:active {
color: $text-color;
cursor: default;
opacity: 1;
font-weight: 500;
}
}
}
}
}

View File

@ -1,3 +1,6 @@
// Application styles must come first in order, to allow for overrides
import 'assets/styles/etherwallet-master.less';
import React from 'react';
import { render } from 'react-dom';
import { syncHistoryWithStore } from 'react-router-redux';
@ -5,8 +8,6 @@ import { syncHistoryWithStore } from 'react-router-redux';
import { Root } from 'components';
import { Routing, history } from './routing';
import { store } from './store';
// application styles
import 'assets/styles/etherwallet-master.less';
const renderRoot = Root => {
let syncedHistory = syncHistoryWithStore(history, store);

View File

@ -20,9 +20,10 @@ export function isValidHex(str: string): boolean {
return false;
}
if (str === '') return true;
str = str.substring(0, 2) == '0x'
? str.substring(2).toUpperCase()
: str.toUpperCase();
str =
str.substring(0, 2) == '0x'
? str.substring(2).toUpperCase()
: str.toUpperCase();
var re = /^[0-9A-F]+$/g;
return re.test(str);
}

View File

@ -0,0 +1,39 @@
import type {
AccessContractAction,
SetInteractiveContractAction
} from 'actions/contracts';
export type State = {
selectedAddress: ?string,
selectedABIJson: ?string,
selectedABIFunctions: ?Array
};
export const initialState: State = {
// Interact
selectedAddress: null,
selectedABIJson: null,
selectedABIFunctions: null
};
type Action = AccessContractAction | SetInteractiveContractAction;
export function contracts(state: State = initialState, action: Action) {
switch (action.type) {
case 'ACCESS_CONTRACT':
return {
...state,
selectedAddress: action.address,
selectedABIJson: action.abiJson
};
case 'SET_INTERACTIVE_CONTRACT':
return {
...state,
selectedABIFunctions: action.functions
};
default:
return state;
}
}

View File

@ -22,6 +22,9 @@ import type { State as CustomTokensState } from './customTokens';
import * as rates from './rates';
import type { State as RatesState } from './rates';
import * as contracts from './contracts';
import type { State as ContractsState } from './contracts';
import { reducer as formReducer } from 'redux-form';
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
@ -35,6 +38,7 @@ export type State = {
wallet: WalletState,
customTokens: CustomTokensState,
rates: RatesState,
contracts: ContractsState,
// Third party reducers (TODO: Fill these out)
form: Object,
routing: Object
@ -49,6 +53,7 @@ export default combineReducers({
...wallet,
...customTokens,
...rates,
...contracts,
form: formReducer,
routing: routerReducer
});

View File

@ -7,6 +7,7 @@ import ViewWallet from 'containers/Tabs/ViewWallet';
import Help from 'containers/Tabs/Help';
import Swap from 'containers/Tabs/Swap';
import SendTransaction from 'containers/Tabs/SendTransaction';
import Contracts from 'containers/Tabs/Contracts';
export const history = getHistory();
export const Routing = () =>
@ -16,6 +17,7 @@ export const Routing = () =>
<Route name="Help" path="/help" component={Help} />
<Route name="Swap" path="/swap" component={Swap} />
<Route name="Send" path="/send-transaction" component={SendTransaction} />
<Route name="Contracts" path="/contracts" component={Contracts} />
<Redirect from="/*" to="/" />
</Route>;

41
common/sagas/contracts.js Normal file
View File

@ -0,0 +1,41 @@
import { takeEvery, put } from 'redux-saga/effects';
import type { Effect } from 'redux-saga/effects';
import translate from 'translations';
import {
AccessContractAction,
setInteractiveContract
} from 'actions/contracts';
import { showNotification } from 'actions/notifications';
import { isValidETHAddress } from 'libs/validators';
function* handleAccessContract(action: AccessContractAction) {
const contractFunctions = [];
if (!action.address || !isValidETHAddress(action.address)) {
yield put(showNotification('danger', translate('ERROR_5'), 5000));
return;
}
try {
const abi = JSON.parse(action.abiJson);
if (abi.constructor !== Array) {
throw new Error('ABI JSON was not an array!');
}
abi.forEach(instruction => {
if (instruction.type === 'function') {
contractFunctions.push(instruction);
}
});
yield put(setInteractiveContract(contractFunctions));
} catch (err) {
console.error('Error parsing contract ABI JSON', err);
yield put(showNotification('danger', translate('ERROR_26'), 5000));
}
}
export default function* contractsSaga(): Generator<Effect, void, any> {
yield takeEvery('ACCESS_CONTRACT', handleAccessContract);
}

8
common/sagas/index.js Normal file
View File

@ -0,0 +1,8 @@
import bity from './bity';
import contracts from './contracts';
import ens from './ens';
import notifications from './notifications';
import rates from './rates';
import wallet from './wallet';
export default { bity, contracts, ens, notifications, rates, wallet };

View File

@ -2,7 +2,11 @@
import type { State } from 'reducers';
import { BaseNode } from 'libs/nodes';
import { NODES, NETWORKS } from 'config/data';
import type { NetworkConfig } from 'config/data';
import type { NetworkConfig, NetworkContract } from 'config/data';
export function getNode(state: State): string {
return state.config.nodeSelection;
}
export function getNodeLib(state: State): BaseNode {
return NODES[state.config.nodeSelection].lib;
@ -11,3 +15,7 @@ export function getNodeLib(state: State): BaseNode {
export function getNetworkConfig(state: State): NetworkConfig {
return NETWORKS[NODES[state.config.nodeSelection].network];
}
export function getNetworkContracts(state: State): ?Array<NetworkContract> {
return getNetworkConfig(state).contracts;
}

View File

@ -5,11 +5,7 @@ import {
} from 'utils/localStorage';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
import notificationsSaga from './sagas/notifications';
import ensSaga from './sagas/ens';
import walletSaga from './sagas/wallet';
import bitySaga from './sagas/bity';
import ratesSaga from './sagas/rates';
import sagas from './sagas';
import { INITIAL_STATE as configInitialState } from 'reducers/config';
import { INITIAL_STATE as customTokensInitialState } from 'reducers/customTokens';
import throttle from 'lodash/throttle';
@ -45,11 +41,11 @@ const configureStore = () => {
};
store = createStore(RootReducer, persistedInitialState, middleware);
sagaMiddleware.run(notificationsSaga);
sagaMiddleware.run(ensSaga);
sagaMiddleware.run(walletSaga);
sagaMiddleware.run(bitySaga);
sagaMiddleware.run(ratesSaga);
// Add all of the sagas to the middleware
Object.keys(sagas).forEach(saga => {
sagaMiddleware.run(sagas[saga]);
});
store.subscribe(
throttle(() => {