More invalid typings fixed.
This commit is contained in:
parent
3efaf68ef8
commit
e5d3f932fd
|
@ -31,7 +31,9 @@ export interface Option {
|
|||
export interface ApiResponseObj {
|
||||
id: string;
|
||||
options: Option[];
|
||||
rate: number;
|
||||
rate: string;
|
||||
limit?: number;
|
||||
min?: number;
|
||||
}
|
||||
|
||||
export interface ApiResponse {
|
||||
|
|
|
@ -1,378 +0,0 @@
|
|||
import { ethPlorer, ETHTokenExplorer, SecureWalletName, InsecureWalletName } from './data';
|
||||
import { EtherscanNode, InfuraNode, RPCNode, Web3Node } from 'libs/nodes';
|
||||
import { networkIdToName } from 'libs/values';
|
||||
import {
|
||||
ETH_DEFAULT,
|
||||
ETH_TREZOR,
|
||||
ETH_LEDGER,
|
||||
ETC_LEDGER,
|
||||
ETC_TREZOR,
|
||||
ETH_TESTNET,
|
||||
EXP_DEFAULT,
|
||||
UBQ_DEFAULT,
|
||||
DPath
|
||||
} from 'config/dpaths';
|
||||
|
||||
export interface BlockExplorerConfig {
|
||||
origin: string;
|
||||
txUrl(txHash: string): string;
|
||||
addressUrl(address: string): string;
|
||||
}
|
||||
|
||||
export interface Token {
|
||||
address: string;
|
||||
symbol: string;
|
||||
decimal: number;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface NetworkContract {
|
||||
name: NetworkKeys;
|
||||
address?: string;
|
||||
abi: string;
|
||||
}
|
||||
|
||||
export interface DPathFormats {
|
||||
trezor: DPath;
|
||||
ledgerNanoS: DPath;
|
||||
mnemonicPhrase: DPath;
|
||||
}
|
||||
|
||||
export interface NetworkConfig {
|
||||
// TODO really try not to allow strings due to custom networks
|
||||
name: NetworkKeys;
|
||||
unit: string;
|
||||
color?: string;
|
||||
blockExplorer?: BlockExplorerConfig;
|
||||
tokenExplorer?: {
|
||||
name: string;
|
||||
address(address: string): string;
|
||||
};
|
||||
chainId: number;
|
||||
tokens: Token[];
|
||||
contracts: NetworkContract[] | null;
|
||||
dPathFormats: DPathFormats;
|
||||
isTestnet?: boolean;
|
||||
}
|
||||
|
||||
export interface CustomNetworkConfig {
|
||||
name: string;
|
||||
unit: string;
|
||||
chainId: number;
|
||||
dPathFormats: DPathFormats | null;
|
||||
}
|
||||
|
||||
export interface NodeConfig {
|
||||
network: NetworkKeys;
|
||||
lib: RPCNode | Web3Node;
|
||||
service: string;
|
||||
estimateGas?: boolean;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export interface CustomNodeConfig {
|
||||
name: string;
|
||||
url: string;
|
||||
port: number;
|
||||
network: string;
|
||||
auth?: {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Networks {
|
||||
[key: string]: NetworkConfig;
|
||||
}
|
||||
|
||||
// Must be a website that follows the ethplorer convention of /tx/[hash] and
|
||||
// address/[address] to generate the correct functions.
|
||||
function makeExplorer(origin: string): BlockExplorerConfig {
|
||||
return {
|
||||
origin,
|
||||
txUrl: hash => `${origin}/tx/${hash}`,
|
||||
addressUrl: address => `${origin}/address/${address}`
|
||||
};
|
||||
}
|
||||
|
||||
const ETH: NetworkConfig = {
|
||||
name: 'ETH',
|
||||
unit: 'ETH',
|
||||
chainId: 1,
|
||||
color: '#0e97c0',
|
||||
blockExplorer: makeExplorer('https://etherscan.io'),
|
||||
tokenExplorer: {
|
||||
name: ethPlorer,
|
||||
address: ETHTokenExplorer
|
||||
},
|
||||
tokens: require('./tokens/eth.json'),
|
||||
contracts: require('./contracts/eth.json'),
|
||||
dPathFormats: {
|
||||
[SecureWalletName.TREZOR]: ETH_TREZOR,
|
||||
[SecureWalletName.LEDGER_NANO_S]: ETH_LEDGER,
|
||||
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_DEFAULT
|
||||
}
|
||||
};
|
||||
|
||||
const Ropsten: NetworkConfig = {
|
||||
name: 'Ropsten',
|
||||
unit: 'ETH',
|
||||
chainId: 3,
|
||||
color: '#adc101',
|
||||
blockExplorer: makeExplorer('https://ropsten.etherscan.io'),
|
||||
tokens: require('./tokens/ropsten.json'),
|
||||
contracts: require('./contracts/ropsten.json'),
|
||||
isTestnet: true,
|
||||
dPathFormats: {
|
||||
[SecureWalletName.TREZOR]: ETH_TESTNET,
|
||||
[SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET,
|
||||
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
|
||||
}
|
||||
};
|
||||
|
||||
const Kovan: NetworkConfig = {
|
||||
name: 'Kovan',
|
||||
unit: 'ETH',
|
||||
chainId: 42,
|
||||
color: '#adc101',
|
||||
blockExplorer: makeExplorer('https://kovan.etherscan.io'),
|
||||
tokens: require('./tokens/ropsten.json'),
|
||||
contracts: require('./contracts/ropsten.json'),
|
||||
isTestnet: true,
|
||||
dPathFormats: {
|
||||
[SecureWalletName.TREZOR]: ETH_TESTNET,
|
||||
[SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET,
|
||||
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
|
||||
}
|
||||
};
|
||||
|
||||
const Rinkeby: NetworkConfig = {
|
||||
name: 'Rinkeby',
|
||||
unit: 'ETH',
|
||||
chainId: 4,
|
||||
color: '#adc101',
|
||||
blockExplorer: makeExplorer('https://rinkeby.etherscan.io'),
|
||||
tokens: require('./tokens/rinkeby.json'),
|
||||
contracts: require('./contracts/rinkeby.json'),
|
||||
isTestnet: true,
|
||||
dPathFormats: {
|
||||
[SecureWalletName.TREZOR]: ETH_TESTNET,
|
||||
[SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET,
|
||||
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
|
||||
}
|
||||
};
|
||||
|
||||
const ETC: NetworkConfig = {
|
||||
name: 'ETC',
|
||||
unit: 'ETC',
|
||||
chainId: 61,
|
||||
color: '#669073',
|
||||
blockExplorer: makeExplorer('https://gastracker.io'),
|
||||
tokens: require('./tokens/etc.json'),
|
||||
contracts: require('./contracts/etc.json'),
|
||||
dPathFormats: {
|
||||
[SecureWalletName.TREZOR]: ETC_TREZOR,
|
||||
[SecureWalletName.LEDGER_NANO_S]: ETC_LEDGER,
|
||||
[InsecureWalletName.MNEMONIC_PHRASE]: ETC_TREZOR
|
||||
}
|
||||
};
|
||||
|
||||
const UBQ: NetworkConfig = {
|
||||
name: 'UBQ',
|
||||
unit: 'UBQ',
|
||||
chainId: 8,
|
||||
color: '#b37aff',
|
||||
blockExplorer: makeExplorer('https://ubiqscan.io/en'),
|
||||
tokens: require('./tokens/ubq.json'),
|
||||
contracts: require('./contracts/ubq.json'),
|
||||
dPathFormats: {
|
||||
[SecureWalletName.TREZOR]: UBQ_DEFAULT,
|
||||
[SecureWalletName.LEDGER_NANO_S]: UBQ_DEFAULT,
|
||||
[InsecureWalletName.MNEMONIC_PHRASE]: UBQ_DEFAULT
|
||||
}
|
||||
};
|
||||
|
||||
const EXP: NetworkConfig = {
|
||||
name: 'EXP',
|
||||
unit: 'EXP',
|
||||
chainId: 2,
|
||||
color: '#673ab7',
|
||||
// tslint:disable:no-http-string - Unavailable behind HTTPS right now
|
||||
blockExplorer: makeExplorer('http://www.gander.tech'),
|
||||
// tslint:enable:no-http-string
|
||||
tokens: require('./tokens/exp.json'),
|
||||
contracts: require('./contracts/exp.json'),
|
||||
dPathFormats: {
|
||||
[SecureWalletName.TREZOR]: EXP_DEFAULT,
|
||||
[SecureWalletName.LEDGER_NANO_S]: EXP_DEFAULT,
|
||||
[InsecureWalletName.MNEMONIC_PHRASE]: EXP_DEFAULT
|
||||
}
|
||||
};
|
||||
|
||||
export const NETWORKS: Networks = {
|
||||
ETH,
|
||||
Ropsten,
|
||||
Kovan,
|
||||
Rinkeby,
|
||||
ETC,
|
||||
UBQ,
|
||||
EXP
|
||||
};
|
||||
|
||||
export type NetworkKeys = keyof typeof NETWORKS;
|
||||
|
||||
export enum NodeName {
|
||||
ETH_MEW = 'eth_mew',
|
||||
ETH_MYCRYPTO = 'eth_mycrypto',
|
||||
ETH_ETHSCAN = 'eth_ethscan',
|
||||
ETH_INFURA = 'eth_infura',
|
||||
ROP_MEW = 'rop_mew',
|
||||
ROP_INFURA = 'rop_infura',
|
||||
KOV_ETHSCAN = 'kov_ethscan',
|
||||
RIN_ETHSCAN = 'rin_ethscan',
|
||||
RIN_INFURA = 'rin_infura',
|
||||
ETC_EPOOL = 'etc_epool',
|
||||
UBQ = 'ubq',
|
||||
EXP_TECH = 'exp_tech'
|
||||
}
|
||||
|
||||
type NonWeb3NodeConfigs = { [key in NodeName]: NodeConfig };
|
||||
|
||||
interface Web3NodeConfig {
|
||||
web3?: NodeConfig;
|
||||
}
|
||||
export interface NodeConfigs extends NonWeb3NodeConfigs, Web3NodeConfig {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const NODES: NodeConfigs = {
|
||||
eth_mew: {
|
||||
network: 'ETH',
|
||||
lib: new RPCNode('https://api.myetherapi.com/eth'),
|
||||
service: 'MyEtherWallet',
|
||||
estimateGas: true
|
||||
},
|
||||
eth_mycrypto: {
|
||||
network: 'ETH',
|
||||
lib: new RPCNode('https://api.mycryptoapi.com/eth'),
|
||||
service: 'MyCrypto',
|
||||
estimateGas: true
|
||||
},
|
||||
eth_ethscan: {
|
||||
network: 'ETH',
|
||||
service: 'Etherscan.io',
|
||||
lib: new EtherscanNode('https://api.etherscan.io/api'),
|
||||
estimateGas: false
|
||||
},
|
||||
eth_infura: {
|
||||
network: 'ETH',
|
||||
service: 'infura.io',
|
||||
lib: new InfuraNode('https://mainnet.infura.io/mew'),
|
||||
estimateGas: false
|
||||
},
|
||||
rop_mew: {
|
||||
network: 'Ropsten',
|
||||
service: 'MyEtherWallet',
|
||||
lib: new RPCNode('https://api.myetherapi.com/rop'),
|
||||
estimateGas: false
|
||||
},
|
||||
rop_infura: {
|
||||
network: 'Ropsten',
|
||||
service: 'infura.io',
|
||||
lib: new InfuraNode('https://ropsten.infura.io/mew'),
|
||||
estimateGas: false
|
||||
},
|
||||
kov_ethscan: {
|
||||
network: 'Kovan',
|
||||
service: 'Etherscan.io',
|
||||
lib: new EtherscanNode('https://kovan.etherscan.io/api'),
|
||||
estimateGas: false
|
||||
},
|
||||
rin_ethscan: {
|
||||
network: 'Rinkeby',
|
||||
service: 'Etherscan.io',
|
||||
lib: new EtherscanNode('https://rinkeby.etherscan.io/api'),
|
||||
estimateGas: false
|
||||
},
|
||||
rin_infura: {
|
||||
network: 'Rinkeby',
|
||||
service: 'infura.io',
|
||||
lib: new InfuraNode('https://rinkeby.infura.io/mew'),
|
||||
estimateGas: false
|
||||
},
|
||||
etc_epool: {
|
||||
network: 'ETC',
|
||||
service: 'Epool.io',
|
||||
lib: new RPCNode('https://mewapi.epool.io'),
|
||||
estimateGas: false
|
||||
},
|
||||
ubq: {
|
||||
network: 'UBQ',
|
||||
service: 'ubiqscan.io',
|
||||
lib: new RPCNode('https://pyrus2.ubiqscan.io'),
|
||||
estimateGas: true
|
||||
},
|
||||
exp_tech: {
|
||||
network: 'EXP',
|
||||
service: 'Expanse.tech',
|
||||
lib: new RPCNode('https://node.expanse.tech/'),
|
||||
estimateGas: true
|
||||
}
|
||||
};
|
||||
|
||||
interface Web3NodeInfo {
|
||||
networkId: string;
|
||||
lib: Web3Node;
|
||||
}
|
||||
|
||||
export async function setupWeb3Node(): Promise<Web3NodeInfo> {
|
||||
const { web3 } = window as any;
|
||||
|
||||
if (!web3 || !web3.currentProvider || !web3.currentProvider.sendAsync) {
|
||||
throw new Error(
|
||||
'Web3 not found. Please check that MetaMask is installed, or that MyCrypto is open in Mist.'
|
||||
);
|
||||
}
|
||||
|
||||
const lib = new Web3Node();
|
||||
const networkId = await lib.getNetVersion();
|
||||
const accounts = await lib.getAccounts();
|
||||
|
||||
if (!accounts.length) {
|
||||
throw new Error('No accounts found in MetaMask / Mist.');
|
||||
}
|
||||
|
||||
if (networkId === 'loading') {
|
||||
throw new Error('MetaMask / Mist is still loading. Please refresh the page and try again.');
|
||||
}
|
||||
|
||||
return { networkId, lib };
|
||||
}
|
||||
|
||||
export async function isWeb3NodeAvailable(): Promise<boolean> {
|
||||
try {
|
||||
await setupWeb3Node();
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const Web3Service = 'MetaMask / Mist';
|
||||
|
||||
export interface NodeConfigOverride extends NodeConfig {
|
||||
network: any;
|
||||
}
|
||||
|
||||
export async function initWeb3Node(): Promise<void> {
|
||||
const { networkId, lib } = await setupWeb3Node();
|
||||
const web3: NodeConfigOverride = {
|
||||
network: networkIdToName(networkId),
|
||||
service: Web3Service,
|
||||
lib,
|
||||
estimateGas: false,
|
||||
hidden: true
|
||||
};
|
||||
|
||||
NODES.web3 = web3;
|
||||
}
|
|
@ -29,7 +29,7 @@ export interface NormalizedBityRates {
|
|||
}
|
||||
|
||||
export interface NormalizedShapeshiftRate extends NormalizedRate {
|
||||
id: string;
|
||||
id: number;
|
||||
options: WhitelistedCoins[];
|
||||
rate: number;
|
||||
limit: number;
|
||||
|
|
|
@ -1,279 +0,0 @@
|
|||
import { delay, SagaIterator } from 'redux-saga';
|
||||
import {
|
||||
call,
|
||||
cancel,
|
||||
fork,
|
||||
put,
|
||||
take,
|
||||
takeLatest,
|
||||
takeEvery,
|
||||
select,
|
||||
race
|
||||
} from 'redux-saga/effects';
|
||||
import {
|
||||
NODES,
|
||||
NETWORKS,
|
||||
NodeConfig,
|
||||
CustomNodeConfig,
|
||||
CustomNetworkConfig,
|
||||
Web3Service,
|
||||
NodeConfigs
|
||||
} from 'config';
|
||||
import {
|
||||
makeCustomNodeId,
|
||||
getCustomNodeConfigFromId,
|
||||
makeNodeConfigFromCustomConfig
|
||||
} from 'utils/node';
|
||||
import { makeCustomNetworkId, getNetworkConfigFromId } from 'utils/network';
|
||||
import { Omit } from 'react-redux';
|
||||
import {
|
||||
getNode,
|
||||
getNodeConfig,
|
||||
getCustomNodeConfigs,
|
||||
getCustomNetworkConfigs,
|
||||
getOffline
|
||||
} from 'selectors/config';
|
||||
import { AppState } from 'reducers';
|
||||
import { TypeKeys } from 'actions/config/constants';
|
||||
import {
|
||||
toggleOfflineConfig,
|
||||
changeNode,
|
||||
changeNodeIntent,
|
||||
setLatestBlock,
|
||||
removeCustomNetwork,
|
||||
AddCustomNodeAction,
|
||||
ChangeNodeIntentAction
|
||||
} from 'actions/config';
|
||||
import { showNotification } from 'actions/notifications';
|
||||
import { translateRaw } from 'translations';
|
||||
import { Web3Wallet } from 'libs/wallet';
|
||||
import { TypeKeys as WalletTypeKeys } from 'actions/wallet/constants';
|
||||
import { State as ConfigState, INITIAL_STATE as configInitialState } from 'reducers/config';
|
||||
import { SetWalletAction } from 'actions/wallet';
|
||||
|
||||
export const getConfig = (state: AppState): ConfigState => state.config;
|
||||
|
||||
let hasCheckedOnline = false;
|
||||
export function* pollOfflineStatus(): SagaIterator {
|
||||
while (true) {
|
||||
const node: NodeConfig = 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(node.lib.ping.bind(node.lib)),
|
||||
timeout: call(delay, 5000)
|
||||
});
|
||||
|
||||
if (pingSucceeded && isOffline) {
|
||||
// If we were able to ping but redux says we're offline, mark online
|
||||
yield put(
|
||||
showNotification('success', 'Your connection to the network has been restored!', 3000)
|
||||
);
|
||||
yield put(toggleOfflineConfig());
|
||||
} else if (!pingSucceeded && !isOffline) {
|
||||
// If we were unable to ping but redux says we're online, mark offline
|
||||
// If they had been online, show an error.
|
||||
// If they hadn't been online, just inform them with a warning.
|
||||
if (hasCheckedOnline) {
|
||||
yield put(
|
||||
showNotification(
|
||||
'danger',
|
||||
`You’ve lost your connection to the network, check your internet
|
||||
connection or try changing networks from the dropdown at the
|
||||
top right of the page.`,
|
||||
Infinity
|
||||
)
|
||||
);
|
||||
} else {
|
||||
yield put(
|
||||
showNotification(
|
||||
'info',
|
||||
'You are currently offline. Some features will be unavailable.',
|
||||
5000
|
||||
)
|
||||
);
|
||||
}
|
||||
yield put(toggleOfflineConfig());
|
||||
} else {
|
||||
// If neither case was true, try again in 5s
|
||||
yield call(delay, 5000);
|
||||
}
|
||||
hasCheckedOnline = true;
|
||||
} else {
|
||||
yield call(delay, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fork our recurring API call, watch for the need to cancel.
|
||||
export function* handlePollOfflineStatus(): SagaIterator {
|
||||
const pollOfflineStatusTask = yield fork(pollOfflineStatus);
|
||||
yield take('CONFIG_STOP_POLL_OFFLINE_STATE');
|
||||
yield cancel(pollOfflineStatusTask);
|
||||
}
|
||||
|
||||
// @HACK For now we reload the app when doing a language swap to force non-connected
|
||||
// data to reload. Also the use of timeout to avoid using additional actions for now.
|
||||
export function* reload(): SagaIterator {
|
||||
setTimeout(() => location.reload(), 1150);
|
||||
}
|
||||
|
||||
export function* handleNodeChangeIntent(action: ChangeNodeIntentAction): SagaIterator {
|
||||
const currentNode: string = yield select(getNode);
|
||||
const currentConfig: NodeConfig = yield select(getNodeConfig);
|
||||
const customNets: CustomNetworkConfig[] = yield select(getCustomNetworkConfigs);
|
||||
const currentNetwork =
|
||||
getNetworkConfigFromId(currentConfig.network, customNets) || NETWORKS[currentConfig.network];
|
||||
|
||||
function* bailOut(message: string) {
|
||||
yield put(showNotification('danger', message, 5000));
|
||||
yield put(changeNode(currentNode, currentConfig, currentNetwork));
|
||||
}
|
||||
|
||||
let actionConfig = NODES[action.payload];
|
||||
|
||||
if (!actionConfig) {
|
||||
const customConfigs: CustomNodeConfig[] = yield select(getCustomNodeConfigs);
|
||||
const config = getCustomNodeConfigFromId(action.payload, customConfigs);
|
||||
if (config) {
|
||||
actionConfig = makeNodeConfigFromCustomConfig(config);
|
||||
}
|
||||
}
|
||||
|
||||
if (!actionConfig) {
|
||||
return yield* bailOut(`Attempted to switch to unknown node '${action.payload}'`);
|
||||
}
|
||||
|
||||
// Grab latest block from the node, before switching, to confirm it's online
|
||||
// Give it 5 seconds before we call it offline
|
||||
let latestBlock;
|
||||
let timeout;
|
||||
try {
|
||||
const { lb, to } = yield race({
|
||||
lb: call(actionConfig.lib.getCurrentBlock.bind(actionConfig.lib)),
|
||||
to: call(delay, 5000)
|
||||
});
|
||||
latestBlock = lb;
|
||||
timeout = to;
|
||||
} catch (err) {
|
||||
// Whether it times out or errors, same message
|
||||
timeout = true;
|
||||
}
|
||||
|
||||
if (timeout) {
|
||||
return yield* bailOut(translateRaw('ERROR_32'));
|
||||
}
|
||||
|
||||
const actionNetwork = getNetworkConfigFromId(actionConfig.network, customNets);
|
||||
|
||||
if (!actionNetwork) {
|
||||
return yield* bailOut(
|
||||
`Unknown custom network for your node '${action.payload}', try re-adding it`
|
||||
);
|
||||
}
|
||||
|
||||
yield put(setLatestBlock(latestBlock));
|
||||
yield put(changeNode(action.payload, actionConfig, actionNetwork));
|
||||
|
||||
// TODO - re-enable once DeterministicWallet state is fixed to flush properly.
|
||||
// DeterministicWallet keeps path related state we need to flush before we can stop reloading
|
||||
|
||||
// const currentWallet: IWallet | null = yield select(getWalletInst);
|
||||
// if there's no wallet, do not reload as there's no component state to resync
|
||||
// if (currentWallet && currentConfig.network !== actionConfig.network) {
|
||||
|
||||
const isNewNetwork = currentConfig.network !== actionConfig.network;
|
||||
const newIsWeb3 = actionConfig.service === Web3Service;
|
||||
// don't reload when web3 is selected; node will automatically re-set and state is not an issue here
|
||||
if (isNewNetwork && !newIsWeb3) {
|
||||
yield call(reload);
|
||||
}
|
||||
}
|
||||
|
||||
export function* switchToNewNode(action: AddCustomNodeAction): SagaIterator {
|
||||
const nodeId = makeCustomNodeId(action.payload);
|
||||
yield put(changeNodeIntent(nodeId));
|
||||
}
|
||||
|
||||
interface INetworksInUse {
|
||||
[networkName: string]: boolean;
|
||||
}
|
||||
// If there are any orphaned custom networks, purge them
|
||||
export function* cleanCustomNetworks(): SagaIterator {
|
||||
const customNodes: CustomNodeConfig[] = yield select(getCustomNodeConfigs);
|
||||
const customNetworks: CustomNetworkConfig[] = yield select(getCustomNetworkConfigs);
|
||||
const networksInUse = customNodes.reduce(
|
||||
(prev, conf) => {
|
||||
prev[conf.network] = true;
|
||||
return prev;
|
||||
},
|
||||
{} as INetworksInUse
|
||||
);
|
||||
|
||||
for (const net of customNetworks) {
|
||||
if (!networksInUse[makeCustomNetworkId(net)]) {
|
||||
yield put(removeCustomNetwork(net));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unset web3 as the selected node if a non-web3 wallet has been selected
|
||||
export function* unsetWeb3NodeOnWalletEvent(action: SetWalletAction): SagaIterator {
|
||||
const node = yield select(getNode);
|
||||
const nodeConfig = yield select(getNodeConfig);
|
||||
const newWallet = action.payload;
|
||||
const isWeb3Wallet = newWallet instanceof Web3Wallet;
|
||||
|
||||
if (node !== 'web3' || isWeb3Wallet) {
|
||||
return;
|
||||
}
|
||||
|
||||
// switch back to a node with the same network as MetaMask/Mist
|
||||
yield put(changeNodeIntent(equivalentNodeOrDefault(nodeConfig)));
|
||||
}
|
||||
|
||||
export function* unsetWeb3Node(): SagaIterator {
|
||||
const node = yield select(getNode);
|
||||
|
||||
if (node !== 'web3') {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeConfig: NodeConfig = yield select(getNodeConfig);
|
||||
const newNode = equivalentNodeOrDefault(nodeConfig);
|
||||
|
||||
yield put(changeNodeIntent(newNode));
|
||||
}
|
||||
|
||||
export const equivalentNodeOrDefault = (nodeConfig: NodeConfig): keyof NodeConfigs => {
|
||||
const node: keyof Omit<NodeConfigs, 'web3'> | '' = Object.keys(NODES)
|
||||
.filter(key => key !== 'web3')
|
||||
.reduce<keyof Omit<NodeConfigs, 'web3'> | ''>((found, key: keyof Omit<NodeConfigs, 'web3'>) => {
|
||||
const config = NODES[key];
|
||||
if (found.length) {
|
||||
return found;
|
||||
}
|
||||
if (nodeConfig.network === config.network) {
|
||||
return (found = key);
|
||||
}
|
||||
return found;
|
||||
}, '');
|
||||
|
||||
// if no equivalent node was found, use the app default
|
||||
return node ? node : configInitialState.nodeSelection;
|
||||
};
|
||||
|
||||
export default function* configSaga(): SagaIterator {
|
||||
yield takeLatest(TypeKeys.CONFIG_POLL_OFFLINE_STATUS, handlePollOfflineStatus);
|
||||
yield takeEvery(TypeKeys.CONFIG_NODE_CHANGE_INTENT, handleNodeChangeIntent);
|
||||
yield takeEvery(TypeKeys.CONFIG_LANGUAGE_CHANGE, reload);
|
||||
yield takeEvery(TypeKeys.CONFIG_ADD_CUSTOM_NODE, switchToNewNode);
|
||||
yield takeEvery(TypeKeys.CONFIG_REMOVE_CUSTOM_NODE, cleanCustomNetworks);
|
||||
yield takeEvery(TypeKeys.CONFIG_NODE_WEB3_UNSET, unsetWeb3Node);
|
||||
yield takeEvery(WalletTypeKeys.WALLET_SET, unsetWeb3NodeOnWalletEvent);
|
||||
yield takeEvery(WalletTypeKeys.WALLET_RESET, unsetWeb3NodeOnWalletEvent);
|
||||
}
|
128
common/store.ts
128
common/store.ts
|
@ -1,128 +0,0 @@
|
|||
import throttle from 'lodash/throttle';
|
||||
import { routerMiddleware } from 'react-router-redux';
|
||||
import { State as ConfigState, INITIAL_STATE as configInitialState } from 'reducers/config';
|
||||
import {
|
||||
State as CustomTokenState,
|
||||
INITIAL_STATE as customTokensInitialState
|
||||
} from 'reducers/customTokens';
|
||||
import { State as SwapState, INITIAL_STATE as swapInitialState } from 'reducers/swap';
|
||||
import { applyMiddleware, createStore, Store } from 'redux';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import createSagaMiddleware from 'redux-saga';
|
||||
import { loadStatePropertyOrEmptyObject, saveState } from 'utils/localStorage';
|
||||
import RootReducer, { AppState } from './reducers';
|
||||
import { getNodeConfigFromId } from 'utils/node';
|
||||
import { getNetworkConfigFromId } from 'utils/network';
|
||||
import { dedupeCustomTokens } from 'utils/tokens';
|
||||
import sagas from './sagas';
|
||||
|
||||
const configureStore = () => {
|
||||
const logger = createLogger({
|
||||
collapsed: true
|
||||
});
|
||||
const sagaMiddleware = createSagaMiddleware();
|
||||
let middleware;
|
||||
let store: Store<AppState>;
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
middleware = composeWithDevTools(
|
||||
applyMiddleware(sagaMiddleware, logger, routerMiddleware(history as any))
|
||||
);
|
||||
} else {
|
||||
middleware = applyMiddleware(sagaMiddleware, routerMiddleware(history as any));
|
||||
}
|
||||
|
||||
const localSwapState = loadStatePropertyOrEmptyObject<SwapState>('swap');
|
||||
const swapState =
|
||||
localSwapState && localSwapState.step === 3
|
||||
? {
|
||||
...swapInitialState,
|
||||
...localSwapState
|
||||
}
|
||||
: { ...swapInitialState };
|
||||
|
||||
const savedConfigState = loadStatePropertyOrEmptyObject<ConfigState>('config');
|
||||
|
||||
// If they have a saved node, make sure we assign that too. The node selected
|
||||
// isn't serializable, so we have to assign it here.
|
||||
if (savedConfigState && savedConfigState.nodeSelection) {
|
||||
const savedNode = getNodeConfigFromId(
|
||||
savedConfigState.nodeSelection,
|
||||
savedConfigState.customNodes
|
||||
);
|
||||
// If we couldn't find it, revert to defaults
|
||||
if (savedNode) {
|
||||
savedConfigState.node = savedNode;
|
||||
const network = getNetworkConfigFromId(savedNode.network, savedConfigState.customNetworks);
|
||||
if (network) {
|
||||
savedConfigState.network = network;
|
||||
}
|
||||
} else {
|
||||
savedConfigState.nodeSelection = configInitialState.nodeSelection;
|
||||
}
|
||||
}
|
||||
|
||||
// Dedupe custom tokens initially
|
||||
const savedCustomTokensState =
|
||||
loadStatePropertyOrEmptyObject<CustomTokenState>('customTokens') || customTokensInitialState;
|
||||
const initialNetwork =
|
||||
(savedConfigState && savedConfigState.network) || configInitialState.network;
|
||||
const customTokens = dedupeCustomTokens(initialNetwork.tokens, savedCustomTokensState);
|
||||
|
||||
const persistedInitialState = {
|
||||
config: {
|
||||
...configInitialState,
|
||||
...savedConfigState
|
||||
},
|
||||
customTokens,
|
||||
// ONLY LOAD SWAP STATE FROM LOCAL STORAGE IF STEP WAS 3
|
||||
swap: swapState
|
||||
};
|
||||
// if 'web3' has persisted as node selection, reset to app default
|
||||
// necessary because web3 is only initialized as a node upon MetaMask / Mist unlock
|
||||
if (persistedInitialState.config.nodeSelection === 'web3') {
|
||||
persistedInitialState.config.nodeSelection = configInitialState.nodeSelection;
|
||||
}
|
||||
|
||||
store = createStore<AppState>(RootReducer, persistedInitialState as AppState, middleware);
|
||||
|
||||
// Add all of the sagas to the middleware
|
||||
Object.values(sagas).forEach(saga => {
|
||||
sagaMiddleware.run(saga);
|
||||
});
|
||||
|
||||
store.subscribe(
|
||||
throttle(() => {
|
||||
const state: AppState = store.getState();
|
||||
saveState({
|
||||
config: {
|
||||
nodeSelection: state.config.nodeSelection,
|
||||
languageSelection: state.config.languageSelection,
|
||||
customNodes: state.config.customNodes,
|
||||
customNetworks: state.config.customNetworks
|
||||
},
|
||||
swap: {
|
||||
...state.swap,
|
||||
options: {
|
||||
byId: {},
|
||||
allIds: []
|
||||
},
|
||||
bityRates: {
|
||||
byId: {},
|
||||
allIds: []
|
||||
},
|
||||
shapeshiftRates: {
|
||||
byId: {},
|
||||
allIds: []
|
||||
}
|
||||
},
|
||||
customTokens: state.customTokens
|
||||
});
|
||||
}, 50)
|
||||
);
|
||||
|
||||
return store;
|
||||
};
|
||||
|
||||
export const configuredStore = configureStore();
|
|
@ -1,3 +1,18 @@
|
|||
declare module 'hdkey' {
|
||||
export function fromMasterSeed(seed: Buffer): any;
|
||||
interface HDKeyProps {
|
||||
versions: any[];
|
||||
depth: number;
|
||||
index: number;
|
||||
_privateKey: any;
|
||||
_publicKey: any;
|
||||
chainCode: any;
|
||||
_fingerprint: any;
|
||||
parentFingerprint: number;
|
||||
}
|
||||
|
||||
declare class HDCLass<HDKeyProps> {
|
||||
constructor();
|
||||
}
|
||||
|
||||
declare namespace HDKey {
|
||||
|
||||
}
|
||||
|
|
|
@ -1,122 +0,0 @@
|
|||
import {
|
||||
CustomNetworkConfig,
|
||||
DPathFormats,
|
||||
InsecureWalletName,
|
||||
NetworkConfig,
|
||||
NETWORKS,
|
||||
SecureWalletName,
|
||||
WalletName,
|
||||
walletNames
|
||||
} from 'config';
|
||||
import { DPath, EXTRA_PATHS } from 'config/dpaths';
|
||||
import sortedUniq from 'lodash/sortedUniq';
|
||||
import difference from 'lodash/difference';
|
||||
|
||||
export function makeCustomNetworkId(config: CustomNetworkConfig): string {
|
||||
return config.chainId ? `${config.chainId}` : `${config.name}:${config.unit}`;
|
||||
}
|
||||
|
||||
export function makeNetworkConfigFromCustomConfig(config: CustomNetworkConfig): NetworkConfig {
|
||||
// TODO - re-enable this block and classify customConfig after user-inputted dPaths are implemented
|
||||
// -------------------------------------------------
|
||||
// this still provides the type safety we want
|
||||
// as we know config coming in is CustomNetworkConfig
|
||||
// meaning name will be a string
|
||||
// then we cast it as any to keep it as a network key
|
||||
// interface Override extends NetworkConfig {
|
||||
// name: any;
|
||||
// }
|
||||
// -------------------------------------------------
|
||||
|
||||
// TODO - allow for user-inputted dPaths so we don't need to use any below and can use supplied dPaths
|
||||
// In the meantime, networks with an unknown chainId will have HD wallets disabled
|
||||
const customConfig: any = {
|
||||
...config,
|
||||
color: '#000',
|
||||
tokens: [],
|
||||
contracts: []
|
||||
};
|
||||
|
||||
return customConfig;
|
||||
}
|
||||
|
||||
export function getNetworkConfigFromId(
|
||||
id: keyof typeof NETWORKS,
|
||||
configs: CustomNetworkConfig[]
|
||||
): NetworkConfig | undefined {
|
||||
if (NETWORKS[id]) {
|
||||
return NETWORKS[id];
|
||||
}
|
||||
|
||||
const customConfig = configs.find(conf => makeCustomNetworkId(conf) === id);
|
||||
if (customConfig) {
|
||||
return makeNetworkConfigFromCustomConfig(customConfig);
|
||||
}
|
||||
}
|
||||
|
||||
type PathType = keyof DPathFormats;
|
||||
|
||||
type DPathFormat =
|
||||
| SecureWalletName.TREZOR
|
||||
| SecureWalletName.LEDGER_NANO_S
|
||||
| InsecureWalletName.MNEMONIC_PHRASE;
|
||||
|
||||
export function getPaths(pathType: PathType): DPath[] {
|
||||
const networkPaths: DPath[] = [];
|
||||
Object.values(NETWORKS).forEach(networkConfig => {
|
||||
const path = networkConfig.dPathFormats ? networkConfig.dPathFormats[pathType] : [];
|
||||
if (path) {
|
||||
networkPaths.push(path as DPath);
|
||||
}
|
||||
});
|
||||
const paths = networkPaths.concat(EXTRA_PATHS);
|
||||
return sortedUniq(paths);
|
||||
}
|
||||
|
||||
export function getSingleDPath(format: DPathFormat, network: NetworkConfig): DPath {
|
||||
const dPathFormats = network.dPathFormats;
|
||||
return dPathFormats[format];
|
||||
}
|
||||
|
||||
export function isNetworkUnit(network: NetworkConfig, unit: string) {
|
||||
const validNetworks = Object.values(NETWORKS).filter((n: NetworkConfig) => n.unit === unit);
|
||||
return validNetworks.includes(network);
|
||||
}
|
||||
|
||||
export function isWalletFormatSupportedOnNetwork(
|
||||
format: WalletName,
|
||||
network: NetworkConfig
|
||||
): boolean {
|
||||
const CHECK_FORMATS: DPathFormat[] = [
|
||||
SecureWalletName.LEDGER_NANO_S,
|
||||
SecureWalletName.TREZOR,
|
||||
InsecureWalletName.MNEMONIC_PHRASE
|
||||
];
|
||||
|
||||
const isHDFormat = (f: string): f is DPathFormat => CHECK_FORMATS.includes(f as DPathFormat);
|
||||
|
||||
// Ensure DPath's are found
|
||||
if (isHDFormat(format)) {
|
||||
const dPath = network.dPathFormats && network.dPathFormats[format];
|
||||
return !!dPath;
|
||||
}
|
||||
|
||||
// Ensure Web3 is only enabled on ETH or ETH Testnets (MetaMask does not support other networks)
|
||||
if (format === SecureWalletName.WEB3) {
|
||||
return isNetworkUnit(network, 'ETH');
|
||||
}
|
||||
|
||||
// All other wallet formats are supported
|
||||
return true;
|
||||
}
|
||||
|
||||
export function allWalletFormatsSupportedOnNetwork(network: NetworkConfig): WalletName[] {
|
||||
return walletNames.filter((walletName: WalletName) =>
|
||||
isWalletFormatSupportedOnNetwork(walletName, network)
|
||||
);
|
||||
}
|
||||
|
||||
export function unSupportedWalletFormatsOnNetwork(network: NetworkConfig): WalletName[] {
|
||||
const supportedFormats = allWalletFormatsSupportedOnNetwork(network);
|
||||
return difference(walletNames, supportedFormats);
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
import { CustomNode } from 'libs/nodes';
|
||||
import { NODES, NodeConfig, CustomNodeConfig, NodeConfigs } from 'config';
|
||||
|
||||
export function makeCustomNodeId(config: CustomNodeConfig): string {
|
||||
return `${config.url}:${config.port}`;
|
||||
}
|
||||
|
||||
export function getCustomNodeConfigFromId(
|
||||
id: string,
|
||||
configs: CustomNodeConfig[]
|
||||
): CustomNodeConfig | undefined {
|
||||
return configs.find(node => makeCustomNodeId(node) === id);
|
||||
}
|
||||
|
||||
export function getNodeConfigFromId(
|
||||
id: keyof NodeConfigs,
|
||||
configs: CustomNodeConfig[]
|
||||
): NodeConfig | undefined {
|
||||
if (NODES[id]) {
|
||||
return NODES[id];
|
||||
}
|
||||
|
||||
const config = getCustomNodeConfigFromId(id, configs);
|
||||
if (config) {
|
||||
return makeNodeConfigFromCustomConfig(config);
|
||||
}
|
||||
}
|
||||
|
||||
export function makeNodeConfigFromCustomConfig(config: CustomNodeConfig): NodeConfig {
|
||||
interface Override extends NodeConfig {
|
||||
network: any;
|
||||
}
|
||||
|
||||
const customConfig: Override = {
|
||||
network: config.network,
|
||||
lib: new CustomNode(config),
|
||||
service: 'your custom node',
|
||||
estimateGas: true
|
||||
};
|
||||
|
||||
return customConfig;
|
||||
}
|
|
@ -1,12 +1,11 @@
|
|||
import { BrowserWindow, Menu, shell } from 'electron';
|
||||
import { URL } from 'url';
|
||||
import MENU from './menu';
|
||||
import updater from './updater';
|
||||
import { APP_TITLE } from '../constants';
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||
|
||||
// Cached reference, preventing recreations
|
||||
let window: BrowserWindow | null;
|
||||
let window: any;
|
||||
|
||||
// Construct new BrowserWindow
|
||||
export default function getWindow() {
|
||||
|
@ -39,7 +38,7 @@ export default function getWindow() {
|
|||
window = null;
|
||||
});
|
||||
|
||||
window.webContents.on('new-window', (ev, urlStr) => {
|
||||
window.webContents.on('new-window', (ev: any, urlStr: any) => {
|
||||
// Kill all new window requests by default
|
||||
ev.preventDefault();
|
||||
|
||||
|
|
|
@ -23,26 +23,24 @@ describe('ensure whitelist', () => {
|
|||
|
||||
describe('swap reducer', () => {
|
||||
const shapeshiftApiResponse = {
|
||||
['1SSTANT']: {
|
||||
id: '1STANT',
|
||||
options: [
|
||||
{
|
||||
id: '1ST',
|
||||
status: 'available',
|
||||
image: 'https://shapeshift.io/images/coins/firstblood.png',
|
||||
name: 'FirstBlood'
|
||||
},
|
||||
{
|
||||
id: 'ANT',
|
||||
status: 'available',
|
||||
image: 'https://shapeshift.io/images/coins/aragon.png',
|
||||
name: 'Aragon'
|
||||
}
|
||||
],
|
||||
rate: '0.24707537',
|
||||
limit: 5908.29166225,
|
||||
min: 7.86382979
|
||||
}
|
||||
id: '1STANT',
|
||||
options: [
|
||||
{
|
||||
id: '1ST',
|
||||
status: 'available',
|
||||
image: 'https://shapeshift.io/images/coins/firstblood.png',
|
||||
name: 'FirstBlood'
|
||||
},
|
||||
{
|
||||
id: 'ANT',
|
||||
status: 'available',
|
||||
image: 'https://shapeshift.io/images/coins/aragon.png',
|
||||
name: 'Aragon'
|
||||
}
|
||||
],
|
||||
rate: '0.24707537',
|
||||
limit: 5908.29166225,
|
||||
min: 7.86382979
|
||||
};
|
||||
|
||||
const bityApiResponse = {
|
||||
|
|
Loading…
Reference in New Issue