MyCrypto/common/sagas/wallet/wallet.ts

329 lines
10 KiB
TypeScript

import { showNotification } from 'actions/notifications';
import {
setBalanceFullfilled,
setBalancePending,
setBalanceRejected,
setTokenBalancesPending,
setTokenBalancesFulfilled,
setTokenBalancesRejected,
setWallet,
setWalletPending,
setWalletConfig,
UnlockKeystoreAction,
UnlockMnemonicAction,
UnlockPrivateKeyAction,
ScanWalletForTokensAction,
SetWalletTokensAction,
TypeKeys,
SetTokenBalancePendingAction,
setTokenBalanceFulfilled,
setTokenBalanceRejected,
setPasswordPrompt
} from 'actions/wallet';
import { Wei } from 'libs/units';
import { changeNodeIntent, web3UnsetNode, TypeKeys as ConfigTypeKeys } from 'actions/config';
import { AddCustomTokenAction, TypeKeys as CustomTokenTypeKeys } from 'actions/customTokens';
import { INode } from 'libs/nodes/INode';
import {
IWallet,
MnemonicWallet,
getPrivKeyWallet,
getKeystoreWallet,
determineKeystoreType,
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 {
getTokens,
getWalletInst,
getWalletConfigTokens,
MergedToken,
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;
}
export function* updateAccountBalance(): SagaIterator {
try {
const isOffline = yield select(getOffline);
if (isOffline) {
return;
}
yield put(setBalancePending());
const wallet: null | IWallet = yield select(getWalletInst);
if (!wallet) {
return;
}
const node: INode = yield select(getNodeLib);
const address: string = yield apply(wallet, wallet.getAddressString);
// network request
const balance: Wei = yield apply(node, node.getBalance, [address]);
yield put(setBalanceFullfilled(balance));
} catch (error) {
yield put(setBalanceRejected());
}
}
export function* updateTokenBalances(): SagaIterator {
try {
const isOffline = yield select(getOffline);
if (isOffline) {
return;
}
const wallet: null | IWallet = yield select(getWalletInst);
const tokens: MergedToken[] = yield select(getWalletConfigTokens);
if (!wallet || !tokens.length) {
return;
}
yield put(setTokenBalancesPending());
const tokenBalances: TokenBalanceLookup = yield call(getTokenBalances, wallet, tokens);
yield put(setTokenBalancesFulfilled(tokenBalances));
} catch (error) {
console.error('Failed to get token balances', error);
yield put(setTokenBalancesRejected());
}
}
export function* updateTokenBalance(action: SetTokenBalancePendingAction): SagaIterator {
try {
const isOffline = yield select(getOffline);
if (isOffline) {
return;
}
const wallet: null | IWallet = yield select(getWalletInst);
const { tokenSymbol } = action.payload;
const allTokens: Token[] = yield select(getAllTokens);
const token = allTokens.find(t => t.symbol === tokenSymbol);
if (!wallet) {
return;
}
if (!token) {
throw Error('Token not found');
}
const tokenBalances: TokenBalanceLookup = yield call(getTokenBalances, wallet, [token]);
yield put(setTokenBalanceFulfilled(tokenBalances));
} catch (error) {
console.error('Failed to get token balance', error);
yield put(setTokenBalanceRejected());
}
}
export function* scanWalletForTokens(action: ScanWalletForTokensAction): SagaIterator {
try {
const isOffline = yield select(getOffline);
if (isOffline) {
return;
}
const wallet = action.payload;
const tokens: MergedToken[] = yield select(getTokens);
yield put(setTokenBalancesPending());
// Fetch all token balances, save ones we want to the config
const balances: TokenBalanceLookup = yield call(getTokenBalances, wallet, tokens);
const tokensToSave: string[] = yield call(filterScannedTokenBalances, wallet, balances);
const config: WalletConfig = yield call(saveWalletConfig, wallet, { tokens: tokensToSave });
yield put(setWalletConfig(config));
yield put(setTokenBalancesFulfilled(balances));
} catch (err) {
console.error('Failed to scan for tokens', err);
yield put(setTokenBalancesRejected());
}
}
export function* handleSetWalletTokens(action: SetWalletTokensAction): SagaIterator {
const wallet: null | IWallet = yield select(getWalletInst);
if (!wallet) {
return;
}
const config: WalletConfig = yield call(saveWalletConfig, wallet, { tokens: action.payload });
yield put(setWalletConfig(config));
}
export function* updateBalances(): SagaIterator {
const updateAccount = yield fork(updateAccountBalance);
const updateToken = yield fork(updateTokenBalances);
yield take(TypeKeys.WALLET_SET);
yield cancel(updateAccount);
yield cancel(updateToken);
}
export function* handleNewWallet(): SagaIterator {
yield call(updateWalletConfig);
yield fork(updateBalances);
}
export function* updateWalletConfig(): SagaIterator {
const wallet: null | IWallet = yield select(getWalletInst);
if (!wallet) {
return;
}
const config: WalletConfig = yield call(loadWalletConfig, wallet);
yield put(setWalletConfig(config));
}
export function* unlockPrivateKey(action: UnlockPrivateKeyAction): SagaIterator {
let wallet: IWallet | null = null;
const { key, password } = action.payload;
try {
wallet = getPrivKeyWallet(key, password);
} catch (e) {
yield put(showNotification('danger', translate('INVALID_PKEY')));
return;
}
yield put(setWallet(wallet));
}
export function* startLoadingSpinner(): SagaIterator {
yield call(delay, 400);
yield put(setWalletPending(true));
}
export function* stopLoadingSpinner(loadingFork: Task | null): SagaIterator {
if (loadingFork !== null && loadingFork !== undefined) {
yield cancel(loadingFork);
}
yield put(setWalletPending(false));
}
export function* unlockKeystore(action: UnlockKeystoreAction): SagaIterator {
const { file, password } = action.payload;
let wallet: null | IWallet = null;
let spinnerTask: null | Task = null;
try {
if (determineKeystoreType(file) === KeystoreTypes.utc) {
spinnerTask = yield fork(startLoadingSpinner);
wallet = signWrapper(yield call(getUtcWallet, file, password));
} else {
wallet = getKeystoreWallet(file, password);
}
} catch (e) {
yield call(stopLoadingSpinner, spinnerTask);
if (
password === '' &&
e.message === 'Private key does not satisfy the curve requirements (ie. it is invalid)'
) {
yield put(setPasswordPrompt());
} else {
yield put(showNotification('danger', translate('ERROR_6')));
}
return;
}
// TODO: provide a more descriptive error than the two 'ERROR_6' (invalid pass) messages above
yield call(stopLoadingSpinner, spinnerTask);
yield put(setWallet(wallet));
}
export function* unlockMnemonic(action: UnlockMnemonicAction): SagaIterator {
let wallet;
const { phrase, pass, path, address } = action.payload;
try {
wallet = MnemonicWallet(phrase, pass, path, address);
} catch (err) {
// TODO: use better error than 'ERROR_14' (wallet not found)
yield put(showNotification('danger', translate('ERROR_14')));
return;
}
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 =>
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);
if (!wallet) {
return;
}
const oldConfig: WalletConfig = yield call(loadWalletConfig, wallet);
const config: WalletConfig = yield call(saveWalletConfig, wallet, {
tokens: [...(oldConfig.tokens || []), action.payload.symbol]
});
yield put(setWalletConfig(config));
// Update token balances
yield fork(updateTokenBalances);
}
export default function* walletSaga(): SagaIterator {
yield [
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),
takeEvery(TypeKeys.WALLET_SET_TOKEN_BALANCE_PENDING, updateTokenBalance),
takeEvery(TypeKeys.WALLET_SET_ACCOUNT_BALANCE, updateAccountBalance),
// Foreign actions
takeEvery(ConfigTypeKeys.CONFIG_TOGGLE_OFFLINE, updateBalances),
takeEvery(CustomTokenTypeKeys.CUSTOM_TOKEN_ADD, handleCustomTokenAdd)
];
}