From b3c7a27cae0d5f907c79abc17b49e0c3e05b51d2 Mon Sep 17 00:00:00 2001
From: Maxwell Lasky <mlasky46@gmail.com>
Date: Sun, 29 Nov 2020 13:29:42 -0500
Subject: [PATCH] feature: add balance fetch retry logic (#2028)

* Adds RPC retry logic

* small refactor in balancesActions.js
---
 app/actions/balancesActions.js    | 112 ++++++++++++++++++------------
 app/actions/nodeStorageActions.js |  16 +++--
 2 files changed, 78 insertions(+), 50 deletions(-)

diff --git a/app/actions/balancesActions.js b/app/actions/balancesActions.js
index 77b96b717..c990fff8c 100644
--- a/app/actions/balancesActions.js
+++ b/app/actions/balancesActions.js
@@ -18,7 +18,8 @@ const MAX_SCRIPT_HASH_CHUNK_SIZE = 3
 type Props = {
   net: string,
   address: string,
-  tokens: Array<TokenItemType>,
+  tokens?: Array<TokenItemType>,
+  isRetry?: boolean,
 }
 
 let inMemoryBalances = {}
@@ -72,14 +73,16 @@ function determineIfBalanceUpdated(
   })
 }
 
-async function getBalances({ net, address }: Props) {
+let RETRY_COUNT = 0
+
+async function getBalances({ net, address, isRetry = false }: Props) {
   const { soundEnabled, tokens } = (await getSettings()) || {
     tokens: [],
     soundEnabled: true,
   }
   const network = findNetworkByDeprecatedLabel(net)
 
-  let endpoint = await getNode(net)
+  let endpoint = await getNode(net, isRetry)
   if (!endpoint) {
     endpoint = await getRPCEndpoint(net)
   }
@@ -113,48 +116,63 @@ async function getBalances({ net, address }: Props) {
         return accum
       }, [])
 
-  const promiseMap = chunks.map(async chunk => {
-    // NOTE: because the RPC nodes will respond with the contract
-    // symbol name, we need to use our original token list
-    // in case two tokens have the same symbol (SWTH vs SWTH OLD)
-    const balanceResults = await api.nep5.getTokenBalances(
-      endpoint,
-      chunk.map(({ scriptHash }) => scriptHash),
-      address,
-    )
-    const hashBasedBalance = {}
+  let shouldRetry = false
+  const results = await Promise.all(
+    chunks.map(async chunk => {
+      // NOTE: because the RPC nodes will respond with the contract
+      // symbol name, we need to use our original token list
+      // in case two tokens have the same symbol (SWTH vs SWTH OLD)
+      const balanceResults = await api.nep5
+        .getTokenBalances(
+          endpoint,
+          chunk.map(({ scriptHash }) => scriptHash),
+          address,
+        )
+        .catch(e => Promise.reject(e))
 
-    chunk.forEach((token, i) => {
-      hashBasedBalance[token.symbol] = Object.values(balanceResults)[i]
-    })
-    return hashBasedBalance
-  })
+      const hashBasedBalance = {}
 
-  const results = await Promise.all(promiseMap)
+      chunk.forEach((token, i) => {
+        hashBasedBalance[token.symbol] = Object.values(balanceResults)[i]
+      })
+      return hashBasedBalance
+    }),
+  ).catch(() => {
+    console.error(
+      `An error occurred fetching token balances using: ${endpoint} attempting to use a new RPC node.`,
+    )
+    shouldRetry = true
+  })
+  if (shouldRetry && RETRY_COUNT < 4) {
+    RETRY_COUNT += 1
+    return getBalances({ net, address, isRetry: true })
+  }
 
-  const parsedTokenBalances = results.reduce((accum, currBalance) => {
-    Object.keys(currBalance).forEach(key => {
-      const foundToken = tokens.find(token => token.symbol === key)
-      if (foundToken && currBalance[key]) {
-        determineIfBalanceUpdated(
+  const parsedTokenBalances =
+    results &&
+    results.reduce((accum, currBalance) => {
+      Object.keys(currBalance).forEach(key => {
+        const foundToken = tokens.find(token => token.symbol === key)
+        if (foundToken && currBalance[key]) {
+          determineIfBalanceUpdated(
+            // $FlowFixMe
+            { [foundToken.symbol]: currBalance[key] },
+            soundEnabled,
+            networkHasChanged,
+            adressHasChanged,
+          )
           // $FlowFixMe
-          { [foundToken.symbol]: currBalance[key] },
-          soundEnabled,
-          networkHasChanged,
-          adressHasChanged,
-        )
-        // $FlowFixMe
-        inMemoryBalances[foundToken.symbol] = currBalance[key]
-        accum.push({
-          [foundToken.scriptHash]: {
-            ...foundToken,
-            balance: currBalance[key],
-          },
-        })
-      }
-    })
-    return accum
-  }, [])
+          inMemoryBalances[foundToken.symbol] = currBalance[key]
+          accum.push({
+            [foundToken.scriptHash]: {
+              ...foundToken,
+              balance: currBalance[key],
+            },
+          })
+        }
+      })
+      return accum
+    }, [])
 
   // Handle manually added script hashses here
   const userGeneratedTokenInfo = []
@@ -185,11 +203,13 @@ async function getBalances({ net, address }: Props) {
       adressHasChanged,
     )
     inMemoryBalances[token.symbol] = token.balance
-    parsedTokenBalances.push({
-      [token.scriptHash]: {
-        ...token,
-      },
-    })
+    if (parsedTokenBalances) {
+      parsedTokenBalances.push({
+        [token.scriptHash]: {
+          ...token,
+        },
+      })
+    }
   })
 
   // asset balances
diff --git a/app/actions/nodeStorageActions.js b/app/actions/nodeStorageActions.js
index 8e16132d5..64b4201f5 100644
--- a/app/actions/nodeStorageActions.js
+++ b/app/actions/nodeStorageActions.js
@@ -122,7 +122,18 @@ export const getRPCEndpoint = async (
   }
 }
 
-export const getNode = async (net: Net): Promise<string> => {
+const setNode = async (node: string, net: Net): Promise<string> =>
+  setStorage(`${STORAGE_KEY}-${net}`, { node, timestamp: new Date().getTime() })
+
+export const getNode = async (
+  net: Net,
+  errorOccurred?: boolean,
+): Promise<string> => {
+  if (errorOccurred) {
+    delete cachedRPCUrl[net]
+    await setNode('', net)
+    return ''
+  }
   const storage = await getStorage(`${STORAGE_KEY}-${net}`).catch(console.error)
   const nodeInStorage = get(storage, 'node')
   const expiration = get(storage, 'timestamp')
@@ -132,9 +143,6 @@ export const getNode = async (net: Net): Promise<string> => {
   return nodeInStorage
 }
 
-const setNode = async (node: string, net: Net): Promise<string> =>
-  setStorage(`${STORAGE_KEY}-${net}`, { node, timestamp: new Date().getTime() })
-
 export default createActions(
   ID,
   ({ url, net }: Props = {}) => async (): Promise<string> => {