Changeset View
Changeset View
Standalone 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 { | import { | ||||
fromSmallestDenomination, | fromSmallestDenomination, | ||||
loadStoredWallet, | loadStoredWallet, | ||||
isValidStoredWallet, | isValidStoredWallet, | ||||
whichUtxosChanged, | |||||
whichUtxosWereConsumed, | |||||
parseChangedUtxos, | |||||
isUtxoArrayEmpty, | |||||
organizeHydratedUtxoDetailsByAddress, | |||||
addHydratedUtxoDetailsToWalletStateIfNew, | |||||
removeConsumedUtxosFromHydratedUtxoDetails, | |||||
} from '@utils/cashMethods'; | } from '@utils/cashMethods'; | ||||
import localforage from 'localforage'; | import localforage from 'localforage'; | ||||
import { currency } from '@components/Common/Ticker'; | import { currency } from '@components/Common/Ticker'; | ||||
import isEmpty from 'lodash.isempty'; | import isEmpty from 'lodash.isempty'; | ||||
import isEqual from 'lodash.isequal'; | import isEqual from 'lodash.isequal'; | ||||
import { utxosConsumedByTransaction } from '../utils/__mocks__/mockChangingUtxos'; | |||||
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: {}, | ||||
▲ Show 20 Lines • Show All 113 Lines • ▼ Show 20 Lines | const loadWalletFromStorageOnStartup = async setWallet => { | ||||
// Loading will remain true until API calls populate this legacy wallet | // Loading will remain true until API calls populate this legacy wallet | ||||
setWallet(wallet); | setWallet(wallet); | ||||
}; | }; | ||||
const haveUtxosChanged = (wallet, 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 | ||||
console.log(`utxos`, utxos); | |||||
console.log(`previousUtxos`, previousUtxos); | |||||
// 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') { | if (typeof utxos === 'undefined') { | ||||
Show All 19 Lines | const haveUtxosChanged = (wallet, utxos, previousUtxos) => { | ||||
return true; | return true; | ||||
} | } | ||||
// If wallet is valid, compare what exists in written wallet state instead of former api call | // If wallet is valid, compare what exists in written wallet state instead of former api call | ||||
let utxosToCompare = previousUtxos; | let utxosToCompare = previousUtxos; | ||||
if (isValidStoredWallet(wallet)) { | if (isValidStoredWallet(wallet)) { | ||||
try { | try { | ||||
utxosToCompare = wallet.state.utxos; | utxosToCompare = wallet.state.utxos; | ||||
console.log(`utxosToCompare`, utxosToCompare); | |||||
} catch (err) { | } catch (err) { | ||||
console.log(`Error setting utxos to wallet.state.utxos`, err); | console.log(`Error setting utxos to wallet.state.utxos`, err); | ||||
console.log(`Wallet at err`, wallet); | console.log(`Wallet at err`, wallet); | ||||
// If this happens, assume utxo set has changed | // If this happens, assume utxo set has changed | ||||
return true; | return true; | ||||
} | } | ||||
} | } | ||||
Show All 25 Lines | const update = async ({ wallet, setWalletState }) => { | ||||
setUtxos(utxos); | setUtxos(utxos); | ||||
// Need to call with wallet as a parameter rather than trusting it is in state, otherwise can sometimes get wallet=false from haveUtxosChanged | // 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( | const utxosHaveChanged = haveUtxosChanged( | ||||
wallet, | wallet, | ||||
utxos, | utxos, | ||||
previousUtxos, | previousUtxos, | ||||
); | ); | ||||
console.log(`wallet`, wallet); | |||||
console.log(`isValidStoredWallet`, isValidStoredWallet(wallet)); | |||||
console.log(`utxosHaveChanged`, utxosHaveChanged); | |||||
// Dev test case | |||||
// If api utxos length does not match hydratedUtxos length, note it | |||||
// TODO catch this, probably just rehydrate them all if you see it | |||||
if (utxos[2].length !== wallet.state.utxos[2].length) { | |||||
console.log( | |||||
`Error: ${utxos[2].length} utxos detected at from API, while only ${wallet.state.utxos[2].length} are in wallet`, | |||||
); | |||||
} | |||||
// 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"); | ||||
return; | return; | ||||
} | } | ||||
const hydratedUtxoDetails = await getHydratedUtxoDetails( | // Determine if you have unseen utxos that must be hydrated | ||||
const changedUtxos = whichUtxosChanged(utxos, previousUtxos); | |||||
console.log(`changedUtxos`, changedUtxos); | |||||
const { utxosConfirmed, utxosToHydrate } = parseChangedUtxos( | |||||
previousUtxos, | |||||
changedUtxos, | |||||
); | |||||
// See if any utxos were consumed since last tick | |||||
const consumedUtxos = whichUtxosWereConsumed( | |||||
utxos, | |||||
previousUtxos, | |||||
utxosConfirmed, | |||||
); | |||||
console.log(`consumedUtxos`, consumedUtxos); | |||||
// If so, remove them from utxo set | |||||
// TODO function remove(utxos) -- what do you need here? | |||||
/* | |||||
You would want to remove these utxos from hydatedUtxoDetails | |||||
So, you will need yet another function for this | |||||
*/ | |||||
console.log(`utxosConfirmed`, utxosConfirmed); | |||||
console.log(`utxosToHydrate`, utxosToHydrate); | |||||
// For now, don't do anything with utxosConfirmed | |||||
// Eventually you may want to update txHistory or show this tx confirmed in the UI | |||||
// need helper methods to determine if there are utxos in these arrays, as length doesn't help; empty arrays still keep address fields | |||||
if (!isUtxoArrayEmpty(utxosConfirmed)) { | |||||
console.log(`Transaction confirmed`); | |||||
// In the future, this is where you could update the tx history | |||||
// Update utxos in wallet.state | |||||
} | |||||
// TODO if utxos have confirmed, update the wallet state, but don't make any api calls, just update the utxos | |||||
// If utxo change is not a new utxo that needs to be hydrated and you have a valid wallet with a balance, do not perform update functions | |||||
if ( | |||||
isUtxoArrayEmpty(utxosToHydrate) && | |||||
isUtxoArrayEmpty(utxosConsumedByTransaction) && | |||||
isValidStoredWallet(wallet) && | |||||
isValidStoredWallet(wallet) | |||||
) { | |||||
console.log( | |||||
`Utxo set has changed but no new utxos need to be hydrated`, | |||||
); | |||||
// Update only the utxos in walletState | |||||
const confirmedTxStateUpdate = { ...walletState, utxos: utxos }; | |||||
console.log(`confirmedTxStateUpdate`, confirmedTxStateUpdate); | |||||
setWalletState(confirmedTxStateUpdate); | |||||
// Also set this in wallet.state | |||||
wallet.state = wallet.confirmedTxStateUpdate; | |||||
setWallet(wallet); | |||||
// Write this state to indexedDb using localForage | |||||
writeWalletState(wallet, confirmedTxStateUpdate); | |||||
// remove api error here; otherwise it will remain if recovering from a rate | |||||
// limit error with an unchanged utxo set | |||||
setApiError(false); | |||||
// then walletState has not changed and does not need to be updated | |||||
//console.timeEnd("update"); | |||||
return; | |||||
} | |||||
let hydratedUtxoDetails; | |||||
// If you have a valid wallet with a balance, only hydrate the new utxos | |||||
if ( | |||||
isValidStoredWallet(wallet) && | |||||
wallet.state.balances.totalBalanceInSatoshis && | |||||
wallet.state.balances.totalBalanceInSatoshis !== 0 | |||||
) { | |||||
try { | |||||
console.log( | |||||
`Wallet is valid and has previous hydratedUtxoDetails stored`, | |||||
); | |||||
const previousHydratedUtxoDetails = | |||||
wallet.state.hydratedUtxoDetails; | |||||
console.log( | |||||
`previousHydratedUtxoDetails`, | |||||
previousHydratedUtxoDetails, | |||||
); | |||||
const changedUtxosHydratedUtxoDetails = await getHydratedUtxoDetails( | |||||
BCH, | |||||
utxosToHydrate, | |||||
); | |||||
console.log( | |||||
`changedUtxosHydratedUtxoDetails`, | |||||
changedUtxosHydratedUtxoDetails, | |||||
); | |||||
// Make sure previousHydratedUtxoDetails does not contain duplicate addresses | |||||
const organizedPreviousHydratedUtxoDetails = organizeHydratedUtxoDetailsByAddress( | |||||
previousHydratedUtxoDetails, | |||||
); | |||||
// Remove any consumed utxos | |||||
const organizedPreviousHydratedUtxoDetailsWithConsumedUtxosRemoved = removeConsumedUtxosFromHydratedUtxoDetails( | |||||
organizedPreviousHydratedUtxoDetails, | |||||
consumedUtxos, | |||||
); | |||||
// Combine with previousHydratedUtxoDetails to get required input for getSlpBalancesAndUtxos() | |||||
hydratedUtxoDetails = addHydratedUtxoDetailsToWalletStateIfNew( | |||||
organizedPreviousHydratedUtxoDetailsWithConsumedUtxosRemoved, | |||||
changedUtxosHydratedUtxoDetails, | |||||
); | |||||
console.log( | |||||
`combined hydratedUtxoDetails`, | |||||
hydratedUtxoDetails, | |||||
); | |||||
} catch (err) { | |||||
// Legacy method, hydrates all utxos | |||||
console.log( | |||||
`Error hydrating only changed utxos, hydrating all`, | |||||
); | |||||
console.log(`Error`, err); | |||||
hydratedUtxoDetails = await getHydratedUtxoDetails( | |||||
BCH, | BCH, | ||||
utxos, | utxos, | ||||
); | ); | ||||
} | |||||
} else { | |||||
// Legacy method, hydrates all utxos | |||||
console.log( | |||||
`Invalid wallet or zero balance, hydrating all utxos`, | |||||
); | |||||
console.log(`wallet.balances`, wallet.balances); | |||||
hydratedUtxoDetails = await getHydratedUtxoDetails(BCH, utxos); | |||||
} | |||||
// Calculate these parameters as before | |||||
// Opportunity to optimize by only calculating from the changed utxos | |||||
const slpBalancesAndUtxos = await getSlpBalancesAndUtxos( | const slpBalancesAndUtxos = getSlpBalancesAndUtxos( | ||||
hydratedUtxoDetails, | hydratedUtxoDetails, | ||||
); | ); | ||||
const txHistory = await getTxHistory(BCH, cashAddresses); | const txHistory = await getTxHistory(BCH, cashAddresses); | ||||
const parsedTxHistory = await getTxData(BCH, txHistory); | const parsedTxHistory = await getTxData(BCH, txHistory); | ||||
const parsedWithTokens = await addTokenTxData(BCH, parsedTxHistory); | const parsedWithTokens = await addTokenTxData(BCH, parsedTxHistory); | ||||
console.log(`slpBalancesAndUtxos`, slpBalancesAndUtxos); | console.log(`slpBalancesAndUtxos`, slpBalancesAndUtxos); | ||||
Show All 18 Lines | const update = async ({ wallet, setWalletState }) => { | ||||
newState.tokens = tokens; | newState.tokens = tokens; | ||||
newState.parsedTxHistory = parsedWithTokens; | newState.parsedTxHistory = parsedWithTokens; | ||||
newState.utxos = utxos; | newState.utxos = utxos; | ||||
newState.hydratedUtxoDetails = hydratedUtxoDetails; | newState.hydratedUtxoDetails = hydratedUtxoDetails; | ||||
console.log(`Setting newState`, newState); | |||||
setWalletState(newState); | setWalletState(newState); | ||||
// Set wallet with new state field | // Set wallet with new state field | ||||
// Note: now that wallet carries state, maintaining a separate walletState object is redundant | // Note: now that wallet carries state, maintaining a separate walletState object is redundant | ||||
// TODO clear up in future diff | // TODO clear up in future diff | ||||
wallet.state = wallet.newState; | wallet.state = wallet.newState; | ||||
setWallet(wallet); | setWallet(wallet); | ||||
▲ Show 20 Lines • Show All 657 Lines • ▼ Show 20 Lines | |||||
useAsyncTimeout(async () => { | useAsyncTimeout(async () => { | ||||
const wallet = await getWallet(); | const wallet = await getWallet(); | ||||
update({ | update({ | ||||
wallet, | wallet, | ||||
setWalletState, | setWalletState, | ||||
}).finally(() => { | }).finally(() => { | ||||
setLoading(false); | setLoading(false); | ||||
}); | }); | ||||
}, 10000); | }, 3000); | ||||
const initializeWebsocket = (cashAddress, slpAddress) => { | const initializeWebsocket = (cashAddress, slpAddress) => { | ||||
// console.log(`initializeWebsocket(${cashAddress}, ${slpAddress})`); | // console.log(`initializeWebsocket(${cashAddress}, ${slpAddress})`); | ||||
// This function parses 3 cases | // This function parses 3 cases | ||||
// 1: edge case, websocket is in state but not properly connected | // 1: edge case, websocket is in state but not properly connected | ||||
// > Remove it from state and forget about it, fall back to normal notifications | // > Remove it from state and forget about it, fall back to normal notifications | ||||
// 2: edge-ish case, websocket is in state and connected but user has changed wallet | // 2: edge-ish case, websocket is in state and connected but user has changed wallet | ||||
// > Unsubscribe from old addresses and subscribe to new ones | // > Unsubscribe from old addresses and subscribe to new ones | ||||
▲ Show 20 Lines • Show All 373 Lines • Show Last 20 Lines |