sagas.ts
1 import { SagaIterator, delay, Task } from 'redux-saga'; 2 import { apply, call, fork, put, select, takeEvery, take, cancel } from 'redux-saga/effects'; 3 4 import { translateRaw } from 'translations'; 5 import { INode } from 'libs/nodes/INode'; 6 import { Wei } from 'libs/units'; 7 import { Token } from 'types/network'; 8 import { 9 IWallet, 10 MnemonicWallet, 11 getPrivKeyWallet, 12 getKeystoreWallet, 13 determineKeystoreType, 14 KeystoreTypes, 15 getUtcWallet, 16 signWrapper, 17 WalletConfig 18 } from 'libs/wallet'; 19 import { loadWalletConfig, saveWalletConfig } from 'utils/localStorage'; 20 import { AppState } from 'features/reducers'; 21 import * as derivedSelectors from 'features/selectors'; 22 import * as configMetaTypes from 'features/config/meta/types'; 23 import * as configMetaSelectors from 'features/config/meta/selectors'; 24 import * as configNodesSelectors from 'features/config/nodes/selectors'; 25 import * as configSelectors from 'features/config/selectors'; 26 import { notificationsActions } from 'features/notifications'; 27 import { customTokensTypes, customTokensSelectors } from 'features/customTokens'; 28 import * as types from './types'; 29 import * as actions from './actions'; 30 import * as selectors from './selectors'; 31 32 export function* getTokenBalancesSaga(wallet: IWallet, tokens: Token[]) { 33 const node: INode = yield select(configNodesSelectors.getNodeLib); 34 const address: string = yield apply(wallet, wallet.getAddressString); 35 const tokenBalances: types.TokenBalance[] = yield apply(node, node.getTokenBalances, [ 36 address, 37 tokens 38 ]); 39 return tokens.reduce<{ [TokenSymbol: string]: types.TokenBalance }>((acc, t, i) => { 40 acc[t.symbol] = tokenBalances[i]; 41 return acc; 42 }, {}); 43 } 44 45 // Return an array of the tokens that meet any of the following conditions: 46 // 1. Non-zero balance 47 // 2. It was in the previous wallet's config 48 // 3. It's a custom token that the user added 49 export function* filterScannedTokenBalances(wallet: IWallet, balances: types.TokenBalanceLookup) { 50 const customTokens: AppState['customTokens'] = yield select( 51 customTokensSelectors.getCustomTokens 52 ); 53 const oldConfig: WalletConfig = yield call(loadWalletConfig, wallet); 54 return Object.keys(balances).filter(symbol => { 55 if (balances[symbol] && !balances[symbol].balance.isZero()) { 56 return true; 57 } 58 if (oldConfig.tokens && oldConfig.tokens.includes(symbol)) { 59 return true; 60 } 61 if (customTokens.find(token => token.symbol === symbol)) { 62 return true; 63 } 64 }); 65 } 66 67 export function* updateAccountBalance(): SagaIterator { 68 try { 69 const isOffline = yield select(configMetaSelectors.getOffline); 70 if (isOffline) { 71 return; 72 } 73 74 yield put(actions.setBalancePending()); 75 const wallet: null | IWallet = yield select(selectors.getWalletInst); 76 if (!wallet) { 77 return; 78 } 79 const node: INode = yield select(configNodesSelectors.getNodeLib); 80 const address: string = yield apply(wallet, wallet.getAddressString); 81 // network request 82 const balance: Wei = yield apply(node, node.getBalance, [address]); 83 yield put(actions.setBalanceFullfilled(balance)); 84 } catch (error) { 85 yield put(actions.setBalanceRejected()); 86 } 87 } 88 89 export function* retryTokenBalances(): SagaIterator { 90 const tokens: types.MergedToken[] = yield select(derivedSelectors.getWalletConfigTokens); 91 if (tokens && tokens.length) { 92 yield call(updateTokenBalances); 93 } else { 94 const wallet: null | IWallet = yield select(selectors.getWalletInst); 95 if (wallet) { 96 yield call(scanWalletForTokensSaga, wallet); 97 } 98 } 99 } 100 101 export function* updateTokenBalances(): SagaIterator { 102 try { 103 const isOffline = yield select(configMetaSelectors.getOffline); 104 if (isOffline) { 105 return; 106 } 107 108 const wallet: null | IWallet = yield select(selectors.getWalletInst); 109 const tokens: types.MergedToken[] = yield select(derivedSelectors.getWalletConfigTokens); 110 if (!wallet || !tokens.length) { 111 return; 112 } 113 yield put(actions.setTokenBalancesPending()); 114 const tokenBalances: types.TokenBalanceLookup = yield call( 115 getTokenBalancesSaga, 116 wallet, 117 tokens 118 ); 119 yield put(actions.setTokenBalancesFulfilled(tokenBalances)); 120 } catch (error) { 121 console.error('Failed to get token balances', error); 122 yield put(actions.setTokenBalancesRejected()); 123 } 124 } 125 126 export function* updateTokenBalance(action: types.SetTokenBalancePendingAction): SagaIterator { 127 try { 128 const isOffline = yield select(configMetaSelectors.getOffline); 129 if (isOffline) { 130 return; 131 } 132 133 const wallet: null | IWallet = yield select(selectors.getWalletInst); 134 const { tokenSymbol } = action.payload; 135 const allTokens: Token[] = yield select(configSelectors.getAllTokens); 136 const token = allTokens.find(t => t.symbol === tokenSymbol); 137 138 if (!wallet) { 139 return; 140 } 141 142 if (!token) { 143 throw Error('Token not found'); 144 } 145 146 const tokenBalances: types.TokenBalanceLookup = yield call(getTokenBalancesSaga, wallet, [ 147 token 148 ]); 149 150 yield put(actions.setTokenBalanceFulfilled(tokenBalances)); 151 } catch (error) { 152 console.error('Failed to get token balance', error); 153 yield put(actions.setTokenBalanceRejected()); 154 } 155 } 156 157 export function* handleScanWalletAction(action: types.ScanWalletForTokensAction): SagaIterator { 158 yield call(scanWalletForTokensSaga, action.payload); 159 } 160 161 export function* scanWalletForTokensSaga(wallet: IWallet): SagaIterator { 162 try { 163 const isOffline = yield select(configMetaSelectors.getOffline); 164 if (isOffline) { 165 return; 166 } 167 168 const tokens: types.MergedToken[] = yield select(derivedSelectors.getTokens); 169 yield put(actions.setTokenBalancesPending()); 170 171 // Fetch all token balances, save ones we want to the config 172 const balances: types.TokenBalanceLookup = yield call(getTokenBalancesSaga, wallet, tokens); 173 const tokensToSave: string[] = yield call(filterScannedTokenBalances, wallet, balances); 174 const config: WalletConfig = yield call(saveWalletConfig, wallet, { tokens: tokensToSave }); 175 yield put(actions.setWalletConfig(config)); 176 177 yield put(actions.setTokenBalancesFulfilled(balances)); 178 } catch (err) { 179 console.error('Failed to scan for tokens', err); 180 yield put(actions.setTokenBalancesRejected()); 181 } 182 } 183 184 export function* handleSetWalletTokens(action: types.SetWalletTokensAction): SagaIterator { 185 const wallet: null | IWallet = yield select(selectors.getWalletInst); 186 if (!wallet) { 187 return; 188 } 189 190 const config: WalletConfig = yield call(saveWalletConfig, wallet, { tokens: action.payload }); 191 yield put(actions.setWalletConfig(config)); 192 } 193 194 export function* updateBalances(): SagaIterator { 195 const updateAccount = yield fork(updateAccountBalance); 196 const updateToken = yield fork(updateTokenBalances); 197 198 yield take(types.WalletActions.SET); 199 yield cancel(updateAccount); 200 yield cancel(updateToken); 201 } 202 203 export function* handleNewWallet(): SagaIterator { 204 yield call(updateWalletConfig); 205 yield fork(updateBalances); 206 } 207 208 export function* updateWalletConfig(): SagaIterator { 209 const wallet: null | IWallet = yield select(selectors.getWalletInst); 210 if (!wallet) { 211 return; 212 } 213 const config: WalletConfig = yield call(loadWalletConfig, wallet); 214 yield put(actions.setWalletConfig(config)); 215 } 216 217 export function* unlockPrivateKeySaga(action: types.UnlockPrivateKeyAction): SagaIterator { 218 let wallet: IWallet | null = null; 219 const { key, password } = action.payload; 220 221 try { 222 wallet = getPrivKeyWallet(key, password); 223 } catch (e) { 224 yield put(notificationsActions.showNotification('danger', translateRaw('INVALID_PKEY'))); 225 return; 226 } 227 yield put(actions.setWallet(wallet)); 228 } 229 230 export function* startLoadingSpinner(): SagaIterator { 231 yield call(delay, 400); 232 yield put(actions.setWalletPending(true)); 233 } 234 235 export function* stopLoadingSpinner(loadingFork: Task | null): SagaIterator { 236 if (loadingFork !== null && loadingFork !== undefined) { 237 yield cancel(loadingFork); 238 } 239 yield put(actions.setWalletPending(false)); 240 } 241 242 export function* unlockKeystoreSaga(action: types.UnlockKeystoreAction): SagaIterator { 243 const { file, password } = action.payload; 244 let wallet: null | IWallet = null; 245 let spinnerTask: null | Task = null; 246 try { 247 if (determineKeystoreType(file) === KeystoreTypes.utc) { 248 spinnerTask = yield fork(startLoadingSpinner); 249 wallet = signWrapper(yield call(getUtcWallet, file, password)); 250 } else { 251 wallet = getKeystoreWallet(file, password); 252 } 253 } catch (e) { 254 yield call(stopLoadingSpinner, spinnerTask); 255 if ( 256 password === '' && 257 e.message === 'Private key does not satisfy the curve requirements (ie. it is invalid)' 258 ) { 259 yield put(actions.setPasswordPrompt()); 260 } else { 261 yield put(notificationsActions.showNotification('danger', translateRaw('ERROR_6'))); 262 } 263 return; 264 } 265 266 // TODO: provide a more descriptive error than the two 'ERROR_6' (invalid pass) messages above 267 yield call(stopLoadingSpinner, spinnerTask); 268 yield put(actions.setWallet(wallet)); 269 } 270 271 export function* unlockMnemonicSaga(action: types.UnlockMnemonicAction): SagaIterator { 272 let wallet; 273 const { phrase, pass, path, address } = action.payload; 274 275 try { 276 wallet = MnemonicWallet(phrase, pass, path, address); 277 } catch (err) { 278 // TODO: use better error than 'ERROR_14' (wallet not found) 279 yield put(notificationsActions.showNotification('danger', translateRaw('ERROR_14'))); 280 return; 281 } 282 283 yield put(actions.setWallet(wallet)); 284 } 285 286 export function* handleCustomTokenAdd( 287 action: customTokensTypes.AddCustomTokenAction 288 ): SagaIterator { 289 // Add the custom token to our current wallet's config 290 const wallet: null | IWallet = yield select(selectors.getWalletInst); 291 if (!wallet) { 292 return; 293 } 294 const oldConfig: WalletConfig = yield call(loadWalletConfig, wallet); 295 const config: WalletConfig = yield call(saveWalletConfig, wallet, { 296 tokens: [...(oldConfig.tokens || []), action.payload.symbol] 297 }); 298 yield put(actions.setWalletConfig(config)); 299 300 // Update token balances 301 yield fork(updateTokenBalances); 302 } 303 304 export function* walletSaga(): SagaIterator { 305 yield [ 306 takeEvery(types.WalletActions.UNLOCK_PRIVATE_KEY, unlockPrivateKeySaga), 307 takeEvery(types.WalletActions.UNLOCK_KEYSTORE, unlockKeystoreSaga), 308 takeEvery(types.WalletActions.UNLOCK_MNEMONIC, unlockMnemonicSaga), 309 takeEvery(types.WalletActions.SET, handleNewWallet), 310 takeEvery(types.WalletActions.SCAN_WALLET_FOR_TOKENS, handleScanWalletAction), 311 takeEvery(types.WalletActions.SET_WALLET_TOKENS, handleSetWalletTokens), 312 takeEvery(types.WalletActions.SET_TOKEN_BALANCE_PENDING, updateTokenBalance), 313 takeEvery(types.WalletActions.REFRESH_ACCOUNT_BALANCE, updateAccountBalance), 314 takeEvery(types.WalletActions.REFRESH_TOKEN_BALANCES, retryTokenBalances), 315 // Foreign actions 316 takeEvery(configMetaTypes.CONFIG_META.TOGGLE_OFFLINE, updateBalances), 317 takeEvery(customTokensTypes.CustomTokensActions.ADD, handleCustomTokenAdd) 318 ]; 319 }