Changeset View
Standalone View
web/cashtab/src/hooks/useWallet.js
/* eslint-disable react-hooks/exhaustive-deps */ | /* eslint-disable react-hooks/exhaustive-deps */ | ||||
import React, { useState, useEffect } from 'react'; | import React, { useState, useEffect } from 'react'; | ||||
import Paragraph from 'antd/lib/typography/Paragraph'; | import Paragraph from 'antd/lib/typography/Paragraph'; | ||||
import { notification } from 'antd'; | import { notification } from 'antd'; | ||||
import useAsyncTimeout from '@hooks/useAsyncTimeout'; | import useAsyncTimeout from '@hooks/useAsyncTimeout'; | ||||
import usePrevious from '@hooks/usePrevious'; | import usePrevious from '@hooks/usePrevious'; | ||||
import useBCH from '@hooks/useBCH'; | import useBCH from '@hooks/useBCH'; | ||||
import BigNumber from 'bignumber.js'; | import BigNumber from 'bignumber.js'; | ||||
import { fromSmallestDenomination } from '@utils/cashMethods'; | import { | ||||
fromSmallestDenomination, | |||||
tokenBalancesToBigNumber, | |||||
} from '@utils/cashMethods'; | |||||
import localforage from 'localforage'; | import localforage from 'localforage'; | ||||
import { currency } from '@components/Common/Ticker'; | import { currency } from '@components/Common/Ticker'; | ||||
import _ from 'lodash'; | import _ from 'lodash'; | ||||
const useWallet = () => { | const useWallet = () => { | ||||
const [wallet, setWallet] = useState(false); | const [wallet, setWallet] = useState(false); | ||||
const [fiatPrice, setFiatPrice] = useState(null); | const [fiatPrice, setFiatPrice] = useState(null); | ||||
const [ws, setWs] = useState(null); | const [ws, setWs] = useState(null); | ||||
const [apiError, setApiError] = useState(false); | const [apiError, setApiError] = useState(false); | ||||
const [walletState, setWalletState] = useState({ | const [walletState, setWalletState] = useState({ | ||||
balances: {}, | balances: {}, | ||||
hydratedUtxoDetails: {}, | |||||
tokens: [], | tokens: [], | ||||
slpBalancesAndUtxos: [], | slpBalancesAndUtxos: {}, | ||||
parsedTxHistory: [], | parsedTxHistory: [], | ||||
utxos: [], | |||||
}); | }); | ||||
const { | const { | ||||
getBCH, | getBCH, | ||||
getUtxos, | getUtxos, | ||||
getHydratedUtxoDetails, | getHydratedUtxoDetails, | ||||
getSlpBalancesAndUtxos, | getSlpBalancesAndUtxos, | ||||
getTxHistory, | getTxHistory, | ||||
getTxData, | getTxData, | ||||
▲ Show 20 Lines • Show All 80 Lines • ▼ Show 20 Lines | const deriveAccount = async (BCH, { masterHDNode, path }) => { | ||||
cashAddress, | cashAddress, | ||||
slpAddress, | slpAddress, | ||||
fundingWif: BCH.HDNode.toWIF(node), | fundingWif: BCH.HDNode.toWIF(node), | ||||
fundingAddress: BCH.SLP.Address.toSLPAddress(cashAddress), | fundingAddress: BCH.SLP.Address.toSLPAddress(cashAddress), | ||||
legacyAddress: BCH.SLP.Address.toLegacyAddress(cashAddress), | legacyAddress: BCH.SLP.Address.toLegacyAddress(cashAddress), | ||||
}; | }; | ||||
}; | }; | ||||
const haveUtxosChanged = (utxos, previousUtxos) => { | const haveUtxosChanged = (wallet, utxos, previousUtxos) => { | ||||
// Relevant points for this array comparing exercise | // Relevant points for this array comparing exercise | ||||
// https://stackoverflow.com/questions/13757109/triple-equal-signs-return-false-for-arrays-in-javascript-why | // https://stackoverflow.com/questions/13757109/triple-equal-signs-return-false-for-arrays-in-javascript-why | ||||
// https://stackoverflow.com/questions/7837456/how-to-compare-arrays-in-javascript | // https://stackoverflow.com/questions/7837456/how-to-compare-arrays-in-javascript | ||||
// If this is initial state | // If this is initial state | ||||
if (utxos === null) { | if (utxos === null) { | ||||
// Then make sure to get slpBalancesAndUtxos | // Then make sure to get slpBalancesAndUtxos | ||||
return true; | return true; | ||||
} | } | ||||
// If this is the first time the wallet received utxos | // If this is the first time the wallet received utxos | ||||
if (typeof utxos === 'undefined') { | |||||
// Then they have certainly changed | |||||
return true; | |||||
} | |||||
if (typeof previousUtxos === 'undefined') { | |||||
// Compare to what you have in localStorage on startup | |||||
// If previousUtxos are undefined, see if you have previousUtxos in wallet state | |||||
// If you do, and it has everything you need, set wallet state with that instead of calling hydrateUtxos on all utxos | |||||
if ( | if ( | ||||
typeof previousUtxos === 'undefined' || | wallet && | ||||
typeof utxos === 'undefined' | wallet.state && | ||||
wallet.state.balances && | |||||
wallet.state.utxos && | |||||
wallet.state.hydratedUtxoDetails && | |||||
wallet.state.parsedTxHistory && | |||||
wallet.state.slpBalancesAndUtxos && | |||||
wallet.state.tokens | |||||
Fabien: If I understand correctly this has to be updated each time something is added to the wallet… | |||||
bytesofmanAuthorUnsubmitted Done Inline ActionsYes, this is a bit overkill. The specific case I'm trying to catch here is cashtab users who are opening this new diff's version for the first time. The first time they load the app, the wallet object will not have the info necessary to populate without calling hydrateUtxos. But, I figured in any event all of the data needs to be there in order to populate without calling hydrateUtxos, so might as well confirm this. I haven't seen cases in testing where one of these items is randomly missing, only the specific case where an older wallet loading this diff for the first time would, by design, not have the newer fields added in this diff. It probably makes sense to have something like an isWalletStateValid function, could have more general use later and appropriate here. Will amend. bytesofman: Yes, this is a bit overkill. The specific case I'm trying to catch here is cashtab users who… | |||||
bytesofmanAuthorUnsubmitted Done Inline ActionsDoing some research -- there are ways to make this check shorter, but I think it makes the code a bit harder to understand without offering any performance benefit. I think moving this logic into a described function (which may need to be expanded as more info is added to the wallet state in local storage) is the best approach. bytesofman: Doing some research -- there are ways to make this check shorter, but I think it makes the code… | |||||
) { | ) { | ||||
// Then they have certainly changed | // Convert all the token balance figures to big numbers | ||||
const liveWalletState = tokenBalancesToBigNumber(wallet.state); | |||||
FabienUnsubmitted Not Done Inline ActionsIs there a future use for wallet.state ? Otherwise wallet.state.tokens seems to be enough Fabien: Is there a future use for `wallet.state` ? Otherwise `wallet.state.tokens` seems to be enough | |||||
bytesofmanAuthorUnsubmitted Done Inline ActionsFunction should convert a cached wallet state with invalid BigNumber items to a valid wallet state with the correct BigNumber types. It would be possible to limit this only to the token fields, but then the logic to insert this into wallet.state would need to go back into useWallet.js. I think there's a good chance other fields (like txHistory) will need this same treatment, and I might come across other adjustments that need to be made to allow the wallet to treat the cached state like the from-API live state -- so I keeping the logic in this function makes sense. I will update the function name to reflect this, right now it is named at the hyper-specific case of token type conversion (which is all it does now). bytesofman: Function should convert a cached wallet state with invalid `BigNumber` items to a valid wallet… | |||||
return setWalletState(liveWalletState); | |||||
} | |||||
const cachedUtxos = wallet.state.utxos; | |||||
// Compare | |||||
const utxoArraysUnchangedFromCache = _.isEqual(utxos, cachedUtxos); | |||||
// If utxos are not the same as previousUtxos | |||||
FabienUnsubmitted Not Done Inline Actionsare the same Fabien: `are the same` | |||||
if (utxoArraysUnchangedFromCache) { | |||||
// then utxos have not changed | |||||
console.log(`Utxo set is the same as local wallet`); | |||||
return false; | |||||
// otherwise, | |||||
} else { | |||||
// utxos have changed | |||||
return true; | return true; | ||||
} | } | ||||
FabienUnsubmitted Not Done Inline ActionsRemove else clause. In fact if there was no console logging this could be much simpler: return !_.isEqual(utxos, cachedUtxos); Fabien: Remove else clause. In fact if there was no console logging this could be much simpler: `return… | |||||
bytesofmanAuthorUnsubmitted Done Inline ActionsGood point this is much better. console.log was just for my testing/debugging and doesn't need to be in the production app. bytesofman: Good point this is much better. `console.log` was just for my testing/debugging and doesn't… | |||||
} | |||||
// return true for empty array, since this means you definitely do not want to skip the next API call | // return true for empty array, since this means you definitely do not want to skip the next API call | ||||
if (utxos && utxos.length === 0) { | if (utxos && utxos.length === 0) { | ||||
return true; | return true; | ||||
} | } | ||||
// Compare utxo sets | // Compare utxo sets | ||||
const utxoArraysUnchanged = _.isEqual(utxos, previousUtxos); | const utxoArraysUnchanged = _.isEqual(utxos, previousUtxos); | ||||
Show All 26 Lines | const update = async ({ wallet, setWalletState }) => { | ||||
// If an error is returned or utxos from only 1 address are returned | // If an error is returned or utxos from only 1 address are returned | ||||
if (!utxos || _.isEmpty(utxos) || utxos.error || utxos.length < 2) { | if (!utxos || _.isEmpty(utxos) || utxos.error || utxos.length < 2) { | ||||
// Throw error here to prevent more attempted api calls | // Throw error here to prevent more attempted api calls | ||||
// as you are likely already at rate limits | // as you are likely already at rate limits | ||||
throw new Error('Error fetching utxos'); | throw new Error('Error fetching utxos'); | ||||
} | } | ||||
setUtxos(utxos); | setUtxos(utxos); | ||||
const utxosHaveChanged = haveUtxosChanged(utxos, previousUtxos); | // Need to call with wallet as a parameter rather than trusting it is in state, otherwise can sometimes get wallet=false from haveUtxosChanged | ||||
const utxosHaveChanged = haveUtxosChanged( | |||||
wallet, | |||||
utxos, | |||||
previousUtxos, | |||||
); | |||||
// If the utxo set has not changed, | // If the utxo set has not changed, | ||||
if (!utxosHaveChanged) { | if (!utxosHaveChanged) { | ||||
// remove api error here; otherwise it will remain if recovering from a rate | // remove api error here; otherwise it will remain if recovering from a rate | ||||
// limit error with an unchanged utxo set | // limit error with an unchanged utxo set | ||||
setApiError(false); | setApiError(false); | ||||
// then walletState has not changed and does not need to be updated | // then walletState has not changed and does not need to be updated | ||||
//console.timeEnd("update"); | //console.timeEnd("update"); | ||||
Show All 32 Lines | const update = async ({ wallet, setWalletState }) => { | ||||
); | ); | ||||
newState.balances = normalizeBalance(slpBalancesAndUtxos); | newState.balances = normalizeBalance(slpBalancesAndUtxos); | ||||
newState.tokens = tokens; | newState.tokens = tokens; | ||||
newState.parsedTxHistory = parsedWithTokens; | newState.parsedTxHistory = parsedWithTokens; | ||||
newState.utxos = utxos; | |||||
newState.hydratedUtxoDetails = hydratedUtxoDetails; | |||||
setWalletState(newState); | setWalletState(newState); | ||||
// Write this state to indexedDb using localForage | // Write this state to indexedDb using localForage | ||||
writeWalletState(wallet, newState); | writeWalletState(wallet, newState); | ||||
// If everything executed correctly, remove apiError | // If everything executed correctly, remove apiError | ||||
setApiError(false); | setApiError(false); | ||||
} catch (error) { | } catch (error) { | ||||
console.log(`Error in update({wallet, setWalletState})`); | console.log(`Error in update({wallet, setWalletState})`); | ||||
▲ Show 20 Lines • Show All 113 Lines • ▼ Show 20 Lines | const migrateLegacyWallet = async (BCH, wallet) => { | ||||
} | } | ||||
return wallet; | return wallet; | ||||
}; | }; | ||||
const writeWalletState = async (wallet, newState) => { | const writeWalletState = async (wallet, newState) => { | ||||
// Add new state as an object on the active wallet | // Add new state as an object on the active wallet | ||||
wallet.state = newState; | wallet.state = newState; | ||||
console.log(`wallet with state`, wallet); | |||||
try { | try { | ||||
await localforage.setItem('wallet', wallet); | await localforage.setItem('wallet', wallet); | ||||
console.log(`Wallet new state successfully written`); | |||||
} catch (err) { | } catch (err) { | ||||
console.log(`Error in writeWalletState()`); | console.log(`Error in writeWalletState()`); | ||||
console.log(err); | console.log(err); | ||||
} | } | ||||
}; | }; | ||||
const getWalletDetails = async wallet => { | const getWalletDetails = async wallet => { | ||||
if (!wallet) { | if (!wallet) { | ||||
▲ Show 20 Lines • Show All 897 Lines • Show Last 20 Lines |
If I understand correctly this has to be updated each time something is added to the wallet state, which is prone to errors. Is there a more generic way to ensure that all members of an object are non null ?