diff --git a/common/actions/config/actionTypes.ts b/common/actions/config/actionTypes.ts index 14b875aaead..be46023a7d0 100644 --- a/common/actions/config/actionTypes.ts +++ b/common/actions/config/actionTypes.ts @@ -1,5 +1,5 @@ import { TypeKeys } from './constants'; -import { CustomNodeConfig, Web3NodeConfig } from 'types/node'; +import { CustomNodeConfig, StaticNodeConfig } from 'types/node'; import { CustomNetworkConfig } from 'types/network'; /*** Toggle Offline ***/ @@ -80,7 +80,7 @@ export interface Web3UnsetNodeAction { /*** Set Web3 as a Node ***/ export interface Web3setNodeAction { type: TypeKeys.CONFIG_NODE_WEB3_SET; - payload: { id: 'web3'; config: Web3NodeConfig }; + payload: { id: 'web3'; config: StaticNodeConfig }; } export type CustomNetworkAction = AddCustomNetworkAction | RemoveCustomNetworkAction; diff --git a/common/components/BalanceSidebar/AccountInfo.tsx b/common/components/BalanceSidebar/AccountInfo.tsx index b7b801f1859..cd5d98550a2 100644 --- a/common/components/BalanceSidebar/AccountInfo.tsx +++ b/common/components/BalanceSidebar/AccountInfo.tsx @@ -10,6 +10,7 @@ import { getNetworkConfig, getOffline } from 'selectors/config'; import { AppState } from 'reducers'; import { NetworkConfig } from 'types/network'; import { TRefreshAccountBalance, refreshAccountBalance } from 'actions/wallet'; +import { etherChainExplorerInst } from 'config/data'; import './AccountInfo.scss'; interface OwnProps { @@ -193,6 +194,13 @@ class AccountInfo extends React.Component { )} + {network.name === 'ETH' && ( +
  • + + {`${network.name} (${etherChainExplorerInst.origin})`} + +
  • + )} {!!tokenExplorer && (
  • diff --git a/common/components/ExtendedNotifications/TransactionSucceeded.tsx b/common/components/ExtendedNotifications/TransactionSucceeded.tsx index 49206528fbe..c2e1e51d316 100644 --- a/common/components/ExtendedNotifications/TransactionSucceeded.tsx +++ b/common/components/ExtendedNotifications/TransactionSucceeded.tsx @@ -3,6 +3,7 @@ import { Link } from 'react-router-dom'; import translate from 'translations'; import { NewTabLink } from 'components/ui'; import { BlockExplorerConfig } from 'types/network'; +import { etherChainExplorerInst } from 'config/data'; export interface TransactionSucceededProps { txHash: string; @@ -11,6 +12,7 @@ export interface TransactionSucceededProps { const TransactionSucceeded = ({ txHash, blockExplorer }: TransactionSucceededProps) => { let verifyBtn: React.ReactElement | undefined; + let altVerifyBtn: React.ReactElement | undefined; if (blockExplorer) { verifyBtn = ( @@ -18,6 +20,15 @@ const TransactionSucceeded = ({ txHash, blockExplorer }: TransactionSucceededPro ); } + // TODO: In the future, we'll want to refactor staticNetworks so that multiple blockexplorers can be configured per network. + // This requires a large refactor, so for now we'll hard-code the etherchain link when etherscan is shown to verify your transaction + if (blockExplorer && blockExplorer.origin === 'https://etherscan.io') { + altVerifyBtn = ( + + {translate('VERIFY_TX', { $block_explorer: etherChainExplorerInst.name })} + + ); + } return (
    @@ -25,6 +36,7 @@ const TransactionSucceeded = ({ txHash, blockExplorer }: TransactionSucceededPro {translate('SUCCESS_3')} {txHash}

    {verifyBtn} + {altVerifyBtn} {translate('NAV_CHECKTXSTATUS')} diff --git a/common/components/Footer/index.tsx b/common/components/Footer/index.tsx index 0fe77ef22e8..a67761732e9 100644 --- a/common/components/Footer/index.tsx +++ b/common/components/Footer/index.tsx @@ -107,6 +107,10 @@ const FRIENDS: Link[] = [ { link: 'https://etherscan.io/', text: 'Etherscan' + }, + { + link: 'https://etherchain.org/', + text: 'Etherchain' } ]; diff --git a/common/components/Header/components/CustomNodeModal.tsx b/common/components/Header/components/CustomNodeModal.tsx index 1ae60847587..7395b8d648d 100644 --- a/common/components/Header/components/CustomNodeModal.tsx +++ b/common/components/Header/components/CustomNodeModal.tsx @@ -11,9 +11,9 @@ import { getCustomNodeConfigs, getStaticNetworkConfigs } from 'selectors/config'; -import { CustomNode } from 'libs/nodes'; import { Input, Dropdown } from 'components/ui'; import './CustomNodeModal.scss'; +import { shepherdProvider } from 'libs/nodes'; const CUSTOM = { label: 'Custom', value: 'custom' }; @@ -329,9 +329,7 @@ class CustomNodeModal extends React.Component { : {}) }; - const lib = new CustomNode(node); - - return { ...node, lib }; + return { ...node, lib: shepherdProvider }; } private getConflictedNode(): CustomNodeConfig | undefined { diff --git a/common/config/data.tsx b/common/config/data.tsx index 0e189681d42..e72e16a4285 100644 --- a/common/config/data.tsx +++ b/common/config/data.tsx @@ -3,6 +3,7 @@ import NewTabLink from 'components/ui/NewTabLink'; import { getValues } from '../utils/helpers'; import packageJson from '../../package.json'; import { GasPriceSetting } from 'types/network'; +import { makeExplorer } from 'utils/helpers'; export const languages = require('./languages.json'); export const discordURL = 'https://discord.gg/VSaTXEA'; @@ -33,6 +34,12 @@ export const BTCTxExplorer = (txHash: string): string => `${blockChainInfo}/tx/$ export const ETHAddressExplorer = (address: string): string => `${etherScan}/address/${address}`; export const ETHTokenExplorer = (address: string): string => `${ethPlorer}/address/${address}`; +export const etherChainExplorerInst = makeExplorer({ + name: 'Etherchain', + origin: 'https://www.etherchain.org', + addressPath: 'account' +}); + export const donationAddressMap = { BTC: '32oirLEzZRhi33RCXDF9WHJjEb8RsrSss3', ETH: '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520', diff --git a/common/containers/Tabs/CheckTransaction/index.tsx b/common/containers/Tabs/CheckTransaction/index.tsx index e9c464da6e4..1aed3b5709e 100644 --- a/common/containers/Tabs/CheckTransaction/index.tsx +++ b/common/containers/Tabs/CheckTransaction/index.tsx @@ -10,6 +10,7 @@ import { AppState } from 'reducers'; import { NetworkConfig } from 'types/network'; import './index.scss'; import translate from 'translations'; +import { etherChainExplorerInst } from 'config/data'; interface StateProps { network: NetworkConfig; @@ -43,6 +44,11 @@ class CheckTransaction extends React.Component { public render() { const { network } = this.props; const { hash } = this.state; + console.log(network); + const CHECK_TX_KEY = + network.name === 'ETH' + ? 'CHECK_TX_STATUS_DESCRIPTION_MULTIPLE' + : 'CHECK_TX_STATUS_DESCRIPTION_2'; return ( @@ -52,9 +58,12 @@ class CheckTransaction extends React.Component {

    {translate('CHECK_TX_STATUS_DESCRIPTION_1')} {!network.isCustom && - translate('CHECK_TX_STATUS_DESCRIPTION_2', { + translate(CHECK_TX_KEY, { $block_explorer: network.blockExplorer.name, - $block_explorer_link: network.blockExplorer.origin + $block_explorer_link: network.blockExplorer.origin, + // On ETH networks, we also show Etherchain. Otherwise, these variables are ignored + $block_explorer_2: etherChainExplorerInst.name, + $block_explorer_link_2: etherChainExplorerInst.origin })}

    diff --git a/common/containers/Tabs/Contracts/components/Interact/components/InteractExplorer/index.tsx b/common/containers/Tabs/Contracts/components/Interact/components/InteractExplorer/index.tsx index 15265d45194..ec455719d33 100644 --- a/common/containers/Tabs/Contracts/components/Interact/components/InteractExplorer/index.tsx +++ b/common/containers/Tabs/Contracts/components/Interact/components/InteractExplorer/index.tsx @@ -10,12 +10,11 @@ import { connect } from 'react-redux'; import { Fields } from './components'; import { setDataField, TSetDataField } from 'actions/transaction'; import { Data } from 'libs/units'; -import { Web3Node } from 'libs/nodes'; -import RpcNode from 'libs/nodes/rpc'; import { Input, Dropdown } from 'components/ui'; +import { INode } from 'libs/nodes'; interface StateProps { - nodeLib: RpcNode | Web3Node; + nodeLib: INode; to: AppState['transaction']['fields']['to']; dataExists: boolean; } diff --git a/common/libs/nodes/custom/index.ts b/common/libs/nodes/custom/index.ts deleted file mode 100644 index 79254e74568..00000000000 --- a/common/libs/nodes/custom/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import RPCNode from '../rpc'; -import RPCClient from '../rpc/client'; -import { CustomNodeConfig } from 'types/node'; -import { Omit } from 'react-router'; - -export default class CustomNode extends RPCNode { - constructor(config: Omit) { - super(config.id); - - const headers: { [key: string]: string } = {}; - if (config.auth) { - const { username, password } = config.auth; - headers.Authorization = `Basic ${btoa(`${username}:${password}`)}`; - } - - this.client = new RPCClient(config.id, headers); - } -} diff --git a/common/libs/nodes/etherscan/client.ts b/common/libs/nodes/etherscan/client.ts deleted file mode 100644 index b8518e4e875..00000000000 --- a/common/libs/nodes/etherscan/client.ts +++ /dev/null @@ -1,29 +0,0 @@ -import RPCClient from '../rpc/client'; -import { JsonRpcResponse } from '../rpc/types'; -import { EtherscanRequest } from './types'; - -export default class EtherscanClient extends RPCClient { - public encodeRequest(request: EtherscanRequest): string { - const encoded = new URLSearchParams(); - Object.keys(request).forEach((key: keyof EtherscanRequest) => { - if (request[key]) { - encoded.set(key, request[key]); - } - }); - return encoded.toString(); - } - - public call = (request: EtherscanRequest): Promise => - fetch(this.endpoint, { - method: 'POST', - headers: new Headers({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }), - body: this.encodeRequest(request) - }).then(r => r.json()); - - public batch = (requests: EtherscanRequest[]): Promise => { - const promises = requests.map(req => this.call(req)); - return Promise.all(promises); - }; -} diff --git a/common/libs/nodes/etherscan/index.ts b/common/libs/nodes/etherscan/index.ts deleted file mode 100644 index 9ad55e03cb5..00000000000 --- a/common/libs/nodes/etherscan/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import RPCNode from '../rpc'; -import EtherscanClient from './client'; -import EtherscanRequests from './requests'; - -export default class EtherscanNode extends RPCNode { - public client: EtherscanClient; - public requests: EtherscanRequests; - - constructor(endpoint: string) { - super(endpoint); - this.client = new EtherscanClient(endpoint); - this.requests = new EtherscanRequests(); - } -} diff --git a/common/libs/nodes/etherscan/requests.ts b/common/libs/nodes/etherscan/requests.ts deleted file mode 100644 index aaea211af5e..00000000000 --- a/common/libs/nodes/etherscan/requests.ts +++ /dev/null @@ -1,84 +0,0 @@ -import ERC20 from 'libs/erc20'; -import RPCRequests from '../rpc/requests'; -import { - CallRequest, - EstimateGasRequest, - GetBalanceRequest, - GetTokenBalanceRequest, - GetTransactionCountRequest, - GetTransactionByHashRequest, - SendRawTxRequest, - GetCurrentBlockRequest -} from './types'; -import { Token } from 'types/network'; -import { IHexStrWeb3Transaction, IHexStrTransaction } from 'libs/transaction'; - -export default class EtherscanRequests extends RPCRequests { - public sendRawTx(signedTx: string): SendRawTxRequest { - return { - module: 'proxy', - action: 'eth_sendRawTransaction', - hex: signedTx - }; - } - - public estimateGas(transaction: IHexStrWeb3Transaction): EstimateGasRequest { - return { - module: 'proxy', - action: 'eth_estimateGas', - to: transaction.to, - value: transaction.value, - data: transaction.data, - from: transaction.from - }; - } - - public getBalance(address: string): GetBalanceRequest { - return { - module: 'account', - action: 'balance', - tag: 'latest', - address - }; - } - - public ethCall(transaction: Pick): CallRequest { - return { - module: 'proxy', - action: 'eth_call', - to: transaction.to, - data: transaction.data - }; - } - - public getTransactionCount(address: string): GetTransactionCountRequest { - return { - module: 'proxy', - action: 'eth_getTransactionCount', - tag: 'latest', - address - }; - } - - public getTransactionByHash(txhash: string): GetTransactionByHashRequest { - return { - module: 'proxy', - action: 'eth_getTransactionByHash', - txhash - }; - } - - public getTokenBalance(address: string, token: Token): GetTokenBalanceRequest { - return this.ethCall({ - to: token.address, - data: ERC20.balanceOf.encodeInput({ _owner: address }) - }); - } - - public getCurrentBlock(): GetCurrentBlockRequest { - return { - module: 'proxy', - action: 'eth_blockNumber' - }; - } -} diff --git a/common/libs/nodes/etherscan/types.ts b/common/libs/nodes/etherscan/types.ts deleted file mode 100644 index cfa6c1db652..00000000000 --- a/common/libs/nodes/etherscan/types.ts +++ /dev/null @@ -1,63 +0,0 @@ -export interface EtherscanReqBase { - module: string; - action?: string; -} - -export interface SendRawTxRequest extends EtherscanReqBase { - module: 'proxy'; - action: 'eth_sendRawTransaction'; - hex: string; -} - -export interface GetBalanceRequest extends EtherscanReqBase { - module: 'account'; - action: 'balance'; - address: string; - tag: 'latest'; -} - -export interface CallRequest extends EtherscanReqBase { - module: 'proxy'; - action: 'eth_call'; - to: string; - data: string; -} - -export type GetTokenBalanceRequest = CallRequest; - -export interface EstimateGasRequest extends EtherscanReqBase { - module: 'proxy'; - action: 'eth_estimateGas'; - to: string; - value: string | number; - data: string; - from: string; -} - -export interface GetTransactionCountRequest extends EtherscanReqBase { - module: 'proxy'; - action: 'eth_getTransactionCount'; - address: string; - tag: 'latest'; -} - -export interface GetTransactionByHashRequest extends EtherscanReqBase { - module: 'proxy'; - action: 'eth_getTransactionByHash'; - txhash: string; -} - -export interface GetCurrentBlockRequest extends EtherscanReqBase { - module: 'proxy'; - action: 'eth_blockNumber'; -} - -export type EtherscanRequest = - | SendRawTxRequest - | GetBalanceRequest - | CallRequest - | GetTokenBalanceRequest - | EstimateGasRequest - | GetTransactionCountRequest - | GetTransactionByHashRequest - | GetCurrentBlockRequest; diff --git a/common/libs/nodes/index.ts b/common/libs/nodes/index.ts index 1cf9b15fc79..5e30908dab8 100644 --- a/common/libs/nodes/index.ts +++ b/common/libs/nodes/index.ts @@ -1,6 +1,160 @@ -export { default as RPCNode } from './rpc'; -export { default as InfuraNode } from './infura'; -export { default as EtherscanNode } from './etherscan'; -export { default as CustomNode } from './custom'; -export { default as Web3Node } from './web3'; +import { shepherd, redux } from 'mycrypto-shepherd'; +import { INode } from '.'; +import { tokenBalanceHandler } from './tokenBalanceProxy'; +import { IProviderConfig } from 'mycrypto-shepherd/dist/lib/ducks/providerConfigs'; + +type DeepPartial = Partial<{ [key in keyof T]: Partial }>; + +export const makeProviderConfig = (options: DeepPartial = {}): IProviderConfig => { + const defaultConfig: IProviderConfig = { + concurrency: 2, + network: 'ETH', + requestFailureThreshold: 3, + supportedMethods: { + getNetVersion: true, + ping: true, + sendCallRequest: true, + sendCallRequests: true, + getBalance: true, + estimateGas: true, + getTransactionCount: true, + getCurrentBlock: true, + sendRawTx: true, + + getTransactionByHash: true, + getTransactionReceipt: true, + + /*web3 methods*/ + signMessage: true, + sendTransaction: true + }, + timeoutThresholdMs: 5000 + }; + + return { + ...defaultConfig, + ...options, + supportedMethods: { + ...defaultConfig.supportedMethods, + ...(options.supportedMethods ? options.supportedMethods : {}) + } + }; +}; +let shepherdProvider: INode; +shepherd + .init() + .then( + provider => (shepherdProvider = (new Proxy(provider, tokenBalanceHandler) as any) as INode) + ); + +export const getShepherdManualMode = () => + redux.store.getState().providerBalancer.balancerConfig.manual; +export const getShepherdOffline = () => + redux.store.getState().providerBalancer.balancerConfig.offline; + +export const makeWeb3Network = (network: string) => `WEB3_${network}`; +export const stripWeb3Network = (network: string) => network.replace('WEB3_', ''); +export const isAutoNode = (nodeName: string) => nodeName.endsWith('_auto') || nodeName === 'web3'; + +const regEthConf = makeProviderConfig({ network: 'ETH' }); +shepherd.useProvider('rpc', 'eth_mycrypto', regEthConf, 'https://api.mycryptoapi.com/eth'); +shepherd.useProvider('etherscan', 'eth_ethscan', regEthConf, 'https://api.etherscan.io/api'); +shepherd.useProvider('infura', 'eth_infura', regEthConf, 'https://mainnet.infura.io/mycrypto'); +shepherd.useProvider( + 'rpc', + 'eth_blockscale', + regEthConf, + 'https://api.dev.blockscale.net/dev/parity' +); + +const regRopConf = makeProviderConfig({ network: 'Ropsten' }); +shepherd.useProvider('infura', 'rop_infura', regRopConf, 'https://ropsten.infura.io/mycrypto'); + +const regKovConf = makeProviderConfig({ network: 'Kovan' }); +shepherd.useProvider('etherscan', 'kov_ethscan', regKovConf, 'https://kovan.etherscan.io/api'); + +const regRinConf = makeProviderConfig({ network: 'Rinkeby' }); +shepherd.useProvider('infura', 'rin_ethscan', regRinConf, 'https://rinkeby.infura.io/mycrypto'); +shepherd.useProvider('etherscan', 'rin_infura', regRinConf, 'https://rinkeby.etherscan.io/api'); + +const regEtcConf = makeProviderConfig({ network: 'ETC' }); +shepherd.useProvider('rpc', 'etc_epool', regEtcConf, 'https://mewapi.epool.io'); + +const regUbqConf = makeProviderConfig({ network: 'UBQ' }); +shepherd.useProvider('rpc', 'ubq', regUbqConf, 'https://pyrus2.ubiqscan.io'); + +const regExpConf = makeProviderConfig({ network: 'EXP' }); +shepherd.useProvider('rpc', 'exp_tech', regExpConf, 'https://node.expanse.tech/'); + +const regPoaConf = makeProviderConfig({ network: 'POA' }); +shepherd.useProvider('rpc', 'poa', regPoaConf, 'https://core.poa.network'); + +const regTomoConf = makeProviderConfig({ network: 'TOMO' }); +shepherd.useProvider('rpc', 'tomo', regTomoConf, 'https://core.tomocoin.io'); + +const regEllaConf = makeProviderConfig({ network: 'ELLA' }); +shepherd.useProvider('rpc', 'ella', regEllaConf, 'https://jsonrpc.ellaism.org'); + +/** + * Pseudo-networks to support metamask / web3 interaction + */ +const web3EthConf = makeProviderConfig({ + network: makeWeb3Network('ETH'), + supportedMethods: { sendRawTx: false, sendTransaction: false, signMessage: false } +}); +shepherd.useProvider('rpc', 'web3_eth_mycrypto', web3EthConf, 'https://api.mycryptoapi.com/eth'); +shepherd.useProvider('etherscan', 'web3_eth_ethscan', web3EthConf, 'https://api.etherscan.io/api'); +shepherd.useProvider( + 'infura', + 'web3_eth_infura', + web3EthConf, + 'https://mainnet.infura.io/mycrypto' +); +shepherd.useProvider( + 'rpc', + 'web3_eth_blockscale', + web3EthConf, + 'https://api.dev.blockscale.net/dev/parity' +); + +const web3RopConf = makeProviderConfig({ + network: makeWeb3Network('Ropsten'), + supportedMethods: { sendRawTx: false, sendTransaction: false, signMessage: false } +}); +shepherd.useProvider( + 'infura', + 'web3_rop_infura', + web3RopConf, + 'https://ropsten.infura.io/mycrypto' +); + +const web3KovConf = makeProviderConfig({ + network: makeWeb3Network('Kovan'), + supportedMethods: { sendRawTx: false, sendTransaction: false, signMessage: false } +}); +shepherd.useProvider( + 'etherscan', + 'web3_kov_ethscan', + web3KovConf, + 'https://kovan.etherscan.io/api' +); + +const web3RinConf = makeProviderConfig({ + network: makeWeb3Network('Rinkeby'), + supportedMethods: { sendRawTx: false, sendTransaction: false, signMessage: false } +}); +shepherd.useProvider( + 'infura', + 'web3_rin_ethscan', + web3RinConf, + 'https://rinkeby.infura.io/mycrypto' +); +shepherd.useProvider( + 'etherscan', + 'web3_rin_infura', + web3RinConf, + 'https://rinkeby.etherscan.io/api' +); + +export { shepherdProvider, shepherd }; export * from './INode'; diff --git a/common/libs/nodes/infura/client.ts b/common/libs/nodes/infura/client.ts deleted file mode 100644 index 2bc400abd9f..00000000000 --- a/common/libs/nodes/infura/client.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { randomBytes } from 'crypto'; -import RPCClient from '../rpc/client'; - -export default class InfuraClient extends RPCClient { - public id(): number { - return parseInt(randomBytes(5).toString('hex'), 16); - } -} diff --git a/common/libs/nodes/infura/index.ts b/common/libs/nodes/infura/index.ts deleted file mode 100644 index d5d0d259c5e..00000000000 --- a/common/libs/nodes/infura/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import RPCNode from '../rpc'; -import InfuraClient from './client'; - -export default class InfuraNode extends RPCNode { - public client: InfuraClient; - - constructor(endpoint: string) { - super(endpoint); - this.client = new InfuraClient(endpoint); - } -} diff --git a/common/libs/nodes/tokenBalanceProxy.ts b/common/libs/nodes/tokenBalanceProxy.ts new file mode 100644 index 00000000000..6bcab000139 --- /dev/null +++ b/common/libs/nodes/tokenBalanceProxy.ts @@ -0,0 +1,82 @@ +import { Token } from 'shared/types/network'; +import ERC20 from 'libs/erc20'; +import { TokenValue } from 'libs/units'; +import { IProvider } from 'mycrypto-shepherd/dist/lib/types'; + +export const tokenBalanceHandler: ProxyHandler = { + get(target, propKey) { + const tokenBalanceShim = (address: string, token: Token) => { + const sendCallRequest: (...rpcArgs: any[]) => Promise = Reflect.get( + target, + 'sendCallRequest' + ); + return sendCallRequest({ + to: token.address, + data: ERC20.balanceOf.encodeInput({ _owner: address }) + }) + .then(result => ({ balance: TokenValue(result), error: null })) + .catch(err => ({ + balance: TokenValue('0'), + error: `Caught error: ${err}` + })); + }; + + const splitBatches = (address: string, tokens: Token[]) => { + const batchSize = 10; + type SplitBatch = { address: string; tokens: Token[] }[]; + const splitBatch: SplitBatch = []; + for (let i = 0; i < tokens.length; i++) { + const arrIdx = Math.ceil((i + 1) / batchSize) - 1; + const token = tokens[i]; + + if (!splitBatch[arrIdx]) { + splitBatch.push({ address, tokens: [token] }); + } else { + splitBatch[arrIdx] = { address, tokens: [...splitBatch[arrIdx].tokens, token] }; + } + } + return splitBatch; + }; + + const tokenBalancesShim = (address: string, tokens: Token[]) => { + const sendCallRequests: (...rpcArgs: any[]) => Promise = Reflect.get( + target, + 'sendCallRequests' + ); + + return sendCallRequests( + tokens.map(t => ({ + to: t.address, + data: ERC20.balanceOf.encodeInput({ _owner: address }) + })) + ).then(response => + response.map(item => { + if (item) { + return { + balance: TokenValue(item), + error: null + }; + } else { + return { + balance: TokenValue('0'), + error: 'Invalid object shape' + }; + } + }) + ); + }; + + if (propKey.toString() === 'getTokenBalance') { + return (address: string, token: Token) => tokenBalanceShim(address, token); + } else if (propKey.toString() === 'getTokenBalances') { + return (address: string, tokens: Token[]) => + Promise.all( + splitBatches(address, tokens).map(({ address: addr, tokens: tkns }) => + tokenBalancesShim(addr, tkns) + ) + ).then(res => res.reduce((acc, curr) => [...acc, ...curr], [])); + } else { + return Reflect.get(target, propKey); + } + } +}; diff --git a/common/libs/wallet/non-deterministic/web3.ts b/common/libs/wallet/non-deterministic/web3.ts index 141f9cd8184..b4f477c657c 100644 --- a/common/libs/wallet/non-deterministic/web3.ts +++ b/common/libs/wallet/non-deterministic/web3.ts @@ -3,7 +3,7 @@ import { IFullWallet } from '../IWallet'; import { bufferToHex } from 'ethereumjs-util'; import { configuredStore } from 'store'; import { getNodeLib, getNetworkNameByChainId } from 'selectors/config'; -import Web3Node, { isWeb3Node } from 'libs/nodes/web3'; +import Web3Node from 'libs/nodes/web3'; import { INode } from 'libs/nodes/INode'; export default class Web3Wallet implements IFullWallet { @@ -31,11 +31,12 @@ export default class Web3Wallet implements IFullWallet { if (!nodeLib) { throw new Error(''); } + /* if (!isWeb3Node(nodeLib)) { throw new Error('Web3 wallets can only be used with a Web3 node.'); - } + }*/ - return nodeLib.signMessage(msgHex, this.address); + return (nodeLib as Web3Node).signMessage(msgHex, this.address); } public async sendTransaction(serializedTransaction: string): Promise { @@ -57,11 +58,16 @@ export default class Web3Wallet implements IFullWallet { }; const state = configuredStore.getState(); - const nodeLib: Web3Node | INode | undefined = getNodeLib(state); + const nodeLib: Web3Node = getNodeLib(state) as any; + + if (!nodeLib) { + throw new Error(''); + } + /* if (!isWeb3Node(nodeLib)) { throw new Error('Web3 wallets can only be used with a Web3 node.'); - } + }*/ await this.networkCheck(nodeLib); return nodeLib.sendTransaction(web3Tx); diff --git a/common/reducers/config/networks/staticNetworks.ts b/common/reducers/config/networks/staticNetworks.ts index 7677501aebb..c8af189716d 100644 --- a/common/reducers/config/networks/staticNetworks.ts +++ b/common/reducers/config/networks/staticNetworks.ts @@ -1,56 +1,27 @@ import { ethPlorer, ETHTokenExplorer, - SecureWalletName, + gasPriceDefaults, InsecureWalletName, - gasPriceDefaults + SecureWalletName } from 'config/data'; import { - ETH_DEFAULT, - ETH_TREZOR, - ETH_LEDGER, + ELLA_DEFAULT, ETC_LEDGER, ETC_TREZOR, + ETH_DEFAULT, + ETH_LEDGER, ETH_TESTNET, + ETH_TREZOR, EXP_DEFAULT, - UBQ_DEFAULT, POA_DEFAULT, TOMO_DEFAULT, - ELLA_DEFAULT + UBQ_DEFAULT } from 'config/dpaths'; import { ConfigAction } from 'actions/config'; -import { BlockExplorerConfig } from 'types/network'; +import { makeExplorer } from 'utils/helpers'; import { StaticNetworksState as State } from './types'; -// Must be a website that follows the ethplorer convention of /tx/[hash] and -// address/[address] to generate the correct functions. -// TODO: put this in utils / libs -interface ExplorerConfig { - name: string; - origin: string; - txPath?: string; - addressPath?: string; - blockPath?: string; -} - -export function makeExplorer(expConfig: ExplorerConfig): BlockExplorerConfig { - const config: ExplorerConfig = { - // Defaults - txPath: 'tx', - addressPath: 'address', - blockPath: 'block', - ...expConfig - }; - - return { - name: config.origin, - origin: config.origin, - txUrl: hash => `${config.origin}/${config.txPath}/${hash}`, - addressUrl: address => `${config.origin}/${config.addressPath}/${address}`, - blockUrl: blockNum => `${config.origin}/${config.blockPath}/${blockNum}` - }; -} - const testnetDefaultGasPrice = { min: 0.1, max: 40, diff --git a/common/reducers/config/nodes/selectedNode.ts b/common/reducers/config/nodes/selectedNode.ts index 9b468c197dd..b73b11270e2 100644 --- a/common/reducers/config/nodes/selectedNode.ts +++ b/common/reducers/config/nodes/selectedNode.ts @@ -9,7 +9,7 @@ import { import { SelectedNodeState as State } from './types'; export const INITIAL_STATE: State = { - nodeId: 'eth_mycrypto', + nodeId: 'eth_auto', pending: false }; diff --git a/common/reducers/config/nodes/staticNodes.ts b/common/reducers/config/nodes/staticNodes.ts index 25998ede2ba..887a8b8f0ef 100644 --- a/common/reducers/config/nodes/staticNodes.ts +++ b/common/reducers/config/nodes/staticNodes.ts @@ -1,12 +1,19 @@ -import { EtherscanNode, InfuraNode, RPCNode } from 'libs/nodes'; import { TypeKeys, NodeAction } from 'actions/config'; -import { StaticNodesState as State } from './types'; +import { shepherdProvider } from 'libs/nodes'; +import { StaticNodesState } from './types'; -export const INITIAL_STATE: State = { +export const INITIAL_STATE: StaticNodesState = { + eth_auto: { + network: 'ETH', + isCustom: false, + lib: shepherdProvider, + service: 'AUTO', + estimateGas: true + }, eth_mycrypto: { network: 'ETH', isCustom: false, - lib: new RPCNode('https://api.mycryptoapi.com/eth'), + lib: shepherdProvider, service: 'MyCrypto', estimateGas: true }, @@ -14,96 +21,166 @@ export const INITIAL_STATE: State = { network: 'ETH', isCustom: false, service: 'Etherscan.io', - lib: new EtherscanNode('https://api.etherscan.io/api'), + lib: shepherdProvider, estimateGas: false }, + eth_infura: { network: 'ETH', isCustom: false, service: 'infura.io', - lib: new InfuraNode('https://mainnet.infura.io/mycrypto'), + lib: shepherdProvider, estimateGas: false }, eth_blockscale: { network: 'ETH', isCustom: false, - lib: new RPCNode('https://api.dev.blockscale.net/dev/parity'), + lib: shepherdProvider, service: 'Blockscale beta', estimateGas: true }, + + rop_auto: { + network: 'Ropsten', + isCustom: false, + service: 'AUTO', + lib: shepherdProvider, + estimateGas: false + }, rop_infura: { network: 'Ropsten', isCustom: false, service: 'infura.io', - lib: new InfuraNode('https://ropsten.infura.io/mycrypto'), + lib: shepherdProvider, + estimateGas: false + }, + + kov_auto: { + network: 'Kovan', + isCustom: false, + service: 'AUTO', + lib: shepherdProvider, estimateGas: false }, kov_ethscan: { network: 'Kovan', isCustom: false, service: 'Etherscan.io', - lib: new EtherscanNode('https://kovan.etherscan.io/api'), + lib: shepherdProvider, + estimateGas: false + }, + + rin_auto: { + network: 'Rinkeby', + isCustom: false, + service: 'AUTO', + lib: shepherdProvider, estimateGas: false }, rin_ethscan: { network: 'Rinkeby', isCustom: false, service: 'Etherscan.io', - lib: new EtherscanNode('https://rinkeby.etherscan.io/api'), + lib: shepherdProvider, estimateGas: false }, rin_infura: { network: 'Rinkeby', isCustom: false, service: 'infura.io', - lib: new InfuraNode('https://rinkeby.infura.io/mycrypto'), + lib: shepherdProvider, + estimateGas: false + }, + + etc_auto: { + network: 'ETC', + isCustom: false, + service: 'AUTO', + lib: shepherdProvider, estimateGas: false }, etc_epool: { network: 'ETC', isCustom: false, service: 'Epool.io', - lib: new RPCNode('https://mewapi.epool.io'), + lib: shepherdProvider, estimateGas: false }, + + ubq_auto: { + network: 'UBQ', + isCustom: false, + service: 'AUTO', + lib: shepherdProvider, + estimateGas: true + }, ubq: { network: 'UBQ', isCustom: false, service: 'ubiqscan.io', - lib: new RPCNode('https://pyrus2.ubiqscan.io'), + lib: shepherdProvider, + estimateGas: true + }, + + exp_auto: { + network: 'EXP', + isCustom: false, + service: 'AUTO', + lib: shepherdProvider, estimateGas: true }, exp_tech: { network: 'EXP', isCustom: false, service: 'Expanse.tech', - lib: new RPCNode('https://node.expanse.tech/'), + lib: shepherdProvider, + estimateGas: true + }, + poa_auto: { + network: 'POA', + isCustom: false, + service: 'AUTO', + lib: shepherdProvider, estimateGas: true }, poa: { network: 'POA', isCustom: false, service: 'poa.network', - lib: new RPCNode('https://core.poa.network'), + lib: shepherdProvider, + estimateGas: true + }, + tomo_auto: { + network: 'TOMO', + isCustom: false, + service: 'AUTO', + lib: shepherdProvider, estimateGas: true }, tomo: { network: 'TOMO', isCustom: false, service: 'tomocoin.io', - lib: new RPCNode('https://core.tomocoin.io'), + lib: shepherdProvider, + estimateGas: true + }, + ella_auto: { + network: 'ELLA', + isCustom: false, + service: 'AUTO', + lib: shepherdProvider, estimateGas: true }, ella: { network: 'ELLA', isCustom: false, service: 'ellaism.org', - lib: new RPCNode('https://jsonrpc.ellaism.org'), + lib: shepherdProvider, estimateGas: true } }; -export const staticNodes = (state: State = INITIAL_STATE, action: NodeAction) => { +const staticNodes = (state: StaticNodesState = INITIAL_STATE, action: NodeAction) => { switch (action.type) { case TypeKeys.CONFIG_NODE_WEB3_SET: return { ...state, [action.payload.id]: action.payload.config }; @@ -115,3 +192,5 @@ export const staticNodes = (state: State = INITIAL_STATE, action: NodeAction) => return state; } }; + +export { StaticNodesState, staticNodes }; diff --git a/common/reducers/config/nodes/types.ts b/common/reducers/config/nodes/types.ts index fa2afa71085..d39ed60bd79 100644 --- a/common/reducers/config/nodes/types.ts +++ b/common/reducers/config/nodes/types.ts @@ -1,6 +1,6 @@ // Moving state types into their own file resolves an annoying webpack bug // https://github.com/angular/angular-cli/issues/2034 -import { NonWeb3NodeConfigs, Web3NodeConfigs, CustomNodeConfig } from 'types/node'; +import { StaticNodeConfigs, CustomNodeConfig } from 'types/node'; export interface CustomNodesState { [customNodeId: string]: CustomNodeConfig; @@ -18,4 +18,4 @@ interface NodeChangePending { export type SelectedNodeState = NodeLoaded | NodeChangePending; -export type StaticNodesState = NonWeb3NodeConfigs & Web3NodeConfigs; +export type StaticNodesState = StaticNodeConfigs; diff --git a/common/sagas/config/node.ts b/common/sagas/config/node.ts index 03f6825fb1a..5f84755360c 100644 --- a/common/sagas/config/node.ts +++ b/common/sagas/config/node.ts @@ -7,7 +7,6 @@ import { take, takeEvery, select, - race, apply, takeLatest } from 'redux-saga/effects'; @@ -35,22 +34,25 @@ import { resetWallet } from 'actions/wallet'; import { translateRaw } from 'translations'; import { StaticNodeConfig, CustomNodeConfig, NodeConfig } from 'types/node'; import { CustomNetworkConfig, StaticNetworkConfig } from 'types/network'; +import { + getShepherdOffline, + isAutoNode, + shepherd, + shepherdProvider, + stripWeb3Network, + makeProviderConfig +} from 'libs/nodes'; -let hasCheckedOnline = false; export function* pollOfflineStatus(): SagaIterator { + let hasCheckedOnline = false; while (true) { - const nodeConfig: StaticNodeConfig = yield select(getNodeConfig); const isOffline: boolean = yield select(getOffline); // If our offline state disagrees with the browser, run a check // Don't check if the user is in another tab or window const shouldPing = !hasCheckedOnline || navigator.onLine === isOffline; if (shouldPing && !document.hidden) { - const { pingSucceeded } = yield race({ - pingSucceeded: call(nodeConfig.lib.ping.bind(nodeConfig.lib)), - timeout: call(delay, 5000) - }); - + const pingSucceeded = yield call(getShepherdOffline); if (pingSucceeded && isOffline) { // If we were able to ping but redux says we're offline, mark online yield put( @@ -134,30 +136,9 @@ export function* handleNodeChangeIntent({ nextNodeConfig = yield select(getStaticNodeFromId, nodeIdToSwitchTo); } - // Grab current block from the node, before switching, to confirm it's online - // Give it 5 seconds before we call it offline - let currentBlock; - let timeout; - try { - const { lb, to } = yield race({ - lb: apply(nextNodeConfig.lib, nextNodeConfig.lib.getCurrentBlock), - to: call(delay, 5000) - }); - currentBlock = lb; - timeout = to; - } catch (err) { - console.error(err); - // Whether it times out or errors, same message - timeout = true; - } - - if (timeout) { - return yield* bailOut(translateRaw('ERROR_32')); - } - const nextNetwork: StaticNetworkConfig | CustomNetworkConfig = yield select( getNetworkConfigById, - nextNodeConfig.network + stripWeb3Network(nextNodeConfig.network) ); if (!nextNetwork) { @@ -166,6 +147,28 @@ export function* handleNodeChangeIntent({ ); } + if (isAutoNode(nodeIdToSwitchTo)) { + shepherd.auto(); + if (currentConfig.network !== nextNodeConfig.network) { + yield apply(shepherd, shepherd.switchNetworks, [nextNodeConfig.network]); + } + } else { + try { + yield apply(shepherd, shepherd.manual, [nodeIdToSwitchTo, false]); + } catch (err) { + console.error(err); + return yield* bailOut(translateRaw('ERROR_32')); + } + } + + let currentBlock; + try { + currentBlock = yield apply(shepherdProvider, shepherdProvider.getCurrentBlock); + } catch (err) { + console.error(err); + return yield* bailOut(translateRaw('ERROR_32')); + } + yield put(setLatestBlock(currentBlock)); yield put(changeNode({ networkId: nextNodeConfig.network, nodeId: nodeIdToSwitchTo })); @@ -174,7 +177,14 @@ export function* handleNodeChangeIntent({ } } -export function* switchToNewNode(action: AddCustomNodeAction): SagaIterator { +export function* handleAddCustomNode(action: AddCustomNodeAction): SagaIterator { + const { payload: { config } } = action; + shepherd.useProvider( + 'myccustom', + config.id, + makeProviderConfig({ network: config.network }), + config + ); yield put(changeNodeIntent(action.payload.id)); } @@ -208,5 +218,5 @@ export const node = [ takeEvery(TypeKeys.CONFIG_NODE_CHANGE_FORCE, handleNodeChangeForce), takeLatest(TypeKeys.CONFIG_POLL_OFFLINE_STATUS, handlePollOfflineStatus), takeEvery(TypeKeys.CONFIG_LANGUAGE_CHANGE, reload), - takeEvery(TypeKeys.CONFIG_ADD_CUSTOM_NODE, switchToNewNode) + takeEvery(TypeKeys.CONFIG_ADD_CUSTOM_NODE, handleAddCustomNode) ]; diff --git a/common/sagas/config/web3.ts b/common/sagas/config/web3.ts index fb0945a453a..f18d804c047 100644 --- a/common/sagas/config/web3.ts +++ b/common/sagas/config/web3.ts @@ -1,27 +1,101 @@ import { TypeKeys as WalletTypeKeys } from 'actions/wallet/constants'; import { Web3Wallet } from 'libs/wallet'; import { SagaIterator } from 'redux-saga'; -import { select, put, takeEvery, call } from 'redux-saga/effects'; -import { changeNodeForce, TypeKeys, web3SetNode } from 'actions/config'; -import { getNodeId, getStaticAltNodeIdToWeb3, getNetworkNameByChainId } from 'selectors/config'; -import { setupWeb3Node, Web3Service } from 'libs/nodes/web3'; -import { Web3NodeConfig } from 'types/node'; -import { SetWalletAction } from 'actions/wallet'; +import { select, put, takeEvery, call, apply, take } from 'redux-saga/effects'; +import { + changeNodeForce, + TypeKeys, + web3SetNode, + web3UnsetNode, + changeNodeIntent +} from 'actions/config'; +import { + getNodeId, + getStaticAltNodeIdToWeb3, + getNetworkNameByChainId, + getNetworkConfig, + getWeb3Node +} from 'selectors/config'; +import { setupWeb3Node, Web3Service, isWeb3Node } from 'libs/nodes/web3'; +import { SetWalletAction, setWallet } from 'actions/wallet'; +import { + shepherd, + makeProviderConfig, + getShepherdManualMode, + makeWeb3Network, + stripWeb3Network, + shepherdProvider +} from 'libs/nodes'; +import { NetworkConfig } from 'shared/types/network'; +import { StaticNodeConfig } from 'shared/types/node'; +import { showNotification } from 'actions/notifications'; +import translate from 'translations'; + +let web3Added = false; export function* initWeb3Node(): SagaIterator { const { networkId, lib } = yield call(setupWeb3Node); - const network = yield select(getNetworkNameByChainId, networkId); + const network: string = yield select(getNetworkNameByChainId, networkId); + const web3Network = makeWeb3Network(network); - const config: Web3NodeConfig = { + const config: StaticNodeConfig = { isCustom: false, - network, + network: web3Network as any, service: Web3Service, - lib, + lib: shepherdProvider, estimateGas: false, hidden: true }; + if (getShepherdManualMode()) { + yield apply(shepherd, shepherd.auto); + } + + if (!web3Added) { + shepherd.useProvider('web3', 'web3', makeProviderConfig({ network: web3Network })); + } + + web3Added = true; + yield put(web3SetNode({ id: 'web3', config })); + return lib; +} + +// inspired by v3: +// https://github.com/kvhnuke/etherwallet/blob/417115b0ab4dd2033d9108a1a5c00652d38db68d/app/scripts/controllers/decryptWalletCtrl.js#L311 +export function* unlockWeb3(): SagaIterator { + try { + const nodeLib = yield call(initWeb3Node); + yield put(changeNodeIntent('web3')); + yield take( + (action: any) => + action.type === TypeKeys.CONFIG_NODE_CHANGE && action.payload.nodeId === 'web3' + ); + + const web3Node: any | null = yield select(getWeb3Node); + if (!web3Node) { + throw Error('Web3 node config not found!'); + } + const network = web3Node.network; + + if (!isWeb3Node(nodeLib)) { + throw new Error('Cannot use Web3 wallet without a Web3 node.'); + } + + const accounts: string = yield apply(nodeLib, nodeLib.getAccounts); + const address = accounts[0]; + + if (!address) { + throw new Error('No accounts found in MetaMask / Mist.'); + } + const wallet = new Web3Wallet(address, stripWeb3Network(network)); + yield put(setWallet(wallet)); + } catch (err) { + console.error(err); + // unset web3 node so node dropdown isn't disabled + yield put(web3UnsetNode()); + yield put(showNotification('danger', translate(err.message))); + } } // unset web3 as the selected node if a non-web3 wallet has been selected @@ -34,6 +108,13 @@ export function* unsetWeb3NodeOnWalletEvent(action: SetWalletAction): SagaIterat return; } + const network: NetworkConfig = yield select(getNetworkConfig); + + if (getShepherdManualMode()) { + yield apply(shepherd, shepherd.auto); + } + yield apply(shepherd, shepherd.switchNetworks, [stripWeb3Network(network.name)]); + const altNode = yield select(getStaticAltNodeIdToWeb3); // forcefully switch back to a node with the same network as MetaMask/Mist yield put(changeNodeForce(altNode)); @@ -46,6 +127,13 @@ export function* unsetWeb3Node(): SagaIterator { return; } + const network: NetworkConfig = yield select(getNetworkConfig); + + if (getShepherdManualMode()) { + yield apply(shepherd, shepherd.auto); + } + yield apply(shepherd, shepherd.switchNetworks, [stripWeb3Network(network.name)]); + const altNode = yield select(getStaticAltNodeIdToWeb3); // forcefully switch back to a node with the same network as MetaMask/Mist yield put(changeNodeForce(altNode)); @@ -53,5 +141,6 @@ export function* unsetWeb3Node(): SagaIterator { export const web3 = [ takeEvery(TypeKeys.CONFIG_NODE_WEB3_UNSET, unsetWeb3Node), - takeEvery(WalletTypeKeys.WALLET_SET, unsetWeb3NodeOnWalletEvent) + takeEvery(WalletTypeKeys.WALLET_SET, unsetWeb3NodeOnWalletEvent), + takeEvery(WalletTypeKeys.WALLET_UNLOCK_WEB3, unlockWeb3) ]; diff --git a/common/sagas/wallet/wallet.ts b/common/sagas/wallet/wallet.ts index 28ef299e55f..7272fad8ad9 100644 --- a/common/sagas/wallet/wallet.ts +++ b/common/sagas/wallet/wallet.ts @@ -21,7 +21,7 @@ import { setPasswordPrompt } from 'actions/wallet'; import { Wei } from 'libs/units'; -import { changeNodeIntent, web3UnsetNode, TypeKeys as ConfigTypeKeys } from 'actions/config'; +import { TypeKeys as ConfigTypeKeys } from 'actions/config'; import { AddCustomTokenAction, TypeKeys as CustomTokenTypeKeys } from 'actions/customTokens'; import { INode } from 'libs/nodes/INode'; import { @@ -33,12 +33,11 @@ import { KeystoreTypes, getUtcWallet, signWrapper, - Web3Wallet, WalletConfig } from 'libs/wallet'; import { SagaIterator, delay, Task } from 'redux-saga'; import { apply, call, fork, put, select, takeEvery, take, cancel } from 'redux-saga/effects'; -import { getNodeLib, getAllTokens, getOffline, getWeb3Node } from 'selectors/config'; +import { getNodeLib, getAllTokens, getOffline } from 'selectors/config'; import { getTokens, getWalletInst, @@ -47,12 +46,9 @@ import { TokenBalance } from 'selectors/wallet'; import translate from 'translations'; -import Web3Node, { isWeb3Node } from 'libs/nodes/web3'; import { loadWalletConfig, saveWalletConfig } from 'utils/localStorage'; import { getTokenBalances, filterScannedTokenBalances } from './helpers'; import { Token } from 'types/network'; -import { Web3NodeConfig } from '../../../shared/types/node'; -import { initWeb3Node } from 'sagas/config/web3'; export interface TokenBalanceLookup { [symbol: string]: TokenBalance; @@ -256,44 +252,6 @@ export function* unlockMnemonic(action: UnlockMnemonicAction): SagaIterator { yield put(setWallet(wallet)); } -// inspired by v3: -// https://github.com/kvhnuke/etherwallet/blob/417115b0ab4dd2033d9108a1a5c00652d38db68d/app/scripts/controllers/decryptWalletCtrl.js#L311 -export function* unlockWeb3(): SagaIterator { - try { - yield call(initWeb3Node); - yield put(changeNodeIntent('web3')); - yield take( - (action: any) => - action.type === ConfigTypeKeys.CONFIG_NODE_CHANGE && action.payload.nodeId === 'web3' - ); - - const web3Node: Web3NodeConfig | null = yield select(getWeb3Node); - if (!web3Node) { - throw Error('Web3 node config not found!'); - } - const network = web3Node.network; - const nodeLib: Web3Node = web3Node.lib; - - if (!isWeb3Node(nodeLib)) { - throw new Error('Cannot use Web3 wallet without a Web3 node.'); - } - - const accounts: string = yield apply(nodeLib, nodeLib.getAccounts); - const address = accounts[0]; - - if (!address) { - throw new Error('No accounts found in MetaMask / Mist.'); - } - const wallet = new Web3Wallet(address, network); - yield put(setWallet(wallet)); - } catch (err) { - console.error(err); - // unset web3 node so node dropdown isn't disabled - yield put(web3UnsetNode()); - yield put(showNotification('danger', translate(err.message))); - } -} - export function* handleCustomTokenAdd(action: AddCustomTokenAction): SagaIterator { // Add the custom token to our current wallet's config const wallet: null | IWallet = yield select(getWalletInst); @@ -315,7 +273,6 @@ export default function* walletSaga(): SagaIterator { takeEvery(TypeKeys.WALLET_UNLOCK_PRIVATE_KEY, unlockPrivateKey), takeEvery(TypeKeys.WALLET_UNLOCK_KEYSTORE, unlockKeystore), takeEvery(TypeKeys.WALLET_UNLOCK_MNEMONIC, unlockMnemonic), - takeEvery(TypeKeys.WALLET_UNLOCK_WEB3, unlockWeb3), takeEvery(TypeKeys.WALLET_SET, handleNewWallet), takeEvery(TypeKeys.WALLET_SCAN_WALLET_FOR_TOKENS, scanWalletForTokens), takeEvery(TypeKeys.WALLET_SET_WALLET_TOKENS, handleSetWalletTokens), diff --git a/common/selectors/config/networks.ts b/common/selectors/config/networks.ts index c2126b53e4c..a3107af3ad7 100644 --- a/common/selectors/config/networks.ts +++ b/common/selectors/config/networks.ts @@ -6,6 +6,7 @@ import { NetworkContract } from 'types/network'; import { getNodeConfig } from 'selectors/config'; +import { stripWeb3Network } from 'libs/nodes'; const getConfig = (state: AppState) => state.config; export const getNetworks = (state: AppState) => getConfig(state).networks; @@ -31,7 +32,8 @@ export const getStaticNetworkIds = (state: AppState): StaticNetworkIds[] => export const isStaticNetworkId = ( state: AppState, networkId: string -): networkId is StaticNetworkIds => Object.keys(getStaticNetworkConfigs(state)).includes(networkId); +): networkId is StaticNetworkIds => + Object.keys(getStaticNetworkConfigs(state)).includes(stripWeb3Network(networkId)); export const getStaticNetworkConfig = (state: AppState): StaticNetworkConfig | undefined => { const selectedNetwork = getSelectedNetwork(state); @@ -44,7 +46,8 @@ export const getStaticNetworkConfig = (state: AppState): StaticNetworkConfig | u return defaultNetwork; }; -export const getSelectedNetwork = (state: AppState) => getNodeConfig(state).network; +export const getSelectedNetwork = (state: AppState) => + stripWeb3Network(getNodeConfig(state).network); export const getCustomNetworkConfig = (state: AppState): CustomNetworkConfig | undefined => { const selectedNetwork = getSelectedNetwork(state); diff --git a/common/selectors/config/nodes.ts b/common/selectors/config/nodes.ts index bba5b5d8aa1..e461d6c6e86 100644 --- a/common/selectors/config/nodes.ts +++ b/common/selectors/config/nodes.ts @@ -4,17 +4,13 @@ import { getCustomNetworkConfigs, isStaticNetworkId } from 'selectors/config'; -import { - CustomNodeConfig, - StaticNodeConfig, - StaticNodeId, - Web3NodeConfig, - StaticNodeWithWeb3Id -} from 'types/node'; +import { CustomNodeConfig, StaticNodeConfig, StaticNodeId } from 'types/node'; +import { StaticNetworkIds } from 'types/network'; const getConfig = (state: AppState) => state.config; import { INITIAL_STATE as SELECTED_NODE_INITIAL_STATE } from 'reducers/config/nodes/selectedNode'; +import { shepherdProvider, INode, stripWeb3Network } from 'libs/nodes'; export const getNodes = (state: AppState) => getConfig(state).nodes; @@ -33,7 +29,8 @@ export const getStaticAltNodeIdToWeb3 = (state: AppState) => { return SELECTED_NODE_INITIAL_STATE.nodeId; } const res = Object.entries(configs).find( - ([_, config]: [StaticNodeId, StaticNodeConfig]) => web3.network === config.network + ([_, config]: [StaticNodeId, StaticNodeConfig]) => + stripWeb3Network(web3.network) === config.network ); if (res) { return res[0]; @@ -44,7 +41,7 @@ export const getStaticAltNodeIdToWeb3 = (state: AppState) => { export const getStaticNodeFromId = (state: AppState, nodeId: StaticNodeId) => getStaticNodeConfigs(state)[nodeId]; -export const isStaticNodeId = (state: AppState, nodeId: string): nodeId is StaticNodeWithWeb3Id => +export const isStaticNodeId = (state: AppState, nodeId: string): nodeId is StaticNodeId => Object.keys(getStaticNodeConfigs(state)).includes(nodeId); const getStaticNodeConfigs = (state: AppState) => getNodes(state).staticNodes; @@ -56,12 +53,11 @@ export const getStaticNodeConfig = (state: AppState) => { return defaultNetwork; }; -export const getWeb3Node = (state: AppState): Web3NodeConfig | null => { - const isWeb3Node = (nodeId: string, _: StaticNodeConfig | Web3NodeConfig): _ is Web3NodeConfig => - nodeId === 'web3'; +export const getWeb3Node = (state: AppState): StaticNodeConfig | null => { + const isWeb3Node = (nodeId: string) => nodeId === 'web3'; const currNode = getStaticNodeConfig(state); const currNodeId = getNodeId(state); - if (currNode && currNodeId && isWeb3Node(currNodeId, currNode)) { + if (currNode && currNodeId && isWeb3Node(currNodeId)) { return currNode; } return null; @@ -108,12 +104,8 @@ export function getNodeConfig(state: AppState): StaticNodeConfig | CustomNodeCon return config; } -export function getNodeLib(state: AppState) { - const config = getNodeConfig(state); - if (!config) { - throw Error('No node lib found when trying to select from state'); - } - return config.lib; +export function getNodeLib(_: AppState): INode { + return shepherdProvider; } export interface NodeOption { @@ -127,7 +119,8 @@ export interface NodeOption { export function getStaticNodeOptions(state: AppState): NodeOption[] { const staticNetworkConfigs = getStaticNetworkConfigs(state); return Object.entries(getStaticNodes(state)).map(([nodeId, node]: [string, StaticNodeConfig]) => { - const associatedNetwork = staticNetworkConfigs[node.network]; + const associatedNetwork = + staticNetworkConfigs[stripWeb3Network(node.network) as StaticNetworkIds]; const opt: NodeOption = { isCustom: node.isCustom, value: nodeId, diff --git a/common/store/configAndTokens.ts b/common/store/configAndTokens.ts index 7dd7ae0d435..1f75dc40c3b 100644 --- a/common/store/configAndTokens.ts +++ b/common/store/configAndTokens.ts @@ -14,8 +14,8 @@ import { getCustomNetworkConfigs } from 'selectors/config'; import RootReducer, { AppState } from 'reducers'; -import CustomNode from 'libs/nodes/custom'; import { CustomNodeConfig } from 'types/node'; +import { shepherd, makeProviderConfig, shepherdProvider, isAutoNode } from 'libs/nodes'; const appInitialState = RootReducer(undefined as any, { type: 'inital_state' }); type DeepPartial = { [P in keyof T]?: DeepPartial }; @@ -113,6 +113,7 @@ function rehydrateNodes( customNodes, staticNodes ); + return nextNodeState; } @@ -135,6 +136,13 @@ function getSavedSelectedNode( ? staticNodes[savedNodeId] : customNodes[savedNodeId]; + if (nodeConfigExists) { + if (isAutoNode(savedNodeId)) { + shepherd.switchNetworks(nodeConfigExists.network); + } else { + shepherd.manual(savedNodeId, false); + } + } return { nodeId: nodeConfigExists ? savedNodeId : initialState.nodeId, pending: false }; } @@ -152,7 +160,14 @@ function rehydrateCustomNodes( return hydratedNodes; } - const lib = new CustomNode(configToHydrate); + shepherd.useProvider( + 'myccustom', + configToHydrate.id, + makeProviderConfig({ network: configToHydrate.network }), + configToHydrate + ); + + const lib = shepherdProvider; const hydratedNode: CustomNodeConfig = { ...configToHydrate, lib }; return { ...hydratedNodes, [customNodeId]: hydratedNode }; }, diff --git a/common/translations/lang/en.json b/common/translations/lang/en.json index ed8da020558..f362d6775a7 100644 --- a/common/translations/lang/en.json +++ b/common/translations/lang/en.json @@ -449,11 +449,12 @@ "CHECK_TX_STATUS_TITLE": "Check Transaction Status", "CHECK_TX_STATUS_DESCRIPTION_1": "Enter your Transaction Hash to check on its status. ", "CHECK_TX_STATUS_DESCRIPTION_2": "If you don’t know your Transaction Hash, you can look it up on [$block_explorer]($block_explorer_link) by looking up your address.", + "CHECK_TX_STATUS_DESCRIPTION_MULTIPLE": "If you don’t know your Transaction Hash, you can look it up on [$block_explorer]($block_explorer_link) or [$block_explorer_2]($block_explorer_link_2) by looking up your address.", "CHECK_TX_TITLE": "Transaction Found", "TX_STATUS": "Status", "TX_BLOCK_NUMB": "Block Number", "TX_GAS_USED": "Gas Used", - "VERIFY_TX": "Verify transaction on $block_explorer", + "VERIFY_TX": "Verify ($block_explorer)", "SWAP_DEPOSIT_INPUT_LABEL": "Deposit", "SWAP_RECEIVE_INPUT_LABEL": "Receive", "SWAP_MAX_ERROR": "Maximum $rate_max $origin_id", diff --git a/common/utils/helpers.ts b/common/utils/helpers.ts index 9e42eff9b74..64bdb5776ed 100644 --- a/common/utils/helpers.ts +++ b/common/utils/helpers.ts @@ -1,6 +1,7 @@ import qs from 'query-string'; import has from 'lodash/has'; import EthTx from 'ethereumjs-tx'; +import { BlockExplorerConfig } from 'types/network'; interface IObjectValue { [key: string]: any; @@ -73,3 +74,29 @@ export function signTransactionWithSignature(tx: EthTx, signature: string): Buff return tx.serialize(); } + +interface ExplorerConfig { + name: string; + origin: string; + txPath?: string; + addressPath?: string; + blockPath?: string; +} + +export function makeExplorer(expConfig: ExplorerConfig): BlockExplorerConfig { + const config: ExplorerConfig = { + // Defaults + txPath: 'tx', + addressPath: 'address', + blockPath: 'block', + ...expConfig + }; + + return { + name: config.name, + origin: config.origin, + txUrl: hash => `${config.origin}/${config.txPath}/${hash}`, + addressUrl: address => `${config.origin}/${config.addressPath}/${address}`, + blockUrl: blockNum => `${config.origin}/${config.blockPath}/${blockNum}` + }; +} diff --git a/package.json b/package.json index 8660a7f0b2b..6cf40d93d30 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "ledgerco": "1.2.1", "lodash": "4.17.5", "moment": "2.22.0", + "mycrypto-shepherd": "1.0.0", "normalizr": "3.2.4", "qrcode": "1.2.0", "qrcode.react": "0.8.0", diff --git a/shared/types/node.d.ts b/shared/types/node.d.ts index d0d6952abc2..36b029ee504 100644 --- a/shared/types/node.d.ts +++ b/shared/types/node.d.ts @@ -1,13 +1,12 @@ -import { RPCNode, Web3Node } from 'libs/nodes'; +import { INode } from 'libs/nodes'; import { StaticNetworkIds } from './network'; import { StaticNodesState, CustomNodesState } from 'reducers/config/nodes'; -import CustomNode from 'libs/nodes/custom'; interface CustomNodeConfig { id: string; isCustom: true; name: string; - lib: CustomNode; + lib: INode; service: 'your custom node'; url: string; network: string; @@ -20,39 +19,39 @@ interface CustomNodeConfig { interface StaticNodeConfig { isCustom: false; network: StaticNetworkIds; - lib: RPCNode | Web3Node; + lib: INode; service: string; estimateGas?: boolean; hidden?: boolean; } -interface Web3NodeConfig extends StaticNodeConfig { - lib: Web3Node; -} - declare enum StaticNodeId { + ETH_AUTO = 'eth_auto', ETH_MYCRYPTO = 'eth_mycrypto', ETH_ETHSCAN = 'eth_ethscan', ETH_INFURA = 'eth_infura', ETH_BLOCKSCALE = 'eth_blockscale', + ROP_AUTO = 'rop_auto', ROP_INFURA = 'rop_infura', + KOV_AUTO = 'kov_auto', KOV_ETHSCAN = 'kov_ethscan', + RIN_AUTO = 'rin_auto', RIN_ETHSCAN = 'rin_ethscan', RIN_INFURA = 'rin_infura', + ETC_AUTO = 'etc_auto', ETC_EPOOL = 'etc_epool', + UBQ_AUTO = 'ubq_auto', UBQ = 'ubq', + EXP_AUTO = 'exp_auto', EXP_TECH = 'exp_tech', + POA_AUTO = 'poa_auto', POA = 'poa', + TOMO_AUTO = 'tomo_auto', TOMO = 'tomo', + ELLA_AUTO = 'ella_auto', ELLA = 'ella' } -type StaticNodeWithWeb3Id = StaticNodeId | 'web3'; - -type NonWeb3NodeConfigs = { [key in StaticNodeId]: StaticNodeConfig }; - -interface Web3NodeConfigs { - web3?: Web3NodeConfig; -} +type StaticNodeConfigs = { [key in StaticNodeId]: StaticNodeConfig } & { web3?: StaticNodeConfig }; type NodeConfig = StaticNodesState[StaticNodeId] | CustomNodesState[string]; diff --git a/spec/integration/RpcNodeTestConfig.js b/spec/integration/RpcNodeTestConfig.js index f9928c31be2..0f980d08951 100644 --- a/spec/integration/RpcNodeTestConfig.js +++ b/spec/integration/RpcNodeTestConfig.js @@ -1,5 +1,3 @@ module.exports = { - RpcNodes: ['eth_mycrypto', 'etc_epool', 'etc_epool', 'rop_mew'], - EtherscanNodes: ['eth_ethscan', 'kov_ethscan', 'rin_ethscan'], - InfuraNodes: ['eth_infura', 'rop_infura', 'rin_infura'] + RpcNodes: ['eth_mycrypto', 'etc_epool', 'etc_epool', 'rop_mew'] }; diff --git a/spec/integration/data.int.ts b/spec/integration/data.int.ts index fbfdc457aa6..23c4c1a3d75 100644 --- a/spec/integration/data.int.ts +++ b/spec/integration/data.int.ts @@ -1,9 +1,7 @@ -import { RPCNode } from '../../common/libs/nodes'; import { Validator, ValidatorResult } from 'jsonschema'; import { schema } from '../../common/libs/validators'; import 'url-search-params-polyfill'; -import EtherscanNode from 'libs/nodes/etherscan'; -import InfuraNode from 'libs/nodes/infura'; +import RPCNode from 'libs/nodes/rpc'; import RpcNodeTestConfig from './RpcNodeTestConfig'; import { StaticNodeConfig } from 'types/node'; import { staticNodesExpectedState } from '../reducers/config/nodes/staticNodes.spec'; @@ -69,19 +67,11 @@ function testRpcRequests(node: RPCNode, service: string) { } const mapNodeEndpoints = (nodes: { [key: string]: StaticNodeConfig }) => { - const { RpcNodes, EtherscanNodes, InfuraNodes } = RpcNodeTestConfig; + const { RpcNodes } = RpcNodeTestConfig; RpcNodes.forEach(n => { testRpcRequests(nodes[n].lib as RPCNode, `${nodes[n].service} ${nodes[n].network}`); }); - - EtherscanNodes.forEach(n => { - testRpcRequests(nodes[n].lib as EtherscanNode, `${nodes[n].service} ${nodes[n].network}`); - }); - - InfuraNodes.forEach(n => { - testRpcRequests(nodes[n].lib as InfuraNode, `${nodes[n].service} ${nodes[n].network}`); - }); }; mapNodeEndpoints((staticNodesExpectedState.initialState as any) as { diff --git a/spec/reducers/config/__snapshots__/config.spec.ts.snap b/spec/reducers/config/__snapshots__/config.spec.ts.snap index 02f759aeec3..4dc590b32bf 100644 --- a/spec/reducers/config/__snapshots__/config.spec.ts.snap +++ b/spec/reducers/config/__snapshots__/config.spec.ts.snap @@ -1,37 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`handleNodeChangeIntent* should race getCurrentBlock and delay 1`] = ` +exports[`handleNodeChangeIntent* should get the next network 1`] = ` Object { "@@redux-saga/IO": true, - "RACE": Object { - "lb": Object { - "@@redux-saga/IO": true, - "CALL": Object { - "args": Array [], - "context": RpcNode { - "client": RPCClient { - "batch": [Function], - "call": [Function], - "createHeaders": [Function], - "decorateRequest": [Function], - "endpoint": "https://jsonrpc.ellaism.org", - "headers": Object {}, - }, - "requests": RPCRequests {}, - }, - "fn": [Function], - }, - }, - "to": Object { - "@@redux-saga/IO": true, - "CALL": Object { - "args": Array [ - 5000, - ], - "context": null, - "fn": [Function], - }, - }, + "SELECT": Object { + "args": Array [ + "ELLA", + ], + "selector": [Function], }, } `; @@ -40,52 +16,10 @@ exports[`handleNodeChangeIntent* should select getCustomNodeConfig and match rac Object { "@@redux-saga/IO": true, "SELECT": Object { - "args": Array [], + "args": Array [ + "CustomNetworkId", + ], "selector": [Function], }, } `; - -exports[`pollOfflineStatus* should race pingSucceeded and timeout 1`] = ` -Object { - "@@redux-saga/IO": true, - "RACE": Object { - "pingSucceeded": Object { - "@@redux-saga/IO": true, - "CALL": Object { - "args": Array [], - "context": null, - "fn": [Function], - }, - }, - "timeout": Object { - "@@redux-saga/IO": true, - "CALL": Object { - "args": Array [ - 5000, - ], - "context": null, - "fn": [Function], - }, - }, - }, -} -`; - -exports[`pollOfflineStatus* should toggle offline and show notification if navigator agrees with isOffline and ping fails 1`] = ` -Object { - "@@redux-saga/IO": true, - "PUT": Object { - "action": Object { - "payload": Object { - "duration": 5000, - "id": 0.001, - "level": "info", - "msg": "You are currently offline. Some features will be unavailable.", - }, - "type": "SHOW_NOTIFICATION", - }, - "channel": null, - }, -} -`; diff --git a/spec/reducers/config/config.spec.ts b/spec/reducers/config/config.spec.ts index 5bef5b56684..79c28f70c8d 100644 --- a/spec/reducers/config/config.spec.ts +++ b/spec/reducers/config/config.spec.ts @@ -1,6 +1,6 @@ import { configuredStore } from 'store'; import { delay, SagaIterator } from 'redux-saga'; -import { call, cancel, fork, put, take, select } from 'redux-saga/effects'; +import { call, cancel, fork, put, take, select, apply } from 'redux-saga/effects'; import { cloneableGenerator, createMockTask } from 'redux-saga/utils'; import { toggleOffline, @@ -21,9 +21,9 @@ import { getOffline, isStaticNodeId, getStaticNodeFromId, - getNetworkConfigById, getCustomNodeFromId, - getStaticAltNodeIdToWeb3 + getStaticAltNodeIdToWeb3, + getNetworkConfig } from 'selectors/config'; import { Web3Wallet } from 'libs/wallet'; import { showNotification } from 'actions/notifications'; @@ -34,6 +34,7 @@ import { metaExpectedState } from './meta/meta.spec'; import { selectedNodeExpectedState } from './nodes/selectedNode.spec'; import { customNodesExpectedState, firstCustomNodeId } from './nodes/customNodes.spec'; import { unsetWeb3Node, unsetWeb3NodeOnWalletEvent } from 'sagas/config/web3'; +import { shepherd } from 'mycrypto-shepherd'; // init module configuredStore.getState(); @@ -53,10 +54,6 @@ describe('pollOfflineStatus*', () => { pingSucceeded: true, timeout: false }; - const raceFailure = { - pingSucceeded: false, - timeout: true - }; let originalHidden: any; let originalOnLine: any; @@ -87,10 +84,6 @@ describe('pollOfflineStatus*', () => { 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)); }); @@ -98,17 +91,16 @@ describe('pollOfflineStatus*', () => { it('should call delay if document is hidden', () => { data.hiddenDoc = data.gen.clone(); doc.hidden = true; - expect(data.hiddenDoc.next(togglingToOnline.offline).value).toEqual(call(delay, 1000)); - doc.hidden = false; - }); - - it('should race pingSucceeded and timeout', () => { data.isOfflineClone = data.gen.clone(); data.shouldDelayClone = data.gen.clone(); - expect(data.gen.next(togglingToOffline.offline).value).toMatchSnapshot(); + expect(data.hiddenDoc.next(togglingToOnline.offline).value).toEqual(call(delay, 1000)); + + doc.hidden = false; }); it('should toggle offline and show notification if navigator disagrees with isOffline and ping succeeds', () => { + data.gen.next(raceSuccess); + expect(data.gen.next(raceSuccess).value).toEqual( put(showNotification('success', 'Your connection to the network has been restored!', 3000)) ); @@ -117,10 +109,10 @@ describe('pollOfflineStatus*', () => { it('should toggle offline and show notification if navigator agrees with isOffline and ping fails', () => { nav.onLine = togglingToOffline.offline; - expect(data.isOfflineClone.next(togglingToOnline.offline)); - expect(data.isOfflineClone.next(raceFailure).value).toMatchSnapshot(); + + data.isOfflineClone.next(false); + data.isOfflineClone.next(false); expect(data.isOfflineClone.next().value).toEqual(put(toggleOffline())); - nav.onLine = togglingToOnline.offline; }); }); @@ -158,12 +150,6 @@ describe('handleNodeChangeIntent*', () => { const changeNodeIntentAction = changeNodeIntent(newNodeId); const latestBlock = '0xa'; - const raceSuccess = { - lb: latestBlock - }; - const raceFailure = { - to: true - }; const data = {} as any; data.gen = cloneableGenerator(handleNodeChangeIntent)(changeNodeIntentAction); @@ -198,21 +184,35 @@ describe('handleNodeChangeIntent*', () => { expect(data.gen.next(defaultNodeConfig).value).toEqual(select(getStaticNodeFromId, newNodeId)); }); - it('should race getCurrentBlock and delay', () => { + it('should get the next network', () => { expect(data.gen.next(newNodeConfig).value).toMatchSnapshot(); }); it('should show error and revert to previous node if check times out', () => { data.clone1 = data.gen.clone(); - shouldBailOut(data.clone1, raceFailure, translateRaw('ERROR_32')); + data.clone1.next(true); + expect(data.clone1.throw('err').value).toEqual(select(getNodeId)); + expect(data.clone1.next(defaultNodeId).value).toEqual( + put(showNotification('danger', translateRaw('ERROR_32'), 5000)) + ); + expect(data.clone1.next().value).toEqual( + put(changeNode({ networkId: defaultNodeConfig.network, nodeId: defaultNodeId })) + ); + expect(data.clone1.next().done).toEqual(true); }); - it('should getNetworkConfigById', () => { - expect(data.gen.next(raceSuccess).value).toEqual( - select(getNetworkConfigById, newNodeConfig.network) + + it('should sucessfully switch to the manual node', () => { + expect(data.gen.next(latestBlock).value).toEqual( + apply(shepherd, shepherd.manual, [newNodeId, false]) ); }); + + it('should get the current block', () => { + data.gen.next(); + }); + it('should put setLatestBlock', () => { - expect(data.gen.next(raceSuccess).value).toEqual(put(setLatestBlock(latestBlock))); + expect(data.gen.next(latestBlock).value).toEqual(put(setLatestBlock(latestBlock))); }); it('should put changeNode', () => { @@ -241,7 +241,7 @@ describe('handleNodeChangeIntent*', () => { expect(data.customNode.next(defaultNodeConfig).value).toEqual( select(getCustomNodeFromId, firstCustomNodeId) ); - expect(data.customNode.next(customNodeConfigs).value).toMatchSnapshot(); + expect(data.customNode.next(customNodeConfigs.customNode1).value).toMatchSnapshot(); }); const customNodeIdNotFound = firstCustomNodeId + 'notFound'; @@ -278,8 +278,17 @@ describe('unsetWeb3Node*', () => { expect(gen.next().value).toEqual(select(getNodeId)); }); + it('should get the current network', () => { + expect(gen.next(node).value).toEqual(select(getNetworkConfig)); + }); + + it('should switch networks', () => { + expect(gen.next({ name: '' }).value).toEqual(apply(shepherd, shepherd.switchNetworks, [''])); + }); + it('should select an alternative node to web3', () => { - expect(gen.next(node).value).toEqual(select(getStaticAltNodeIdToWeb3)); + // get a 'no visual difference' error here + expect(gen.next().value).toEqual(select(getStaticAltNodeIdToWeb3)); }); it('should put changeNodeForce', () => { @@ -308,6 +317,14 @@ describe('unsetWeb3NodeOnWalletEvent*', () => { expect(gen.next().value).toEqual(select(getNodeId)); }); + it('should get the current network', () => { + expect(gen.next(mockNodeId).value).toEqual(select(getNetworkConfig)); + }); + + it('should switch networks', () => { + expect(gen.next({ name: '' }).value).toEqual(apply(shepherd, shepherd.switchNetworks, [''])); + }); + it('should select an alternative node to web3', () => { expect(gen.next(mockNodeId).value).toEqual(select(getStaticAltNodeIdToWeb3)); }); diff --git a/spec/reducers/config/nodes/selectedNode.spec.ts b/spec/reducers/config/nodes/selectedNode.spec.ts index db4c683546e..81031ac588d 100644 --- a/spec/reducers/config/nodes/selectedNode.spec.ts +++ b/spec/reducers/config/nodes/selectedNode.spec.ts @@ -14,9 +14,6 @@ export const actions = { }; describe('selected node reducer', () => { - it(' should return the initial state', () => - expect(selectedNode(undefined, {} as any)).toEqual(expectedState.initialState)); - it('should handle a node change', () => expect(selectedNode(undefined, actions.changeNode)).toEqual(expectedState.nodeChange)); diff --git a/spec/reducers/config/nodes/staticNodes.spec.ts b/spec/reducers/config/nodes/staticNodes.spec.ts index ec414f871c8..78c4c8cce45 100644 --- a/spec/reducers/config/nodes/staticNodes.spec.ts +++ b/spec/reducers/config/nodes/staticNodes.spec.ts @@ -1,12 +1,12 @@ import { configuredStore } from 'store'; import { web3SetNode, web3UnsetNode } from 'actions/config'; import { staticNodes, INITIAL_STATE } from 'reducers/config/nodes/staticNodes'; -import { Web3NodeConfig } from 'types/node'; import { Web3Service } from 'libs/nodes/web3'; +import { StaticNodeConfig } from 'types/node'; configuredStore.getState(); const web3Id = 'web3'; -const web3Node: Web3NodeConfig = { +const web3Node: StaticNodeConfig = { isCustom: false, network: 'ETH', service: Web3Service, @@ -27,11 +27,6 @@ const actions = { }; describe('static nodes reducer', () => { - it('should return the inital state', () => - // turn the JSON into a string because we're storing function in the state - expect(JSON.stringify(staticNodes(undefined, {} as any))).toEqual( - JSON.stringify(expectedState.initialState) - )); it('should handle setting the web3 node', () => expect(staticNodes(INITIAL_STATE, actions.web3SetNode)).toEqual(expectedState.setWeb3)); diff --git a/spec/sagas/__snapshots__/wallet.spec.tsx.snap b/spec/sagas/__snapshots__/wallet.spec.tsx.snap index 1cebb6d45a0..96a44bf6e1c 100644 --- a/spec/sagas/__snapshots__/wallet.spec.tsx.snap +++ b/spec/sagas/__snapshots__/wallet.spec.tsx.snap @@ -170,11 +170,7 @@ Object { "@@redux-saga/IO": true, "PUT": Object { "action": Object { - "payload": Web3Wallet { - "address": "0xe2EdC95134bbD88443bc6D55b809F7d0C2f0C854", - "network": undefined, - }, - "type": "WALLET_SET", + "type": "CONFIG_NODE_WEB3_UNSET", }, "channel": null, }, diff --git a/spec/sagas/wallet.spec.tsx b/spec/sagas/wallet.spec.tsx index 1244dc382f3..b294a3de34a 100644 --- a/spec/sagas/wallet.spec.tsx +++ b/spec/sagas/wallet.spec.tsx @@ -24,7 +24,6 @@ import { unlockPrivateKey, unlockKeystore, unlockMnemonic, - unlockWeb3, getTokenBalances, startLoadingSpinner, stopLoadingSpinner @@ -37,7 +36,7 @@ import { showNotification } from 'actions/notifications'; import translate from 'translations'; import { IFullWallet, IV3Wallet, fromV3 } from 'ethereumjs-wallet'; import { Token } from 'types/network'; -import { initWeb3Node } from 'sagas/config/web3'; +import { initWeb3Node, unlockWeb3 } from 'sagas/config/web3'; // init module configuredStore.getState(); @@ -328,7 +327,7 @@ describe('unlockWeb3*', () => { }); it('should put changeNodeIntent', () => { - expect(data.gen.next(accounts).value).toEqual(put(changeNodeIntent('web3'))); + expect(data.gen.next(nodeLib).value).toEqual(put(changeNodeIntent('web3'))); }); it('should yield take on node change', () => { @@ -346,7 +345,10 @@ describe('unlockWeb3*', () => { it('should throw & catch if node is not web3 node', () => { data.clone = data.gen.clone(); - expect(data.clone.next(nodeLib).value).toEqual(put(web3UnsetNode())); + + expect(data.clone.throw(Error('Cannot use Web3 wallet without a Web3 node.')).value).toEqual( + put(web3UnsetNode()) + ); expect(data.clone.next().value).toEqual( put(showNotification('danger', translate('Cannot use Web3 wallet without a Web3 node.'))) ); diff --git a/tsconfig.json b/tsconfig.json index 8e31a85fe7e..924b0246908 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,7 +25,8 @@ "./electron-app/", "./shared/", "spec", - "./node_modules/types-rlp/index.d.ts" + "./node_modules/types-rlp/index.d.ts", + "./node_modules/mycrypto-shepherd/dist/lib/types/btoa.d.ts" ], "awesomeTypescriptLoaderOptions": { "transpileOnly": true diff --git a/webpack_config/makeConfig.js b/webpack_config/makeConfig.js index e858049d7eb..41f794fbf9c 100644 --- a/webpack_config/makeConfig.js +++ b/webpack_config/makeConfig.js @@ -280,7 +280,9 @@ module.exports = function(opts = {}) { path: path.resolve(config.path.output, options.outputDir), filename: options.isProduction ? `[name].${commitHash}.js` : '[name].js', publicPath: isDownloadable && options.isProduction ? './' : '/', - crossOriginLoading: 'anonymous' + crossOriginLoading: 'anonymous', + // Fix workers & HMR https://github.com/webpack/webpack/issues/6642 + globalObject: options.isProduction ? undefined : 'self' }; // The final bundle