diff --git a/web/cashtab/src/components/Configure/Configure.js b/web/cashtab/src/components/Configure/Configure.js --- a/web/cashtab/src/components/Configure/Configure.js +++ b/web/cashtab/src/components/Configure/Configure.js @@ -253,7 +253,6 @@ // Track number of times a different wallet is activated Event('Configure.js', 'Activate', ''); await activateWallet(walletToActivate); - await updateSavedWallets(wallet); }; async function submit() { diff --git a/web/cashtab/src/components/Wallet/Wallet.js b/web/cashtab/src/components/Wallet/Wallet.js --- a/web/cashtab/src/components/Wallet/Wallet.js +++ b/web/cashtab/src/components/Wallet/Wallet.js @@ -203,14 +203,33 @@ const WalletInfo = () => { const ContextValue = React.useContext(WalletContext); - const { - wallet, - fiatPrice, - balances, - tokens, - parsedTxHistory, - apiError, - } = ContextValue; + const { wallet, fiatPrice, apiError } = ContextValue; + let balances; + let parsedTxHistory; + let tokens; + // use parameters from wallet.state object and not legacy separate parameters, if they are in state + // handle edge case of user with old wallet who has not opened latest Cashtab version yet + + // If the wallet object from ContextValue has a `state key`, then check which keys are in the wallet object + // Else set it as blank + const paramsInWalletState = wallet.state ? Object.keys(wallet.state) : []; + // If wallet.state includes balances and parsedTxHistory params, use these + // These are saved in indexedDb in the latest version of the app, hence accessible more quickly + if ( + paramsInWalletState.includes('balances') && + paramsInWalletState.includes('parsedTxHistory') && + paramsInWalletState.includes('tokens') + ) { + balances = wallet.state.balances; + parsedTxHistory = wallet.state.parsedTxHistory; + tokens = wallet.state.tokens; + } else { + // If balances and parsedTxHistory are not in the wallet.state object, load them from Context + // This is how the app used to work + balances = ContextValue.balances; + parsedTxHistory = ContextValue.parsedTxHistory; + tokens = ContextValue.tokens; + } const [address, setAddress] = React.useState('cashAddress'); const [activeTab, setActiveTab] = React.useState('txHistory'); diff --git a/web/cashtab/src/components/Wallet/__mocks__/walletAndBalancesMock.js b/web/cashtab/src/components/Wallet/__mocks__/walletAndBalancesMock.js --- a/web/cashtab/src/components/Wallet/__mocks__/walletAndBalancesMock.js +++ b/web/cashtab/src/components/Wallet/__mocks__/walletAndBalancesMock.js @@ -1,4 +1,4 @@ -import BigNumber from 'bignumber.js'; +// @generated export const walletWithBalancesMock = { wallet: { @@ -152,3 +152,190 @@ }, ], }; + +export const walletWithBalancesAndTokensWithCorrectState = { + wallet: { + name: 'MigrationTestAlpha', + Path245: { + cashAddress: + 'bitcoincash:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy54hkry298', + slpAddress: + 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', + fundingWif: 'KwgNkyijAaxFr5XQdnaYyNMXVSZobgHzSoKKfWiC3Q7Xr4n7iYMG', + fundingAddress: + 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', + legacyAddress: '1EgPUfBgU7ekho3EjtGze87dRADnUE8ojP', + }, + Path145: { + cashAddress: + 'bitcoincash:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9v0lgx569z', + slpAddress: + 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', + fundingWif: 'L2xvTe6CdNxroR6pbdpGWNjAa55AZX5Wm59W5TXMuH31ihNJdDjt', + fundingAddress: + 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', + legacyAddress: '1511T3ynXKgCwXhFijCUWKuTfqbPxFV1AF', + }, + Path1899: { + cashAddress: + 'bitcoincash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cptzgcqy6', + slpAddress: + 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', + fundingWif: 'Kx4FiBMvKK1iXjFk5QTaAK6E4mDGPjmwDZ2HDKGUZpE4gCXMaPe9', + fundingAddress: + 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', + legacyAddress: '1J1Aq5tAAYxZgSDRo8soKM2Rb41z3xrYpm', + }, + state: { + balances: { + totalBalanceInSatoshis: 6047469, + totalBalance: 0.06047469, + }, + tokens: [ + { + info: { + height: 666987, + tx_hash: + 'e7d554c317db71fd5b50fcf0b2cb4cbdce54a09f1732cfaade0820659318e30a', + tx_pos: 2, + value: 546, + satoshis: 546, + txid: + 'e7d554c317db71fd5b50fcf0b2cb4cbdce54a09f1732cfaade0820659318e30a', + vout: 2, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba', + tokenTicker: 'TBS', + tokenName: 'TestBits', + tokenDocumentUrl: 'https://thecryptoguy.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '6.001', + isValid: true, + address: + 'bitcoincash:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy54hkry298', + }, + tokenId: + 'bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba', + balance: '6.001', + hasBaton: false, + }, + ], + parsedTxHistory: [], + }, + }, + balances: { + totalBalanceInSatoshis: 6047469, + totalBalance: 0.06047469, + }, + tokens: [ + { + info: { + height: 666987, + tx_hash: + 'e7d554c317db71fd5b50fcf0b2cb4cbdce54a09f1732cfaade0820659318e30a', + tx_pos: 2, + value: 546, + satoshis: 546, + txid: + 'e7d554c317db71fd5b50fcf0b2cb4cbdce54a09f1732cfaade0820659318e30a', + vout: 2, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba', + tokenTicker: 'TBS', + tokenName: 'TestBits', + tokenDocumentUrl: 'https://thecryptoguy.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '6.001', + isValid: true, + address: + 'bitcoincash:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy54hkry298', + }, + tokenId: + 'bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba', + balance: '6.001', + hasBaton: false, + }, + ], +}; + +export const walletWithBalancesAndTokensWithEmptyState = { + wallet: { + name: 'MigrationTestAlpha', + Path245: { + cashAddress: + 'bitcoincash:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy54hkry298', + slpAddress: + 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', + fundingWif: 'KwgNkyijAaxFr5XQdnaYyNMXVSZobgHzSoKKfWiC3Q7Xr4n7iYMG', + fundingAddress: + 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', + legacyAddress: '1EgPUfBgU7ekho3EjtGze87dRADnUE8ojP', + }, + Path145: { + cashAddress: + 'bitcoincash:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9v0lgx569z', + slpAddress: + 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', + fundingWif: 'L2xvTe6CdNxroR6pbdpGWNjAa55AZX5Wm59W5TXMuH31ihNJdDjt', + fundingAddress: + 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', + legacyAddress: '1511T3ynXKgCwXhFijCUWKuTfqbPxFV1AF', + }, + Path1899: { + cashAddress: + 'bitcoincash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cptzgcqy6', + slpAddress: + 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', + fundingWif: 'Kx4FiBMvKK1iXjFk5QTaAK6E4mDGPjmwDZ2HDKGUZpE4gCXMaPe9', + fundingAddress: + 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', + legacyAddress: '1J1Aq5tAAYxZgSDRo8soKM2Rb41z3xrYpm', + }, + state: {}, + }, + balances: { + totalBalanceInSatoshis: 6047469, + totalBalance: 0.06047469, + }, + tokens: [ + { + info: { + height: 666987, + tx_hash: + 'e7d554c317db71fd5b50fcf0b2cb4cbdce54a09f1732cfaade0820659318e30a', + tx_pos: 2, + value: 546, + satoshis: 546, + txid: + 'e7d554c317db71fd5b50fcf0b2cb4cbdce54a09f1732cfaade0820659318e30a', + vout: 2, + utxoType: 'token', + transactionType: 'send', + tokenId: + 'bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba', + tokenTicker: 'TBS', + tokenName: 'TestBits', + tokenDocumentUrl: 'https://thecryptoguy.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '6.001', + isValid: true, + address: + 'bitcoincash:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy54hkry298', + }, + tokenId: + 'bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba', + balance: '6.001', + hasBaton: false, + }, + ], +}; diff --git a/web/cashtab/src/components/Wallet/__tests__/Wallet.test.js b/web/cashtab/src/components/Wallet/__tests__/Wallet.test.js --- a/web/cashtab/src/components/Wallet/__tests__/Wallet.test.js +++ b/web/cashtab/src/components/Wallet/__tests__/Wallet.test.js @@ -7,6 +7,8 @@ walletWithBalancesAndTokens, walletWithBalancesMock, walletWithoutBalancesMock, + walletWithBalancesAndTokensWithCorrectState, + walletWithBalancesAndTokensWithEmptyState, } from '../__mocks__/walletAndBalancesMock'; import { BrowserRouter as Router } from 'react-router-dom'; @@ -61,6 +63,32 @@ expect(tree).toMatchSnapshot(); }); +test('Wallet with BCH balances and tokens and state field', () => { + useContextMock.mockReturnValue(walletWithBalancesAndTokensWithCorrectState); + const component = renderer.create( + + + + + , + ); + let tree = component.toJSON(); + expect(tree).toMatchSnapshot(); +}); + +test('Wallet with BCH balances and tokens and state field, but no params in state', () => { + useContextMock.mockReturnValue(walletWithBalancesAndTokensWithEmptyState); + const component = renderer.create( + + + + + , + ); + let tree = component.toJSON(); + expect(tree).toMatchSnapshot(); +}); + test('Without wallet defined', () => { useContextMock.mockReturnValue({ wallet: {}, diff --git a/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap b/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap --- a/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap +++ b/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances 1`] = ` Array [ @@ -200,6 +200,206 @@ ] `; +exports[`Wallet with BCH balances and tokens and state field 1`] = ` +Array [ +
+ 0.06047469 + + BCHA +
, +
+ $ + NaN + + USD +
, +
+
+ Copied +
+ + + + + +
+ + + qzagy4 + + 7mvh6qxkvcn3acjnz73rkhkc6y7cpt + + zgcqy6 + +
+
, +
+
+ BCHA +
+
+ SLPA +
+
, +] +`; + +exports[`Wallet with BCH balances and tokens and state field, but no params in state 1`] = ` +Array [ +
+ 0.06047469 + + BCHA +
, +
+ $ + NaN + + USD +
, +
+
+ Copied +
+ + + + + +
+ + + qzagy4 + + 7mvh6qxkvcn3acjnz73rkhkc6y7cpt + + zgcqy6 + +
+
, +
+
+ BCHA +
+
+ SLPA +
+
, +] +`; + exports[`Wallet without BCH balance 1`] = ` Array [
{ + // get wallet object from localforage + const wallet = await getWallet(); + // If wallet object in storage is valid, use it to set state on startup + if (isValidStoredWallet(wallet)) { + // Convert all the token balance figures to big numbers + const liveWalletState = loadStoredWallet(wallet.state); + wallet.state = liveWalletState; + + setWallet(wallet); + return setLoading(false); + } + // Loading will remain true until API calls populate this legacy wallet + setWallet(wallet); + }; + const haveUtxosChanged = (wallet, utxos, previousUtxos) => { // Relevant points for this array comparing exercise // https://stackoverflow.com/questions/13757109/triple-equal-signs-return-false-for-arrays-in-javascript-why @@ -149,9 +165,9 @@ return setWalletState(liveWalletState); } - const cachedUtxos = wallet.state.utxos; - // Compare - return !_.isEqual(utxos, cachedUtxos); + // If wallet in storage is a legacy wallet or otherwise does not have all state fields, + // then assume utxos have changed + return true; } // return true for empty array, since this means you definitely do not want to skip the next API call if (utxos && utxos.length === 0) { @@ -255,6 +271,12 @@ setWalletState(newState); + // Set wallet with new state field + // Note: now that wallet carries state, maintaining a separate walletState object is redundant + // TODO clear up in future diff + wallet.state = wallet.newState; + setWallet(wallet); + // Write this state to indexedDb using localForage writeWalletState(wallet, newState); // If everything executed correctly, remove apiError @@ -569,6 +591,12 @@ return false; } } + // Make sure stored wallet is in correct format to be used as live wallet + if (isValidStoredWallet(walletToActivate)) { + // Convert all the token balance figures to big numbers + const liveWalletState = loadStoredWallet(walletToActivate.state); + walletToActivate.state = liveWalletState; + } return walletToActivate; }; @@ -764,8 +792,7 @@ }; const handleUpdateWallet = async setWallet => { - const wallet = await getWallet(); - setWallet(wallet); + await loadWalletFromStorageOnStartup(setWallet); }; // Parse for incoming BCH transactions @@ -1277,10 +1304,17 @@ setLoading(true); const newWallet = await activateWallet(walletToActivate); setWallet(newWallet); - update({ - wallet: newWallet, - setWalletState, - }).finally(() => setLoading(false)); + if (isValidStoredWallet(walletToActivate)) { + // If you have all state parameters needed in storage, immediately load the wallet + setLoading(false); + } else { + // If the wallet is missing state parameters in storage, wait for API info + // This handles case of unmigrated legacy wallet + update({ + wallet: newWallet, + setWalletState, + }).finally(() => setLoading(false)); + } }, addNewSavedWallet, renameWallet,